getJasmineRequireObj().Spy = function(j$) {
  const nextOrder = (function() {
    let order = 0;

    return function() {
      return order++;
    };
  })();

  /**
   * @classdesc _Note:_ Do not construct this directly. Use {@link spyOn},
   * {@link spyOnProperty}, {@link jasmine.createSpy}, or
   * {@link jasmine.createSpyObj} instead.
   * @class Spy
   * @hideconstructor
   */
  function Spy(name, matchersUtil, optionals) {
    const spy = function(context, args, invokeNew) {
      /**
       * @name Spy.callData
       * @property {object} object - `this` context for the invocation.
       * @property {number} invocationOrder - Order of the invocation.
       * @property {Array} args - The arguments passed for this invocation.
       * @property returnValue - The value that was returned from this invocation.
       */
      const callData = {
        object: context,
        invocationOrder: nextOrder(),
        args: Array.prototype.slice.apply(args)
      };

      callTracker.track(callData);
      const returnValue = strategyDispatcher.exec(context, args, invokeNew);
      callData.returnValue = returnValue;

      return returnValue;
    };
    const { originalFn, customStrategies, defaultStrategyFn } = optionals || {};

    const numArgs = typeof originalFn === 'function' ? originalFn.length : 0,
      wrapper = makeFunc(numArgs, function(context, args, invokeNew) {
        return spy(context, args, invokeNew);
      }),
      strategyDispatcher = new SpyStrategyDispatcher(
        {
          name: name,
          fn: originalFn,
          getSpy: function() {
            return wrapper;
          },
          customStrategies: customStrategies
        },
        matchersUtil
      ),
      callTracker = new j$.CallTracker();

    function makeFunc(length, fn) {
      switch (length) {
        case 1:
          return function wrap1(a) {
            return fn(this, arguments, this instanceof wrap1);
          };
        case 2:
          return function wrap2(a, b) {
            return fn(this, arguments, this instanceof wrap2);
          };
        case 3:
          return function wrap3(a, b, c) {
            return fn(this, arguments, this instanceof wrap3);
          };
        case 4:
          return function wrap4(a, b, c, d) {
            return fn(this, arguments, this instanceof wrap4);
          };
        case 5:
          return function wrap5(a, b, c, d, e) {
            return fn(this, arguments, this instanceof wrap5);
          };
        case 6:
          return function wrap6(a, b, c, d, e, f) {
            return fn(this, arguments, this instanceof wrap6);
          };
        case 7:
          return function wrap7(a, b, c, d, e, f, g) {
            return fn(this, arguments, this instanceof wrap7);
          };
        case 8:
          return function wrap8(a, b, c, d, e, f, g, h) {
            return fn(this, arguments, this instanceof wrap8);
          };
        case 9:
          return function wrap9(a, b, c, d, e, f, g, h, i) {
            return fn(this, arguments, this instanceof wrap9);
          };
        default:
          return function wrap() {
            return fn(this, arguments, this instanceof wrap);
          };
      }
    }

    for (const prop in originalFn) {
      if (prop === 'and' || prop === 'calls') {
        throw new Error(
          "Jasmine spies would overwrite the 'and' and 'calls' properties on the object being spied upon"
        );
      }

      wrapper[prop] = originalFn[prop];
    }

    /**
     * @member {SpyStrategy} - Accesses the default strategy for the spy. This strategy will be used
     * whenever the spy is called with arguments that don't match any strategy
     * created with {@link Spy#withArgs}.
     * @name Spy#and
     * @since 2.0.0
     * @example
     * spyOn(someObj, 'func').and.returnValue(42);
     */
    wrapper.and = strategyDispatcher.and;
    /**
     * Specifies a strategy to be used for calls to the spy that have the
     * specified arguments.
     * @name Spy#withArgs
     * @since 3.0.0
     * @function
     * @param {...*} args - The arguments to match
     * @type {SpyStrategy}
     * @example
     * spyOn(someObj, 'func').withArgs(1, 2, 3).and.returnValue(42);
     * someObj.func(1, 2, 3); // returns 42
     */
    wrapper.withArgs = function() {
      return strategyDispatcher.withArgs.apply(strategyDispatcher, arguments);
    };
    wrapper.calls = callTracker;

    if (defaultStrategyFn) {
      defaultStrategyFn(wrapper.and);
    }

    return wrapper;
  }

  function SpyStrategyDispatcher(strategyArgs, matchersUtil) {
    const baseStrategy = new j$.SpyStrategy(strategyArgs);
    const argsStrategies = new StrategyDict(function() {
      return new j$.SpyStrategy(strategyArgs);
    }, matchersUtil);

    this.and = baseStrategy;

    this.exec = function(spy, args, invokeNew) {
      let strategy = argsStrategies.get(args);

      if (!strategy) {
        if (argsStrategies.any() && !baseStrategy.isConfigured()) {
          throw new Error(
            "Spy '" +
              strategyArgs.name +
              "' received a call with arguments " +
              j$.basicPrettyPrinter_(Array.prototype.slice.call(args)) +
              ' but all configured strategies specify other arguments.'
          );
        } else {
          strategy = baseStrategy;
        }
      }

      return strategy.exec(spy, args, invokeNew);
    };

    this.withArgs = function() {
      return { and: argsStrategies.getOrCreate(arguments) };
    };
  }

  function StrategyDict(strategyFactory, matchersUtil) {
    this.strategies = [];
    this.strategyFactory = strategyFactory;
    this.matchersUtil = matchersUtil;
  }

  StrategyDict.prototype.any = function() {
    return this.strategies.length > 0;
  };

  StrategyDict.prototype.getOrCreate = function(args) {
    let strategy = this.get(args);

    if (!strategy) {
      strategy = this.strategyFactory();
      this.strategies.push({
        args: args,
        strategy: strategy
      });
    }

    return strategy;
  };

  StrategyDict.prototype.get = function(args) {
    for (let i = 0; i < this.strategies.length; i++) {
      if (this.matchersUtil.equals(args, this.strategies[i].args)) {
        return this.strategies[i].strategy;
      }
    }
  };

  return Spy;
};
