'use strict';
const common = require('../common');
const assert = require('node:assert');
const { mock, test } = require('node:test');
test('spies on a function', (t) => {
  const sum = t.mock.fn((arg1, arg2) => {
    return arg1 + arg2;
  });

  assert.strictEqual(sum.mock.calls.length, 0);
  assert.strictEqual(sum(3, 4), 7);
  assert.strictEqual(sum.call(1000, 9, 1), 10);
  assert.strictEqual(sum.mock.calls.length, 2);

  let call = sum.mock.calls[0];
  assert.deepStrictEqual(call.arguments, [3, 4]);
  assert.strictEqual(call.error, undefined);
  assert.strictEqual(call.result, 7);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, undefined);

  call = sum.mock.calls[1];
  assert.deepStrictEqual(call.arguments, [9, 1]);
  assert.strictEqual(call.error, undefined);
  assert.strictEqual(call.result, 10);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, 1000);
});

test('spies on a bound function', (t) => {
  const bound = function(arg1, arg2) {
    return this + arg1 + arg2;
  }.bind(50);
  const sum = t.mock.fn(bound);

  assert.strictEqual(sum.mock.calls.length, 0);
  assert.strictEqual(sum(3, 4), 57);
  assert.strictEqual(sum(9, 1), 60);
  assert.strictEqual(sum.mock.calls.length, 2);

  let call = sum.mock.calls[0];
  assert.deepStrictEqual(call.arguments, [3, 4]);
  assert.strictEqual(call.result, 57);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, undefined);

  call = sum.mock.calls[1];
  assert.deepStrictEqual(call.arguments, [9, 1]);
  assert.strictEqual(call.result, 60);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, undefined);
});

test('spies on a constructor', (t) => {
  class ParentClazz {
    constructor(c) {
      this.c = c;
    }
  }

  class Clazz extends ParentClazz {
    #privateValue;

    constructor(a, b) {
      super(a + b);
      this.a = a;
      this.#privateValue = b;
    }

    getPrivateValue() {
      return this.#privateValue;
    }
  }

  const ctor = t.mock.fn(Clazz);
  const instance = new ctor(42, 85);

  assert(instance instanceof Clazz);
  assert(instance instanceof ParentClazz);
  assert.strictEqual(instance.a, 42);
  assert.strictEqual(instance.getPrivateValue(), 85);
  assert.strictEqual(instance.c, 127);
  assert.strictEqual(ctor.mock.calls.length, 1);

  const call = ctor.mock.calls[0];

  assert.deepStrictEqual(call.arguments, [42, 85]);
  assert.strictEqual(call.error, undefined);
  assert.strictEqual(call.result, instance);
  assert.strictEqual(call.target, Clazz);
  assert.strictEqual(call.this, instance);
});

test('a no-op spy function is created by default', (t) => {
  const fn = t.mock.fn();

  assert.strictEqual(fn.mock.calls.length, 0);
  assert.strictEqual(fn(3, 4), undefined);
  assert.strictEqual(fn.mock.calls.length, 1);

  const call = fn.mock.calls[0];
  assert.deepStrictEqual(call.arguments, [3, 4]);
  assert.strictEqual(call.result, undefined);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, undefined);
});

test('internal no-op function can be reused', (t) => {
  const fn1 = t.mock.fn();
  fn1.prop = true;
  const fn2 = t.mock.fn();

  fn1(1);
  fn2(2);
  fn1(3);

  assert.notStrictEqual(fn1.mock, fn2.mock);
  assert.strictEqual(fn1.mock.calls.length, 2);
  assert.strictEqual(fn2.mock.calls.length, 1);
  assert.strictEqual(fn1.prop, true);
  assert.strictEqual(fn2.prop, undefined);
});

test('functions can be mocked multiple times at once', (t) => {
  function sum(a, b) {
    return a + b;
  }

  function difference(a, b) {
    return a - b;
  }

  function product(a, b) {
    return a * b;
  }

  const fn1 = t.mock.fn(sum, difference);
  const fn2 = t.mock.fn(sum, product);

  assert.strictEqual(fn1(5, 3), 2);
  assert.strictEqual(fn2(5, 3), 15);
  assert.strictEqual(fn2(4, 2), 8);
  assert(!('mock' in sum));
  assert(!('mock' in difference));
  assert(!('mock' in product));
  assert.notStrictEqual(fn1.mock, fn2.mock);
  assert.strictEqual(fn1.mock.calls.length, 1);
  assert.strictEqual(fn2.mock.calls.length, 2);
});

test('internal no-op function can be reused as methods', (t) => {
  const obj = {
    _foo: 5,
    _bar: 9,
    foo() {
      return this._foo;
    },
    bar() {
      return this._bar;
    },
  };

  t.mock.method(obj, 'foo');
  obj.foo.prop = true;
  t.mock.method(obj, 'bar');
  assert.strictEqual(obj.foo(), 5);
  assert.strictEqual(obj.bar(), 9);
  assert.strictEqual(obj.bar(), 9);
  assert.notStrictEqual(obj.foo.mock, obj.bar.mock);
  assert.strictEqual(obj.foo.mock.calls.length, 1);
  assert.strictEqual(obj.bar.mock.calls.length, 2);
  assert.strictEqual(obj.foo.prop, true);
  assert.strictEqual(obj.bar.prop, undefined);
});

test('methods can be mocked multiple times but not at the same time', (t) => {
  const obj = {
    offset: 3,
    sum(a, b) {
      return this.offset + a + b;
    },
  };

  function difference(a, b) {
    return this.offset + (a - b);
  }

  function product(a, b) {
    return this.offset + a * b;
  }

  const originalSum = obj.sum;
  const fn1 = t.mock.method(obj, 'sum', difference);

  assert.strictEqual(obj.sum(5, 3), 5);
  assert.strictEqual(obj.sum(5, 1), 7);
  assert.strictEqual(obj.sum, fn1);
  assert.notStrictEqual(fn1.mock, undefined);
  assert.strictEqual(originalSum.mock, undefined);
  assert.strictEqual(difference.mock, undefined);
  assert.strictEqual(product.mock, undefined);
  assert.strictEqual(fn1.mock.calls.length, 2);

  const fn2 = t.mock.method(obj, 'sum', product);

  assert.strictEqual(obj.sum(5, 3), 18);
  assert.strictEqual(obj.sum, fn2);
  assert.notStrictEqual(fn1, fn2);
  assert.strictEqual(fn2.mock.calls.length, 1);

  obj.sum.mock.restore();
  assert.strictEqual(obj.sum, fn1);
  obj.sum.mock.restore();
  assert.strictEqual(obj.sum, originalSum);
  assert.strictEqual(obj.sum.mock, undefined);
});

test('spies on an object method', (t) => {
  const obj = {
    prop: 5,
    method(a, b) {
      return a + b + this.prop;
    },
  };

  assert.strictEqual(obj.method(1, 3), 9);
  t.mock.method(obj, 'method');
  assert.strictEqual(obj.method.mock.calls.length, 0);
  assert.strictEqual(obj.method(1, 3), 9);

  const call = obj.method.mock.calls[0];

  assert.deepStrictEqual(call.arguments, [1, 3]);
  assert.strictEqual(call.result, 9);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, obj);

  assert.strictEqual(obj.method.mock.restore(), undefined);
  assert.strictEqual(obj.method(1, 3), 9);
  assert.strictEqual(obj.method.mock, undefined);
});

test('spies on a getter', (t) => {
  const obj = {
    prop: 5,
    get method() {
      return this.prop;
    },
  };

  assert.strictEqual(obj.method, 5);

  const getter = t.mock.method(obj, 'method', { getter: true });

  assert.strictEqual(getter.mock.calls.length, 0);
  assert.strictEqual(obj.method, 5);

  const call = getter.mock.calls[0];

  assert.deepStrictEqual(call.arguments, []);
  assert.strictEqual(call.result, 5);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, obj);

  assert.strictEqual(getter.mock.restore(), undefined);
  assert.strictEqual(obj.method, 5);
});

test('spies on a setter', (t) => {
  const obj = {
    prop: 100,
    // eslint-disable-next-line accessor-pairs
    set method(val) {
      this.prop = val;
    },
  };

  assert.strictEqual(obj.prop, 100);
  obj.method = 88;
  assert.strictEqual(obj.prop, 88);

  const setter = t.mock.method(obj, 'method', { setter: true });

  assert.strictEqual(setter.mock.calls.length, 0);
  obj.method = 77;
  assert.strictEqual(obj.prop, 77);
  assert.strictEqual(setter.mock.calls.length, 1);

  const call = setter.mock.calls[0];

  assert.deepStrictEqual(call.arguments, [77]);
  assert.strictEqual(call.result, undefined);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, obj);

  assert.strictEqual(setter.mock.restore(), undefined);
  assert.strictEqual(obj.prop, 77);
  obj.method = 65;
  assert.strictEqual(obj.prop, 65);
});

test('spy functions can be bound', (t) => {
  const sum = t.mock.fn(function(arg1, arg2) {
    return this + arg1 + arg2;
  });
  const bound = sum.bind(1000);

  assert.strictEqual(bound(9, 1), 1010);
  assert.strictEqual(sum.mock.calls.length, 1);

  const call = sum.mock.calls[0];
  assert.deepStrictEqual(call.arguments, [9, 1]);
  assert.strictEqual(call.result, 1010);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, 1000);

  assert.strictEqual(sum.mock.restore(), undefined);
  assert.strictEqual(sum.bind(0)(2, 11), 13);
});

test('mocks prototype methods on an instance', async (t) => {
  class Runner {
    async someTask(msg) {
      return Promise.resolve(msg);
    }

    async method(msg) {
      await this.someTask(msg);
      return msg;
    }
  }
  const msg = 'ok';
  const obj = new Runner();
  assert.strictEqual(await obj.method(msg), msg);

  t.mock.method(obj, obj.someTask.name);
  assert.strictEqual(obj.someTask.mock.calls.length, 0);

  assert.strictEqual(await obj.method(msg), msg);

  const call = obj.someTask.mock.calls[0];

  assert.deepStrictEqual(call.arguments, [msg]);
  assert.strictEqual(await call.result, msg);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, obj);

  const obj2 = new Runner();
  // Ensure that a brand new instance is not mocked
  assert.strictEqual(
    obj2.someTask.mock,
    undefined
  );

  assert.strictEqual(obj.someTask.mock.restore(), undefined);
  assert.strictEqual(await obj.method(msg), msg);
  assert.strictEqual(obj.someTask.mock, undefined);
  assert.strictEqual(Runner.prototype.someTask.mock, undefined);
});

test('spies on async static class methods', async (t) => {
  class Runner {
    static async someTask(msg) {
      return Promise.resolve(msg);
    }

    static async method(msg) {
      await this.someTask(msg);
      return msg;
    }
  }
  const msg = 'ok';
  assert.strictEqual(await Runner.method(msg), msg);

  t.mock.method(Runner, Runner.someTask.name);
  assert.strictEqual(Runner.someTask.mock.calls.length, 0);

  assert.strictEqual(await Runner.method(msg), msg);

  const call = Runner.someTask.mock.calls[0];

  assert.deepStrictEqual(call.arguments, [msg]);
  assert.strictEqual(await call.result, msg);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, Runner);

  assert.strictEqual(Runner.someTask.mock.restore(), undefined);
  assert.strictEqual(await Runner.method(msg), msg);
  assert.strictEqual(Runner.someTask.mock, undefined);
  assert.strictEqual(Runner.prototype.someTask, undefined);

});

test('given null to a mock.method it throws an invalid argument error', (t) => {
  assert.throws(() => t.mock.method(null, {}), { code: 'ERR_INVALID_ARG_TYPE' });
});

test('it should throw given an inexistent property on a object instance', (t) => {
  assert.throws(() => t.mock.method({ abc: 0 }, 'non-existent'), {
    code: 'ERR_INVALID_ARG_VALUE'
  });
});

test('spy functions can be used on classes inheritance', (t) => {
  // Makes sure that having a null-prototype doesn't throw our system off
  class A extends null {
    static someTask(msg) {
      return msg;
    }
    static method(msg) {
      return this.someTask(msg);
    }
  }
  class B extends A {}
  class C extends B {}

  const msg = 'ok';
  assert.strictEqual(C.method(msg), msg);

  t.mock.method(C, C.someTask.name);
  assert.strictEqual(C.someTask.mock.calls.length, 0);

  assert.strictEqual(C.method(msg), msg);

  const call = C.someTask.mock.calls[0];

  assert.deepStrictEqual(call.arguments, [msg]);
  assert.strictEqual(call.result, msg);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, C);

  assert.strictEqual(C.someTask.mock.restore(), undefined);
  assert.strictEqual(C.method(msg), msg);
  assert.strictEqual(C.someTask.mock, undefined);
});

test('spy functions don\'t affect the prototype chain', (t) => {

  class A {
    static someTask(msg) {
      return msg;
    }
  }
  class B extends A {}
  class C extends B {}

  const msg = 'ok';

  const ABeforeMockIsUnchanged = Object.getOwnPropertyDescriptor(A, A.someTask.name);
  const BBeforeMockIsUnchanged = Object.getOwnPropertyDescriptor(B, B.someTask.name);
  const CBeforeMockShouldNotHaveDesc = Object.getOwnPropertyDescriptor(C, C.someTask.name);

  t.mock.method(C, C.someTask.name);
  C.someTask(msg);
  const BAfterMockIsUnchanged = Object.getOwnPropertyDescriptor(B, B.someTask.name);

  const AAfterMockIsUnchanged = Object.getOwnPropertyDescriptor(A, A.someTask.name);
  const CAfterMockHasDescriptor = Object.getOwnPropertyDescriptor(C, C.someTask.name);

  assert.strictEqual(CBeforeMockShouldNotHaveDesc, undefined);
  assert.ok(CAfterMockHasDescriptor);

  assert.deepStrictEqual(ABeforeMockIsUnchanged, AAfterMockIsUnchanged);
  assert.strictEqual(BBeforeMockIsUnchanged, BAfterMockIsUnchanged);
  assert.strictEqual(BBeforeMockIsUnchanged, undefined);

  assert.strictEqual(C.someTask.mock.restore(), undefined);
  const CAfterRestoreKeepsDescriptor = Object.getOwnPropertyDescriptor(C, C.someTask.name);
  assert.ok(CAfterRestoreKeepsDescriptor);
});

test('mocked functions report thrown errors', (t) => {
  const testError = new Error('test error');
  const fn = t.mock.fn(() => {
    throw testError;
  });

  assert.throws(fn, /test error/);
  assert.strictEqual(fn.mock.calls.length, 1);

  const call = fn.mock.calls[0];

  assert.deepStrictEqual(call.arguments, []);
  assert.strictEqual(call.error, testError);
  assert.strictEqual(call.result, undefined);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, undefined);
});

test('mocked constructors report thrown errors', (t) => {
  const testError = new Error('test error');
  class Clazz {
    constructor() {
      throw testError;
    }
  }

  const ctor = t.mock.fn(Clazz);

  assert.throws(() => {
    new ctor();
  }, /test error/);
  assert.strictEqual(ctor.mock.calls.length, 1);

  const call = ctor.mock.calls[0];

  assert.deepStrictEqual(call.arguments, []);
  assert.strictEqual(call.error, testError);
  assert.strictEqual(call.result, undefined);
  assert.strictEqual(call.target, Clazz);
  assert.strictEqual(call.this, undefined);
});

test('mocks a function', (t) => {
  const sum = (arg1, arg2) => arg1 + arg2;
  const difference = (arg1, arg2) => arg1 - arg2;
  const fn = t.mock.fn(sum, difference);

  assert.strictEqual(fn.mock.calls.length, 0);
  assert.strictEqual(fn(3, 4), -1);
  assert.strictEqual(fn(9, 1), 8);
  assert.strictEqual(fn.mock.calls.length, 2);

  let call = fn.mock.calls[0];
  assert.deepStrictEqual(call.arguments, [3, 4]);
  assert.strictEqual(call.result, -1);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, undefined);

  call = fn.mock.calls[1];
  assert.deepStrictEqual(call.arguments, [9, 1]);
  assert.strictEqual(call.result, 8);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, undefined);

  assert.strictEqual(fn.mock.restore(), undefined);
  assert.strictEqual(fn(2, 11), 13);
});

test('mocks a constructor', (t) => {
  class ParentClazz {
    constructor(c) {
      this.c = c;
    }
  }

  class Clazz extends ParentClazz {
    #privateValue;

    constructor(a, b) {
      super(a + b);
      this.a = a;
      this.#privateValue = b;
    }

    getPrivateValue() {
      return this.#privateValue;
    }
  }

  class MockClazz {
    // eslint-disable-next-line no-unused-private-class-members
    #privateValue;

    constructor(z) {
      this.z = z;
    }
  }

  const ctor = t.mock.fn(Clazz, MockClazz);
  const instance = new ctor(42, 85);

  assert(!(instance instanceof MockClazz));
  assert(instance instanceof Clazz);
  assert(instance instanceof ParentClazz);
  assert.strictEqual(instance.a, undefined);
  assert.strictEqual(instance.c, undefined);
  assert.strictEqual(instance.z, 42);
  assert.strictEqual(ctor.mock.calls.length, 1);

  const call = ctor.mock.calls[0];

  assert.deepStrictEqual(call.arguments, [42, 85]);
  assert.strictEqual(call.result, instance);
  assert.strictEqual(call.target, Clazz);
  assert.strictEqual(call.this, instance);
  assert.throws(() => {
    instance.getPrivateValue();
  }, /TypeError: Cannot read private member #privateValue /);
});

test('mocks an object method', (t) => {
  const obj = {
    prop: 5,
    method(a, b) {
      return a + b + this.prop;
    },
  };

  function mockMethod(a) {
    return a + this.prop;
  }

  assert.strictEqual(obj.method(1, 3), 9);
  t.mock.method(obj, 'method', mockMethod);
  assert.strictEqual(obj.method.mock.calls.length, 0);
  assert.strictEqual(obj.method(1, 3), 6);

  const call = obj.method.mock.calls[0];

  assert.deepStrictEqual(call.arguments, [1, 3]);
  assert.strictEqual(call.result, 6);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, obj);

  assert.strictEqual(obj.method.mock.restore(), undefined);
  assert.strictEqual(obj.method(1, 3), 9);
  assert.strictEqual(obj.method.mock, undefined);
});

test('mocks a getter', (t) => {
  const obj = {
    prop: 5,
    get method() {
      return this.prop;
    },
  };

  function mockMethod() {
    return this.prop - 1;
  }

  assert.strictEqual(obj.method, 5);

  const getter = t.mock.method(obj, 'method', mockMethod, { getter: true });

  assert.strictEqual(getter.mock.calls.length, 0);
  assert.strictEqual(obj.method, 4);

  const call = getter.mock.calls[0];

  assert.deepStrictEqual(call.arguments, []);
  assert.strictEqual(call.result, 4);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, obj);

  assert.strictEqual(getter.mock.restore(), undefined);
  assert.strictEqual(obj.method, 5);
});

test('mocks a setter', (t) => {
  const obj = {
    prop: 100,
    // eslint-disable-next-line accessor-pairs
    set method(val) {
      this.prop = val;
    },
  };

  function mockMethod(val) {
    this.prop = -val;
  }

  assert.strictEqual(obj.prop, 100);
  obj.method = 88;
  assert.strictEqual(obj.prop, 88);

  const setter = t.mock.method(obj, 'method', mockMethod, { setter: true });

  assert.strictEqual(setter.mock.calls.length, 0);
  obj.method = 77;
  assert.strictEqual(obj.prop, -77);
  assert.strictEqual(setter.mock.calls.length, 1);

  const call = setter.mock.calls[0];

  assert.deepStrictEqual(call.arguments, [77]);
  assert.strictEqual(call.result, undefined);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, obj);

  assert.strictEqual(setter.mock.restore(), undefined);
  assert.strictEqual(obj.prop, -77);
  obj.method = 65;
  assert.strictEqual(obj.prop, 65);
});

test('mocks a getter with syntax sugar', (t) => {
  const obj = {
    prop: 5,
    get method() {
      return this.prop;
    },
  };

  function mockMethod() {
    return this.prop - 1;
  }
  const getter = t.mock.getter(obj, 'method', mockMethod);
  assert.strictEqual(getter.mock.calls.length, 0);
  assert.strictEqual(obj.method, 4);

  const call = getter.mock.calls[0];

  assert.deepStrictEqual(call.arguments, []);
  assert.strictEqual(call.result, 4);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, obj);

  assert.strictEqual(getter.mock.restore(), undefined);
  assert.strictEqual(obj.method, 5);
});

test('mocks a setter with syntax sugar', (t) => {
  const obj = {
    prop: 100,
    // eslint-disable-next-line accessor-pairs
    set method(val) {
      this.prop = val;
    },
  };

  function mockMethod(val) {
    this.prop = -val;
  }

  assert.strictEqual(obj.prop, 100);
  obj.method = 88;
  assert.strictEqual(obj.prop, 88);

  const setter = t.mock.setter(obj, 'method', mockMethod);

  assert.strictEqual(setter.mock.calls.length, 0);
  obj.method = 77;
  assert.strictEqual(obj.prop, -77);
  assert.strictEqual(setter.mock.calls.length, 1);

  const call = setter.mock.calls[0];

  assert.deepStrictEqual(call.arguments, [77]);
  assert.strictEqual(call.result, undefined);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, obj);

  assert.strictEqual(setter.mock.restore(), undefined);
  assert.strictEqual(obj.prop, -77);
  obj.method = 65;
  assert.strictEqual(obj.prop, 65);
});

test('mocked functions match name and length', (t) => {
  function getNameAndLength(fn) {
    return {
      name: Object.getOwnPropertyDescriptor(fn, 'name'),
      length: Object.getOwnPropertyDescriptor(fn, 'length'),
    };
  }

  function func1() {}
  const func2 = function(a) {}; // eslint-disable-line func-style
  const arrow = (a, b, c) => {};
  const obj = { method(a, b) {} };

  assert.deepStrictEqual(
    getNameAndLength(func1),
    getNameAndLength(t.mock.fn(func1))
  );
  assert.deepStrictEqual(
    getNameAndLength(func2),
    getNameAndLength(t.mock.fn(func2))
  );
  assert.deepStrictEqual(
    getNameAndLength(arrow),
    getNameAndLength(t.mock.fn(arrow))
  );
  assert.deepStrictEqual(
    getNameAndLength(obj.method),
    getNameAndLength(t.mock.method(obj, 'method', func1))
  );
});

test('method() fails if method cannot be redefined', (t) => {
  const obj = {
    prop: 5,
  };

  Object.defineProperty(obj, 'method', {
    configurable: false,
    value(a, b) {
      return a + b + this.prop;
    }
  });

  function mockMethod(a) {
    return a + this.prop;
  }

  assert.throws(() => {
    t.mock.method(obj, 'method', mockMethod);
  }, /Cannot redefine property: method/);
  assert.strictEqual(obj.method(1, 3), 9);
  assert.strictEqual(obj.method.mock, undefined);
});

test('method() fails if field is a property instead of a method', (t) => {
  const obj = {
    prop: 5,
    method: 100,
  };

  function mockMethod(a) {
    return a + this.prop;
  }

  assert.throws(() => {
    t.mock.method(obj, 'method', mockMethod);
  }, /The argument 'methodName' must be a method/);
  assert.strictEqual(obj.method, 100);
  assert.strictEqual(obj.method.mock, undefined);
});

test('mocks can be auto-restored', (t) => {
  let cnt = 0;

  function addOne() {
    cnt++;
    return cnt;
  }

  function addTwo() {
    cnt += 2;
    return cnt;
  }

  const fn = t.mock.fn(addOne, addTwo, { times: 2 });

  assert.strictEqual(fn(), 2);
  assert.strictEqual(fn(), 4);
  assert.strictEqual(fn(), 5);
  assert.strictEqual(fn(), 6);
});

test('mock implementation can be changed dynamically', (t) => {
  let cnt = 0;

  function addOne() {
    cnt++;
    return cnt;
  }

  function addTwo() {
    cnt += 2;
    return cnt;
  }

  function addThree() {
    cnt += 3;
    return cnt;
  }

  const fn = t.mock.fn(addOne);

  assert.strictEqual(fn.mock.callCount(), 0);
  assert.strictEqual(fn(), 1);
  assert.strictEqual(fn(), 2);
  assert.strictEqual(fn(), 3);
  assert.strictEqual(fn.mock.callCount(), 3);

  fn.mock.mockImplementation(addTwo);
  assert.strictEqual(fn(), 5);
  assert.strictEqual(fn(), 7);
  assert.strictEqual(fn.mock.callCount(), 5);

  fn.mock.restore();
  assert.strictEqual(fn(), 8);
  assert.strictEqual(fn(), 9);
  assert.strictEqual(fn.mock.callCount(), 7);

  assert.throws(() => {
    fn.mock.mockImplementationOnce(common.mustNotCall(), 6);
  }, /The value of "onCall" is out of range\. It must be >= 7/);

  fn.mock.mockImplementationOnce(addThree, 7);
  fn.mock.mockImplementationOnce(addTwo, 8);
  assert.strictEqual(fn(), 12);
  assert.strictEqual(fn(), 14);
  assert.strictEqual(fn(), 15);
  assert.strictEqual(fn.mock.callCount(), 10);
  fn.mock.mockImplementationOnce(addThree);
  assert.strictEqual(fn(), 18);
  assert.strictEqual(fn(), 19);
  assert.strictEqual(fn.mock.callCount(), 12);
});

test('local mocks are auto restored after the test finishes', async (t) => {
  const obj = {
    foo() {},
    bar() {},
  };
  const originalFoo = obj.foo;
  const originalBar = obj.bar;

  assert.strictEqual(originalFoo, obj.foo);
  assert.strictEqual(originalBar, obj.bar);

  const mockFoo = t.mock.method(obj, 'foo');

  assert.strictEqual(mockFoo, obj.foo);
  assert.notStrictEqual(originalFoo, obj.foo);
  assert.strictEqual(originalBar, obj.bar);

  t.beforeEach(common.mustCallAtLeast(() => {
    assert.strictEqual(mockFoo, obj.foo);
    assert.strictEqual(originalBar, obj.bar);
  }));

  t.afterEach(common.mustCallAtLeast(() => {
    assert.strictEqual(mockFoo, obj.foo);
    assert.notStrictEqual(originalBar, obj.bar);
  }));

  await t.test('creates mocks that are auto restored', (t) => {
    const mockBar = t.mock.method(obj, 'bar');

    assert.strictEqual(mockFoo, obj.foo);
    assert.strictEqual(mockBar, obj.bar);
    assert.notStrictEqual(originalBar, obj.bar);
  });

  assert.strictEqual(mockFoo, obj.foo);
  assert.strictEqual(originalBar, obj.bar);
});

test('reset mock calls', (t) => {
  const sum = (arg1, arg2) => arg1 + arg2;
  const difference = (arg1, arg2) => arg1 - arg2;
  const fn = t.mock.fn(sum, difference);

  assert.strictEqual(fn(1, 2), -1);
  assert.strictEqual(fn(2, 1), 1);
  assert.strictEqual(fn.mock.calls.length, 2);
  assert.strictEqual(fn.mock.callCount(), 2);

  fn.mock.resetCalls();
  assert.strictEqual(fn.mock.calls.length, 0);
  assert.strictEqual(fn.mock.callCount(), 0);

  assert.strictEqual(fn(3, 2), 1);
});

test('uses top level mock', () => {
  function sum(a, b) {
    return a + b;
  }

  function difference(a, b) {
    return a - b;
  }

  const fn = mock.fn(sum, difference);

  assert.strictEqual(fn.mock.calls.length, 0);
  assert.strictEqual(fn(3, 4), -1);
  assert.strictEqual(fn.mock.calls.length, 1);
  mock.reset();
  assert.strictEqual(fn(3, 4), 7);
  assert.strictEqual(fn.mock.calls.length, 2);
});

test('the getter and setter options cannot be used together', (t) => {
  assert.throws(() => {
    t.mock.method({}, 'method', { getter: true, setter: true });
  }, /The property 'options\.setter' cannot be used with 'options\.getter'/);
});

test('method names must be strings or symbols', (t) => {
  const symbol = Symbol();
  const obj = {
    method() {},
    [symbol]() {},
  };

  t.mock.method(obj, 'method');
  t.mock.method(obj, symbol);

  assert.throws(() => {
    t.mock.method(obj, {});
  }, /The "methodName" argument must be one of type string or symbol/);
});

test('the times option must be an integer >= 1', (t) => {
  assert.throws(() => {
    t.mock.fn({ times: null });
  }, /The "options\.times" property must be of type number/);

  assert.throws(() => {
    t.mock.fn({ times: 0 });
  }, /The value of "options\.times" is out of range/);

  assert.throws(() => {
    t.mock.fn(() => {}, { times: 3.14159 });
  }, /The value of "options\.times" is out of range/);
});

test('spies on a class prototype method', (t) => {
  class Clazz {
    constructor(c) {
      this.c = c;
    }

    getC() {
      return this.c;
    }
  }

  const instance = new Clazz(85);

  assert.strictEqual(instance.getC(), 85);
  t.mock.method(Clazz.prototype, 'getC');

  assert.strictEqual(instance.getC.mock.calls.length, 0);
  assert.strictEqual(instance.getC(), 85);
  assert.strictEqual(instance.getC.mock.calls.length, 1);
  assert.strictEqual(Clazz.prototype.getC.mock.calls.length, 1);

  const call = instance.getC.mock.calls[0];
  assert.deepStrictEqual(call.arguments, []);
  assert.strictEqual(call.result, 85);
  assert.strictEqual(call.error, undefined);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, instance);
});

test('getter() fails if getter options set to false', (t) => {
  assert.throws(() => {
    t.mock.getter({}, 'method', { getter: false });
  }, /The property 'options\.getter' cannot be false/);
});

test('setter() fails if setter options set to false', (t) => {
  assert.throws(() => {
    t.mock.setter({}, 'method', { setter: false });
  }, /The property 'options\.setter' cannot be false/);
});

test('getter() fails if setter options is true', (t) => {
  assert.throws(() => {
    t.mock.getter({}, 'method', { setter: true });
  }, /The property 'options\.setter' cannot be used with 'options\.getter'/);
});

test('setter() fails if getter options is true', (t) => {
  assert.throws(() => {
    t.mock.setter({}, 'method', { getter: true });
  }, /The property 'options\.setter' cannot be used with 'options\.getter'/);
});

test('spies on a property', (t) => {
  const obj = { foo: 42 };
  const prop = t.mock.property(obj, 'foo', 100);

  assert.strictEqual(obj.foo, 100);
  assert.strictEqual(prop.mock.accessCount(), 1);
  assert.strictEqual(prop.mock.accesses[0].type, 'get');
  assert.strictEqual(prop.mock.accesses[0].value, 100);

  obj.foo = 200;
  assert.strictEqual(obj.foo, 200);
  assert.strictEqual(prop.mock.accesses.length, 3);
  assert.strictEqual(prop.mock.accesses[1].type, 'set');
  assert.strictEqual(prop.mock.accesses[1].value, 200);
  assert.strictEqual(prop.mock.accesses[2].type, 'get');
  assert.strictEqual(prop.mock.accesses[2].value, 200);

  obj.foo = 300;
  assert.strictEqual(obj.foo, 300);
  assert.strictEqual(prop.mock.accessCount(), 5);
  assert.strictEqual(prop.mock.accesses[3].type, 'set');
  assert.strictEqual(prop.mock.accesses[3].value, 300);
  assert.strictEqual(prop.mock.accesses[4].type, 'get');
  assert.strictEqual(prop.mock.accesses[4].value, 300);

  prop.mock.resetAccesses();
  assert.strictEqual(prop.mock.accessCount(), 0);

  obj.foo = 500;
  assert.strictEqual(obj.foo, 500);
  assert.strictEqual(prop.mock.accessCount(), 2);
  assert.strictEqual(prop.mock.accesses[0].type, 'set');
  assert.strictEqual(prop.mock.accesses[1].type, 'get');

  prop.mock.resetAccesses();
  assert.strictEqual(prop.mock.accessCount(), 0);

  assert.strictEqual(obj.foo, 500);
  assert.strictEqual(prop.mock.accessCount(), 1);
  assert.strictEqual(prop.mock.accesses[0].type, 'get');

  prop.mock.restore();
  assert.strictEqual(obj.foo, 42);
});

test('spies on a property without providing a value', (t) => {
  const obj = { foo: 123 };
  const prop = t.mock.property(obj, 'foo');

  assert.strictEqual(obj.foo, 123);
  assert.strictEqual(prop.mock.accessCount(), 1);
  assert.strictEqual(prop.mock.accesses[0].type, 'get');
  assert.strictEqual(prop.mock.accesses[0].value, 123);

  obj.foo = 456;
  assert.strictEqual(obj.foo, 456);
  assert.strictEqual(prop.mock.accessCount(), 3);
  assert.strictEqual(prop.mock.accesses[1].type, 'set');
  assert.strictEqual(prop.mock.accesses[1].value, 456);
  assert.strictEqual(prop.mock.accesses[2].type, 'get');
  assert.strictEqual(prop.mock.accesses[2].value, 456);

  prop.mock.restore();
  assert.strictEqual(obj.foo, 123);
});

test('spies on a symbol property', (t) => {
  const symbol = Symbol('foo');
  const obj = { [symbol]: 123 };
  const prop = t.mock.property(obj, symbol, 456);

  assert.strictEqual(obj[symbol], 456);
  assert.strictEqual(prop.mock.accessCount(), 1);

  obj[symbol] = 789;
  assert.strictEqual(obj[symbol], 789);
  assert.strictEqual(prop.mock.accessCount(), 3);
  assert.strictEqual(prop.mock.accesses[1].type, 'set');
  assert.strictEqual(prop.mock.accesses[2].type, 'get');

  prop.mock.restore();
  assert.strictEqual(obj[symbol], 123);
});

test('changes mocked property value dynamically', (t) => {
  const obj = { foo: 1 };

  const prop = t.mock.property(obj, 'foo', 2);
  assert.strictEqual(obj.foo, 2);
  assert.strictEqual(prop.mock.accessCount(), 1);

  prop.mock.mockImplementation(99);
  assert.strictEqual(obj.foo, 99);
  assert.strictEqual(prop.mock.accessCount(), 3);

  prop.mock.mockImplementationOnce(42);
  assert.strictEqual(obj.foo, 42);
  assert.strictEqual(obj.foo, 99);
  assert.strictEqual(prop.mock.accessCount(), 5);

  assert.throws(() => {
    prop.mock.mockImplementationOnce(55, 4);
  }, /The value of "onAccess" is out of range\. It must be >= 5/);

  prop.mock.mockImplementationOnce(100, 5);
  prop.mock.mockImplementationOnce(200, 6);
  assert.strictEqual(obj.foo, 100);
  assert.strictEqual(obj.foo, 200);
  assert.strictEqual(obj.foo, 99);
  assert.strictEqual(prop.mock.accessCount(), 8);

  prop.mock.mockImplementationOnce(555, 10);
  assert.strictEqual(obj.foo, 99);
  assert.strictEqual(obj.foo, 99);
  assert.strictEqual(obj.foo, 555);

  prop.mock.mockImplementation(undefined);
  assert.strictEqual(obj.foo, undefined);
});

test('mocks property value to undefined', (t) => {
  const obj = { foo: 123 };
  const prop = t.mock.property(obj, 'foo', undefined);

  assert.strictEqual(obj.foo, undefined);
  assert.strictEqual(prop.mock.accessCount(), 1);
  assert.strictEqual(prop.mock.accesses[0].type, 'get');
  assert.strictEqual(prop.mock.accesses[0].value, undefined);

  prop.mock.restore();
  assert.strictEqual(obj.foo, 123);
});

test('resetAccesses does not affect property value', (t) => {
  const obj = { foo: 1 };
  const prop = t.mock.property(obj, 'foo', 2);

  obj.foo = 5;
  assert.strictEqual(obj.foo, 5);
  assert.strictEqual(prop.mock.accessCount(), 2);

  prop.mock.resetAccesses();
  assert.strictEqual(obj.foo, 5);
  assert.strictEqual(prop.mock.accessCount(), 1);
  assert.strictEqual(prop.mock.accesses[0].type, 'get');
});

test('restores original property value', (t) => {
  const obj = {
    foo: 10,
  };

  const prop = t.mock.property(obj, 'foo', 20);
  assert.strictEqual(obj.foo, 20);

  prop.mock.restore();
  assert.strictEqual(obj.foo, 10);
});

test('throws if setting a non-writable property', (t) => {
  const obj = {};
  Object.defineProperty(obj, 'bar', {
    value: 1,
    writable: false,
    configurable: true,
    enumerable: true,
  });

  t.mock.property(obj, 'bar', 2);
  assert.strictEqual(obj.bar, 2);

  assert.throws(() => { obj.bar = 3; }, { code: 'ERR_INVALID_ARG_VALUE' });
});

test('throws if property does not exist', (t) => {
  assert.throws(() => {
    t.mock.property({}, 'doesNotExist', 1);
  }, { code: 'ERR_INVALID_ARG_VALUE' });
});

test('throws if object is null', (t) => {
  assert.throws(() => {
    t.mock.property(null, 'foo', 1);
  }, { code: 'ERR_INVALID_ARG_TYPE' });
});

test('local property mocks are auto restored after the test finishes', async (t) => {
  const obj = { foo: 111, bar: 222 };

  assert.strictEqual(obj.foo, 111);
  assert.strictEqual(obj.bar, 222);

  t.mock.property(obj, 'foo', 888);

  assert.strictEqual(obj.foo, 888);
  assert.strictEqual(obj.bar, 222);

  t.beforeEach(common.mustCallAtLeast(() => {
    assert.strictEqual(obj.foo, 888);
    assert.strictEqual(obj.bar, 222);
  }));

  t.afterEach(common.mustCallAtLeast(() => {
    assert.strictEqual(obj.foo, 888);
    assert.strictEqual(obj.bar, 999);
  }));

  await t.test('creates property mocks that are auto restored', (t) => {
    t.mock.property(obj, 'bar', 999);

    assert.strictEqual(obj.foo, 888);
    assert.strictEqual(obj.bar, 999);
  });

  assert.strictEqual(obj.foo, 888);
  assert.strictEqual(obj.bar, 222);
});
