'use strict';
process.env.NODE_TEST_KNOWN_GLOBALS = 0;
const common = require('../common');

const assert = require('node:assert');
const { it, mock, describe } = require('node:test');
const nodeTimers = require('node:timers');
const nodeTimersPromises = require('node:timers/promises');

describe('Mock Timers Test Suite', () => {
  describe('MockTimers API', () => {
    it('should throw an error if trying to enable a timer that is not supported', (t) => {
      assert.throws(() => {
        t.mock.timers.enable({ apis: ['DOES_NOT_EXIST'] });
      }, {
        code: 'ERR_INVALID_ARG_VALUE',
      });
    });

    it('should throw an error if data type of trying to enable a timer is not string', (t) => {
      assert.throws(() => {
        t.mock.timers.enable({ apis: [1] });
      }, {
        code: 'ERR_INVALID_ARG_TYPE',
      });
    });

    it('should throw an error if trying to enable a timer twice', (t) => {
      t.mock.timers.enable();
      assert.throws(() => {
        t.mock.timers.enable();
      }, {
        code: 'ERR_INVALID_STATE',
      });
    });

    it('should not throw if calling reset without enabling timers', (t) => {
      t.mock.timers.reset();
    });

    it('should throw an error if calling tick without enabling timers', (t) => {
      assert.throws(() => {
        t.mock.timers.tick();
      }, {
        code: 'ERR_INVALID_STATE',
      });
    });

    it('should throw an error if calling tick with a negative number', (t) => {
      t.mock.timers.enable();
      assert.throws(() => {
        t.mock.timers.tick(-1);
      }, {
        code: 'ERR_INVALID_ARG_VALUE',
      });
    });

    it('should check that propertyDescriptor gets back after resetting timers', (t) => {
      const getDescriptor = (ctx, fn) => Object.getOwnPropertyDescriptor(ctx, fn);
      const getCurrentTimersDescriptors = () => {
        const timers = [
          'setTimeout',
          'clearTimeout',
          'setInterval',
          'clearInterval',
          'setImmediate',
          'clearImmediate',
        ];

        const globalTimersDescriptors = timers.map((fn) => getDescriptor(global, fn));
        const nodeTimersDescriptors = timers.map((fn) => getDescriptor(nodeTimers, fn));
        const nodeTimersPromisesDescriptors = timers
          .filter((fn) => !fn.includes('clear'))
          .map((fn) => getDescriptor(nodeTimersPromises, fn));

        return {
          global: globalTimersDescriptors,
          nodeTimers: nodeTimersDescriptors,
          nodeTimersPromises: nodeTimersPromisesDescriptors,
        };
      };

      const originalDescriptors = getCurrentTimersDescriptors();

      t.mock.timers.enable();
      const during = getCurrentTimersDescriptors();
      t.mock.timers.reset();
      const after = getCurrentTimersDescriptors();

      for (const env in originalDescriptors) {
        for (const prop in originalDescriptors[env]) {
          const originalDescriptor = originalDescriptors[env][prop];
          const afterDescriptor = after[env][prop];

          assert.deepStrictEqual(
            originalDescriptor,
            afterDescriptor,
          );

          assert.notDeepStrictEqual(
            originalDescriptor,
            during[env][prop],
          );

          assert.notDeepStrictEqual(
            during[env][prop],
            after[env][prop],
          );

        }
      }
    });

    it('should reset all timers when calling .reset function', (t) => {
      t.mock.timers.enable();
      const fn = t.mock.fn();
      global.setTimeout(fn, 1000);
      t.mock.timers.reset();
      assert.deepStrictEqual(Date.now, globalThis.Date.now);
      assert.throws(() => {
        t.mock.timers.tick(1000);
      }, {
        code: 'ERR_INVALID_STATE',
      });

      assert.strictEqual(fn.mock.callCount(), 0);
    });

    it('should reset all timers when calling Symbol.dispose', (t) => {
      t.mock.timers.enable();
      const fn = t.mock.fn();
      global.setTimeout(fn, 1000);
      // TODO(benjamingr) refactor to `using`
      t.mock.timers[Symbol.dispose]();
      assert.throws(() => {
        t.mock.timers.tick(1000);
      }, {
        code: 'ERR_INVALID_STATE',
      });

      assert.strictEqual(fn.mock.callCount(), 0);
    });

    it('should execute in order if timeout is the same', (t) => {
      t.mock.timers.enable();
      const order = [];
      const fn1 = t.mock.fn(() => order.push('f1'));
      const fn2 = t.mock.fn(() => order.push('f2'));
      global.setTimeout(fn1, 1000);
      global.setTimeout(fn2, 1000);
      t.mock.timers.tick(1000);
      assert.strictEqual(fn1.mock.callCount(), 1);
      assert.strictEqual(fn2.mock.callCount(), 1);
      assert.deepStrictEqual(order, ['f1', 'f2']);
    });

    describe('runAll Suite', () => {
      it('should throw an error if calling runAll without enabling timers', (t) => {
        assert.throws(() => {
          t.mock.timers.runAll();
        }, {
          code: 'ERR_INVALID_STATE',
        });
      });

      it('should trigger all timers when calling .runAll function', async (t) => {
        const timeoutFn = t.mock.fn();
        const intervalFn = t.mock.fn();

        t.mock.timers.enable();
        global.setTimeout(timeoutFn, 1111);
        const id = global.setInterval(intervalFn, 9999);
        t.mock.timers.runAll();

        global.clearInterval(id);
        assert.strictEqual(timeoutFn.mock.callCount(), 1);
        assert.strictEqual(intervalFn.mock.callCount(), 1);
      });

      it('should increase the epoch as the tick run for runAll', async (t) => {
        const timeoutFn = t.mock.fn();
        const intervalFn = t.mock.fn();

        t.mock.timers.enable();
        global.setTimeout(timeoutFn, 1111);
        const id = global.setInterval(intervalFn, 9999);
        t.mock.timers.runAll();

        global.clearInterval(id);
        assert.strictEqual(timeoutFn.mock.callCount(), 1);
        assert.strictEqual(intervalFn.mock.callCount(), 1);
        assert.strictEqual(Date.now(), 9999);
      });

      it('should not error if there are not timers to run', (t) => {
        t.mock.timers.enable();
        t.mock.timers.runAll();
        // Should not throw
      });
    });
  });

  describe('globals/timers', () => {
    describe('setTimeout Suite', () => {
      it('should advance in time and trigger timers when calling the .tick function', (t) => {
        mock.timers.enable({ apis: ['setTimeout'] });

        const fn = mock.fn();

        global.setTimeout(fn, 4000);

        mock.timers.tick(4000);
        assert.strictEqual(fn.mock.callCount(), 1);
        mock.timers.reset();
      });

      it('should advance in time and trigger timers when calling the .tick function multiple times', (t) => {
        t.mock.timers.enable({ apis: ['setTimeout'] });
        const fn = t.mock.fn();

        global.setTimeout(fn, 2000);

        t.mock.timers.tick(1000);
        assert.strictEqual(fn.mock.callCount(), 0);
        t.mock.timers.tick(500);
        assert.strictEqual(fn.mock.callCount(), 0);
        t.mock.timers.tick(500);
        assert.strictEqual(fn.mock.callCount(), 1);
      });

      it('should work with the same params as the original setTimeout', (t) => {
        t.mock.timers.enable({ apis: ['setTimeout'] });
        const fn = t.mock.fn();
        const args = ['a', 'b', 'c'];
        global.setTimeout(fn, 2000, ...args);

        t.mock.timers.tick(1000);
        t.mock.timers.tick(500);
        t.mock.timers.tick(500);

        assert.strictEqual(fn.mock.callCount(), 1);
        assert.deepStrictEqual(fn.mock.calls[0].arguments, args);
      });

      it('should keep setTimeout working if timers are disabled', (t, done) => {
        const now = Date.now();
        const timeout = 2;
        const expected = () => now - timeout;
        global.setTimeout(common.mustCall(() => {
          assert.strictEqual(now - timeout, expected());
          done();
        }), timeout);
      });

      it('should change timeout to 1ms when it is >= 2 ** 31', (t) => {
        t.mock.timers.enable({ apis: ['setTimeout'] });
        const fn = t.mock.fn();
        global.setTimeout(fn, 2 ** 31);
        t.mock.timers.tick(1);
        assert.strictEqual(fn.mock.callCount(), 1);
      });

      it('should change the delay to one if timeout < 0', (t) => {
        t.mock.timers.enable({ apis: ['setTimeout'] });
        const fn = t.mock.fn();
        global.setTimeout(fn, -1);
        t.mock.timers.tick(1);
        assert.strictEqual(fn.mock.callCount(), 1);
      });
    });

    describe('clearTimeout Suite', () => {
      it('should not advance in time if clearTimeout was invoked', (t) => {
        t.mock.timers.enable({ apis: ['setTimeout'] });

        const fn = mock.fn();

        const id = global.setTimeout(fn, 4000);
        global.clearTimeout(id);
        t.mock.timers.tick(4000);

        assert.strictEqual(fn.mock.callCount(), 0);
      });

      it('clearTimeout does not throw on null and undefined', (t) => {
        t.mock.timers.enable({ apis: ['setTimeout'] });

        nodeTimers.clearTimeout();
        nodeTimers.clearTimeout(null);
      });
    });

    describe('setInterval Suite', () => {
      it('should tick three times using fake setInterval', (t) => {
        t.mock.timers.enable({ apis: ['setInterval'] });
        const fn = t.mock.fn();

        const id = global.setInterval(fn, 200);

        t.mock.timers.tick(200);
        t.mock.timers.tick(200);
        t.mock.timers.tick(200);

        global.clearInterval(id);

        assert.strictEqual(fn.mock.callCount(), 3);
      });

      it('should work with the same params as the original setInterval', (t) => {
        t.mock.timers.enable({ apis: ['setInterval'] });
        const fn = t.mock.fn();
        const args = ['a', 'b', 'c'];
        const id = global.setInterval(fn, 200, ...args);

        t.mock.timers.tick(200);
        t.mock.timers.tick(200);
        t.mock.timers.tick(200);

        global.clearInterval(id);

        assert.strictEqual(fn.mock.callCount(), 3);
        assert.deepStrictEqual(fn.mock.calls[0].arguments, args);
        assert.deepStrictEqual(fn.mock.calls[1].arguments, args);
        assert.deepStrictEqual(fn.mock.calls[2].arguments, args);
      });
    });

    describe('clearInterval Suite', () => {
      it('should not advance in time if clearInterval was invoked', (t) => {
        t.mock.timers.enable({ apis: ['setInterval'] });

        const fn = mock.fn();
        const id = global.setInterval(fn, 200);
        global.clearInterval(id);
        t.mock.timers.tick(200);

        assert.strictEqual(fn.mock.callCount(), 0);
      });

      it('clearInterval does not throw on null and undefined', (t) => {
        t.mock.timers.enable({ apis: ['setInterval'] });

        nodeTimers.clearInterval();
        nodeTimers.clearInterval(null);
      });
    });

    describe('setImmediate Suite', () => {
      it('should keep setImmediate working if timers are disabled', (t, done) => {
        const now = Date.now();
        const timeout = 2;
        const expected = () => now - timeout;
        global.setImmediate(common.mustCall(() => {
          assert.strictEqual(now - timeout, expected());
          done();
        }));
      });

      it('should work with the same params as the original setImmediate', (t) => {
        t.mock.timers.enable({ apis: ['setImmediate'] });
        const fn = t.mock.fn();
        const args = ['a', 'b', 'c'];
        global.setImmediate(fn, ...args);
        t.mock.timers.tick(9999);

        assert.strictEqual(fn.mock.callCount(), 1);
        assert.deepStrictEqual(fn.mock.calls[0].arguments, args);
      });

      it('should not advance in time if clearImmediate was invoked', (t) => {
        t.mock.timers.enable({ apis: ['setImmediate'] });

        const id = global.setImmediate(common.mustNotCall());
        global.clearImmediate(id);
        t.mock.timers.tick(200);
      });

      it('should advance in time and trigger timers when calling the .tick function', (t) => {
        t.mock.timers.enable({ apis: ['setImmediate'] });
        global.setImmediate(common.mustCall(1));
        t.mock.timers.tick(0);
      });

      it('should execute in order if setImmediate is called multiple times', (t) => {
        t.mock.timers.enable({ apis: ['setImmediate'] });
        const order = [];
        const fn1 = t.mock.fn(common.mustCall(() => order.push('f1'), 1));
        const fn2 = t.mock.fn(common.mustCall(() => order.push('f2'), 1));

        global.setImmediate(fn1);
        global.setImmediate(fn2);

        t.mock.timers.tick(0);

        assert.deepStrictEqual(order, ['f1', 'f2']);
      });

      it('should execute setImmediate first if setTimeout was also called', (t) => {
        t.mock.timers.enable({ apis: ['setImmediate', 'setTimeout'] });
        const order = [];
        const fn1 = t.mock.fn(common.mustCall(() => order.push('f1'), 1));
        const fn2 = t.mock.fn(common.mustCall(() => order.push('f2'), 1));

        global.setTimeout(fn2, 0);
        global.setImmediate(fn1);

        t.mock.timers.tick(100);

        assert.deepStrictEqual(order, ['f1', 'f2']);
      });
    });

    describe('clearImmediate Suite', () => {
      it('clearImmediate does not throw on null and undefined', (t) => {
        t.mock.timers.enable({ apis: ['setImmediate'] });

        nodeTimers.clearImmediate();
        nodeTimers.clearImmediate(null);
      });
    });

    describe('timers/promises', () => {
      describe('setTimeout Suite', () => {
        it('should advance in time and trigger timers when calling the .tick function multiple times', async (t) => {
          t.mock.timers.enable({ apis: ['setTimeout'] });

          const p = nodeTimersPromises.setTimeout(2000);

          t.mock.timers.tick(1000);
          t.mock.timers.tick(500);
          t.mock.timers.tick(500);
          t.mock.timers.tick(500);

          p.then(common.mustCall((result) => {
            assert.strictEqual(result, undefined);
          }));
        });

        it('should work with the same params as the original timers/promises/setTimeout', async (t) => {
          t.mock.timers.enable({ apis: ['setTimeout'] });
          const expectedResult = 'result';
          const controller = new AbortController();
          const p = nodeTimersPromises.setTimeout(2000, expectedResult, {
            ref: true,
            signal: controller.signal,
          });

          t.mock.timers.tick(1000);
          t.mock.timers.tick(500);
          t.mock.timers.tick(500);
          t.mock.timers.tick(500);

          const result = await p;
          assert.strictEqual(result, expectedResult);
        });

        it('should always return the same result as the original timers/promises/setTimeout', async (t) => {
          t.mock.timers.enable({ apis: ['setTimeout'] });
          for (const expectedResult of [undefined, null, false, true, 0, 0n, 1, 1n, '', 'result', {}]) {
            const p = nodeTimersPromises.setTimeout(2000, expectedResult);
            t.mock.timers.tick(2000);
            const result = await p;
            assert.strictEqual(result, expectedResult);
          }
        });

        it('should abort operation if timers/promises/setTimeout received an aborted signal', async (t) => {
          t.mock.timers.enable({ apis: ['setTimeout'] });
          const expectedResult = 'result';
          const controller = new AbortController();
          const p = nodeTimersPromises.setTimeout(2000, expectedResult, {
            ref: true,
            signal: controller.signal,
          });

          t.mock.timers.tick(1000);
          controller.abort();
          t.mock.timers.tick(500);
          t.mock.timers.tick(500);
          t.mock.timers.tick(500);
          await assert.rejects(() => p, {
            name: 'AbortError',
          });
        });
        it('should abort operation even if the .tick was not called', async (t) => {
          t.mock.timers.enable({ apis: ['setTimeout'] });
          const expectedResult = 'result';
          const controller = new AbortController();
          const p = nodeTimersPromises.setTimeout(2000, expectedResult, {
            ref: true,
            signal: controller.signal,
          });

          controller.abort();

          await assert.rejects(() => p, {
            name: 'AbortError',
          });
        });

        it('should abort operation when .abort is called before calling setInterval', async (t) => {
          t.mock.timers.enable({ apis: ['setTimeout'] });
          const expectedResult = 'result';
          const controller = new AbortController();
          controller.abort();
          const p = nodeTimersPromises.setTimeout(2000, expectedResult, {
            ref: true,
            signal: controller.signal,
          });

          await assert.rejects(() => p, {
            name: 'AbortError',
          });
        });

        it('should reject given an an invalid signal instance', async (t) => {
          t.mock.timers.enable({ apis: ['setTimeout'] });
          const expectedResult = 'result';
          const p = nodeTimersPromises.setTimeout(2000, expectedResult, {
            ref: true,
            signal: {},
          });

          await assert.rejects(() => p, {
            name: 'TypeError',
            code: 'ERR_INVALID_ARG_TYPE',
          });
        });

        // Test for https://github.com/nodejs/node/issues/50365
        it('should not affect other timers when aborting', async (t) => {
          const f1 = t.mock.fn();
          const f2 = t.mock.fn();
          t.mock.timers.enable({ apis: ['setTimeout'] });
          const ac = new AbortController();

          // id 1 & pos 1 in priority queue
          nodeTimersPromises.setTimeout(100, undefined, { signal: ac.signal }).then(f1, f1);
          // id 2 & pos 1 in priority queue (id 1 is moved to pos 2)
          nodeTimersPromises.setTimeout(50).then(f2, f2);

          ac.abort(); // BUG: will remove timer at pos 1 not timer with id 1!

          t.mock.timers.runAll();
          await nodeTimersPromises.setImmediate(); // let promises settle

          // First setTimeout is aborted
          assert.strictEqual(f1.mock.callCount(), 1);
          assert.strictEqual(f1.mock.calls[0].arguments[0].code, 'ABORT_ERR');

          // Second setTimeout should resolve, but never settles, because it was eronously removed by ac.abort()
          assert.strictEqual(f2.mock.callCount(), 1);
        });

        // Test for https://github.com/nodejs/node/issues/50365
        it('should not affect other timers when aborted after triggering', async (t) => {
          const f1 = t.mock.fn();
          const f2 = t.mock.fn();
          t.mock.timers.enable({ apis: ['setTimeout'] });
          const ac = new AbortController();

          // id 1 & pos 1 in priority queue
          nodeTimersPromises.setTimeout(50, true, { signal: ac.signal }).then(f1, f1);
          // id 2 & pos 2 in priority queue
          nodeTimersPromises.setTimeout(100).then(f2, f2);

          // First setTimeout resolves
          t.mock.timers.tick(50);
          await nodeTimersPromises.setImmediate(); // let promises settle
          assert.strictEqual(f1.mock.callCount(), 1);
          assert.strictEqual(f1.mock.calls[0].arguments.length, 1);
          assert.strictEqual(f1.mock.calls[0].arguments[0], true);

          // Now timer with id 2 will be at pos 1 in priority queue
          ac.abort(); // BUG: will remove timer at pos 1 not timer with id 1!

          // Second setTimeout should resolve, but never settles, because it was eronously removed by ac.abort()
          t.mock.timers.runAll();
          await nodeTimersPromises.setImmediate(); // let promises settle
          assert.strictEqual(f2.mock.callCount(), 1);
        });

        it('should not affect other timers when clearing timeout inside own callback', (t) => {
          t.mock.timers.enable({ apis: ['setTimeout'] });
          const f = t.mock.fn();

          const timer = nodeTimers.setTimeout(() => {
            f();
            // Clearing the already-expired timeout should do nothing
            nodeTimers.clearTimeout(timer);
          }, 50);
          nodeTimers.setTimeout(f, 50);
          nodeTimers.setTimeout(f, 50);

          t.mock.timers.runAll();
          assert.strictEqual(f.mock.callCount(), 3);
        });

        it('should allow clearing timeout inside own callback', (t) => {
          t.mock.timers.enable({ apis: ['setTimeout'] });
          const f = t.mock.fn();

          const timer = nodeTimers.setTimeout(() => {
            f();
            nodeTimers.clearTimeout(timer);
          }, 50);

          t.mock.timers.runAll();
          assert.strictEqual(f.mock.callCount(), 1);
        });
      });

      describe('setInterval Suite', () => {
        it('should tick three times using fake setInterval', async (t) => {
          t.mock.timers.enable({ apis: ['setInterval'] });

          const interval = 100;
          const intervalIterator = nodeTimersPromises.setInterval(interval, Date.now());

          const first = intervalIterator.next();
          const second = intervalIterator.next();
          const third = intervalIterator.next();

          t.mock.timers.tick(interval);
          t.mock.timers.tick(interval);
          t.mock.timers.tick(interval);
          t.mock.timers.tick(interval);

          const results = await Promise.all([
            first,
            second,
            third,
          ]);

          const finished = await intervalIterator.return();
          assert.deepStrictEqual(finished, { done: true, value: undefined });
          for (const result of results) {
            assert.strictEqual(typeof result.value, 'number');
            assert.strictEqual(result.done, false);
          }
        });
        it('should tick five times testing a real use case', async (t) => {
          t.mock.timers.enable({ apis: ['setInterval'] });

          const expectedIterations = 5;
          const interval = 1000;
          let time = 0;
          async function run() {
            const times = [];
            for await (const _ of nodeTimersPromises.setInterval(interval)) { // eslint-disable-line no-unused-vars
              time += interval;
              times.push(time);
              if (times.length === expectedIterations) break;
            }
            return times;
          }

          const r = run();
          t.mock.timers.tick(interval);
          t.mock.timers.tick(interval);
          t.mock.timers.tick(interval);
          t.mock.timers.tick(interval);
          t.mock.timers.tick(interval);

          const timeResults = await r;
          assert.strictEqual(timeResults.length, expectedIterations);
          for (let it = 1; it < expectedIterations; it++) {
            assert.strictEqual(timeResults[it - 1], interval * it);
          }
        });

        it('should always return the same result as the original timers/promises/setInterval', async (t) => {
          t.mock.timers.enable({ apis: ['setInterval'] });
          for (const expectedResult of [undefined, null, false, true, 0, 0n, 1, 1n, '', 'result', {}]) {
            const intervalIterator = nodeTimersPromises.setInterval(2000, expectedResult);
            const p = intervalIterator.next();
            t.mock.timers.tick(2000);
            const result = await p;
            await intervalIterator.return();
            assert.strictEqual(result.done, false);
            assert.strictEqual(result.value, expectedResult);
          }
        });

        it('should abort operation given an abort controller signal', async (t) => {
          t.mock.timers.enable({ apis: ['setInterval'] });

          const interval = 100;
          const abortController = new AbortController();
          const intervalIterator = nodeTimersPromises.setInterval(interval, Date.now(), {
            signal: abortController.signal,
          });

          const first = intervalIterator.next();
          const second = intervalIterator.next();

          t.mock.timers.tick(interval);
          abortController.abort();
          t.mock.timers.tick(interval);

          const firstResult = await first;
          // Interval * 2 because value can be a little bit greater than interval
          assert.ok(firstResult.value < Date.now() + interval * 2);
          assert.strictEqual(firstResult.done, false);

          await assert.rejects(() => second, {
            name: 'AbortError',
          });
        });

        it('should abort operation when .abort is called before calling setInterval', async (t) => {
          t.mock.timers.enable({ apis: ['setInterval'] });

          const interval = 100;
          const abortController = new AbortController();
          abortController.abort();
          const intervalIterator = nodeTimersPromises.setInterval(interval, Date.now(), {
            signal: abortController.signal,
          });

          const first = intervalIterator.next();
          t.mock.timers.tick(interval);

          await assert.rejects(() => first, {
            name: 'AbortError',
          });
        });

        it('should abort operation given an abort controller signal on a real use case', async (t) => {
          t.mock.timers.enable({ apis: ['setInterval'] });
          const controller = new AbortController();
          const signal = controller.signal;
          const interval = 200;
          const expectedIterations = 2;
          let numIterations = 0;
          async function run() {
            const it = nodeTimersPromises.setInterval(interval, undefined, { signal });
            for await (const _ of it) { // eslint-disable-line no-unused-vars
              numIterations += 1;
              if (numIterations === 5) break;
            }
          }

          const r = run();
          t.mock.timers.tick(interval);
          t.mock.timers.tick(interval);
          controller.abort();
          t.mock.timers.tick(interval);
          t.mock.timers.tick(interval);
          t.mock.timers.tick(interval);
          t.mock.timers.tick(interval);

          await assert.rejects(() => r, {
            name: 'AbortError',
          });
          assert.strictEqual(numIterations, expectedIterations);
        });

        // Test for https://github.com/nodejs/node/issues/50381
        it('should use the mocked interval', (t) => {
          t.mock.timers.enable({ apis: ['setInterval'] });
          const fn = t.mock.fn();
          setInterval(fn, 1000);
          assert.strictEqual(fn.mock.callCount(), 0);
          t.mock.timers.tick(1000);
          assert.strictEqual(fn.mock.callCount(), 1);
          t.mock.timers.tick(1);
          t.mock.timers.tick(1);
          t.mock.timers.tick(1);
          assert.strictEqual(fn.mock.callCount(), 1);
        });

        // Test for https://github.com/nodejs/node/issues/50382
        it('should not prevent due timers to be processed', async (t) => {
          t.mock.timers.enable({ apis: ['setInterval', 'setTimeout'] });
          const f1 = t.mock.fn();
          const f2 = t.mock.fn();

          setInterval(f1, 1000);
          setTimeout(f2, 1001);

          assert.strictEqual(f1.mock.callCount(), 0);
          assert.strictEqual(f2.mock.callCount(), 0);

          t.mock.timers.tick(1001);

          assert.strictEqual(f1.mock.callCount(), 1);
          assert.strictEqual(f2.mock.callCount(), 1);
        });
      });
    });
  });

  describe('scheduler Suite', () => {
    describe('scheduler.wait', () => {
      it('should advance in time and trigger timers when calling the .tick function', (t) => {
        t.mock.timers.enable({ apis: ['scheduler.wait'] });

        const now = Date.now();
        const durationAtMost = 100;

        const p = nodeTimersPromises.scheduler.wait(4000);
        t.mock.timers.tick(4000);

        return p.then(common.mustCall((result) => {
          assert.strictEqual(result, undefined);
          assert.ok(
            Date.now() - now < durationAtMost,
            `time should be advanced less than the ${durationAtMost}ms`
          );
        }));
      });

      it('should advance in time and trigger timers when calling the .tick function multiple times', async (t) => {
        t.mock.timers.enable({ apis: ['scheduler.wait'] });

        const fn = t.mock.fn();

        nodeTimersPromises.scheduler.wait(9999).then(fn);

        t.mock.timers.tick(8999);
        assert.strictEqual(fn.mock.callCount(), 0);
        t.mock.timers.tick(500);

        await nodeTimersPromises.setImmediate();

        assert.strictEqual(fn.mock.callCount(), 0);
        t.mock.timers.tick(500);

        await nodeTimersPromises.setImmediate();
        assert.strictEqual(fn.mock.callCount(), 1);
      });

      it('should work with the same params as the original timers/promises/scheduler.wait', async (t) => {
        t.mock.timers.enable({ apis: ['scheduler.wait'] });
        const controller = new AbortController();
        const p = nodeTimersPromises.scheduler.wait(2000, {
          ref: true,
          signal: controller.signal,
        });

        t.mock.timers.tick(1000);
        t.mock.timers.tick(500);
        t.mock.timers.tick(500);
        t.mock.timers.tick(500);

        const result = await p;
        assert.strictEqual(result, undefined);
      });

      it('should abort operation if timers/promises/scheduler.wait received an aborted signal', async (t) => {
        t.mock.timers.enable({ apis: ['scheduler.wait'] });
        const controller = new AbortController();
        const p = nodeTimersPromises.scheduler.wait(2000, {
          ref: true,
          signal: controller.signal,
        });

        t.mock.timers.tick(1000);
        controller.abort();
        t.mock.timers.tick(500);
        t.mock.timers.tick(500);
        t.mock.timers.tick(500);

        await assert.rejects(() => p, {
          name: 'AbortError',
        });
      });
      it('should abort operation even if the .tick was not called', async (t) => {
        t.mock.timers.enable({ apis: ['scheduler.wait'] });
        const controller = new AbortController();
        const p = nodeTimersPromises.scheduler.wait(2000, {
          ref: true,
          signal: controller.signal,
        });

        controller.abort();

        await assert.rejects(() => p, {
          name: 'AbortError',
        });
      });

      it('should abort operation when .abort is called before calling setInterval', async (t) => {
        t.mock.timers.enable({ apis: ['scheduler.wait'] });
        const controller = new AbortController();
        controller.abort();
        const p = nodeTimersPromises.scheduler.wait(2000, {
          ref: true,
          signal: controller.signal,
        });

        await assert.rejects(() => p, {
          name: 'AbortError',
        });
      });

      it('should reject given an an invalid signal instance', async (t) => {
        t.mock.timers.enable({ apis: ['scheduler.wait'] });
        const p = nodeTimersPromises.scheduler.wait(2000, {
          ref: true,
          signal: {},
        });

        await assert.rejects(() => p, {
          name: 'TypeError',
          code: 'ERR_INVALID_ARG_TYPE',
        });
      });

    });
  });

  describe('Date Suite', () => {
    it('should return the initial UNIX epoch if not specified', (t) => {
      t.mock.timers.enable({ apis: ['Date'] });
      const date = new Date();
      assert.strictEqual(date.getTime(), 0);
      assert.strictEqual(Date.now(), 0);
    });

    it('should throw an error if setTime is called without enabling timers', (t) => {
      assert.throws(
        () => {
          t.mock.timers.setTime(100);
        },
        { code: 'ERR_INVALID_STATE' }
      );
    });

    it('should throw an error if epoch passed to enable is not valid', (t) => {
      assert.throws(
        () => {
          t.mock.timers.enable({ now: -1 });
        },
        { code: 'ERR_INVALID_ARG_VALUE' }
      );

      assert.throws(
        () => {
          t.mock.timers.enable({ now: 'string' });
        },
        { code: 'ERR_INVALID_ARG_TYPE' }
      );

      assert.throws(
        () => {
          t.mock.timers.enable({ now: NaN });
        },
        { code: 'ERR_INVALID_ARG_VALUE' }
      );
    });

    it('should replace the original Date with the mocked one', (t) => {
      t.mock.timers.enable({ apis: ['Date'] });
      assert.ok(Date.isMock);
    });

    it('should return the ticked time when calling Date.now after tick', (t) => {
      t.mock.timers.enable({ apis: ['Date'] });
      const time = 100;
      t.mock.timers.tick(time);
      assert.strictEqual(Date.now(), time);
    });

    it('should return the Date as string when calling it as a function', (t) => {
      t.mock.timers.enable({ apis: ['Date'] });
      const returned = Date();
      // Matches the format: 'Mon Jan 01 1970 00:00:00'
      // We don't care about the date, just the format
      assert.ok(/\w{3}\s\w{3}\s\d{1,2}\s\d{2,4}\s\d{1,2}:\d{2}:\d{2}/.test(returned));
    });

    it('should return the date with different argument calls', (t) => {
      t.mock.timers.enable({ apis: ['Date'] });
      assert.strictEqual(new Date(0).getTime(), 0);
      assert.strictEqual(new Date(100).getTime(), 100);
      assert.strictEqual(new Date('1970-01-01T00:00:00.000Z').getTime(), 0);
      assert.strictEqual(new Date(1970, 0).getFullYear(), 1970);
      assert.strictEqual(new Date(1970, 0).getMonth(), 0);
      assert.strictEqual(new Date(1970, 0, 1).getDate(), 1);
      assert.strictEqual(new Date(1970, 0, 1, 11).getHours(), 11);
      assert.strictEqual(new Date(1970, 0, 1, 11, 10).getMinutes(), 10);
      assert.strictEqual(new Date(1970, 0, 1, 11, 10, 45).getSeconds(), 45);
      assert.strictEqual(new Date(1970, 0, 1, 11, 10, 45, 898).getMilliseconds(), 898);
      assert.strictEqual(new Date(1970, 0, 1, 11, 10, 45, 898).toDateString(), 'Thu Jan 01 1970');
    });

    it('should return native code when calling Date.toString', (t) => {
      t.mock.timers.enable({ apis: ['Date'] });
      assert.strictEqual(Date.toString(), 'function Date() { [native code] }');
    });

    it('should start with a custom epoch if the second argument is specified', (t) => {
      t.mock.timers.enable({ apis: ['Date'], now: 100 });
      const date1 = new Date();
      assert.strictEqual(date1.getTime(), 100);

      t.mock.timers.reset();
      t.mock.timers.enable({ apis: ['Date'], now: new Date(200) });
      const date2 = new Date();
      assert.strictEqual(date2.getTime(), 200);
    });

    it('should replace epoch if setTime is lesser than now and not tick', (t) => {
      t.mock.timers.enable();
      const fn = t.mock.fn();
      const id = setTimeout(fn, 1000);
      t.mock.timers.setTime(800);
      assert.strictEqual(Date.now(), 800);
      t.mock.timers.setTime(500);
      assert.strictEqual(Date.now(), 500);
      assert.strictEqual(fn.mock.callCount(), 0);
      clearTimeout(id);
    });

    it('should not tick time when setTime is called', (t) => {
      t.mock.timers.enable();
      const fn = t.mock.fn();
      const id = setTimeout(fn, 1000);
      t.mock.timers.setTime(1200);
      assert.strictEqual(Date.now(), 1200);
      assert.strictEqual(fn.mock.callCount(), 0);
      clearTimeout(id);
    });
  });

  describe('Api should have same public properties as original', () => {
    it('should have hasRef', (t) => {
      t.mock.timers.enable();
      const timer = setTimeout();
      assert.strictEqual(typeof timer.hasRef, 'function');
      assert.strictEqual(timer.hasRef(), true);
      clearTimeout(timer);
    });

    it('should have ref', (t) => {
      t.mock.timers.enable();
      const timer = setTimeout();
      assert.ok(typeof timer.ref === 'function');
      assert.deepStrictEqual(timer.ref(), timer);
      clearTimeout(timer);
    });

    it('should have unref', (t) => {
      t.mock.timers.enable();
      const timer = setTimeout();
      assert.ok(typeof timer.unref === 'function');
      assert.deepStrictEqual(timer.unref(), timer);
      clearTimeout(timer);
    });

    it('should have refresh', (t) => {
      t.mock.timers.enable();
      const timer = setTimeout();
      assert.ok(typeof timer.refresh === 'function');
      assert.deepStrictEqual(timer.refresh(), timer);
      clearTimeout(timer);
    });
  });
});
