Source: Expectation.js

'use strict';

let ExpectedCall = require('./ExpectedCall.js');
let Tree = require('./Tree/Tree.js');
let ExpectedCallNode = require('./Tree/ExpectedCallNode.js');
let Callback = require('./Callback.js');

/**
 * This is a transitional object that helps convert {@link Mock}s into
 * {@link ExpectedCall}s and build the resulting execution {@link Tree.Tree}.
 */
class Expectation {
  /**
   * Creates a new {@link Expectation}
   * @param {Mock} mock Mock that is being expected.
   * @param {boolean} required If true then mock is required to be called; otherwise it is optional and may be skipped during execution.
   */
  constructor(mock, required) {
    this._expectedCall = new ExpectedCall(mock, [], required, true);
    this._tree = new Tree(new ExpectedCallNode(this._expectedCall));
  }

  /**
   * Updates this expectatations {@link ExpectedCall} to have the specified arguments when it is called.
   * @param {object[]} arguments Required arguments for the {@link Mock}s call.
   * @returns {Expectation} This expectation, which allows chaining.
   */
  withTheseArguments() {
    this._expectedCall.expectedArgs = Array.from(arguments);

    return this;
  }

  /**
   * Updates this expectatations {@link ExpectedCall} to allow any arguments when it is called.
   * @returns {Expectation} This expectation, which allows chaining.
   */
  withAnyArguments() {
    this._expectedCall.checkArgs = false;

    return this;
  }

  /**
   * Updates this expectatations {@link ExpectedCall} to return the specified value.
   * @param {object} returnValue Value that will be returned when the expected call is executed.
   * @returns {Expectation} This expectation, which allows chaining.
   */
  andWillReturn(returnValue) {
    this._expectedCall.returnValue = returnValue;

    return this;
  }

  /**
   * Updates this expectatations {@link ExpectedCall} to throw the specified error.
   * @param {Error} error Error that will be thrown when the expected call is executed.
   * @returns {Expectation} This expectation, which allows chaining.
   */
  andWillThrow(error) {
    this._expectedCall.throwValue = error;

    return this;
  }

  /**
   * Updates this expectations {@link ExpectedCall} to invoke a callback.
   * @param  {object[]} [...args] Arguments that will be passed to the callback when it is invoked.
   * @returns {Expectation} This expectation, which allows chaining.
   */
  andWillCallback(...args) {
    if(this._expectedCall.expectedArgs.length === 0) {
      throw new Error('expectation has no arguments to callback');
    }

    let callbackIndex = -1;

    let i = 0;
    for(let argument of this._expectedCall.expectedArgs) {
      if(argument instanceof Callback) {
        callbackIndex = i;
        break;
      }
      i++;
    }

    if(callbackIndex === -1) {
      throw new Error('expectation has no callback argument');
    }

    this._expectedCall.callbackIndex = callbackIndex;
    this._expectedCall.callbackArgs = args;

    return this;
  }

  /**
   * Combines this expecation with the specifed expectation using `AND`.
   * This means execution order does not matter and these expecations can be executed in either order.
   * @param {Expectation} expectation Expectation to combine with this expecatation.
   * @returns {Expectation} This expectation, which allows chaining.
   */
  and(expectation) {
    this._tree.and(expectation._tree);

    expectation._tree = this._tree;

    return this;
  }

  /**
   * Alias for {@link Expectation#and}
   */
  andAlso(expectation) {
    return this.and(expectation);
  }

  /**
   * Combines this expecation with the specifed expectation using `THEN`.
   * This means execution order matters and this expectation must come before the other.
   * @param {Expectation} expectation Expectation to combine with this expecatation.
   * @returns {Expectation} This expectation, which allows chaining.
   */
  then(expectation) {
    this._tree.then(expectation._tree);

    expectation._tree = this._tree;

    return this;
  }

  /**
   * Alias for {@link Expectation#then}
   */
  andThen(expectation) {
    return this.then(expectation);
  }

  /**
   * Clones this expectation so that is is expected the specified number of times.
   * @param {number} count Number of times this expecation should be expected.
   * @returns {Expectation} This expectation, which allows chaining.
   */
  multipleTimes(count) {
    for(var i = 0; i < count - 1; i++) {
      let expectation = new Expectation(this._expectedCall.mock, this._expectedCall.required);

      expectation._expectedCall.expectedArgs = this._expectedCall.expectedArgs;
      expectation._expectedCall.checkArgs = this._expectedCall.checkArgs;
      expectation._expectedCall.returnValue = this._expectedCall.returnValue;

      this.and(expectation);
    }

    return this;
  }

  /**
   * Makes it so that unexpected calls and out of order calls are ignored and only required calls are checked during execution.
   * @returns {Expectation} This expectation, which allows chaining.
   */
  andOtherCallsShouldBeIgnored() {
    this._tree.ignoreOtherCalls();

    return this;
  }

  /**
   * Alias for {@link Expectation#andOtherCallsShouldBeIgnored}
   */
  withOtherCallsIgnored() {
    return this.andOtherCallsShouldBeIgnored();
  }

  /**
   * Executes the test code and verifies the expectations that were built up.
   * @param {function} thunk Test code.
   */
  when(thunk) {
    return this._tree.execute(thunk);
  }

  /**
   * Alias for {@link Expectation#when}
   */
  after(thunk) {
    return this.when(thunk);
  }
}

module.exports = Expectation;