// https://github.com/nodejs/node/blob/929aada39d0f418193ca03cc360ced8c5b4ce553/test/parallel/test-runner-mocking.js
'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) // eslint-disable-line new-cap

  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 a 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() // eslint-disable-line new-cap, no-new
  }, /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 {
    #privateValue

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

  const ctor = t.mock.fn(Clazz, MockClazz)
  const instance = new ctor(42, 85) // eslint-disable-line new-cap

  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)
  }, /Expected onCall to 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(() => {
    assert.strictEqual(mockFoo, obj.foo)
    assert.strictEqual(originalBar, obj.bar)
  })

  t.afterEach(() => {
    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('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, {})
  }, /Expected methodName to be string,symbol/)
})

test('the times option must be an integer >= 1', (t) => {
  assert.throws(() => {
    t.mock.fn({ times: null })
  }, /Expected options\.times to be number/)

  assert.throws(() => {
    t.mock.fn({ times: 0 })
  }, /Expected options.times to be >= 1/)

  assert.throws(() => {
    t.mock.fn(() => {}, { times: 3.14159 })
  }, /Expected options.times to be an integer/)
})

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'/)
})
