/**
 * Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * 
 */

'use strict';



























const MOCK_CONSTRUCTOR_NAME = 'mockConstructor';

// $FlowFixMe
const RESERVED_KEYWORDS = Object.assign(Object.create(null), {
  arguments: true,
  await: true,
  break: true,
  case: true,
  catch: true,
  class: true,
  const: true,
  continue: true,
  debugger: true,
  default: true,
  delete: true,
  do: true,
  else: true,
  enum: true,
  eval: true,
  export: true,
  extends: true,
  false: true,
  finally: true,
  for: true,
  function: true,
  if: true,
  implements: true,
  import: true,
  in: true,
  instanceof: true,
  interface: true,
  let: true,
  new: true,
  null: true,
  package: true,
  private: true,
  protected: true,
  public: true,
  return: true,
  static: true,
  super: true,
  switch: true,
  this: true,
  throw: true,
  true: true,
  try: true,
  typeof: true,
  var: true,
  void: true,
  while: true,
  with: true,
  yield: true });


function isA(typeName, value) {
  return Object.prototype.toString.apply(value) === '[object ' + typeName + ']';
}

function getType(ref) {
  if (isA('Function', ref)) {
    return 'function';
  } else if (Array.isArray(ref)) {
    return 'array';
  } else if (isA('Object', ref)) {
    return 'object';
  } else if (
  isA('Number', ref) ||
  isA('String', ref) ||
  isA('Boolean', ref) ||
  isA('Symbol', ref))
  {
    return 'constant';
  } else if (isA('Map', ref) || isA('WeakMap', ref) || isA('Set', ref)) {
    return 'collection';
  } else if (isA('RegExp', ref)) {
    return 'regexp';
  } else if (ref === undefined) {
    return 'undefined';
  } else if (ref === null) {
    return 'null';
  } else {
    return null;
  }
}

function isReadonlyProp(object, prop) {
  return (

    (
    prop === 'arguments' ||
    prop === 'caller' ||
    prop === 'callee' ||
    prop === 'name' ||
    prop === 'length') &&

    isA('Function', object) ||


    (
    prop === 'source' ||
    prop === 'global' ||
    prop === 'ignoreCase' ||
    prop === 'multiline') &&

    isA('RegExp', object));


}

function getSlots(object) {
  const slots = {};
  if (!object) {
    return [];
  }

  let parent = Object.getPrototypeOf(object);
  do {
    if (object === Object.getPrototypeOf(Function)) {
      break;
    }
    const ownNames = Object.getOwnPropertyNames(object);
    for (let i = 0; i < ownNames.length; i++) {
      const prop = ownNames[i];
      if (!isReadonlyProp(object, prop)) {
        const propDesc = Object.getOwnPropertyDescriptor(object, prop);
        if (!propDesc.get || object.__esModule) {
          slots[prop] = true;
        }
      }
    }
    object = parent;
  } while (object && (parent = Object.getPrototypeOf(object)) !== null);
  return Object.keys(slots);
}


class ModuleMockerClass {





  /**
                          * @see README.md
                          * @param global Global object of the test environment, used to create
                          * mocks
                          */
  constructor(global) {
    this._environmentGlobal = global;
    this._mockState = new WeakMap();
    this._mockConfigRegistry = new WeakMap();
    this.ModuleMocker = ModuleMockerClass;
  }

  _ensureMockConfig(f) {
    let config = this._mockConfigRegistry.get(f);
    if (!config) {
      config = this._defaultMockConfig();
      this._mockConfigRegistry.set(f, config);
    }
    return config;
  }

  _ensureMockState(f) {
    let state = this._mockState.get(f);
    if (!state) {
      state = this._defaultMockState();
      this._mockState.set(f, state);
    }
    return state;
  }

  _defaultMockConfig() {
    return {
      defaultReturnValue: undefined,
      isReturnValueLastSet: false,
      mockImpl: undefined,
      specificMockImpls: [],
      specificReturnValues: [] };

  }

  _defaultMockState() {
    return {
      calls: [],
      instances: [] };

  }

  _makeComponent(metadata, restore) {
    if (metadata.type === 'object') {
      return new this._environmentGlobal.Object();
    } else if (metadata.type === 'array') {
      return new this._environmentGlobal.Array();
    } else if (metadata.type === 'regexp') {
      return new this._environmentGlobal.RegExp('');
    } else if (
    metadata.type === 'constant' ||
    metadata.type === 'collection' ||
    metadata.type === 'null' ||
    metadata.type === 'undefined')
    {
      return metadata.value;
    } else if (metadata.type === 'function') {
      /* eslint-disable prefer-const */
      let f;
      /* eslint-enable prefer-const */

      const prototype =
      metadata.members &&
      metadata.members.prototype &&
      metadata.members.prototype.members ||
      {};
      const prototypeSlots = getSlots(prototype);
      const mocker = this;
      const mockConstructor = function () {
        const mockState = mocker._ensureMockState(f);
        const mockConfig = mocker._ensureMockConfig(f);
        mockState.instances.push(this);
        mockState.calls.push(Array.prototype.slice.call(arguments));
        if (this instanceof f) {
          // This is probably being called as a constructor
          prototypeSlots.forEach(slot => {
            // Copy prototype methods to the instance to make
            // it easier to interact with mock instance call and
            // return values
            if (prototype[slot].type === 'function') {
              const protoImpl = this[slot];
              this[slot] = mocker.generateFromMetadata(prototype[slot]);
              this[slot]._protoImpl = protoImpl;
            }
          });

          // Run the mock constructor implementation
          return (
            mockConfig.mockImpl &&
            mockConfig.mockImpl.apply(this, arguments));

        }

        let returnValue;
        // If return value is last set, either specific or default, i.e.
        // mockReturnValueOnce()/mockReturnValue() is called and no
        // mockImplementationOnce()/mockImplementation() is called after that.
        // use the set return value.
        if (mockConfig.isReturnValueLastSet) {
          returnValue = mockConfig.specificReturnValues.shift();
          if (returnValue === undefined) {
            returnValue = mockConfig.defaultReturnValue;
          }
        }

        // If mockImplementationOnce()/mockImplementation() is last set,
        // or specific return values are used up, use the mock implementation.
        let specificMockImpl;
        if (returnValue === undefined) {
          specificMockImpl = mockConfig.specificMockImpls.shift();
          if (specificMockImpl === undefined) {
            specificMockImpl = mockConfig.mockImpl;
          }
          if (specificMockImpl) {
            return specificMockImpl.apply(this, arguments);
          }
        }

        // Otherwise use prototype implementation
        if (returnValue === undefined && f._protoImpl) {
          return f._protoImpl.apply(this, arguments);
        }

        return returnValue;
      };

      f = this._createMockFunction(metadata, mockConstructor);
      f._isMockFunction = true;
      f.getMockImplementation = () => this._ensureMockConfig(f).mockImpl;

      this._mockState.set(f, this._defaultMockState());
      this._mockConfigRegistry.set(f, this._defaultMockConfig());

      // $FlowFixMe - defineProperty getters not supported
      Object.defineProperty(f, 'mock', {
        configurable: false,
        enumerable: true,
        get: () => this._ensureMockState(f),
        set: val => this._mockState.set(f, val) });


      f.mockClear = () => {
        this._mockState.delete(f);
      };

      f.mockReset = () => {
        this._mockState.delete(f);
        this._mockConfigRegistry.delete(f);
      };

      f.mockReturnValueOnce = value => {
        // next function call will return this value or default return value
        const mockConfig = this._ensureMockConfig(f);
        mockConfig.isReturnValueLastSet = true;
        mockConfig.specificReturnValues.push(value);
        return f;
      };

      f.mockReturnValue = value => {
        // next function call will return specified return value or this one
        const mockConfig = this._ensureMockConfig(f);
        mockConfig.isReturnValueLastSet = true;
        mockConfig.defaultReturnValue = value;
        return f;
      };

      f.mockImplementationOnce = fn => {
        // next function call will use this mock implementation return value
        // or default mock implementation return value
        const mockConfig = this._ensureMockConfig(f);
        mockConfig.isReturnValueLastSet = false;
        mockConfig.specificMockImpls.push(fn);
        return f;
      };

      f.mockImplementation = fn => {
        // next function call will use mock implementation return value
        const mockConfig = this._ensureMockConfig(f);
        mockConfig.isReturnValueLastSet = false;
        mockConfig.mockImpl = fn;
        return f;
      };

      f.mockReturnThis = () =>
      f.mockImplementation(function () {
        return this;
      });

      if (metadata.mockImpl) {
        f.mockImplementation(metadata.mockImpl);
      }

      f.mockRestore = restore ? restore : () => {};

      return f;
    } else {
      const unknownType = metadata.type || 'undefined type';
      throw new Error('Unrecognized type ' + unknownType);
    }
  }

  _createMockFunction(
  metadata,
  mockConstructor)
  {
    let name = metadata.name;
    // Special case functions named `mockConstructor` to guard for infinite
    // loops.
    if (!name || name === MOCK_CONSTRUCTOR_NAME) {
      return mockConstructor;
    }

    // Preserve `name` property of mocked function.
    const boundFunctionPrefix = 'bound ';
    let bindCall = '';
    // if-do-while for perf reasons. The common case is for the if to fail.
    if (name && name.startsWith(boundFunctionPrefix)) {
      do {
        name = name.substring(boundFunctionPrefix.length);
        // Call bind() just to alter the function name.
        bindCall = '.bind(null)';
      } while (name && name.startsWith(boundFunctionPrefix));
    }

    // It's a syntax error to define functions with a reserved keyword
    // as name.
    if (RESERVED_KEYWORDS[name]) {
      name = '$' + name;
    }

    // It's also a syntax error to define a function with a reserved character
    // as part of it's name.
    if (/[\s-]/.test(name)) {
      name = name.replace(/[\s-]/g, '$');
    }

    const body =
    'return function ' + name + '() {' +
    'return ' + MOCK_CONSTRUCTOR_NAME + '.apply(this,arguments);' +
    '}' + bindCall;
    const createConstructor = new this._environmentGlobal.Function(
    MOCK_CONSTRUCTOR_NAME,
    body);

    return createConstructor(mockConstructor);
  }

  _generateMock(
  metadata,
  callbacks,
  refs)
  {
    const mock = this._makeComponent(metadata);
    if (metadata.refID != null) {
      refs[metadata.refID] = mock;
    }

    getSlots(metadata.members).forEach(slot => {
      const slotMetadata = metadata.members && metadata.members[slot] || {};
      if (slotMetadata.ref != null) {
        callbacks.push(() => mock[slot] = refs[slotMetadata.ref]);
      } else {
        mock[slot] = this._generateMock(slotMetadata, callbacks, refs);
      }
    });

    if (
    metadata.type !== 'undefined' &&
    metadata.type !== 'null' &&
    mock.prototype)
    {
      mock.prototype.constructor = mock;
    }

    return mock;
  }

  /**
     * @see README.md
     * @param metadata Metadata for the mock in the schema returned by the
     * getMetadata method of this module.
     */
  generateFromMetadata(_metadata) {
    const callbacks = [];
    const refs = {};
    const mock = this._generateMock(_metadata, callbacks, refs);
    callbacks.forEach(setter => setter());
    return mock;
  }

  /**
     * @see README.md
     * @param component The component for which to retrieve metadata.
     */
  getMetadata(
  component,
  _refs)
  {
    const refs = _refs || new Map();
    const ref = refs.get(component);
    if (ref != null) {
      return { ref };
    }

    const type = getType(component);
    if (!type) {
      return null;
    }

    const metadata = { type };
    if (
    type === 'constant' ||
    type === 'collection' ||
    type === 'undefined' ||
    type === 'null')
    {
      metadata.value = component;
      return metadata;
    } else if (type === 'function') {
      metadata.name = component.name;
      if (component._isMockFunction) {
        metadata.mockImpl = component.getMockImplementation();
      }
    }

    metadata.refID = refs.size;
    refs.set(component, metadata.refID);

    let members = null;
    // Leave arrays alone
    if (type !== 'array') {
      if (type !== 'undefined') {
        getSlots(component).forEach(slot => {
          if (
          type === 'function' &&
          component._isMockFunction &&
          slot.match(/^mock/))
          {
            return;
          }

          if (
          !component.hasOwnProperty && component[slot] !== undefined ||
          component.hasOwnProperty && component.hasOwnProperty(slot) ||
          type === 'object' && component[slot] != Object.prototype[slot])
          {
            const slotMetadata = this.getMetadata(component[slot], refs);
            if (slotMetadata) {
              if (!members) {
                members = {};
              }
              members[slot] = slotMetadata;
            }
          }
        });
      }

      // If component is native code function, prototype might be undefined
      if (type === 'function' && component.prototype) {
        const prototype = this.getMetadata(component.prototype, refs);
        if (prototype && prototype.members) {
          if (!members) {
            members = {};
          }
          members.prototype = prototype;
        }
      }
    }

    if (members) {
      metadata.members = members;
    }

    return metadata;
  }

  isMockFunction(fn) {
    return !!fn._isMockFunction;
  }

  fn(implementation) {
    const fn = this._makeComponent({ type: 'function' });
    if (implementation) {
      fn.mockImplementation(implementation);
    }
    return fn;
  }

  spyOn(object, methodName) {
    const original = object[methodName];

    if (!this.isMockFunction(original)) {
      if (typeof original !== 'function') {
        throw new Error(
        'Cannot spyOn the ' + methodName + ' property; it is not a function');

      }

      object[methodName] = this._makeComponent({ type: 'function' }, () => {
        object[methodName] = original;
      });
      object[methodName].mockImplementation(function () {
        return original.apply(this, arguments);
      });
    }

    return object[methodName];
  }

  clearAllMocks() {
    this._mockState = new WeakMap();
  }

  resetAllMocks() {
    this._mockConfigRegistry = new WeakMap();
    this._mockState = new WeakMap();
  }}



module.exports = new ModuleMockerClass(global);