/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Provides a mocking framework in Closure to make unit tests easy
 * to write and understand. The methods provided here can be used to replace
 * implementations of existing objects with 'mock' objects to abstract out
 * external services and dependencies thereby isolating the code under test.
 * Apart from mocking, methods are also provided to just monitor calls to an
 * object (spying) and returning specific values for some or all the inputs to
 * methods (stubbing).
 *
 * Manual: http://go/goog.labs.mock
 */


goog.provide('goog.labs.mock');
goog.provide('goog.labs.mock.TimeoutError');
goog.provide('goog.labs.mock.VerificationError');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.debug');
goog.require('goog.debug.Error');
goog.require('goog.functions');
goog.require('goog.labs.mock.timeout');
goog.require('goog.labs.mock.timeout.TimeoutMode');
goog.require('goog.labs.mock.verification');
goog.require('goog.labs.mock.verification.BaseVerificationMode');
goog.require('goog.labs.mock.verification.VerificationMode');
goog.require('goog.object');

goog.setTestOnly('goog.labs.mock');


/**
 * Mocks a given object or class.
 *
 * @param {!Object} objectOrClass An instance or a constructor of a class to be
 *     mocked.
 * @return {!Object} The mocked object.
 */
goog.labs.mock.mock = function(objectOrClass) {
  'use strict';
  // Go over properties of 'objectOrClass' and create a MockManager to
  // be used for stubbing out calls to methods.
  const mockObjectManager =
      new goog.labs.mock.MockObjectManager_(objectOrClass);
  const mockedObject = mockObjectManager.getMockedItem();
  goog.asserts.assertObject(mockedObject);
  return /** @type {!Object} */ (mockedObject);
};


/**
 * Mocks a given function.
 *
 * @param {!Function=} opt_func A function to be mocked.
 * @return {!Function} The mocked function.
 */
goog.labs.mock.mockFunction = function(opt_func) {
  'use strict';
  const mockFuncManager = new goog.labs.mock.MockFunctionManager_(
      goog.labs.mock.getFunctionName_(opt_func || function() {}));
  const mockedFunction = mockFuncManager.getMockedItem();
  goog.asserts.assertFunction(mockedFunction);
  return /** @type {!Function} */ (mockedFunction);
};


/**
 * Mocks a given constructor.
 *
 * @param {function(new:T, ...?)} ctor A constructor function to be mocked.
 * @return {function(new:T, ...?)} The mocked constructor.
 * @template T
 */
goog.labs.mock.mockConstructor = function(ctor) {
  'use strict';
  const mockCtor = goog.labs.mock.mockFunction(ctor);

  // Copy class members from the real constructor to the mock. Do not copy
  // the closure superClass_ property (see goog.inherits), the built-in
  // prototype property, or properties added to Function.prototype
  for (let property in ctor) {
    if (property != 'superClass_' && property != 'prototype' &&
        ctor.hasOwnProperty(property)) {
      mockCtor[property] = ctor[property];
    }
  }
  return mockCtor;
};


/**
 * Spies on a given object.
 *
 * @param {!Object} obj The object to be spied on.
 * @return {!Object} The spy object.
 */
goog.labs.mock.spy = function(obj) {
  'use strict';
  // Go over properties of 'obj' and create a MockSpyManager_ to
  // be used for spying on calls to methods.
  const mockSpyManager = new goog.labs.mock.MockSpyManager_(obj);
  const spyObject = mockSpyManager.getMockedItem();
  goog.asserts.assert(spyObject);
  return spyObject;
};


/**
 * Returns an object that can be used to verify calls to specific methods of a
 * given mock.
 * @param {!Object} obj The mocked object.
 * @param {!goog.labs.mock.verification.VerificationMode=} opt_verificationMode The mode
 *     under which to verify invocations.
 * @return {?} The verifier. Return type {?} to avoid compilation errors.
 * @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
 */
goog.labs.mock.verify = function(obj, opt_verificationMode) {
  'use strict';
  const mode = opt_verificationMode || goog.labs.mock.verification.atLeast(1);
  obj.$verificationModeSetter(mode);

  return obj.$callVerifier;
};

/**
 * Returns an object that can be used to wait for calls to specific methods of a
 * given mock.
 * @param {!Object} obj The mocked object.
 * @param {...(!goog.labs.mock.verification.VerificationMode|
 *   !goog.labs.mock.timeout.TimeoutMode)} verificationOrTimeoutModes
 *   The mode under which to verify invocations.
 * @return {?} The waiter. Return type {?} to avoid compilation errors.
 * @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
 */
goog.labs.mock.waitAndVerify = function(obj, ...verificationOrTimeoutModes) {
  goog.asserts.assert(
      verificationOrTimeoutModes.length <= 2,
      'At most 2 arguments may be passed as Timeout and Verification modes.');
  for (let i = 0; i < 2; i++) {
    const mode = verificationOrTimeoutModes[i];
    if (mode instanceof goog.labs.mock.timeout.TimeoutMode) {
      obj.$timeoutModeSetter(mode);
    } else if (
        mode instanceof goog.labs.mock.verification.BaseVerificationMode) {
      obj.$verificationModeSetter(mode);
    }
  }
  return obj.$callWaiter;
};

/**
 * Returns a name to identify a function. Named functions return their names,
 * unnamed functions return a string of the form '#anonymous{ID}' where ID is
 * a unique identifier for each anonymous function.
 * @private
 * @param {!Function} func The function.
 * @return {string} The function name.
 */
goog.labs.mock.getFunctionName_ = function(func) {
  'use strict';
  let funcName = goog.debug.getFunctionName(func);
  if (funcName == '' || funcName == '[Anonymous]') {
    funcName = '#anonymous' + goog.labs.mock.getUid(func);
  }
  return funcName;
};


/**
 * Returns a nicely formatted, readable representation of a method call.
 * @private
 * @param {string} methodName The name of the method.
 * @param {Array<?>=} opt_args The method arguments.
 * @return {string} The string representation of the method call.
 */
goog.labs.mock.formatMethodCall_ = function(methodName, opt_args) {
  'use strict';
  opt_args = opt_args || [];
  opt_args = opt_args.map(function(arg) {
    'use strict';
    if (typeof arg === 'function') {
      const funcName = goog.labs.mock.getFunctionName_(arg);
      return '<function ' + funcName + '>';
    } else {
      const isObjectWithClass = goog.isObject(arg) &&
          typeof arg !== 'function' && !Array.isArray(arg) &&
          arg.constructor != Object;
      if (isObjectWithClass) {
        return arg.toString();
      }
      return goog.labs.mock.formatValue_(arg);
    }
  });
  return methodName + '(' + opt_args.join(', ') + ')';
};


/**
 * An array to store objects for unique id generation.
 * @private
 * @type {!Array<!Object>}
 */
goog.labs.mock.uid_ = [];


/**
 * A unique Id generator that does not modify the object.
 * @param {Object!} obj The object whose unique ID we want to generate.
 * @return {number} an unique id for the object.
 */
goog.labs.mock.getUid = function(obj) {
  'use strict';
  let index = goog.labs.mock.uid_.indexOf(obj);
  if (index == -1) {
    index = goog.labs.mock.uid_.length;
    goog.labs.mock.uid_.push(obj);
  }
  return index;
};


/**
 * This is just another implementation of goog.debug.deepExpose with a more
 * compact format.
 * @private
 * @param {*} obj The object whose string representation will be returned.
 * @param {boolean=} opt_id Whether to include the id of objects or not.
 *     Defaults to true.
 * @return {string} The string representation of the object.
 */
goog.labs.mock.formatValue_ = function(obj, opt_id) {
  'use strict';
  const id = (opt_id !== undefined) ? opt_id : true;
  const previous = [];
  const output = [];

  const helper = function(obj) {
    'use strict';
    const indentMultiline = function(output) {
      'use strict';
      return output.replace(/\n/g, '\n');
    };


    try {
      if (obj === undefined) {
        output.push('undefined');
      } else if (obj === null) {
        output.push('NULL');
      } else if (typeof obj === 'string') {
        output.push('"' + indentMultiline(obj) + '"');
      } else if (typeof obj === 'symbol') {
        output.push(obj.toString());
      } else if (typeof obj === 'bigint') {
        output.push(obj.toString(10) + 'n');
      } else if (typeof obj === 'function') {
        const funcName = goog.labs.mock.getFunctionName_(obj);
        output.push('<function ' + funcName + '>');
      } else if (goog.isObject(obj)) {
        if (goog.array.contains(previous, obj)) {
          if (id) {
            output.push(
                '<recursive/dupe obj_' + goog.labs.mock.getUid(obj) + '>');
          } else {
            output.push('<recursive/dupe>');
          }
        } else {
          previous.push(obj);
          output.push('{');
          for (let x in obj) {
            output.push(' ');
            output.push(
                '"' + x + '"' +
                ':');
            helper(obj[x]);
          }
          if (id) {
            output.push(' _id:' + goog.labs.mock.getUid(obj));
          }
          output.push('}');
        }
      } else {
        output.push(obj);
      }
    } catch (e) {
      output.push('*** ' + e + ' ***');
    }
  };

  helper(obj);
  return output.join('')
      .replace(/"closure_uid_\d+"/g, '_id')
      .replace(/{ /g, '{');
};


/**
 * Like getOwnPropertyDescriptor but walks prototype chain.
 * @private
 * @param {!Object} obj a prototype
 * @param {string} name property name
 * @return {!Object|undefined} a PropertyDescriptor or undefined if property was
 *     not found.
 */
goog.labs.mock.getPropertyDescriptor_ = function(obj, name) {
  let proto = obj;
  while (proto) {
    const descriptor = Object.getOwnPropertyDescriptor(proto, name);
    if (descriptor) {
      return descriptor;
    }
    proto = Object.getPrototypeOf(proto);
  }
  return undefined;
};


/**
 * Sets expectations on mocked accessor descriptor (trivial `get` and `set`) so
 * that it behaves like an ordinary data descriptor.
 *
 * ```
 * var mockObj = goog.labs.mock.mock(objectBeingMocked);
 * goog.labs.mock.when(mockObj).descriptorProperty.asDataProperty(initialValue);
 * ```
 *
 * @param {?} property The expectations setter, obtained via
 *     `when(mockObj).property`.
 * @param {?=} initialValue Optional initial value for the property.
 * @private
 */
goog.labs.mock.mockDescriptorAsDataProperty_ = function(
    property, initialValue = undefined) {
  let value = initialValue;
  property.get().then(() => value);
  property.set({matches: () => true}).then((v) => value = v);
};


/**
 * Error thrown when verification failed.
 *
 * @param {Array<!goog.labs.mock.MethodBinding_>} recordedCalls
 *     The recorded calls that didn't match the expectation.
 * @param {string} methodName The expected method call.
 * @param {!goog.labs.mock.verification.VerificationMode} verificationMode The
 *     expected verification mode which failed verification.
 * @param {!Array<?>} args The expected arguments.
 * @constructor
 * @extends {goog.debug.Error}
 * @final
 */
goog.labs.mock.VerificationError = function(
    recordedCalls, methodName, verificationMode, args) {
  'use strict';
  const msg = goog.labs.mock.VerificationError.getVerificationErrorMsg_(
      recordedCalls, methodName, verificationMode, args);
  goog.labs.mock.VerificationError.base(this, 'constructor', msg);
};
goog.inherits(goog.labs.mock.VerificationError, goog.debug.Error);


/** @override */
goog.labs.mock.VerificationError.prototype.name = 'VerificationError';

/**
 * Error thrown when timeout triggers before specified action.
 *
 * @param {!Array<!goog.labs.mock.MethodBinding_>} recordedCalls
 *     The recorded calls that didn't match the expectation.
 * @param {string} methodName The expected method call.
 * @param {!goog.labs.mock.verification.VerificationMode} verificationMode The
 *     expected verification mode which failed verification.
 * @param {!Array<?>} args The expected arguments.
 * @constructor
 * @extends {goog.debug.Error}
 * @final
 */
goog.labs.mock.TimeoutError = function(
    recordedCalls, methodName, verificationMode, args) {
  'use strict';
  const msg = goog.labs.mock.TimeoutError.getTimeoutErrorMsg_(
      recordedCalls, methodName, verificationMode, args);
  goog.labs.mock.TimeoutError.base(this, 'constructor', msg);
};
goog.inherits(goog.labs.mock.TimeoutError, goog.debug.Error);

/** @override */
goog.labs.mock.TimeoutError.prototype.name = 'TimeoutError';

/**
 * This array contains the name of the functions that are part of the base
 * Object prototype.
 * Basically a copy of goog.object.PROTOTYPE_FIELDS_.
 * @const
 * @type {!Array<string>}
 * @private
 */
goog.labs.mock.PROTOTYPE_FIELDS_ = [
  'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable',
  'toLocaleString', 'toString', 'valueOf'
];


/**
 * Constructs a descriptive error message for an expected method call.
 * @private
 * @param {Array<!goog.labs.mock.MethodBinding_>} recordedCalls
 *     The recorded calls that didn't match the expectation.
 * @param {string} methodName The expected method call.
 * @param {!goog.labs.mock.verification.VerificationMode} verificationMode The
 *     expected verification mode that failed verification.
 * @param {!Array<?>} args The expected arguments.
 * @return {string} The error message.
 */
goog.labs.mock.VerificationError.getVerificationErrorMsg_ = function(
    recordedCalls, methodName, verificationMode, args) {
  'use strict';
  recordedCalls = recordedCalls.filter(function(binding) {
    'use strict';
    return binding.getMethodName() == methodName;
  });

  const expected = goog.labs.mock.formatMethodCall_(methodName, args);

  let msg =
      '\nExpected: ' + expected.toString() + ' ' + verificationMode.describe();
  msg += '\nRecorded: ';

  if (recordedCalls.length > 0) {
    msg += recordedCalls.join(',\n          ');
  } else {
    msg += 'No recorded calls';
  }

  return msg;
};

/**
 * Constructs a descriptive error message for an expected method call
 * that was not triggered within a specified duration.
 * @private
 * @param {!Array<!goog.labs.mock.MethodBinding_>} recordedCalls
 *     The recorded calls that didn't match the expectation.
 * @param {string} methodName The expected method call.
 * @param {!goog.labs.mock.verification.VerificationMode} verificationMode The
 *     expected verification mode whose criteria was never met.
 * @param {!Array<?>} args The expected arguments.
 * @return {string} The error message.
 */
goog.labs.mock.TimeoutError.getTimeoutErrorMsg_ = function(
    recordedCalls, methodName, verificationMode, args) {
  'use strict';
  const verificationErrorMsg =
      goog.labs.mock.VerificationError.getVerificationErrorMsg_(
          recordedCalls, methodName, verificationMode, args);
  const timeoutErrorMsg =
      'Function call was either not invoked or never met criteria specified ' +
      'by provided verification mode. ' + verificationErrorMsg;
  return timeoutErrorMsg;
};



/**
 * Base class that provides basic functionality for creating, adding and
 * finding bindings, offering an executor method that is called when a call to
 * the stub is made, an array to hold the bindings and the mocked item, among
 * other things.
 *
 * @constructor
 * @struct
 * @private
 */
goog.labs.mock.MockManager_ = function() {
  'use strict';
  /**
   * Proxies the methods for the mocked object or class to execute the stubs.
   * @type {!Object}
   * @protected
   */
  this.mockedItem = {};

  /**
   * A reference to the object or function being mocked.
   * @type {?Object|?Function}
   * @protected
   */
  this.mockee = null;

  /**
   * Holds the stub bindings established so far.
   * @protected
   */
  this.methodBindings = [];

  /**
   * Holds a reference to the binder used to define stubs.
   * @protected
   */
  this.$stubBinder = null;

  /**
   * Record method calls with no stub definitions.
   * @type {!Array<!goog.labs.mock.MethodBinding_>}
   * @private
   */
  this.callRecords_ = [];

  /**
   * Which `VerificationMode` to use during verification.
   * @private
   */
  this.verificationMode_ = goog.labs.mock.verification.atLeast(1);

  /**
   * Which `TimeoutMode` to use during waitAndVerify.
   * @private
   */
  this.timeoutMode_ = goog.labs.mock.timeout.timeout(0);

  /**
   * Maintains a dictionary keyed by methodName, that holds a list of
   * callbacks that should be called anytime the provided methodName is called.
   * @type {!Object<string, !Set<function(!goog.labs.mock.MethodBinding_)>>}
   * @private
   * @const
   */
  this.callListeners_ = {};
};


/**
 * Allows callers of `#verify` to override the default verification
 * mode of this MockManager.
 *
 * @param {!goog.labs.mock.verification.VerificationMode} verificationMode
 * @private
 */
goog.labs.mock.MockManager_.prototype.setVerificationMode_ = function(
    verificationMode) {
  'use strict';
  this.verificationMode_ = verificationMode;
};

/**
 * Allows callers of `#waitAndVerify` to override the default timeout
 * mode of this MockManager.
 *
 * @param {!goog.labs.mock.timeout.TimeoutMode} timeoutMode
 * @private
 */
goog.labs.mock.MockManager_.prototype.setTimeoutMode_ = function(timeoutMode) {
  'use strict';
  this.timeoutMode_ = timeoutMode;
};

/**
 * Handles the first step in creating a stub, returning a stub-binder that
 * is later used to bind a stub for a method.
 *
 * @param {string} methodName The name of the method being bound.
 * @param {...*} var_args The arguments to the method.
 * @return {!goog.labs.mock.StubBinder} The stub binder.
 * @private
 */
goog.labs.mock.MockManager_.prototype.handleMockCall_ = function(
    methodName, var_args) {
  'use strict';
  const args = Array.prototype.slice.call(arguments, 1);
  return new goog.labs.mock.StubBinderImpl_(this, methodName, args);
};


/**
 * Returns the mock object. This should have a stubbed method for each method
 * on the object being mocked.
 *
 * @return {!Object|!Function} The mock object.
 */
goog.labs.mock.MockManager_.prototype.getMockedItem = function() {
  'use strict';
  return this.mockedItem;
};


/**
 * Adds a binding for the method name and arguments to be stubbed.
 *
 * @param {?string} methodName The name of the stubbed method.
 * @param {!Array<?>} args The arguments passed to the method.
 * @param {!Function} func The stub function.
 * @return {!Array<?>} The array of stubs for further sequential stubs to be
 *     appended.
 */
goog.labs.mock.MockManager_.prototype.addBinding = function(
    methodName, args, func) {
  'use strict';
  const binding = new goog.labs.mock.MethodBinding_(methodName, args, func);
  const sequentialStubsArray = [binding];
  goog.array.insertAt(this.methodBindings, sequentialStubsArray, 0);
  return sequentialStubsArray;
};


/**
 * Returns a stub, if defined, for the method name and arguments passed in.
 * If there are multiple stubs for this method name and arguments, then
 * the most recent binding will be used.
 *
 * If the next binding is a sequence of stubs, then they'll be returned
 * in order until only one is left, at which point it will be returned for
 * every subsequent call.
 *
 * @param {string} methodName The name of the stubbed method.
 * @param {!Array<?>} args The arguments passed to the method.
 * @return {?Function} The stub function or null.
 * @protected
 */
goog.labs.mock.MockManager_.prototype.getNextBinding = function(
    methodName, args) {
  'use strict';
  const bindings = this.methodBindings.find(function(bindingArray) {
    'use strict';
    return bindingArray[0].matches(
        methodName, args, false /* isVerification */);
  });
  if (bindings == null) {
    return null;
  }

  if (bindings.length > 1) {
    return bindings.shift().getStub();
  }
  return bindings[0].getStub();
};


/**
 * Returns a stub, if defined, for the method name and arguments passed in as
 * parameters.
 *
 * @param {string} methodName The name of the stubbed method.
 * @param {!Array<?>} args The arguments passed to the method.
 * @return {Function} The stub function or undefined.
 * @protected
 */
goog.labs.mock.MockManager_.prototype.getExecutor = function(methodName, args) {
  'use strict';
  return this.getNextBinding(methodName, args);
};


/**
 * Looks up the list of stubs defined on the mock object and executes the
 * function associated with that stub.
 *
 * @param {string} methodName The name of the method to execute.
 * @param {...*} var_args The arguments passed to the method.
 * @return {*} Value returned by the stub function.
 * @protected
 */
goog.labs.mock.MockManager_.prototype.executeStub = function(
    methodName, var_args) {
  'use strict';
  const args = Array.prototype.slice.call(arguments, 1);

  const callRecord = this.recordCall_(methodName, args);

  if (this.callListeners_[methodName] instanceof Set) {
    this.callListeners_[methodName].forEach((listener) => {
      listener(callRecord);
    });
  }

  const func = this.getExecutor(methodName, args);
  if (func) {
    return func.apply(null, args);
  }
};


/**
 * Records a call to 'methodName' with arguments 'args'.
 *
 * @param {string} methodName The name of the called method.
 * @param {!Array<?>} args The array of arguments.
 * @return {!goog.labs.mock.MethodBinding_} The call record that was recorded.
 * @private
 */
goog.labs.mock.MockManager_.prototype.recordCall_ = function(methodName, args) {
  'use strict';
  const callRecord =
      new goog.labs.mock.MethodBinding_(methodName, args, () => {});

  this.callRecords_.push(callRecord);
  return callRecord;
};


/**
 * Verify invocation of a method with specific arguments.
 *
 * @param {string} methodName The name of the method.
 * @param {...*} var_args The arguments passed.
 * @protected
 */
goog.labs.mock.MockManager_.prototype.verifyInvocation = function(
    methodName, var_args) {
  'use strict';
  const args = Array.prototype.slice.call(arguments, 1);
  const count =
      this.callRecords_
          .filter(function(binding) {
            'use strict';
            return binding.matches(methodName, args, true /* isVerification */);
          })
          .length;

  if (!this.verificationMode_.verify(count)) {
    throw new goog.labs.mock.VerificationError(
        this.callRecords_, methodName, this.verificationMode_, args);
  }
};

/**
 * Wait until a function is called and then resolve.
 * @param {string} methodName The name of the method.
 * @param {...*} args The arguments passed.
 * @return {!Promise} A promise that resolves when the method is called
 *     according to its verification mode.
 * @protected
 */
goog.labs.mock.MockManager_.prototype.waitForCall = function(
    methodName, ...args) {
  let count =
      this.callRecords_
          .filter(function(binding) {
            return binding.matches(methodName, args, true /* isVerification */);
          })
          .length;

  return new Promise((resolve, reject) => {
    // If the function has already been called, immediately resolve.
    if (this.verificationMode_.verify(count)) {
      resolve();
      return;
    }

    const timeout = setTimeout(() => {
      reject(new goog.labs.mock.TimeoutError(
          this.callRecords_, methodName, this.verificationMode_, args));
      this.callListeners_[methodName].delete(listener);
    }, this.timeoutMode_.duration);

    if (!this.callListeners_[methodName]) {
      this.callListeners_[methodName] = new Set();
    }

    /**
     * Listens for calls to this function name
     * @param {!goog.labs.mock.MethodBinding_} callRecord The call record
     *     just added
     * @this {goog.labs.mock.MockManager_}
     */
    const listener = (callRecord) => {
      if (callRecord.matches(methodName, args, true /* isVerification */)) {
        count++;
        if (this.verificationMode_.verify(count)) {
          resolve();
          clearTimeout(timeout);
          this.callListeners_[methodName].delete(listener);
        }
      }
    };

    this.callListeners_[methodName].add(listener);
  });
};



/**
 * Sets up mock for the given object (or class), stubbing out all the defined
 * methods. By default, all stubs return `undefined`, though stubs can be
 * later defined using `goog.labs.mock.when`.
 * @struct
 * @constructor
 * @extends {goog.labs.mock.MockManager_}
 * @param {!Object|!Function} objOrClass The object or class to set up the
 *     mock for. A class is a constructor function.
 * @private
 * @suppress {strictMissingProperties} Part of the
 * go/strict_warnings_migration
 */
goog.labs.mock.MockObjectManager_ = function(objOrClass) {
  'use strict';
  goog.labs.mock.MockObjectManager_.base(this, 'constructor');

  /**
   * Proxies the calls to establish the first step of the stub bindings
   * (object and method name)
   * @private
   */
  this.objectStubBinder_ = {};

  this.mockee = objOrClass;

  /**
   * The call verifier is used to verify the calls. It maps property names to
   * the method that does call verification.
   * @type {!Object<string, function(string, ...)>}
   * @private
   */
  this.objectCallVerifier_ = {};

  /**
   * The call waiter is used to wait for function calls. It returns a resolved
   * promise when the function is eventually called.
   * @type {!Object<string, function(string, ...)>}
   * @private
   * @const
   */
  this.objectCallWaiter_ = {};

  let obj;
  if (typeof objOrClass === 'function') {
    // Create a temporary subclass with a no-op constructor so that we can
    // create an instance and determine what methods it has.
    /**
     * @constructor
     * @final
     */
    const tempCtor = function() {};
    goog.inherits(tempCtor, objOrClass);
    obj = new tempCtor();
  } else {
    obj = objOrClass;
  }

  // Put the object being mocked in the prototype chain of the mock so that
  // it has all the correct properties and instanceof works.
  /**
   * @constructor
   * @final
   */
  const mockedItemCtor = function() {};
  mockedItemCtor.prototype = obj;
  this.mockedItem = new mockedItemCtor();

  const propObj =
      typeof objOrClass === 'function' ? objOrClass.prototype : objOrClass;
  const enumerableProperties = goog.object.getAllPropertyNames(propObj);
  // The non enumerable properties are added due to the fact that IE8 does not
  // enumerate any of the prototype Object functions even when overridden and
  // mocking these is sometimes needed.
  for (let i = 0; i < goog.labs.mock.PROTOTYPE_FIELDS_.length; i++) {
    const prop = goog.labs.mock.PROTOTYPE_FIELDS_[i];
    if (!goog.array.contains(enumerableProperties, prop)) {
      enumerableProperties.push(prop);
    }
  }

  // Adds the properties to the mock, creating a proxy stub for each method on
  // the instance.
  for (let i = 0; i < enumerableProperties.length; i++) {
    const prop = enumerableProperties[i];
    const descriptor =
        goog.labs.mock.getPropertyDescriptor_(propObj, prop) || {};
    if (descriptor.get || descriptor.set) {
      // A separate stub binder and verifier are needed for get/set functions.
      const stubDescriptor = {};
      /** @type {!goog.labs.mock.DescriptorStubBinder} */
      const stubBinder = this.objectStubBinder_[prop] = {
        asDataProperty(v = undefined) {
          goog.labs.mock.mockDescriptorAsDataProperty_(stubBinder, v);
        },
      };
      const callVerifier = this.objectCallVerifier_[prop] = {};
      const callWaiter = this.objectCallWaiter_[prop] = {};
      for (const type of ['get', 'set']) {
        if (!descriptor[type]) {
          continue;
        }
        const specialProp = `${prop}.${type}`;
        stubDescriptor[type] = goog.bind(this.executeStub, this, specialProp);
        stubBinder[type] = goog.bind(this.handleMockCall_, this, specialProp);
        callVerifier[type] =
            goog.bind(this.verifyInvocation, this, specialProp);
        callWaiter[type] = goog.bind(this.waitForCall, this, specialProp);
      }
      Object.defineProperty(this.mockedItem, prop, stubDescriptor);
    } else if (typeof propObj[prop] === 'function') {
      this.mockedItem[prop] = goog.bind(this.executeStub, this, prop);
      // The stub binder used to create bindings.
      this.objectStubBinder_[prop] =
          goog.bind(this.handleMockCall_, this, prop);
      // The verifier verifies the calls.
      this.objectCallVerifier_[prop] =
          goog.bind(this.verifyInvocation, this, prop);
      // The call waiter waits for calls.
      this.objectCallWaiter_[prop] = goog.bind(this.waitForCall, this, prop);
    }
  }
  // The alias for stub binder exposed to the world.
  this.mockedItem.$stubBinder = this.objectStubBinder_;

  // The alias for verifier for the world.
  this.mockedItem.$callVerifier = this.objectCallVerifier_;

  this.mockedItem.$callWaiter = this.objectCallWaiter_;

  this.mockedItem.$verificationModeSetter =
      goog.bind(this.setVerificationMode_, this);

  this.mockedItem.$timeoutModeSetter = goog.bind(this.setTimeoutMode_, this);
};
goog.inherits(goog.labs.mock.MockObjectManager_, goog.labs.mock.MockManager_);



/**
 * Sets up the spying behavior for the given object.
 *
 * @param {!Object} obj The object to be spied on.
 *
 * @constructor
 * @struct
 * @extends {goog.labs.mock.MockObjectManager_}
 * @private
 */
goog.labs.mock.MockSpyManager_ = function(obj) {
  'use strict';
  goog.labs.mock.MockSpyManager_.base(this, 'constructor', obj);
};
goog.inherits(
    goog.labs.mock.MockSpyManager_, goog.labs.mock.MockObjectManager_);


/**
 * Return a stub, if defined, for the method and arguments passed in. If we
 * lack a stub, instead look for a call record that matches the method and
 * arguments.
 *
 * @return {!Function} The stub or the invocation logger, if defined.
 * @override
 */
goog.labs.mock.MockSpyManager_.prototype.getNextBinding = function(
    methodName, args) {
  'use strict';
  let stub = goog.labs.mock.MockSpyManager_.base(
      this, 'getNextBinding', methodName, args);

  if (!stub) {
    stub = goog.bind(this.mockee[methodName], this.mockedItem);
  }

  return stub;
};



/**
 * Sets up mock for the given function, stubbing out. By default, all stubs
 * return `undefined`, though stubs can be later defined using
 * `goog.labs.mock.when`.
 * @struct
 * @constructor
 * @extends {goog.labs.mock.MockManager_}
 * @param {string} name The name of the function to set up the mock for.
 * @private
 * @suppress {strictMissingProperties} Part of the
 * go/strict_warnings_migration
 */
goog.labs.mock.MockFunctionManager_ = function(name) {
  'use strict';
  goog.labs.mock.MockFunctionManager_.base(this, 'constructor');

  this.name_ = name;

  /**
   * The stub binder used to create bindings.
   * Sets the first argument of handleMockCall_ to the function name.
   * @type {!Function}
   * @private
   */
  this.functionStubBinder_ = this.useMockedFunctionName_(this.handleMockCall_);

  this.mockedItem = this.useMockedFunctionName_(this.executeStub);
  this.mockedItem.$stubBinder = this.functionStubBinder_;

  /**
   * The call verifier is used to verify function invocations.
   * Sets the first argument of verifyInvocation to the function name.
   * @type {!Function}
   */
  this.mockedItem.$callVerifier =
      this.useMockedFunctionName_(this.verifyInvocation);

  /**
   * The call waiter is used to wait for function calls.
   * Sets the first argument of waitForCall to the function name.
   * @type {!Function}
   */
  this.mockedItem.$callWaiter = this.useMockedFunctionName_(this.waitForCall);

  // These have to be repeated because if they're set in the base class they
  // will be stubbed by MockObjectManager.
  this.mockedItem.$verificationModeSetter =
      goog.bind(this.setVerificationMode_, this);
  this.mockedItem.$timeoutModeSetter = goog.bind(this.setTimeoutMode_, this);
};
goog.inherits(goog.labs.mock.MockFunctionManager_, goog.labs.mock.MockManager_);


/**
 * Given a method, returns a new function that calls the first one setting
 * the first argument to the mocked function name.
 * This is used to dynamically override the stub binders and call verifiers.
 * @private
 * @param {Function} nextFunc The function to override.
 * @return {!Function} The overloaded function.
 */
goog.labs.mock.MockFunctionManager_.prototype.useMockedFunctionName_ = function(
    nextFunc) {
  'use strict';
  const mockFunctionManager = this;
  // Avoid using 'this' because this function may be called with 'new'.
  return function(var_args) {
    'use strict';
    const args = Array.prototype.slice.call(arguments);
    const name = '#mockFor<' + mockFunctionManager.name_ + '>';
    goog.array.insertAt(args, name, 0);
    return nextFunc.apply(mockFunctionManager, args);
  };
};


/**
 * A stub binder is an object that helps define the stub by binding
 * method name to the stub method.
 * @interface
 */
goog.labs.mock.StubBinder = function() {};


/**
 * Defines the function to be called for the method name and arguments bound
 * to this `StubBinder`.
 *
 * If `then` or `thenReturn` has been previously called
 * on this `StubBinder` then the given stub `func` will be called
 * only after the stubs passed previously have been called.  Afterwards,
 * if no other calls are made to `then` or `thenReturn` for this
 * `StubBinder` then the given `func` will be used for every further
 * invocation.
 * See #when for complete examples.
 * TODO(vbhasin): Add support for the 'Answer' interface.
 *
 * @param {!Function} func The function to call.
 * @return {!goog.labs.mock.StubBinder} Returns itself for chaining.
 */
goog.labs.mock.StubBinder.prototype.then = goog.abstractMethod;


/**
 * Defines the constant return value for the stub represented by this
 * `StubBinder`.
 *
 * @param {*} value The value to return.
 * @return {!goog.labs.mock.StubBinder} Returns itself for chaining.
 */
goog.labs.mock.StubBinder.prototype.thenReturn = goog.abstractMethod;


/**
 * A `StubBinder` which uses `MockManager_` to manage stub
 * bindings.
 *
 * @param {!goog.labs.mock.MockManager_}
 *   mockManager The mock manager.
 * @param {?string} name The method name.
 * @param {!Array<?>} args The other arguments to the method.
 *
 * @implements {goog.labs.mock.StubBinder}
 * @private @constructor @struct @final
 */
goog.labs.mock.StubBinderImpl_ = function(mockManager, name, args) {
  'use strict';
  /**
   * The mock manager instance.
   * @type {!goog.labs.mock.MockManager_}
   * @private
   */
  this.mockManager_ = mockManager;

  /**
   * Holds the name of the method to be bound.
   * @type {?string}
   * @private
   */
  this.name_ = name;

  /**
   * Holds the arguments for the method.
   * @type {!Array<?>}
   * @private
   */
  this.args_ = args;

  /**
   * Stores a reference to the list of stubs to allow chaining sequential
   * stubs.
   * @private {!Array<?>}
   */
  this.sequentialStubsArray_ = [];
};


/**
 * @override
 */
goog.labs.mock.StubBinderImpl_.prototype.then = function(func) {
  'use strict';
  if (this.sequentialStubsArray_.length) {
    this.sequentialStubsArray_.push(
        new goog.labs.mock.MethodBinding_(this.name_, this.args_, func));
  } else {
    this.sequentialStubsArray_ =
        this.mockManager_.addBinding(this.name_, this.args_, func);
  }
  return this;
};


/**
 * @override
 */
goog.labs.mock.StubBinderImpl_.prototype.thenReturn = function(value) {
  'use strict';
  return this.then(goog.functions.constant(value));
};


/**
 * A type for a mocked accessor descriptor.
 *
 * @typedef {{
 *   get: (!Function|undefined),
 *   set: (!Function|undefined),
 *   asDataProperty: function(?=):void,
 * }}
 */
goog.labs.mock.DescriptorStubBinder;


/**
 * Facilitates (and is the first step in) setting up stubs. Obtains an object
 * on which, the method to be mocked is called to create a stub. Sample usage:
 *
 * var mockObj = goog.labs.mock.mock(objectBeingMocked);
 * goog.labs.mock.when(mockObj).getFoo(3).thenReturn(4);
 *
 * Subsequent calls to `when` take precedence over earlier calls, allowing
 * users to set up default stubs in setUp methods and then override them in
 * individual tests.
 *
 * If a user wants sequential calls to their stub to return different
 * values, they can chain calls to `then` or `thenReturn` as
 * follows:
 *
 * var mockObj = goog.labs.mock.mock(objectBeingMocked);
 * goog.labs.mock.when(mockObj).getFoo(3)
 *     .thenReturn(4)
 *     .then(function() {
 *         throw new Error('exceptional case');
 *     });
 * @param {!Object} mockObject The mocked object.
 * @return {?} The property binder. Return type {?} to avoid compilation
 *     errors.
 * @suppress {strictMissingProperties} Part of the
 * go/strict_warnings_migration
 */
goog.labs.mock.when = function(mockObject) {
  'use strict';
  goog.asserts.assert(mockObject.$stubBinder, 'Stub binder cannot be null!');
  return mockObject.$stubBinder;
};



/**
 * Represents a binding between a method name, args and a stub.
 *
 * @param {?string} methodName The name of the method being stubbed.
 * @param {!Array<?>} args The arguments passed to the method.
 * @param {!Function} stub The stub function to be called for the given
 *     method.
 * @constructor
 * @struct
 * @private
 */
goog.labs.mock.MethodBinding_ = function(methodName, args, stub) {
  'use strict';
  /**
   * The name of the method being stubbed.
   * @type {?string}
   * @private
   */
  this.methodName_ = methodName;

  /**
   * The arguments for the method being stubbed.
   * @type {!Array<?>}
   * @private
   */
  this.args_ = args;

  /**
   * The stub function.
   * @type {!Function}
   * @private
   */
  this.stub_ = stub;
};


/**
 * @return {!Function} The stub to be executed.
 */
goog.labs.mock.MethodBinding_.prototype.getStub = function() {
  'use strict';
  return this.stub_;
};


/**
 * @override
 * @return {string} A readable string representation of the binding
 *  as a method call.
 */
goog.labs.mock.MethodBinding_.prototype.toString = function() {
  'use strict';
  return goog.labs.mock.formatMethodCall_(this.methodName_ || '', this.args_);
};


/**
 * @return {string} The method name for this binding.
 */
goog.labs.mock.MethodBinding_.prototype.getMethodName = function() {
  'use strict';
  return this.methodName_ || '';
};


/**
 * Determines whether the given args match the stored args_. Used to determine
 * which stub to invoke for a method.
 *
 * @param {string} methodName The name of the method being stubbed.
 * @param {!Array<?>} args An array of arguments.
 * @param {boolean} isVerification Whether this is a function verification
 *     call or not.
 * @return {boolean} If it matches the stored arguments.
 */
goog.labs.mock.MethodBinding_.prototype.matches = function(
    methodName, args, isVerification) {
  'use strict';
  const specs = isVerification ? args : this.args_;
  const calls = isVerification ? this.args_ : args;

  // TODO(vbhasin): More elaborate argument matching. Think about matching
  //    objects.
  return this.methodName_ == methodName &&
      goog.array.equals(calls, specs, function(arg, spec) {
        'use strict';
        // Duck-type to see if this is an object that implements the
        // goog.labs.testing.Matcher interface.
        if (spec && typeof spec.matches === 'function') {
          return spec.matches(arg);
        } else {
          return goog.array.defaultCompareEquality(spec, arg);
        }
      });
};
