// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

// deno-lint-ignore-file no-console

import {
  assert,
  assertEquals,
  assertNotEquals,
  delay,
  execCode,
  unreachable,
} from "./test_util.ts";

Deno.test(async function functionParameterBindingSuccess() {
  const { promise, resolve } = Promise.withResolvers<void>();
  let count = 0;

  const nullProto = (newCount: number) => {
    count = newCount;
    resolve();
  };

  Reflect.setPrototypeOf(nullProto, null);

  setTimeout(nullProto, 500, 1);
  await promise;
  // count should be reassigned
  assertEquals(count, 1);
});

Deno.test(async function stringifyAndEvalNonFunctions() {
  // eval can only access global scope
  const global = globalThis as unknown as {
    globalPromise: ReturnType<typeof Promise.withResolvers<void>>;
    globalCount: number;
  };

  global.globalPromise = Promise.withResolvers<void>();
  global.globalCount = 0;

  const notAFunction =
    "globalThis.globalCount++; globalThis.globalPromise.resolve();" as unknown as () =>
      void;

  setTimeout(notAFunction, 500);

  await global.globalPromise.promise;

  // count should be incremented
  assertEquals(global.globalCount, 1);

  Reflect.deleteProperty(global, "globalPromise");
  Reflect.deleteProperty(global, "globalCount");
});

Deno.test(async function timeoutSuccess() {
  const { promise, resolve } = Promise.withResolvers<void>();
  let count = 0;
  setTimeout(() => {
    count++;
    resolve();
  }, 500);
  await promise;
  // count should increment
  assertEquals(count, 1);
});

Deno.test(async function timeoutEvalNoScopeLeak() {
  // eval can only access global scope
  const global = globalThis as unknown as {
    globalPromise: ReturnType<typeof Promise.withResolvers<Error>>;
  };
  global.globalPromise = Promise.withResolvers();
  setTimeout(
    `
    try {
      console.log(core);
      globalThis.globalPromise.reject(new Error("Didn't throw."));
    } catch (error) {
      globalThis.globalPromise.resolve(error);
    }` as unknown as () => void,
    0,
  );
  const error = await global.globalPromise.promise;
  assertEquals(error.name, "ReferenceError");
  Reflect.deleteProperty(global, "globalPromise");
});

Deno.test(async function evalPrimordial() {
  const global = globalThis as unknown as {
    globalPromise: ReturnType<typeof Promise.withResolvers<void>>;
  };
  global.globalPromise = Promise.withResolvers<void>();
  const originalEval = globalThis.eval;
  let wasCalled = false;
  globalThis.eval = (argument) => {
    wasCalled = true;
    return originalEval(argument);
  };
  setTimeout(
    "globalThis.globalPromise.resolve();" as unknown as () => void,
    0,
  );
  await global.globalPromise.promise;
  assert(!wasCalled);
  Reflect.deleteProperty(global, "globalPromise");
  globalThis.eval = originalEval;
});

Deno.test(async function timeoutArgs() {
  const { promise, resolve } = Promise.withResolvers<void>();
  const arg = 1;
  let capturedArgs: unknown[] = [];
  setTimeout(
    function () {
      capturedArgs = [...arguments];
      resolve();
    },
    10,
    arg,
    arg.toString(),
    [arg],
  );
  await promise;
  assertEquals(capturedArgs, [
    arg,
    arg.toString(),
    [arg],
  ]);
});

Deno.test(async function timeoutCancelSuccess() {
  let count = 0;
  const id = setTimeout(() => {
    count++;
  }, 1);
  // Cancelled, count should not increment
  clearTimeout(id);
  await delay(600);
  assertEquals(count, 0);
});

Deno.test(async function timeoutCancelMultiple() {
  function uncalled(): never {
    throw new Error("This function should not be called.");
  }

  // Set timers and cancel them in the same order.
  const t1 = setTimeout(uncalled, 10);
  const t2 = setTimeout(uncalled, 10);
  const t3 = setTimeout(uncalled, 10);
  clearTimeout(t1);
  clearTimeout(t2);
  clearTimeout(t3);

  // Set timers and cancel them in reverse order.
  const t4 = setTimeout(uncalled, 20);
  const t5 = setTimeout(uncalled, 20);
  const t6 = setTimeout(uncalled, 20);
  clearTimeout(t6);
  clearTimeout(t5);
  clearTimeout(t4);

  // Sleep until we're certain that the cancelled timers aren't gonna fire.
  await delay(50);
});

Deno.test(async function timeoutCancelInvalidSilentFail() {
  // Expect no panic
  const { promise, resolve } = Promise.withResolvers<void>();
  let count = 0;
  const id = setTimeout(() => {
    count++;
    // Should have no effect
    clearTimeout(id);
    resolve();
  }, 500);
  await promise;
  assertEquals(count, 1);

  // Should silently fail (no panic)
  clearTimeout(2147483647);
});

Deno.test(async function intervalSuccess() {
  const { promise, resolve } = Promise.withResolvers<void>();
  let count = 0;
  const id = setInterval(() => {
    count++;
    clearInterval(id);
    resolve();
  }, 100);
  await promise;
  // Clear interval
  clearInterval(id);
  // count should increment twice
  assertEquals(count, 1);
  // Similar false async leaking alarm.
  // Force next round of polling.
  await delay(0);
});

Deno.test(async function intervalCancelSuccess() {
  let count = 0;
  const id = setInterval(() => {
    count++;
  }, 1);
  clearInterval(id);
  await delay(500);
  assertEquals(count, 0);
});

Deno.test(async function intervalOrdering() {
  const timers: number[] = [];
  let timeouts = 0;
  function onTimeout() {
    ++timeouts;
    for (let i = 1; i < timers.length; i++) {
      clearTimeout(timers[i]);
    }
  }
  for (let i = 0; i < 10; i++) {
    timers[i] = setTimeout(onTimeout, 1);
  }
  await delay(500);
  assertEquals(timeouts, 1);
});

Deno.test(function intervalCancelInvalidSilentFail() {
  // Should silently fail (no panic)
  clearInterval(2147483647);
});

// If a repeating timer is dispatched, the next interval that should first is based on
// when the timer is dispatched, not when the timer handler completes.
Deno.test(async function callbackTakesLongerThanInterval() {
  const { promise, resolve } = Promise.withResolvers<void>();
  const output: number[] = [];
  let last = 0;
  const id = setInterval(() => {
    const now = performance.now();
    if (last > 0) {
      output.push(now - last);
      if (output.length >= 10) {
        resolve();
        clearTimeout(id);
      }
    }
    last = now;
    while (performance.now() - now < 300) {
      /* hot loop */
    }
  }, 100);
  await promise;
  const total = output.reduce((t, n) => t + n, 0) / output.length;
  console.log(output);
  assert(total < 350 && total > 299, "Total was out of range: " + total);
});

// https://github.com/denoland/deno/issues/11398
Deno.test(async function clearTimeoutAfterNextTimerIsDue1() {
  const { promise, resolve } = Promise.withResolvers<void>();

  setTimeout(() => {
    resolve();
  }, 300);

  const interval = setInterval(() => {
    Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 400);
    // Both the interval and the timeout's due times are now in the past.
    clearInterval(interval);
  }, 100);

  await promise;
});

// https://github.com/denoland/deno/issues/11398
Deno.test(async function clearTimeoutAfterNextTimerIsDue2() {
  const { promise, resolve } = Promise.withResolvers<void>();

  const timeout1 = setTimeout(unreachable, 100);

  setTimeout(() => {
    resolve();
  }, 200);

  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 300);
  // Both of the timeouts' due times are now in the past.
  clearTimeout(timeout1);

  await promise;
});

Deno.test(async function fireCallbackImmediatelyWhenDelayOverMaxValue() {
  let count = 0;
  setTimeout(() => {
    count++;
  }, 2 ** 31);
  await delay(1);
  assertEquals(count, 1);
});

Deno.test(async function timeoutCallbackThis() {
  const { promise, resolve } = Promise.withResolvers<void>();
  let capturedThis: unknown;
  const obj = {
    foo() {
      capturedThis = this;
      resolve();
    },
  };
  setTimeout(obj.foo, 1);
  await promise;
  assertEquals(capturedThis, globalThis);
});

Deno.test(async function timeoutBindThis() {
  const thisCheckPassed = [null, undefined, globalThis];

  const thisCheckFailed = [
    0,
    "",
    true,
    false,
    {},
    [],
    "foo",
    () => {},
    Object.prototype,
  ];

  for (const thisArg of thisCheckPassed) {
    const { promise, resolve } = Promise.withResolvers<void>();
    let hasThrown = 0;
    try {
      setTimeout.call(thisArg, () => resolve(), 1);
      hasThrown = 1;
    } catch (err) {
      if (err instanceof TypeError) {
        hasThrown = 2;
      } else {
        hasThrown = 3;
      }
    }
    await promise;
    assertEquals(hasThrown, 1);
  }

  for (const thisArg of thisCheckFailed) {
    let hasThrown = 0;
    try {
      setTimeout.call(thisArg, () => {}, 1);
      hasThrown = 1;
    } catch (err) {
      if (err instanceof TypeError) {
        hasThrown = 2;
      } else {
        hasThrown = 3;
      }
    }
    assertEquals(hasThrown, 2);
  }
});

Deno.test(function clearTimeoutShouldConvertToNumber() {
  let called = false;
  const obj = {
    valueOf(): number {
      called = true;
      return 1;
    },
  };
  clearTimeout((obj as unknown) as number);
  assert(called);
});

Deno.test(function setTimeoutShouldThrowWithBigint() {
  let hasThrown = 0;
  try {
    setTimeout(() => {}, (1n as unknown) as number);
    hasThrown = 1;
  } catch (err) {
    if (err instanceof TypeError) {
      hasThrown = 2;
    } else {
      hasThrown = 3;
    }
  }
  assertEquals(hasThrown, 2);
});

Deno.test(function clearTimeoutShouldThrowWithBigint() {
  let hasThrown = 0;
  try {
    clearTimeout((1n as unknown) as number);
    hasThrown = 1;
  } catch (err) {
    if (err instanceof TypeError) {
      hasThrown = 2;
    } else {
      hasThrown = 3;
    }
  }
  assertEquals(hasThrown, 2);
});

Deno.test(function testFunctionName() {
  assertEquals(clearTimeout.name, "clearTimeout");
  assertEquals(clearInterval.name, "clearInterval");
});

Deno.test(function testFunctionParamsLength() {
  assertEquals(setTimeout.length, 1);
  assertEquals(setInterval.length, 1);
  assertEquals(clearTimeout.length, 0);
  assertEquals(clearInterval.length, 0);
});

Deno.test(function clearTimeoutAndClearIntervalNotBeEquals() {
  assertNotEquals(clearTimeout, clearInterval);
});

Deno.test(async function timerOrdering() {
  const array: number[] = [];
  const { promise: donePromise, resolve } = Promise.withResolvers<void>();

  function push(n: number) {
    array.push(n);
    if (array.length === 6) {
      resolve();
    }
  }

  setTimeout(() => {
    push(1);
    setTimeout(() => push(4));
  }, 0);
  setTimeout(() => {
    push(2);
    setTimeout(() => push(5));
  }, 0);
  setTimeout(() => {
    push(3);
    setTimeout(() => push(6));
  }, 0);

  await donePromise;

  assertEquals(array, [1, 2, 3, 4, 5, 6]);
});

Deno.test(async function timerBasicMicrotaskOrdering() {
  let s = "";
  let count = 0;
  const { promise, resolve } = Promise.withResolvers<void>();
  setTimeout(() => {
    Promise.resolve().then(() => {
      count++;
      s += "de";
      if (count === 2) {
        resolve();
      }
    });
  });
  setTimeout(() => {
    count++;
    s += "no";
    if (count === 2) {
      resolve();
    }
  });
  await promise;
  assertEquals(s, "deno");
});

Deno.test(async function timerNestedMicrotaskOrdering() {
  let s = "";
  const { promise, resolve } = Promise.withResolvers<void>();
  s += "0";
  setTimeout(() => {
    s += "4";
    setTimeout(() => (s += "A"));
    Promise.resolve()
      .then(() => {
        setTimeout(() => {
          s += "B";
          resolve();
        });
      })
      .then(() => {
        s += "5";
      });
  });
  setTimeout(() => (s += "6"));
  Promise.resolve().then(() => (s += "2"));
  Promise.resolve().then(() =>
    setTimeout(() => {
      s += "7";
      Promise.resolve()
        .then(() => (s += "8"))
        .then(() => {
          s += "9";
        });
    })
  );
  Promise.resolve().then(() => Promise.resolve().then(() => (s += "3")));
  s += "1";
  await promise;
  assertEquals(s, "0123456789AB");
});

Deno.test(function testQueueMicrotask() {
  assertEquals(typeof queueMicrotask, "function");
});

Deno.test(async function timerIgnoresDateOverride() {
  const OriginalDate = Date;
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  let hasThrown = 0;
  try {
    const overrideCalled: () => number = () => {
      reject("global Date override used over original Date object");
      return 0;
    };
    const DateOverride = () => {
      overrideCalled();
    };
    globalThis.Date = DateOverride as DateConstructor;
    globalThis.Date.now = overrideCalled;
    globalThis.Date.UTC = overrideCalled;
    globalThis.Date.parse = overrideCalled;
    queueMicrotask(() => {
      resolve();
    });
    await promise;
    hasThrown = 1;
  } catch (err) {
    if (typeof err === "string") {
      assertEquals(err, "global Date override used over original Date object");
      hasThrown = 2;
    } else if (err instanceof TypeError) {
      hasThrown = 3;
    } else {
      hasThrown = 4;
    }
  } finally {
    globalThis.Date = OriginalDate;
  }
  assertEquals(hasThrown, 1);
});

Deno.test({
  name: "unrefTimer",
  permissions: { run: true, read: true },
  fn: async () => {
    const [statusCode, output] = await execCode(`
      const timer = setTimeout(() => console.log("1"), 1);
      Deno.unrefTimer(timer);
    `);
    assertEquals(statusCode, 0);
    assertEquals(output, "");
  },
});

Deno.test({
  name: "unrefTimer - mix ref and unref 1",
  permissions: { run: true, read: true },
  fn: async () => {
    const [statusCode, output] = await execCode(`
      const timer1 = setTimeout(() => console.log("1"), 200);
      const timer2 = setTimeout(() => console.log("2"), 400);
      const timer3 = setTimeout(() => console.log("3"), 600);
      Deno.unrefTimer(timer3);
    `);
    assertEquals(statusCode, 0);
    assertEquals(output, "1\n2\n");
  },
});

Deno.test({
  name: "unrefTimer - mix ref and unref 2",
  permissions: { run: true, read: true },
  fn: async () => {
    const [statusCode, output] = await execCode(`
      const timer1 = setTimeout(() => console.log("1"), 200);
      const timer2 = setTimeout(() => console.log("2"), 400);
      const timer3 = setTimeout(() => console.log("3"), 600);
      Deno.unrefTimer(timer1);
      Deno.unrefTimer(timer2);
    `);
    assertEquals(statusCode, 0);
    assertEquals(output, "1\n2\n3\n");
  },
});

Deno.test({
  name: "unrefTimer - unref interval",
  permissions: { run: true, read: true },
  fn: async () => {
    const [statusCode, output] = await execCode(`
      let i = 0;
      const timer1 = setInterval(() => {
        console.log("1");
        i++;
        if (i === 5) {
          Deno.unrefTimer(timer1);
        }
      }, 10);
    `);
    assertEquals(statusCode, 0);
    assertEquals(output, "1\n1\n1\n1\n1\n");
  },
});

Deno.test({
  name: "unrefTimer - unref then ref 1",
  permissions: { run: true, read: true },
  fn: async () => {
    const [statusCode, output] = await execCode(`
      const timer1 = setTimeout(() => console.log("1"), 10);
      Deno.unrefTimer(timer1);
      Deno.refTimer(timer1);
    `);
    assertEquals(statusCode, 0);
    assertEquals(output, "1\n");
  },
});

Deno.test({
  name: "unrefTimer - unref then ref",
  permissions: { run: true, read: true },
  fn: async () => {
    const [statusCode, output] = await execCode(`
      const timer1 = setTimeout(() => {
        console.log("1");
        Deno.refTimer(timer2);
      }, 10);
      const timer2 = setTimeout(() => console.log("2"), 20);
      Deno.unrefTimer(timer2);
    `);
    assertEquals(statusCode, 0);
    assertEquals(output, "1\n2\n");
  },
});

Deno.test({
  name: "unrefTimer - invalid calls do nothing",
  fn: () => {
    Deno.unrefTimer(NaN);
    Deno.refTimer(NaN);
  },
});

Deno.test({
  name: "AbortSignal.timeout() with no listeners",
  permissions: { run: true, read: true },
  fn: async () => {
    const [statusCode, output] = await execCode(`
      const signal = AbortSignal.timeout(2000);

      // This unref timer expires before the signal, and if it does expire, then
      // it means the signal has kept the event loop alive.
      const timer = setTimeout(() => console.log("Unexpected!"), 1500);
      Deno.unrefTimer(timer);
    `);
    assertEquals(statusCode, 0);
    assertEquals(output, "");
  },
});

Deno.test({
  name: "AbortSignal.timeout() with listeners",
  permissions: { run: true, read: true },
  fn: async () => {
    const [statusCode, output] = await execCode(`
      const signal = AbortSignal.timeout(1000);
      signal.addEventListener("abort", () => console.log("Event fired!"));
    `);
    assertEquals(statusCode, 0);
    assertEquals(output, "Event fired!\n");
  },
});

Deno.test({
  name: "AbortSignal.timeout() with removed listeners",
  permissions: { run: true, read: true },
  fn: async () => {
    const [statusCode, output] = await execCode(`
      const signal = AbortSignal.timeout(2000);

      const callback = () => console.log("Unexpected: Event fired");
      signal.addEventListener("abort", callback);

      setTimeout(() => {
        console.log("Removing the listener.");
        signal.removeEventListener("abort", callback);
      }, 500);

      Deno.unrefTimer(
        setTimeout(() => console.log("Unexpected: Unref timer"), 1500)
      );
    `);
    assertEquals(statusCode, 0);
    assertEquals(output, "Removing the listener.\n");
  },
});

Deno.test({
  name: "AbortSignal.timeout() with listener for a non-abort event",
  permissions: { run: true, read: true },
  fn: async () => {
    const [statusCode, output] = await execCode(`
      const signal = AbortSignal.timeout(2000);

      signal.addEventListener("someOtherEvent", () => {
        console.log("Unexpected: someOtherEvent called");
      });

      Deno.unrefTimer(
        setTimeout(() => console.log("Unexpected: Unref timer"), 1500)
      );
    `);
    assertEquals(statusCode, 0);
    assertEquals(output, "");
  },
});

// Regression test for https://github.com/denoland/deno/issues/19866
Deno.test({
  name: "regression for #19866",
  fn: async () => {
    const timeoutsFired = [];

    // deno-lint-ignore require-await
    async function start(n: number) {
      let i = 0;
      const intervalId = setInterval(() => {
        i++;
        if (i > 2) {
          clearInterval(intervalId!);
        }
        timeoutsFired.push(n);
      }, 20);
    }

    for (let n = 0; n < 100; n++) {
      start(n);
    }

    // 3s should be plenty of time for all the intervals to fire
    // but it might still be flaky on CI.
    await new Promise((resolve) => setTimeout(resolve, 3000));
    assertEquals(timeoutsFired.length, 300);
  },
});

// Regression test for https://github.com/denoland/deno/issues/20367
Deno.test({
  name: "regression for #20367",
  fn: async () => {
    const { promise, resolve } = Promise.withResolvers<number>();
    const start = performance.now();
    setTimeout(() => {
      const end = performance.now();
      resolve(end - start);
    }, 1000);
    clearTimeout(setTimeout(() => {}, 1000));

    const result = await promise;
    assert(result >= 1000);
  },
});

// Regression test for https://github.com/denoland/deno/issues/20663
Deno.test({
  name: "regression for #20663",
  fn: () => {
    AbortSignal.timeout(2000);
  },
});