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

goog.module('goog.labs.mockTest');
goog.setTestOnly('goog.labs.mockTest');

const TimeoutError = goog.require('goog.labs.mock.TimeoutError');
const VerificationError = goog.require('goog.labs.mock.VerificationError');
const array = goog.require('goog.array');
const mock = goog.require('goog.labs.mock');
const mockTimeout = goog.require('goog.labs.mock.timeout');
const string = goog.require('goog.string');
const testSuite = goog.require('goog.testing.testSuite');
/** @suppress {extraRequire} Declares globals */
goog.require('goog.labs.testing.AnythingMatcher');
/** @suppress {extraRequire} Declares globals */
goog.require('goog.labs.testing.GreaterThanMatcher');

const ParentClass = function() {};
ParentClass.prototype.method1 = function() {};
ParentClass.prototype.x = 1;
ParentClass.prototype.val = 0;
ParentClass.prototype.incrementVal = function() {
  this.val++;
};

const ChildClass = function() {};
goog.inherits(ChildClass, ParentClass);
ChildClass.prototype.method2 = function() {};
ChildClass.prototype.y = 2;

class ParentClassEs6 {
  /** Parent method */
  parent() {}
  /** Parent accessor descriptor method */
  get parentName() {
    fail('Descriptor get method should not be called.');
  }
  /** Parent accessor descriptor method */
  set parentName(value) {
    fail('Descriptor set method should not be called.');
  }
}

class ChildClassEs6 extends ParentClassEs6 {
  /** Child method */
  child() {}
}

/**
 * Asserts that the given string contains a list of others strings
 * in the given order.
 */
function assertContainsInOrder(str, var_args) {
  /** @suppress {checkTypes} suppression added to enable type checking */
  const expected = array.splice(arguments, 1);
  const indices = array.map(expected, function(val) {
    return str.indexOf(val);
  });

  for (let i = 0; i < expected.length; i++) {
    let msg = 'Missing "' + expected[i] + '" from "' + str + '"';
    assertTrue(msg, indices[i] != -1);

    if (i > 0) {
      msg = '"' + expected[i - 1] + '" should come before "' + expected[i] +
          '" in "' + str + '"';
      assertTrue(msg, indices[i] > indices[i - 1]);
    }
  }
}

/**
 * @param {!Object} obj
 * @param {string} propertyName
 */
function assertHasOwnProperty(obj, propertyName) {
  assertTrue(Object.prototype.hasOwnProperty.call(obj, propertyName));
}

testSuite({
  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testParentClass() {
    const parentMock = mock.mock(ParentClass);

    assertNotUndefined(parentMock.method1);
    assertUndefined(parentMock.method1());
    assertUndefined(parentMock.method2);
    assertNotUndefined(parentMock.x);
    assertUndefined(parentMock.y);
    assertTrue(
        'Mock should be an instance of the mocked class.',
        parentMock instanceof ParentClass);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testParentClassEs6() {
    const parentMock = mock.mock(ParentClassEs6);

    assertHasOwnProperty(parentMock, 'parent');
    assertUndefined(parentMock.parent());
    assertHasOwnProperty(parentMock, 'parentName');
    assertUndefined(parentMock.parentName);
    assertTrue(
        'Mock should be an instance of the mocked class.',
        parentMock instanceof ParentClassEs6);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testChildClass() {
    const childMock = mock.mock(ChildClass);

    assertNotUndefined(childMock.method1);
    assertUndefined(childMock.method1());
    assertNotUndefined(childMock.method2);
    assertUndefined(childMock.method2());
    assertNotUndefined(childMock.x);
    assertNotUndefined(childMock.y);
    assertTrue(
        'Mock should be an instance of the mocked class.',
        childMock instanceof ChildClass);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testChildClassEs6() {
    const childMock = mock.mock(ChildClassEs6);

    assertHasOwnProperty(childMock, 'parent');
    assertUndefined(childMock.parent());
    assertHasOwnProperty(childMock, 'child');
    assertUndefined(childMock.child());
    assertHasOwnProperty(childMock, 'parentName');
    assertUndefined(childMock.parentName);
    assertTrue(
        'Mock should be an instance of the mocked class.',
        childMock instanceof ChildClassEs6);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testParentClassInstance() {
    /** @suppress {checkTypes} suppression added to enable type checking */
    const parentMock = mock.mock(new ParentClass());

    assertNotUndefined(parentMock.method1);
    assertUndefined(parentMock.method1());
    assertUndefined(parentMock.method2);
    assertNotUndefined(parentMock.x);
    assertUndefined(parentMock.y);
    assertTrue(
        'Mock should be an instance of the mocked class.',
        parentMock instanceof ParentClass);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testParentClassEs6Instance() {
    const parentMock = mock.mock(new ParentClassEs6());

    assertNotUndefined(parentMock.parent);
    assertUndefined(parentMock.parent());
    assertTrue(
        'Mock should be an instance of the mocked class.',
        parentMock instanceof ParentClassEs6);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testChildClassInstance() {
    /** @suppress {checkTypes} suppression added to enable type checking */
    const childMock = mock.mock(new ChildClass());

    assertNotUndefined(childMock.method1);
    assertUndefined(childMock.method1());
    assertNotUndefined(childMock.method2);
    assertUndefined(childMock.method2());
    assertNotUndefined(childMock.x);
    assertNotUndefined(childMock.y);
    assertTrue(
        'Mock should be an instance of the mocked class.',
        childMock instanceof ChildClass);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testChildClassEs6Instance() {
    const childMock = mock.mock(new ChildClassEs6());

    assertNotUndefined(childMock.parent);
    assertUndefined(childMock.parent());
    assertNotUndefined(childMock.child);
    assertUndefined(childMock.child());
    assertTrue(
        'Mock should be an instance of the mocked class.',
        childMock instanceof ChildClassEs6);
  },

  testNonEnumerableProperties() {
    const mockObject = mock.mock({});
    assertNotUndefined(mockObject.toString);
    mock.when(mockObject).toString().then(function() {
      return 'toString';
    });
    assertEquals('toString', mockObject.toString());
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testBasicStubbing() {
    const obj = {
      method1: function(i) {
        return 2 * i;
      },
      method2: function(i, str) {
        return str;
      },
      method3: function(x) {
        return x;
      }
    };

    const mockObj = mock.mock(obj);
    mock.when(mockObj).method1(2).then(function(i) {
      return i;
    });

    assertEquals(4, obj.method1(2));
    assertEquals(2, mockObj.method1(2));
    assertUndefined(mockObj.method1(4));

    mock.when(mockObj).method2(1, 'hi').then(function(i) {
      return 'oh';
    });
    assertEquals('hi', obj.method2(1, 'hi'));
    assertEquals('oh', mockObj.method2(1, 'hi'));
    assertUndefined(mockObj.method2(3, 'foo'));

    mock.when(mockObj).method3(4).thenReturn(10);
    assertEquals(4, obj.method3(4));
    assertEquals(10, mockObj.method3(4));
    mock.verify(mockObj).method3(4);
    assertUndefined(mockObj.method3(5));
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testAccessorGetStubbing() {
    const obj = {
      get property() {
        fail('Original implementation should never be called.');
      },
    };

    const mockObj = mock.mock(obj);
    mock.when(mockObj).property.get().thenReturn('test');

    mock.verify(mockObj, mock.verification.times(0)).property.get();
    assertEquals('test', mockObj.property);
    mock.verify(mockObj, mock.verification.times(1)).property.get();
    // Set is not defined.
    assertThrows(() => {
      mockObj.property = 42;
    });
    assertThrows(() => {
      mock.when(mockObj).property.set().thenReturn('test');
    });
    assertThrows(() => {
      mock.verify(mockObj, mock.verification.times(0)).property.set();
    });
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testAccessorSetStubbing() {
    const obj = {
      set property(value) {
        fail('Original implementation should never be called.');
      },
    };

    const mockObj = mock.mock(obj);

    mock.verify(mockObj, mock.verification.times(0)).property.set(42);
    mockObj.property = 42;
    mock.verify(mockObj, mock.verification.times(1)).property.set(42);
    mock.verify(mockObj, mock.verification.times(0)).property.set(1);
    // Get is not defined.
    assertUndefined(mockObj.property);
    assertThrows(() => {
      mock.when(mockObj).property.get().thenReturn('test');
    });
    assertThrows(() => {
      mock.verify(mockObj, mock.verification.times(0)).property.get();
    });
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testAccessorGetAndSetStubbing() {
    const obj = {
      get property() {
        fail('Original implementation should never be called.');
      },
      set property(value) {
        fail('Original implementation should never be called.');
      },
    };

    const mockObj = mock.mock(obj);
    mock.when(mockObj).property.asDataProperty(/* initialValue= */ 1);

    assertEquals(1, mockObj.property);
    mockObj.property = 42;
    assertEquals(42, mockObj.property);

    mock.verify(mockObj, mock.verification.times(2)).property.get();
    mock.verify(mockObj, mock.verification.times(1)).property.set(42);
  },

  testMockFunctions() {
    function x(i) {
      return i;
    }

    const mockedFunc = mock.mockFunction(x);
    mock.when(mockedFunc)(100).thenReturn(10);
    mock.when(mockedFunc)(50).thenReturn(25);

    assertEquals(100, x(100));
    assertEquals(10, mockedFunc(100));
    assertEquals(25, mockedFunc(50));
  },

  testMockFunctionsWithNullableParameters() {
    const func = function(nullableObject) {
      return 0;
    };
    const mockedFunc = mock.mockFunction(func);
    mock.when(mockedFunc)(null).thenReturn(-1);

    assertEquals(0, func(null));
    assertEquals(-1, mockedFunc(null));
  },

  testMockConstructor() {
    const Ctor = function() {
      this.isMock = false;
    };
    const mockInstance = {isMock: true};
    const MockCtor = mock.mockConstructor(Ctor);
    mock.when(MockCtor)().thenReturn(mockInstance);
    assertEquals(mockInstance, new MockCtor());
  },

  /** @suppress {missingProperties} suppression added to enable type checking */
  testMockConstructorCopiesProperties() {
    const Ctor = function() {};
    Ctor.myParam = true;
    const MockCtor = mock.mockConstructor(Ctor);
    assertTrue(MockCtor.myParam);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testStubbingConsecutiveCalls() {
    const obj = {
      method: function(i) {
        return i * 42;
      }
    };

    const mockObj = mock.mock(obj);
    mock.when(mockObj).method(1).thenReturn(3).thenReturn(4);

    assertEquals(42, obj.method(1));
    assertEquals(3, mockObj.method(1));
    assertEquals(4, mockObj.method(1));
    assertEquals(4, mockObj.method(1));

    const x = function(i) {
      return i;
    };
    const mockedFunc = mock.mockFunction(x);
    mock.when(mockedFunc)(100).thenReturn(10).thenReturn(25);

    assertEquals(100, x(100));
    assertEquals(10, mockedFunc(100));
    assertEquals(25, mockedFunc(100));
    assertEquals(25, mockedFunc(100));
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testStubbingMultipleObjectStubsNonConflictingArgsAllShouldWork() {
    const obj = {
      method: function(i) {
        return i * 2;
      }
    };
    const mockObj = mock.mock(obj);

    mock.when(mockObj).method(2).thenReturn(100);
    mock.when(mockObj).method(5).thenReturn(45);

    assertEquals(100, mockObj.method(2));
    assertEquals(45, mockObj.method(5));
  },


  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testStubbingMultipleObjectStubsConflictingArgsMostRecentShouldPrevail() {
    const obj = {
      method: function(i) {
        return i * 2;
      }
    };
    const mockObj = mock.mock(obj);

    mock.when(mockObj).method(2).thenReturn(100);
    mock.when(mockObj).method(2).thenReturn(45);

    assertEquals(45, mockObj.method(2));
  },

  testStubbingMultipleFunctionStubsNonConflictingArgsAllShouldWork() {
    const x = function(i) {
      return i;
    };
    const mockedFunc = mock.mockFunction(x);

    mock.when(mockedFunc)(100).thenReturn(10);
    mock.when(mockedFunc)(10).thenReturn(132);

    assertEquals(10, mockedFunc(100));
    assertEquals(132, mockedFunc(10));
  },


  testStubbingMultipleFunctionStubsConflictingArgsMostRecentShouldPrevail() {
    const x = function(i) {
      return i;
    };
    const mockedFunc = mock.mockFunction(x);

    mock.when(mockedFunc)(100).thenReturn(10);
    mock.when(mockedFunc)(100).thenReturn(132);

    assertEquals(132, mockedFunc(100));
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testSpying() {
    const obj = {
      method1: function(i) {
        return 2 * i;
      },
      method2: function(i) {
        return 5 * i;
      }
    };

    const spyObj = mock.spy(obj);
    mock.when(spyObj).method1(2).thenReturn(5);

    assertEquals(2, obj.method1(1));
    assertEquals(5, spyObj.method1(2));
    mock.verify(spyObj).method1(2);
    assertEquals(2, spyObj.method1(1));
    mock.verify(spyObj).method1(1);
    assertEquals(20, spyObj.method2(4));
    mock.verify(spyObj).method2(4);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testSpyingSelfInteraction() {
    class A {
      method1() {
        this.method2();
      }
      method2() {}
    }
    const spyObj = mock.spy(new A());

    spyObj.method1();
    mock.verify(spyObj).method2();
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testSpyParentClassInstance() {
    /** @suppress {checkTypes} suppression added to enable type checking */
    const parent = new ParentClass();
    const parentMock = mock.spy(parent);

    assertNotUndefined(parentMock.method1);
    assertUndefined(parentMock.method1());
    assertUndefined(parentMock.method2);
    assertNotUndefined(parentMock.x);
    assertUndefined(parentMock.y);
    assertTrue(
        'Mock should be an instance of the mocked class.',
        parentMock instanceof ParentClass);
    const incrementedOrigVal = parent.val + 1;
    parentMock.incrementVal();
    assertEquals(
        'Changes in the spied object should reflect in the spy.',
        incrementedOrigVal, parentMock.val);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testSpyChildClassInstance() {
    /** @suppress {checkTypes} suppression added to enable type checking */
    const child = new ChildClass();
    const childMock = mock.spy(child);

    assertNotUndefined(childMock.method1);
    assertUndefined(childMock.method1());
    assertNotUndefined(childMock.method2);
    assertUndefined(childMock.method2());
    assertNotUndefined(childMock.x);
    assertNotUndefined(childMock.y);
    assertTrue(
        'Mock should be an instance of the mocked class.',
        childMock instanceof ParentClass);
    const incrementedOrigVal = child.val + 1;
    childMock.incrementVal();
    assertEquals(
        'Changes in the spied object should reflect in the spy.',
        incrementedOrigVal, childMock.val);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testVerifyForObjects() {
    const obj = {
      method1: function(i) {
        return 2 * i;
      },
      method2: function(i) {
        return 5 * i;
      }
    };

    const mockObj = mock.mock(obj);
    mock.when(mockObj).method1(2).thenReturn(5);

    assertEquals(5, mockObj.method1(2));
    mock.verify(mockObj).method1(2);
    const e = assertThrows(goog.partial(mock.verify(mockObj).method2, 2));
    assertTrue(e instanceof VerificationError);
  },

  testVerifyForFunctions() {
    const func = function(i) {
      return i;
    };

    const mockFunc = mock.mockFunction(func);
    mock.when(mockFunc)(2).thenReturn(55);
    assertEquals(55, mockFunc(2));
    mock.verify(mockFunc)(2);
    mock.verify(mockFunc)(lessThan(3));

    const e = assertThrows(goog.partial(mock.verify(mockFunc), 3));
    assertTrue(e instanceof VerificationError);
  },

  testVerifyForFunctionsWithNullableParameters() {
    const func = function(nullableObject) {};
    const mockFuncCalled = mock.mockFunction(func);
    const mockFuncNotCalled = mock.mockFunction(func);

    mockFuncCalled(null);

    mock.verify(mockFuncCalled)(null);
    const e = assertThrows(goog.partial(mock.verify(mockFuncNotCalled), null));
    assertTrue(e instanceof VerificationError);
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testVerifyPassesWhenVerificationModeReturnsTrue() {
    const trueMode = {
      verify: function(number) {
        return true;
      },
      describe: function() {
        return '';
      }
    };

    const mockObj = mock.mock({doThing: function() {}});

    mock.verify(mockObj, trueMode).doThing();
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testVerifyFailsWhenVerificationModeReturnsFalse() {
    const falseMode = {
      verify: function(number) {
        return false;
      },
      describe: function() {
        return '';
      }
    };
    const mockObj = mock.mock({doThing: function() {}});

    assertThrows(mock.verify(mockObj, falseMode).doThing);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testVerificationErrorMessagePutsVerificationModeInRightPlace() {
    const modeDescription = 'test';
    const mode = {
      verify: function(number) {
        return false;
      },
      describe: function() {
        return modeDescription;
      }
    };
    const mockObj = mock.mock({methodName: function() {}});
    mockObj.methodName(2);

    /** @suppress {checkTypes} suppression added to enable type checking */
    const e = assertThrows(mock.verify(mockObj, mode).methodName);
    // The mode description should be between the expected method
    // invocation and a newline.
    assertTrue(
        string.contains(e.message, 'methodName() ' + modeDescription + '\n'));
  },


  /**
   * When a function invocation verification fails, it should show the failed
   * expectation call, as well as the recorded calls to the same method.
   * @suppress {strictMissingProperties,checkTypes} suppression added to enable
   * type checking
   */
  testVerificationErrorMessages() {
    const mockObj = mock.mock({
      method: function(i) {
        return i;
      }
    });

    // Failure when there are no recorded calls.
    let e = assertThrows(function() {
      mock.verify(mockObj).method(4);
    });
    assertTrue(e instanceof VerificationError);
    let expected = '\nExpected: method(4) at least 1 times\n' +
        'Recorded: No recorded calls';
    assertEquals(expected, e.message);


    // Failure when there are recorded calls with ints and functions
    // as arguments.
    const callback = function() {};
    const callbackId = mock.getUid(callback);

    mockObj.method(1);
    mockObj.method(2);
    mockObj.method(callback);

    e = assertThrows(function() {
      mock.verify(mockObj).method(3);
    });
    assertTrue(e instanceof VerificationError);

    expected = '\nExpected: method(3) at least 1 times\n' +
        'Recorded: method(1),\n' +
        '          method(2),\n' +
        '          method(<function #anonymous' + callbackId + '>)';
    assertEquals(expected, e.message);

    // With mockFunctions
    const mockCallback = mock.mockFunction(callback);
    e = assertThrows(function() {
      mock.verify(mockCallback)(5);
    });
    expected = '\nExpected: #mockFor<#anonymous' + callbackId +
        '>(5) at least' +
        ' 1 times\n' +
        'Recorded: No recorded calls';

    mockCallback(8);
    mock.verify(mockCallback)(8);
    assertEquals(expected, e.message);

    // Objects with circular references should not fail.
    const obj = {x: 1};
    obj.y = obj;

    mockCallback(obj);
    e = assertThrows(function() {
      mock.verify(mockCallback)(5);
    });
    assertTrue(e instanceof VerificationError);

    // Should respect string representation of different custom classes.
    const myClass = function() {};
    myClass.prototype.toString = function() {
      return '<superClass>';
    };

    const mockFunction = mock.mockFunction(function f() {});
    mockFunction(new myClass());

    e = assertThrows(function() {
      mock.verify(mockFunction)(5);
    });
    expected = '\nExpected: #mockFor<f>(5) at least 1 times\n' +
        'Recorded: #mockFor<f>(<superClass>)';
    assertEquals(expected, e.message);
  },

  async testWait() {
    const mockParent = mock.mock(ParentClass);

    setTimeout(/**
                  @suppress {strictMissingProperties} suppression added to
                  enable type checking
                */
               () => {
                 mockParent.method1();
               },
               0);

    await mock.waitAndVerify(mockParent).method1();
  },

  async testMockFunctionWait() {
    const mockFunc = mock.mockFunction();

    setTimeout(() => {
      mockFunc();
    }, 0);

    await mock.waitAndVerify(mockFunc)();
  },

  async testWaitOnMultipleMethodCalls() {
    const mockParent = mock.mock(ParentClass);
    const timeoutMode = mockTimeout.timeout(150);
    const verificationMode = mock.verification.times(2);

    setTimeout(/**
                  @suppress {strictMissingProperties} suppression added to
                  enable type checking
                */
               () => {
                 mockParent.method1();
               },
               0);
    setTimeout(/**
                  @suppress {strictMissingProperties} suppression added to
                  enable type checking
                */
               () => {
                 mockParent.method1();
               },
               0);

    await mock.waitAndVerify(mockParent, verificationMode, timeoutMode)
        .method1();
  },

  async testMockFunctionWaitOnMultipleMethodCalls() {
    const mockFunc = mock.mockFunction();
    const timeoutMode = mockTimeout.timeout(150);
    const verificationMode = mock.verification.times(2);

    setTimeout(() => {
      mockFunc();
    }, 0);
    setTimeout(() => {
      mockFunc();
    }, 0);

    await mock.waitAndVerify(mockFunc, verificationMode, timeoutMode)();
  },

  async testWaitOnDifferentFunctions() {
    const mockParent = mock.mock(ParentClass);

    setTimeout(/**
                  @suppress {strictMissingProperties} suppression added to
                  enable type checking
                */
               () => {
                 mockParent.incrementVal();
               },
               0);

    setTimeout(/**
                  @suppress {strictMissingProperties} suppression added to
                  enable type checking
                */
               () => {
                 mockParent.method1();
               },
               0);

    await mock.waitAndVerify(mockParent).method1();
    await mock.waitAndVerify(mockParent).incrementVal();
  },

  async testWaitOnSameFunctionWithDifferentArgs() {
    const mockParent = mock.mock(ParentClass);

    setTimeout(/**
                  @suppress {strictMissingProperties} suppression added to
                  enable type checking
                */
               () => {
                 mockParent.method1(1);
               },
               0);

    setTimeout(/**
                  @suppress {strictMissingProperties} suppression added to
                  enable type checking
                */
               () => {
                 mockParent.method1(2);
               },
               0);

    await mock.waitAndVerify(mockParent).method1(2);
    await mock.waitAndVerify(mockParent).method1(1);
  },

  async testMockFunctionWaitWithDifferentArgs() {
    const mockFunc = mock.mockFunction();

    setTimeout(() => {
      mockFunc(1);
    }, 0);

    setTimeout(() => {
      mockFunc(2);
    }, 0);

    await mock.waitAndVerify(mockFunc)(2);
    await mock.waitAndVerify(mockFunc)(1);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  async testWaitWithTimeoutMode() {
    const mockParent = mock.mock(ParentClass);
    const timeoutMode = mockTimeout.timeout(1);

    setTimeout(/**
                  @suppress {strictMissingProperties} suppression added to
                  enable type checking
                */
               () => {
                 mockParent.method1();
               },
               50);

    const e = await assertRejects(
        mock.waitAndVerify(mockParent, timeoutMode).method1());
    assertTrue(e instanceof TimeoutError);
    assertEquals(
        e.message,
        'Function call was either not invoked or never met criteria specified ' +
            'by provided verification mode. \n' +
            'Expected: method1() at least 1 times\n' +
            'Recorded: No recorded calls');
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  async testMockFunctionWaitWithTimeoutMode() {
    const func = function() {};
    const funcId = mock.getUid(func);
    const mockFunc = mock.mockFunction(func);
    const timeoutMode = mockTimeout.timeout(1);

    setTimeout(() => {
      mockFunc();
    }, 50);

    const e = await assertRejects(mock.waitAndVerify(mockFunc, timeoutMode)());
    assertTrue(e instanceof TimeoutError);
    assertEquals(
        e.message,
        'Function call was either not invoked or never met criteria specified ' +
            'by provided verification mode. \n' +
            'Expected: #mockFor<#anonymous' + funcId +
            '>() at least 1 times\n' +
            'Recorded: No recorded calls');
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  async testWaitWithVerificationMode() {
    const mockParent = mock.mock(ParentClass);
    const verificationMode = mock.verification.times(2);

    mockParent.method1();

    const e = await assertRejects(
        mock.waitAndVerify(mockParent, verificationMode).method1());
    assertEquals(
        e.message,
        'Function call was either not invoked or never met criteria specified ' +
            'by provided verification mode. \n' +
            'Expected: method1() 2 times\n' +
            'Recorded: method1()');
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  async testMockFunctionWaitWithVerificationMode() {
    const func = function() {};
    const funcId = mock.getUid(func);
    const mockFunc = mock.mockFunction(func);
    const verificationMode = mock.verification.times(2);

    mockFunc();

    const e =
        await assertRejects(mock.waitAndVerify(mockFunc, verificationMode)());
    assertEquals(
        e.message,
        'Function call was either not invoked or never met criteria specified ' +
            'by provided verification mode. \n' +
            'Expected: #mockFor<#anonymous' + funcId + '>() 2 times\n' +
            'Recorded: #mockFor<#anonymous' + funcId + '>()');
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  async testWaitOnSameMethodTwice() {
    const mockParent = mock.mock(ParentClass);

    mockParent.method1();

    await mock.waitAndVerify(mockParent).method1();
    await mock.waitAndVerify(mockParent).method1();
  },

  async testMockFunctionWaitOnSameMethodTwice() {
    const mockFunc = mock.mockFunction();

    mockFunc();

    await mock.waitAndVerify(mockFunc)();
    await mock.waitAndVerify(mockFunc)();
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  async testWaitWithTimeoutAndVerificationMode() {
    const mockParent = mock.mock(ParentClass);
    const timeoutMode = mockTimeout.timeout(150);
    const verificationMode = mock.verification.times(2);

    setTimeout(/**
                  @suppress {strictMissingProperties} suppression added to
                  enable type checking
                */
               () => {
                 mockParent.method1();
               },
               50);

    setTimeout(/**
                  @suppress {strictMissingProperties} suppression added to
                  enable type checking
                */
               () => {
                 mockParent.method1();
               },
               250);

    const e = await assertRejects(
        mock.waitAndVerify(mockParent, timeoutMode, verificationMode)
            .method1());
    assertTrue(e instanceof TimeoutError);
    assertEquals(
        e.message,
        'Function call was either not invoked or never met criteria specified ' +
            'by provided verification mode. \n' +
            'Expected: method1() 2 times\n' +
            'Recorded: method1()');
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  async testMockFunctionWaitWithTimeoutAndVerificationMode() {
    const func = function() {};
    const funcId = mock.getUid(func);
    const mockFunc = mock.mockFunction(func);
    const timeoutMode = mockTimeout.timeout(150);
    const verificationMode = mock.verification.times(2);

    setTimeout(() => {
      mockFunc();
    }, 50);

    setTimeout(() => {
      mockFunc();
    }, 250);

    const e = await assertRejects(
        mock.waitAndVerify(mockFunc, timeoutMode, verificationMode)());
    assertTrue(e instanceof TimeoutError);
    assertEquals(
        e.message,
        'Function call was either not invoked or never met criteria specified ' +
            'by provided verification mode. \n' +
            'Expected: #mockFor<#anonymous' + funcId + '>() 2 times\n' +
            'Recorded: #mockFor<#anonymous' + funcId + '>()');
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  async testPassingVerificationModeBeforeTimeoutMode() {
    const mockParent = mock.mock(ParentClass);
    const timeoutMode = mockTimeout.timeout(150);
    const verificationMode = mock.verification.times(2);

    setTimeout(/**
                  @suppress {strictMissingProperties} suppression added to
                  enable type checking
                */
               () => {
                 mockParent.method1();
               },
               50);

    setTimeout(/**
                  @suppress {strictMissingProperties} suppression added to
                  enable type checking
                */
               () => {
                 mockParent.method1();
               },
               250);

    const e = await assertRejects(
        mock.waitAndVerify(mockParent, verificationMode, timeoutMode)
            .method1());
    assertTrue(e instanceof TimeoutError);
    assertEquals(
        e.message,
        'Function call was either not invoked or never met criteria specified ' +
            'by provided verification mode. \n' +
            'Expected: method1() 2 times\n' +
            'Recorded: method1()');
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  async testMockFunctionPassingVerificationModeBeforeTimeoutMode() {
    const func = function() {};
    const funcId = mock.getUid(func);
    const mockFunc = mock.mockFunction(func);
    const timeoutMode = mockTimeout.timeout(150);
    const verificationMode = mock.verification.times(2);

    setTimeout(() => {
      mockFunc();
    }, 50);

    setTimeout(() => {
      mockFunc();
    }, 250);

    const e = await assertRejects(
        mock.waitAndVerify(mockFunc, verificationMode, timeoutMode)());
    assertTrue(e instanceof TimeoutError);
    assertEquals(
        e.message,
        'Function call was either not invoked or never met criteria specified ' +
            'by provided verification mode. \n' +
            'Expected: #mockFor<#anonymous' + funcId + '>() 2 times\n' +
            'Recorded: #mockFor<#anonymous' + funcId + '>()');
  },


  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testMatchers() {
    const obj = {
      method1: function(i) {
        return 2 * i;
      },
      method2: function(i) {
        return 5 * i;
      }
    };

    const mockObj = mock.mock(obj);

    mock.when(mockObj).method1(greaterThan(4)).thenReturn(100);
    mock.when(mockObj).method1(lessThan(4)).thenReturn(40);

    assertEquals(100, mockObj.method1(5));
    assertEquals(100, mockObj.method1(6));
    assertEquals(40, mockObj.method1(2));
    assertEquals(40, mockObj.method1(1));
    assertUndefined(mockObj.method1(4));
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testMatcherVerify() {
    const obj = {
      method: function(i) {
        return 2 * i;
      }
    };

    // Using spy objects.
    const spy = mock.spy(obj);

    spy.method(6);

    mock.verify(spy).method(greaterThan(4));
    let e = assertThrows(goog.partial(mock.verify(spy).method, lessThan(4)));
    assertTrue(e instanceof VerificationError);

    // Using mocks
    const mockObj = mock.mock(obj);

    mockObj.method(8);

    mock.verify(mockObj).method(greaterThan(7));
    e = assertThrows(goog.partial(mock.verify(mockObj).method, lessThan(7)));
    assertTrue(e instanceof mock.VerificationError);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testMatcherVerifyCollision() {
    const obj = {
      method: function(i) {
        return 2 * i;
      }
    };
    const mockObj = mock.mock(obj);

    mock.when(mockObj).method(5).thenReturn(100);
    assertNotEquals(100, mockObj.method(greaterThan(2)));
  },

  testMatcherVerifyCollisionBetweenMatchers() {
    const obj = {
      method: function(i) {
        return 2 * i;
      }
    };
    const mockObj = mock.mock(obj);

    mock.when(mockObj).method(anything()).thenReturn(100);

    const e =
        assertThrows(goog.partial(mock.verify(mockObj).method, anything()));
    assertTrue(e instanceof VerificationError);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testVerifyForUnmockedMethods() {
    const Task = function() {};
    Task.prototype.run = function() {};

    const mockTask = mock.mock(Task);
    mockTask.run();

    mock.verify(mockTask).run();
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testFormatMethodCall() {
    /** @suppress {visibility} suppression added to enable type checking */
    const formatMethodCall = mock.formatMethodCall_;
    assertEquals('alert()', formatMethodCall('alert'));
    assertEquals('sum(2, 4)', formatMethodCall('sum', [2, 4]));
    assertEquals('sum("2", "4")', formatMethodCall('sum', ['2', '4']));
    assertEquals(
        'call(<function unicorn>)',
        formatMethodCall('call', [function unicorn() {}]));

    const arg = {x: 1, y: {hello: 'world'}};
    assertEquals(
        'call(' + mock.formatValue_(arg) + ')',
        formatMethodCall('call', [arg]));
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testGetFunctionName() {
    const f1 = function() {};
    const f2 = function() {};
    const named = function myName() {};

    assert(string.startsWith(mock.getFunctionName_(f1), '#anonymous'));
    assert(string.startsWith(mock.getFunctionName_(f2), '#anonymous'));
    assertNotEquals(mock.getFunctionName_(f1), mock.getFunctionName_(f2));
    assertEquals('myName', mock.getFunctionName_(named));
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testFormatObject() {
    let obj;
    let obj2;
    let obj3;

    obj = {x: 1};
    assertEquals(
        '{"x":1 _id:' + mock.getUid(obj) + '}', mock.formatValue_(obj));
    assertEquals('{"x":1}', mock.formatValue_(obj, false /* id */));

    obj = {x: 'hello'};
    assertEquals(
        '{"x":"hello" _id:' + mock.getUid(obj) + '}', mock.formatValue_(obj));
    assertEquals('{"x":"hello"}', mock.formatValue_(obj, false /* id */));

    obj3 = {};
    obj2 = {y: obj3};
    obj3.x = obj2;
    assertEquals(
        '{"x":{"y":<recursive/dupe obj_' + mock.getUid(obj3) + '> ' +
            '_id:' + mock.getUid(obj2) + '} ' +
            '_id:' + mock.getUid(obj3) + '}',
        mock.formatValue_(obj3));
    assertEquals(
        '{"x":{"y":<recursive/dupe>}}',
        mock.formatValue_(obj3, false /* id */));


    obj = {x: function y() {}};
    assertEquals(
        '{"x":<function y> _id:' + mock.getUid(obj) + '}',
        mock.formatValue_(obj));
    assertEquals('{"x":<function y>}', mock.formatValue_(obj, false /* id */));
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testFormatSymbols() {
    if (typeof window['Symbol'] !== 'function') {
      return;  // skip browsers without symbols
    }
    if (Symbol().toString() !== 'Symbol()') {
      // skip testing in environments with a likely-polyfilled symbol,
      // as their Symbol wouldn't have the right typeof value.
      return;
    }
    assertEquals('Symbol()', mock.formatValue_(Symbol()));
    assertEquals('Symbol(foobar)', mock.formatValue_(Symbol('foobar')));
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testFormatBigInts() {
    if (typeof window['BigInt'] !== 'function') {
      return;  // skip browsers without bigints
    }
    assertEquals('30n', mock.formatValue_(BigInt(30)));
  },

  testGetUid() {
    const obj1 = {};
    const obj2 = {};
    const func1 = function() {};
    const func2 = function() {};

    assertNotEquals(mock.getUid(obj1), mock.getUid(obj2));
    assertNotEquals(mock.getUid(func1), mock.getUid(func2));
    assertNotEquals(mock.getUid(obj1), mock.getUid(func2));
    assertEquals(mock.getUid(obj1), mock.getUid(obj1));
    assertEquals(mock.getUid(func1), mock.getUid(func1));
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testMockEs6ClassMethods() {
    const Foo = class {
      a() {
        fail('real object should never be called');
      }
    };

    const mockObj = mock.mock(Foo);
    mock.when(mockObj).a().thenReturn('a');
    assertThrowsJsUnitException(function() {
      new Foo().a();
    });
    assertEquals('a', mockObj.a());
    mock.verify(mockObj).a();
  },

});
