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

goog.module('goog.testing.MockClockTest');
goog.setTestOnly();

const GoogPromise = goog.require('goog.Promise');
const MockClock = goog.require('goog.testing.MockClock');
const PropertyReplacer = goog.require('goog.testing.PropertyReplacer');
const Timer = goog.require('goog.Timer');
const array = goog.require('goog.array');
const events = goog.require('goog.events');
const functions = goog.require('goog.functions');
const googAsyncNextTick = goog.require('goog.async.nextTick');
const googAsyncRun = goog.require('goog.async.run');
const recordFunction = goog.require('goog.testing.recordFunction');
const testSuite = goog.require('goog.testing.testSuite');

const stubs = new PropertyReplacer();

/**
 * Waits using native promises.
 *
 * @param {number} ms
 * @return {!Promise<void>} Resolves after ms
 */
function waitFor(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

testSuite({
  tearDown() {
    stubs.reset();
  },

  testAsyncSupport: {
    setUp() {
      this.clock = new MockClock(true);
    },

    tearDown() {
      this.clock.uninstall();
    },

    async testInterleavedGoogPromiseCallbacks() {
      const result = [];

      await GoogPromise.resolve(null);
      result.push(1);

      setTimeout(() => result.push(3), 5);
      result.push(2);
      await this.clock.tickAsync(6);
      result.push(4);

      const promise = GoogPromise.resolve(null)
                          .then(() => Timer.promise(5))
                          .then(() => result.push(5))
                          .then(() => Timer.promise(5));

      await this.clock.tickAsync(11);
      await promise;
      result.push(6);

      assertArrayEquals([1, 2, 3, 4, 5, 6], result);
    },

    async testTimerInNativePromise() {
      const result = [];
      const promise = Promise.resolve()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then(() => Timer.promise(5))
                          .then(() => result.push(1))
                          .then(() => waitFor(5));

      await this.clock.tickAsync(11);
      await promise;
      result.push(2);

      assertArrayEquals([1, 2], result);
    },

    async testInterleavedNativePromiseCallbacks() {
      function waitFor(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
      }
      const result = [];

      await Promise.resolve();
      result.push(1);

      setTimeout(() => result.push(3), 5);
      result.push(2);
      await this.clock.tickAsync(6);
      result.push(4);

      const promise = Timer.promise(5)
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then(() => result.push(5))
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then(() => waitFor(5))
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then()
                          .then(() => result.push(6));

      await this.clock.tickAsync(11);
      await promise;
      result.push(7);

      assertArrayEquals([1, 2, 3, 4, 5, 6, 7], result);
    },

    async testSequenceOfTimeouts() {
      const result = [];

      async function runAsyncCode() {
        for (var i = 0; i < 30; i += 3) {
          result.push(i);
          await waitFor(10);
          result.push(i + 1);
          await Timer.promise(10);
          result.push(i + 2);
        }
      }

      runAsyncCode();
      await this.clock.tickAsync(600);

      assertArrayEquals(array.range(30), result);
    },

    async testTickAsyncInterval() {
      let counter = 0;
      const intervalId = setInterval(() => {
        counter++;
      }, 100);
      await this.clock.tickAsync(99);
      assertEquals(counter, 0);
      await this.clock.tickAsync(1);
      assertEquals(counter, 1);
      await this.clock.tickAsync(200);
      assertEquals(counter, 3);
      clearInterval(intervalId);
      await this.clock.tickAsync(100);
      assertEquals(counter, 3);
    },

    async testTickAsyncMustSettlePromiseThrows() {
      const error = /** @type {!Error} */ (await assertRejects(
          this.clock.tickAsyncMustSettlePromise(0, Promise.resolve())));
      assertEquals(
          error.message,
          'Assertion failed: Synchronous MockClock does not support ' +
              'tickAsyncMustSettlePromise.');
    },
  },

  testAsyncMockClock: {
    setUp() {
      this.clock = MockClock.createAsyncMockClock();
      this.clock.install();
    },

    tearDown() {
      this.clock.dispose();
    },

    async testTickAsyncResolvesGoogPromise() {
      const promise = Timer.promise(100).then(() => 'value');
      await this.clock.tickAsync(100);
      assertEquals(await promise, 'value');
    },

    async testTickAsyncResolvesPromise() {
      const promise = waitFor(100).then(() => 'value');
      await this.clock.tickAsync(100);
      assertEquals(await promise, 'value');
    },

    async testTickAsyncMustSettlePromiseRejectsGoogPromise() {
      const promise = Timer.promise(100).then(() => {
        throw new Error('reject');
      });
      const error = /** @type {!Error} */ (await assertRejects(
          this.clock.tickAsyncMustSettlePromise(100, promise)));
      assertEquals(error.message, 'reject');
    },

    async testTickAsyncMustSettlePromiseRejectsPromise() {
      const promise = waitFor(100).then(() => {
        throw new Error('reject');
      });

      const error =
          /** @type {!Error} */ (await this.clock.tickAsyncMustSettlePromise(
              100, assertRejects(promise)));
      assertEquals(error.message, 'reject');
    },

    async testTickAsyncMustSettlePromiseResolvesGoogPromise() {
      const promise = Timer.promise(100).then(() => 'value');
      assertEquals(
          await this.clock.tickAsyncMustSettlePromise(100, promise), 'value');
    },

    async testTickAsyncMustSettlePromiseResolvesPromise() {
      const promise = waitFor(100).then(() => 'value');
      assertEquals(
          await this.clock.tickAsyncMustSettlePromise(100, promise), 'value');
    },

    async testTickThrows() {
      assertThrows(
          'Async MockClock does not support tick. Use tickAsync() instead.',
          () => this.clock.tick());
    },

    async testTickPromiseThrows() {
      assertThrows(
          'Async MockClock does not support tickPromise.',
          () => this.clock.tickPromise(waitFor(100), 100));
    }
  },

  testTimeWarp: {
    async testSetsTime() {
      const clock = new MockClock(true);

      const newDate = new Date(2121, 12, 12);

      // Schedule an callback in the interval and make sure it sees the new date
      setTimeout(() => assertEquals(+newDate, Date.now()), 999999);

      await clock.doTimeWarpAsync(newDate);
      assertEquals(+newDate, Date.now());
    },

    async testSkipsSetIntervals() {
      const clock = new MockClock(true);

      const fn = recordFunction();
      setInterval(fn, 7);
      await clock.doTimeWarpAsync(new Date(2121, 12, 12));

      assertEquals(1, fn.getCallCount());
      // Expect the interval to continue in future ticks.
      clock.tick(7);
      assertEquals(2, fn.getCallCount());
    },

    async testSkipsRecursiveSetTimeouts() {
      const clock = new MockClock(true);

      const fn = recordFunction(() => setTimeout(fn, 7));
      setTimeout(fn, 7);
      await clock.doTimeWarpAsync(new Date(2121, 12, 12));

      assertEquals(1, fn.getCallCount());
      // Expect the timeout to continue in future ticks.
      clock.tick(7);
      assertEquals(2, fn.getCallCount());
    },
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testMockClockWasInstalled() {
    const clock = new MockClock();
    const originalTimeout = window.setTimeout;
    clock.install();
    assertNotEquals(window.setTimeout, originalTimeout);
    setTimeout(() => {}, 100);
    assertEquals(1, clock.getTimeoutsMade());
    setInterval(() => {}, 200);
    assertEquals(2, clock.getTimeoutsMade());
    clock.uninstall();
    assertEquals(window.setTimeout, originalTimeout);
    assertNull(clock.replacer_);
  },

  testSetTimeoutAndTick() {
    const clock = new MockClock(true);
    let m10 = false;
    let m15 = false;
    let m20 = false;
    let m5 = false;

    setTimeout(() => {
      m5 = true;
    }, 5);
    setTimeout(() => {
      m10 = true;
    }, 10);
    setTimeout(() => {
      m15 = true;
    }, 15);
    setTimeout(() => {
      m20 = true;
    }, 20);
    assertEquals(4, clock.getTimeoutsMade());

    assertEquals(4, clock.tick(4));
    assertEquals(4, clock.getCurrentTime());
    assertEquals(0, clock.getCallbacksTriggered());

    assertFalse(m5);
    assertFalse(m10);
    assertFalse(m15);
    assertFalse(m20);

    assertEquals(5, clock.tick(1));
    assertEquals(5, clock.getCurrentTime());
    assertEquals(1, clock.getCallbacksTriggered());

    assertTrue('m5 should now be true', m5);
    assertFalse(m10);
    assertFalse(m15);
    assertFalse(m20);

    assertEquals(10, clock.tick(5));
    assertEquals(10, clock.getCurrentTime());
    assertEquals(2, clock.getCallbacksTriggered());

    assertTrue('m5 should be true', m5);
    assertTrue('m10 should now be true', m10);
    assertFalse(m15);
    assertFalse(m20);

    assertEquals(15, clock.tick(5));
    assertEquals(15, clock.getCurrentTime());
    assertEquals(3, clock.getCallbacksTriggered());

    assertTrue('m5 should be true', m5);
    assertTrue('m10 should be true', m10);
    assertTrue('m15 should now be true', m15);
    assertFalse(m20);

    assertEquals(20, clock.tick(5));
    assertEquals(20, clock.getCurrentTime());
    assertEquals(4, clock.getCallbacksTriggered());

    assertTrue('m5 should be true', m5);
    assertTrue('m10 should be true', m10);
    assertTrue('m15 should be true', m15);
    assertTrue('m20 should now be true', m20);

    clock.uninstall();
  },

  testSetImmediateAndTick() {
    const clock = new MockClock(true);
    let tick0 = false;
    let tick1 = false;
    setImmediate(() => {
      tick0 = true;
    });
    setImmediate(() => {
      tick1 = true;
    });
    assertEquals(2, clock.getTimeoutsMade());
    assertEquals(0, clock.getCallbacksTriggered());

    clock.tick(0);
    assertEquals(2, clock.getCallbacksTriggered());
    assertTrue(tick0);
    assertTrue(tick1);

    clock.uninstall();
  },

  testAsyncNextTick() {
    const clock = new MockClock(true);
    let tick0 = false;
    let tick1 = false;
    googAsyncNextTick(() => {
      tick0 = true;
    });
    googAsyncNextTick(() => {
      tick1 = true;
    });
    assertEquals(2, clock.getTimeoutsMade());
    assertEquals(0, clock.getCallbacksTriggered());

    clock.tick(0);
    assertEquals(2, clock.getCallbacksTriggered());
    assertTrue(tick0);
    assertTrue(tick1);

    clock.uninstall();
  },

  async testAsyncNextTick_AsyncClock() {
    const clock = MockClock.createAsyncMockClock();
    clock.install();
    let tick0 = false;
    let tick1 = false;
    googAsyncNextTick(() => {
      tick0 = true;
    });
    googAsyncNextTick(() => {
      tick1 = true;
    });
    assertEquals(2, clock.getTimeoutsMade());
    assertEquals(0, clock.getCallbacksTriggered());

    await clock.tickAsync(0);
    assertEquals(2, clock.getCallbacksTriggered());
    assertTrue(tick0);
    assertTrue(tick1);

    clock.uninstall();
  },

  testSetInterval() {
    const clock = new MockClock(true);
    let times = 0;
    setInterval(() => {
      times++;
    }, 100);

    clock.tick(500);
    assertEquals(5, clock.getCallbacksTriggered());
    assertEquals(5, times);
    clock.tick(100);
    assertEquals(6, clock.getCallbacksTriggered());
    assertEquals(6, times);
    clock.tick(100);
    assertEquals(7, clock.getCallbacksTriggered());
    assertEquals(7, times);
    clock.tick(50);
    assertEquals(7, clock.getCallbacksTriggered());
    assertEquals(7, times);
    clock.tick(50);
    assertEquals(8, clock.getCallbacksTriggered());
    assertEquals(8, times);

    clock.uninstall();
  },

  testRequestAnimationFrame() {
    globalThis.requestAnimationFrame = () => {};
    const clock = new MockClock(true);
    const times = [];
    const recFunc = recordFunction((now) => {
      times.push(now);
    });
    globalThis.requestAnimationFrame(recFunc);
    clock.tick(50);
    assertEquals(1, clock.getCallbacksTriggered());
    assertEquals(1, recFunc.getCallCount());
    assertEquals(20, times[0]);

    globalThis.requestAnimationFrame(recFunc);
    clock.tick(100);
    assertEquals(2, clock.getCallbacksTriggered());
    assertEquals(2, recFunc.getCallCount());
    assertEquals(70, times[1]);

    clock.uninstall();
  },

  testClearTimeout() {
    const clock = new MockClock(true);
    let ran = false;
    const c = setTimeout(() => {
      ran = true;
    }, 100);
    clock.tick(50);
    assertFalse(ran);
    clearTimeout(c);
    clock.tick(100);
    assertFalse(ran);
    clock.uninstall();
  },

  testClearInterval() {
    const clock = new MockClock(true);
    let times = 0;
    const c = setInterval(() => {
      times++;
    }, 100);

    clock.tick(500);
    assertEquals(5, times);
    clock.tick(100);
    assertEquals(6, times);
    clock.tick(100);
    clearInterval(c);
    assertEquals(7, times);
    clock.tick(50);
    assertEquals(7, times);
    clock.tick(50);
    assertEquals(7, times);

    clock.uninstall();
  },

  testClearInterval2() {
    // Tests that we can clear the interval from inside the function
    const clock = new MockClock(true);
    let times = 0;
    const c = setInterval(() => {
      times++;
      if (times == 6) {
        clearInterval(c);
      }
    }, 100);

    clock.tick(500);
    assertEquals(5, times);
    clock.tick(100);
    assertEquals(6, times);
    clock.tick(100);
    assertEquals(6, times);
    clock.tick(50);
    assertEquals(6, times);
    clock.tick(50);
    assertEquals(6, times);

    clock.uninstall();
  },

  testCancelRequestAnimationFrame() {
    globalThis.requestAnimationFrame = () => {};
    globalThis.cancelRequestAnimationFrame = () => {};
    const clock = new MockClock(true);
    let ran = false;
    const c = globalThis.requestAnimationFrame(() => {
      ran = true;
    });
    clock.tick(10);
    assertFalse(ran);
    globalThis.cancelRequestAnimationFrame(c);
    clock.tick(20);
    assertFalse(ran);
    clock.uninstall();
  },

  testMockGoogNow() {
    assertNotEquals(0, goog.now());
    const clock = new MockClock(true);
    assertEquals(0, goog.now());
    clock.tick(50);
    assertEquals(50, goog.now());
    clock.uninstall();
    assertNotEquals(50, goog.now());
  },

  testMockDateNow() {
    assertNotEquals(0, Date.now());
    const clock = new MockClock(true);
    assertEquals(0, Date.now());
    clock.tick(50);
    assertEquals(50, Date.now());
    clock.uninstall();
    assertNotEquals(50, Date.now());
  },

  testMockDateNow_unmockDateNow_autoInstall() {
    assertNotEquals(0, Date.now());
    const clock = new MockClock(true);
    clock.unmockDateNow();
    assertNotEquals(0, Date.now());
    clock.uninstall();
    assertNotEquals(0, Date.now());
  },

  testMockDateNow_unmockDateNow_manualInstall() {
    assertNotEquals(0, Date.now());
    const clock = new MockClock();
    clock.unmockDateNow();
    clock.install();
    assertNotEquals(0, Date.now());
    clock.uninstall();
    assertNotEquals(0, Date.now());
  },

  testTimeoutDelay() {
    const clock = new MockClock(true);
    let m10 = false;
    let m20 = false;
    let m5 = false;

    setTimeout(() => {
      m5 = true;
    }, 5);
    setTimeout(() => {
      m10 = true;
    }, 10);
    setTimeout(() => {
      m20 = true;
    }, 20);

    // Fire 3ms early, so m5 fires at t=2
    clock.setTimeoutDelay(-3);
    clock.tick(1);
    assertFalse(m5);
    assertFalse(m10);
    clock.tick(1);
    assertTrue(m5);
    assertFalse(m10);

    // Fire 3ms late, so m10 fires at t=13
    clock.setTimeoutDelay(3);
    assertEquals(12, clock.tick(10));
    assertEquals(12, clock.getCurrentTime());
    assertFalse(m10);
    clock.tick(1);
    assertTrue(m10);
    assertFalse(m20);

    // Fire 10ms early, so m20 fires now, since it's after t=10
    clock.setTimeoutDelay(-10);
    assertFalse(m20);
    assertEquals(14, clock.tick(1));
    assertEquals(14, clock.getCurrentTime());
    assertTrue(m20);

    clock.uninstall();
  },

  testTimerCallbackCanCreateIntermediateTimer() {
    const clock = new MockClock(true);
    const sequence = [];

    // Create 3 timers: 1, 2, and 3.  Timer 1 should fire at T=1, timer 2 at
    // T=2, and timer 3 at T=3.  The catch: Timer 2 is created by the
    // callback within timer 0.

    // Testing method: Create a simple string sequencing each timer and at
    // what time it fired.

    setTimeout(() => {
      sequence.push('timer1 at T=' + goog.now());
      setTimeout(() => {
        sequence.push('timer2 at T=' + goog.now());
      }, 1);
    }, 1);

    setTimeout(() => {
      sequence.push('timer3 at T=' + goog.now());
    }, 3);

    clock.tick(4);

    assertEquals(
        'Each timer should fire in sequence at the correct time.',
        'timer1 at T=1, timer2 at T=2, timer3 at T=3', sequence.join(', '));

    clock.uninstall();
  },

  testCorrectArgumentsPassedToCallback() {
    const clock = new MockClock(true);
    let timeoutId;
    let timeoutExecuted = false;

    timeoutId = setTimeout(function(arg) {
      assertEquals('"this" must be globalThis', globalThis, this);
      assertEquals(
          'The timeout ID must be the first parameter', timeoutId, arg);
      assertEquals('Exactly one argument must be passed', 1, arguments.length);
      timeoutExecuted = true;
    }, 1);

    clock.tick(4);

    assertTrue('The timeout was not executed', timeoutExecuted);

    clock.uninstall();
  },

  testTickZero() {
    const clock = new MockClock(true);
    let calls = 0;

    setTimeout(() => {
      assertEquals('I need to be first', 0, calls);
      calls++;
    }, 0);

    setTimeout(() => {
      assertEquals('I need to be second', 1, calls);
      calls++;
    }, 0);

    clock.tick(0);
    assertEquals(2, calls);

    setTimeout(() => {
      assertEquals('I need to be third', 2, calls);
      calls++;
    }, 0);

    clock.tick(0);
    assertEquals(3, calls);

    assertEquals('Time should still be zero', 0, goog.now());

    clock.uninstall();
  },

  testReset() {
    const clock = new MockClock(true);

    const id = setTimeout(() => {
      fail('Timeouts should be cleared after a reset');
    }, 0);

    clock.reset();
    clock.tick(999999);

    let calls = 0;
    setTimeout(() => {
      calls++;
    }, 10);
    clearTimeout(id);
    clock.tick(100);
    assertEquals(
        'New timeout should still run after clearing from before reset', 1,
        calls);

    clock.uninstall();
  },

  testNewClockWithOldTimeoutId() {
    let clock = new MockClock(true);

    const id = setTimeout(() => {
      fail('Timeouts should be cleared after uninstall');
    }, 0);

    clock.uninstall();
    clock = new MockClock(true);

    let calls = 0;
    setTimeout(() => {
      calls++;
    }, 10);
    clearTimeout(id);
    clock.tick(100);
    assertEquals(
        'Timeout should still run after cancelling from old clock', 1, calls);
    clock.uninstall();
  },

  /**
     @suppress {visibility,checkTypes} suppression added to enable type
     checking
   */
  testQueueInsertionHelper() {
    const queue = [];

    function queueToString() {
      const buffer = [];
      for (let i = 0; i < queue.length; i++) {
        buffer.push(queue[i].runAtMillis);
      }
      return buffer.join(',');
    }

    MockClock.insert_({runAtMillis: 2}, queue);
    assertEquals('Only item', '2', queueToString());

    MockClock.insert_({runAtMillis: 4}, queue);
    assertEquals('Biggest item', '4,2', queueToString());

    MockClock.insert_({runAtMillis: 5}, queue);
    assertEquals('An even bigger item', '5,4,2', queueToString());

    MockClock.insert_({runAtMillis: 1}, queue);
    assertEquals('Smallest item', '5,4,2,1', queueToString());

    MockClock.insert_({runAtMillis: 1, dup: true}, queue);
    assertEquals('Duplicate smallest item', '5,4,2,1,1', queueToString());
    assertTrue('Duplicate item comes at a smaller index', queue[3].dup);

    MockClock.insert_({runAtMillis: 3}, queue);
    MockClock.insert_({runAtMillis: 3, dup: true}, queue);
    assertEquals('Duplicate a middle item', '5,4,3,3,2,1,1', queueToString());
    assertTrue('Duplicate item comes at a smaller index', queue[2].dup);
  },

  testIsTimeoutSet() {
    const clock = new MockClock(true);
    const timeoutKey = setTimeout(() => {}, 1);
    assertTrue(
        `Timeout ${timeoutKey} should be set`, clock.isTimeoutSet(timeoutKey));
    const nextTimeoutKey = timeoutKey + 1;
    assertFalse(
        `Timeout ${nextTimeoutKey} should not be set`,
        clock.isTimeoutSet(nextTimeoutKey));
    clearTimeout(timeoutKey);
    assertFalse(
        `Timeout ${timeoutKey} should no longer be set`,
        clock.isTimeoutSet(timeoutKey));
    const newTimeoutKey = setTimeout(() => {}, 1);
    clock.tick(5);
    assertFalse(
        `Timeout ${timeoutKey} should not be set`,
        clock.isTimeoutSet(timeoutKey));
    assertTrue(
        `Timeout ${newTimeoutKey} should be set`,
        clock.isTimeoutSet(newTimeoutKey));
    clock.uninstall();
  },

  testBalksOnTimeoutsGreaterThanMaxInt() {
    // Browsers have trouble with timeout greater than max int, so we
    // want Mock Clock to fail if this happens.
    const clock = new MockClock(true);
    // Functions on window don't seem to be able to throw exceptions in
    // IE6.  Explicitly reading the property makes it work.
    const setTimeout = window.setTimeout;
    assertThrows('Timeouts > MAX_INT should fail', () => {
      setTimeout(() => {}, 2147483648);
    });
    assertThrows('Timeouts much greater than MAX_INT should fail', () => {
      setTimeout(() => {}, 2147483648 * 10);
    });
    clock.uninstall();
  },

  testCorrectSetTimeoutIsRestored() {
    const safe = functions.error('should not have been called');
    stubs.set(window, 'setTimeout', safe);

    const clock = new MockClock(true);
    assertNotEquals('setTimeout is replaced', safe, window.setTimeout);
    clock.uninstall();
    // NOTE: If this assertion proves to be flaky in IE, the string value of
    // the two functions have to be compared as described in
    // goog.testing.TestCase#finalize.
    assertEquals('setTimeout is restored', safe, window.setTimeout);
  },

  testMozRequestAnimationFrame() {
    // Setting this function will indirectly tell the mock clock to mock it out.
    stubs.set(window, 'mozRequestAnimationFrame', () => {});

    const clock = new MockClock(true);

    const mozBeforePaint = recordFunction();
    events.listen(window, 'MozBeforePaint', mozBeforePaint);

    window.mozRequestAnimationFrame(null);
    assertEquals(0, mozBeforePaint.getCallCount());

    clock.tick(MockClock.REQUEST_ANIMATION_FRAME_TIMEOUT);
    assertEquals(1, mozBeforePaint.getCallCount());
    clock.dispose();
  },

  testClearBeforeSet() {
    const clock = new MockClock(true);
    const expectedId = MockClock.nextId;
    window.clearTimeout(expectedId);

    const fn = recordFunction();
    const actualId = window.setTimeout(fn, 0);
    assertEquals(
        'In order for this test to work, we have to guess the ids in advance',
        expectedId, actualId);
    clock.tick(1);
    assertEquals(1, fn.getCallCount());
    clock.dispose();
  },

  testNonFunctionArguments() {
    const clock = new MockClock(true);

    // Unlike normal setTimeout and friends, we only accept functions (not
    // strings, not undefined, etc). Make sure that if we get a non-function, we
    // fail early rather than on the next .tick() operation.

    assertThrows(
        'setTimeout with a non-function value should fail', /**
                                                               @suppress {checkTypes}
                                                               suppression added
                                                               to enable type
                                                               checking
                                                             */
        () => {
          window.setTimeout(undefined, 0);
        });
    clock.tick(1);

    assertThrows('setTimeout with a string should fail', () => {
      window.setTimeout('throw new Error("setTimeout string eval!");', 0);
    });
    clock.tick(1);

    clock.dispose();
  },

  testUnspecifiedTimeout() {
    const clock = new MockClock(true);
    let m0a = false;
    let m0b = false;
    let m10 = false;

    setTimeout(() => {
      m0a = true;
    });
    setTimeout(() => {
      m10 = true;
    }, 10);
    assertEquals(2, clock.getTimeoutsMade());

    assertFalse(m0a);
    assertFalse(m0b);
    assertFalse(m10);

    assertEquals(0, clock.tick(0));
    assertEquals(0, clock.getCurrentTime());

    assertTrue(m0a);
    assertFalse(m0b);
    assertFalse(m10);

    setTimeout(() => {
      m0b = true;
    });
    assertEquals(3, clock.getTimeoutsMade());

    assertEquals(0, clock.tick(0));
    assertEquals(0, clock.getCurrentTime());

    assertTrue(m0a);
    assertTrue(m0b);
    assertFalse(m10);

    assertEquals(10, clock.tick(10));
    assertEquals(10, clock.getCurrentTime());

    assertTrue(m0a);
    assertTrue(m0b);
    assertTrue(m10);

    clock.uninstall();
  },

  async testUninstallWithoutResetScheduler() {
    googAsyncRun.resetSchedulerForTest();
    const promise = GoogPromise.resolve();
    const clock = new MockClock(true);
    clock.tick(1);
    // Queue up a GoogPromise callback to execute. This will never execute
    // when the async queue is reset, leaving GoogPromise.executing_ set
    // forever.
    promise.then(() => {});
    clock.uninstall();

    let executorRan = false;
    promise.then(() => new Promise(() => {
                   executorRan = true;
                 }));
    // GoogPromise.then will be stuck waiting for executeCallbacks_ to run.
    await Promise.resolve();
    assertFalse(executorRan);
  },

  async testUninstallWithResetScheduler() {
    googAsyncRun.resetSchedulerForTest();
    const clock = new MockClock(true);
    const promise = GoogPromise.resolve();
    clock.tick(1);
    // Queue up a GoogPromise callback to execute. This will be executed the
    // next time the browser returns to the event queue.
    promise.then(() => {});
    clock.uninstall(true);

    let executorRan = false;
    promise.then(() => new Promise(() => {
                   executorRan = true;
                 }));
    // goog.async.run queue is flushed when returning to the event queue.
    await Promise.resolve();
    assertTrue(executorRan);
  },

  testUnspecifiedInterval() {
    const clock = new MockClock(true);
    let times = 0;
    const handle = setInterval(() => {
      if (++times >= 5) {
        clearInterval(handle);
      }
    });

    clock.tick(0);
    assertEquals(5, times);

    clock.uninstall();
  },

  testTickPromise() {
    const clock = new MockClock(true);

    const p = GoogPromise.resolve('foo');
    assertEquals('foo', clock.tickPromise(p));

    const rejected = GoogPromise.reject(new Error('failed'));
    let e = assertThrows(() => {
      clock.tickPromise(rejected);
    });
    assertEquals('failed', e.message);

    const delayed = Timer.promise(500, 'delayed');
    e = assertThrows(() => {
      clock.tickPromise(delayed);
    });
    assertEquals(
        'Promise was expected to be resolved after mock clock tick.',
        e.message);
    assertEquals('delayed', clock.tickPromise(delayed, 500));

    clock.dispose();
  },
});
