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

// Requires to be run with `--allow-net` flag

import {
  assert,
  assertEquals,
  assertMatch,
  assertThrows,
} from "../../../../test_util/std/testing/asserts.ts";
import { deferred } from "../../../../test_util/std/async/deferred.ts";

Deno.test({
  name: "worker terminate",
  fn: async function () {
    const jsWorker = new Worker(
      import.meta.resolve("./test_worker.js"),
      { type: "module" },
    );
    const tsWorker = new Worker(
      import.meta.resolve("./test_worker.ts"),
      { type: "module", name: "tsWorker" },
    );

    const promise1 = deferred();
    jsWorker.onmessage = (e) => {
      promise1.resolve(e.data);
    };

    const promise2 = deferred();
    tsWorker.onmessage = (e) => {
      promise2.resolve(e.data);
    };

    jsWorker.postMessage("Hello World");
    assertEquals(await promise1, "Hello World");
    tsWorker.postMessage("Hello World");
    assertEquals(await promise2, "Hello World");
    tsWorker.terminate();
    jsWorker.terminate();
  },
});

Deno.test({
  name: "worker from data url",
  async fn() {
    const tsWorker = new Worker(
      "data:application/typescript;base64,aWYgKHNlbGYubmFtZSAhPT0gInRzV29ya2VyIikgewogIHRocm93IEVycm9yKGBJbnZhbGlkIHdvcmtlciBuYW1lOiAke3NlbGYubmFtZX0sIGV4cGVjdGVkIHRzV29ya2VyYCk7Cn0KCm9ubWVzc2FnZSA9IGZ1bmN0aW9uIChlKTogdm9pZCB7CiAgcG9zdE1lc3NhZ2UoZS5kYXRhKTsKICBjbG9zZSgpOwp9Owo=",
      { type: "module", name: "tsWorker" },
    );

    const promise = deferred();
    tsWorker.onmessage = (e) => {
      promise.resolve(e.data);
    };

    tsWorker.postMessage("Hello World");
    assertEquals(await promise, "Hello World");
    tsWorker.terminate();
  },
});

Deno.test({
  name: "worker nested",
  fn: async function () {
    const nestedWorker = new Worker(
      import.meta.resolve("./nested_worker.js"),
      { type: "module", name: "nested" },
    );

    const promise = deferred();
    nestedWorker.onmessage = (e) => {
      promise.resolve(e.data);
    };

    nestedWorker.postMessage("Hello World");
    assertEquals(await promise, { type: "msg", text: "Hello World" });
    nestedWorker.terminate();
  },
});

Deno.test({
  name: "worker throws when executing",
  fn: async function () {
    const throwingWorker = new Worker(
      import.meta.resolve("./throwing_worker.js"),
      { type: "module" },
    );

    const promise = deferred();
    // deno-lint-ignore no-explicit-any
    throwingWorker.onerror = (e: any) => {
      e.preventDefault();
      promise.resolve(e.message);
    };

    assertMatch(await promise as string, /Uncaught Error: Thrown error/);
    throwingWorker.terminate();
  },
});

Deno.test({
  name: "worker globals",
  fn: async function () {
    const workerOptions: WorkerOptions = { type: "module" };
    const w = new Worker(
      import.meta.resolve("./worker_globals.ts"),
      workerOptions,
    );

    const promise = deferred();
    w.onmessage = (e) => {
      promise.resolve(e.data);
    };

    w.postMessage("Hello, world!");
    assertEquals(await promise, "true, true, true, true");
    w.terminate();
  },
});

Deno.test({
  name: "worker fetch API",
  fn: async function () {
    const fetchingWorker = new Worker(
      import.meta.resolve("./fetching_worker.js"),
      { type: "module" },
    );

    const promise = deferred();
    // deno-lint-ignore no-explicit-any
    fetchingWorker.onerror = (e: any) => {
      e.preventDefault();
      promise.reject(e.message);
    };
    // Defer promise.resolve() to allow worker to shut down
    fetchingWorker.onmessage = (e) => {
      promise.resolve(e.data);
    };

    assertEquals(await promise, "Done!");
    fetchingWorker.terminate();
  },
});

Deno.test({
  name: "worker terminate busy loop",
  fn: async function () {
    const promise = deferred();

    const busyWorker = new Worker(
      import.meta.resolve("./busy_worker.js"),
      { type: "module" },
    );

    let testResult = 0;

    busyWorker.onmessage = (e) => {
      testResult = e.data;
      if (testResult >= 10000) {
        busyWorker.terminate();
        busyWorker.onmessage = (_e) => {
          throw new Error("unreachable");
        };
        setTimeout(() => {
          promise.resolve(testResult);
        }, 100);
      }
    };

    busyWorker.postMessage("ping");
    assertEquals(await promise, 10000);
  },
});

Deno.test({
  name: "worker race condition",
  fn: async function () {
    // See issue for details
    // https://github.com/denoland/deno/issues/4080
    const promise = deferred();

    const racyWorker = new Worker(
      import.meta.resolve("./racy_worker.js"),
      { type: "module" },
    );

    racyWorker.onmessage = (_e) => {
      setTimeout(() => {
        promise.resolve();
      }, 100);
    };

    racyWorker.postMessage("START");
    await promise;
  },
});

Deno.test({
  name: "worker is event listener",
  fn: async function () {
    let messageHandlersCalled = 0;
    let errorHandlersCalled = 0;

    const promise1 = deferred();
    const promise2 = deferred();

    const worker = new Worker(
      import.meta.resolve("./event_worker.js"),
      { type: "module" },
    );

    worker.onmessage = (_e: Event) => {
      messageHandlersCalled++;
    };
    worker.addEventListener("message", (_e: Event) => {
      messageHandlersCalled++;
    });
    worker.addEventListener("message", (_e: Event) => {
      messageHandlersCalled++;
      promise1.resolve();
    });

    worker.onerror = (e) => {
      errorHandlersCalled++;
      e.preventDefault();
    };
    worker.addEventListener("error", (_e: Event) => {
      errorHandlersCalled++;
    });
    worker.addEventListener("error", (_e: Event) => {
      errorHandlersCalled++;
      promise2.resolve();
    });

    worker.postMessage("ping");
    await promise1;
    assertEquals(messageHandlersCalled, 3);

    worker.postMessage("boom");
    await promise2;
    assertEquals(errorHandlersCalled, 3);
    worker.terminate();
  },
});

Deno.test({
  name: "worker scope is event listener",
  fn: async function () {
    const worker = new Worker(
      import.meta.resolve("./event_worker_scope.js"),
      { type: "module" },
    );

    const promise = deferred();
    worker.onmessage = (e: MessageEvent) => {
      promise.resolve(e.data);
    };
    worker.onerror = (_e) => {
      throw new Error("unreachable");
    };

    worker.postMessage("boom");
    worker.postMessage("ping");
    assertEquals(await promise, {
      messageHandlersCalled: 4,
      errorHandlersCalled: 4,
    });
    worker.terminate();
  },
});

Deno.test({
  name: "worker with Deno namespace",
  fn: async function () {
    const denoWorker = new Worker(
      import.meta.resolve("./deno_worker.ts"),
      { type: "module", deno: { permissions: "inherit" } },
    );

    const promise = deferred();
    denoWorker.onmessage = (e) => {
      denoWorker.terminate();
      promise.resolve(e.data);
    };

    denoWorker.postMessage("Hello World");
    assertEquals(await promise, "Hello World");
  },
});

Deno.test({
  name: "worker with crypto in scope",
  fn: async function () {
    const w = new Worker(
      import.meta.resolve("./worker_crypto.js"),
      { type: "module" },
    );

    const promise = deferred();
    w.onmessage = (e) => {
      promise.resolve(e.data);
    };

    w.postMessage(null);
    assertEquals(await promise, true);
    w.terminate();
  },
});

Deno.test({
  name: "Worker event handler order",
  fn: async function () {
    const promise = deferred();
    const w = new Worker(
      import.meta.resolve("./test_worker.ts"),
      { type: "module", name: "tsWorker" },
    );
    const arr: number[] = [];
    w.addEventListener("message", () => arr.push(1));
    w.onmessage = (_e) => {
      arr.push(2);
    };
    w.addEventListener("message", () => arr.push(3));
    w.addEventListener("message", () => {
      promise.resolve();
    });
    w.postMessage("Hello World");
    await promise;
    assertEquals(arr, [1, 2, 3]);
    w.terminate();
  },
});

Deno.test({
  name: "Worker immediate close",
  fn: async function () {
    const promise = deferred();
    const w = new Worker(
      import.meta.resolve("./immediately_close_worker.js"),
      { type: "module" },
    );
    setTimeout(() => {
      promise.resolve();
    }, 1000);
    await promise;
    w.terminate();
  },
});

Deno.test({
  name: "Worker post undefined",
  fn: async function () {
    const promise = deferred();
    const worker = new Worker(
      import.meta.resolve("./post_undefined.ts"),
      { type: "module" },
    );

    const handleWorkerMessage = (e: MessageEvent) => {
      console.log("main <- worker:", e.data);
      worker.terminate();
      promise.resolve();
    };

    worker.addEventListener("messageerror", () => console.log("message error"));
    worker.addEventListener("error", () => console.log("error"));
    worker.addEventListener("message", handleWorkerMessage);

    console.log("\npost from parent");
    worker.postMessage(undefined);
    await promise;
  },
});

Deno.test("Worker inherits permissions", async function () {
  const worker = new Worker(
    import.meta.resolve("./read_check_worker.js"),
    { type: "module", deno: { permissions: "inherit" } },
  );

  const promise = deferred();
  worker.onmessage = (e) => {
    promise.resolve(e.data);
  };

  worker.postMessage(null);
  assertEquals(await promise, true);
  worker.terminate();
});

Deno.test("Worker limit children permissions", async function () {
  const worker = new Worker(
    import.meta.resolve("./read_check_worker.js"),
    { type: "module", deno: { permissions: { read: false } } },
  );

  const promise = deferred();
  worker.onmessage = (e) => {
    promise.resolve(e.data);
  };

  worker.postMessage(null);
  assertEquals(await promise, false);
  worker.terminate();
});

Deno.test("Worker limit children permissions granularly", async function () {
  const worker = new Worker(
    import.meta.resolve("./read_check_granular_worker.js"),
    {
      type: "module",
      deno: {
        permissions: {
          env: ["foo"],
          hrtime: true,
          net: ["foo", "bar:8000"],
          ffi: [new URL("foo", import.meta.url), "bar"],
          read: [new URL("foo", import.meta.url), "bar"],
          run: [new URL("foo", import.meta.url), "bar", "./baz"],
          write: [new URL("foo", import.meta.url), "bar"],
        },
      },
    },
  );
  const promise = deferred();
  worker.onmessage = ({ data }) => promise.resolve(data);
  assertEquals(await promise, {
    envGlobal: "prompt",
    envFoo: "granted",
    envAbsent: "prompt",
    hrtime: "granted",
    netGlobal: "prompt",
    netFoo: "granted",
    netFoo8000: "granted",
    netBar: "prompt",
    netBar8000: "granted",
    ffiGlobal: "prompt",
    ffiFoo: "granted",
    ffiBar: "granted",
    ffiAbsent: "prompt",
    readGlobal: "prompt",
    readFoo: "granted",
    readBar: "granted",
    readAbsent: "prompt",
    runGlobal: "prompt",
    runFoo: "granted",
    runBar: "granted",
    runBaz: "granted",
    runAbsent: "prompt",
    writeGlobal: "prompt",
    writeFoo: "granted",
    writeBar: "granted",
    writeAbsent: "prompt",
  });
  worker.terminate();
});

Deno.test("Nested worker limit children permissions", async function () {
  /** This worker has permissions but doesn't grant them to its children */
  const worker = new Worker(
    import.meta.resolve("./parent_read_check_worker.js"),
    { type: "module", deno: { permissions: "inherit" } },
  );
  const promise = deferred();
  worker.onmessage = ({ data }) => promise.resolve(data);
  assertEquals(await promise, {
    envGlobal: "prompt",
    envFoo: "prompt",
    envAbsent: "prompt",
    hrtime: "prompt",
    netGlobal: "prompt",
    netFoo: "prompt",
    netFoo8000: "prompt",
    netBar: "prompt",
    netBar8000: "prompt",
    ffiGlobal: "prompt",
    ffiFoo: "prompt",
    ffiBar: "prompt",
    ffiAbsent: "prompt",
    readGlobal: "prompt",
    readFoo: "prompt",
    readBar: "prompt",
    readAbsent: "prompt",
    runGlobal: "prompt",
    runFoo: "prompt",
    runBar: "prompt",
    runBaz: "prompt",
    runAbsent: "prompt",
    writeGlobal: "prompt",
    writeFoo: "prompt",
    writeBar: "prompt",
    writeAbsent: "prompt",
  });
  worker.terminate();
});

// This test relies on env permissions not being granted on main thread
Deno.test({
  name:
    "Worker initialization throws on worker permissions greater than parent thread permissions",
  permissions: { env: false },
  fn: function () {
    assertThrows(
      () => {
        const worker = new Worker(
          import.meta.resolve("./deno_worker.ts"),
          { type: "module", deno: { permissions: { env: true } } },
        );
        worker.terminate();
      },
      Deno.errors.PermissionDenied,
      "Can't escalate parent thread permissions",
    );
  },
});

Deno.test("Worker with disabled permissions", async function () {
  const worker = new Worker(
    import.meta.resolve("./no_permissions_worker.js"),
    { type: "module", deno: { permissions: "none" } },
  );

  const promise = deferred();
  worker.onmessage = (e) => {
    promise.resolve(e.data);
  };

  worker.postMessage(null);
  assertEquals(await promise, true);
  worker.terminate();
});

Deno.test("Worker permissions are not inherited with empty permission object", async function () {
  const worker = new Worker(
    import.meta.resolve("./permission_echo.js"),
    { type: "module", deno: { permissions: {} } },
  );

  const promise = deferred();
  worker.onmessage = (e) => {
    promise.resolve(e.data);
  };

  worker.postMessage(null);
  assertEquals(await promise, {
    env: "prompt",
    hrtime: "prompt",
    net: "prompt",
    ffi: "prompt",
    read: "prompt",
    run: "prompt",
    write: "prompt",
  });
  worker.terminate();
});

Deno.test("Worker permissions are not inherited with single specified permission", async function () {
  const worker = new Worker(
    import.meta.resolve("./permission_echo.js"),
    { type: "module", deno: { permissions: { net: true } } },
  );

  const promise = deferred();
  worker.onmessage = (e) => {
    promise.resolve(e.data);
  };

  worker.postMessage(null);
  assertEquals(await promise, {
    env: "prompt",
    hrtime: "prompt",
    net: "granted",
    ffi: "prompt",
    read: "prompt",
    run: "prompt",
    write: "prompt",
  });
  worker.terminate();
});

Deno.test("Worker with invalid permission arg", function () {
  assertThrows(
    () =>
      new Worker(`data:,close();`, {
        type: "module",
        // @ts-expect-error invalid env value
        deno: { permissions: { env: "foo" } },
      }),
    TypeError,
    'Error parsing args at position 0: (deno.permissions.env) invalid value: string "foo", expected "inherit" or boolean or string[]',
  );
});

Deno.test({
  name: "worker location",
  fn: async function () {
    const promise = deferred();
    const workerModuleHref = import.meta.resolve("./worker_location.ts");
    const w = new Worker(workerModuleHref, { type: "module" });
    w.onmessage = (e) => {
      promise.resolve(e.data);
    };
    w.postMessage("Hello, world!");
    assertEquals(await promise, `${workerModuleHref}, true`);
    w.terminate();
  },
});

Deno.test({
  name: "worker with relative specifier",
  fn: async function () {
    assertEquals(location.href, "http://127.0.0.1:4545/");
    const w = new Worker(
      "./workers/test_worker.ts",
      { type: "module", name: "tsWorker" },
    );
    const promise = deferred();
    w.onmessage = (e) => {
      promise.resolve(e.data);
    };
    w.postMessage("Hello, world!");
    assertEquals(await promise, "Hello, world!");
    w.terminate();
  },
});

Deno.test({
  name: "Worker with top-level-await",
  fn: async function () {
    const result = deferred();
    const worker = new Worker(
      import.meta.resolve("./worker_with_top_level_await.ts"),
      { type: "module" },
    );
    worker.onmessage = (e) => {
      if (e.data == "ready") {
        worker.postMessage("trigger worker handler");
      } else if (e.data == "triggered worker handler") {
        result.resolve();
      } else {
        result.reject(new Error("Handler didn't run during top-level delay."));
      }
    };
    await result;
    worker.terminate();
  },
});

Deno.test({
  name: "Worker with native HTTP",
  fn: async function () {
    const result = deferred();
    const worker = new Worker(
      import.meta.resolve("./http_worker.js"),
      { type: "module", deno: { permissions: "inherit" } },
    );
    worker.onmessage = () => {
      result.resolve();
    };
    await result;

    assert(worker);
    const response = await fetch("http://localhost:4506");
    assert(await response.arrayBuffer());
    worker.terminate();
  },
});

Deno.test({
  name: "structured cloning postMessage",
  fn: async function () {
    const worker = new Worker(
      import.meta.resolve("./worker_structured_cloning.ts"),
      { type: "module" },
    );

    const result = deferred();
    worker.onmessage = (e) => {
      result.resolve(e.data);
    };

    worker.postMessage("START");
    // deno-lint-ignore no-explicit-any
    const data = await result as any;
    // self field should reference itself (circular ref)
    assert(data === data.self);
    // fields a and b refer to the same array
    assertEquals(data.a, ["a", true, 432]);
    assertEquals(data.b, ["a", true, 432]);
    data.b[0] = "b";
    data.a[2] += 5;
    assertEquals(data.a, ["b", true, 437]);
    assertEquals(data.b, ["b", true, 437]);
    // c is a set
    const len = data.c.size;
    data.c.add(1); // This value is already in the set.
    data.c.add(2);
    assertEquals(len + 1, data.c.size);
    worker.terminate();
  },
});

Deno.test({
  name: "worker with relative specifier",
  fn: async function () {
    assertEquals(location.href, "http://127.0.0.1:4545/");
    const w = new Worker(
      "./workers/test_worker.ts",
      { type: "module", name: "tsWorker" },
    );
    const promise = deferred();
    w.onmessage = (e) => {
      promise.resolve(e.data);
    };
    w.postMessage("Hello, world!");
    assertEquals(await promise, "Hello, world!");
    w.terminate();
  },
});

Deno.test({
  name: "worker SharedArrayBuffer",
  fn: async function () {
    const promise = deferred();
    const workerOptions: WorkerOptions = { type: "module" };
    const w = new Worker(
      import.meta.resolve("./shared_array_buffer.ts"),
      workerOptions,
    );
    const sab1 = new SharedArrayBuffer(1);
    const sab2 = new SharedArrayBuffer(1);
    const bytes1 = new Uint8Array(sab1);
    const bytes2 = new Uint8Array(sab2);
    assertEquals(bytes1[0], 0);
    assertEquals(bytes2[0], 0);
    w.onmessage = () => {
      w.postMessage([sab1, sab2]);
      w.onmessage = () => {
        promise.resolve();
      };
    };
    await promise;
    assertEquals(bytes1[0], 1);
    assertEquals(bytes2[0], 2);
    w.terminate();
  },
});

Deno.test({
  name: "Send MessagePorts from / to workers",
  fn: async function () {
    const worker = new Worker(
      import.meta.resolve("./message_port.ts"),
      { type: "module" },
    );
    const channel = new MessageChannel();

    const promise1 = deferred();
    const promise2 = deferred();
    const promise3 = deferred();
    const result = deferred();
    worker.onmessage = (e) => {
      promise1.resolve([e.data, e.ports.length]);
      const port1 = e.ports[0];
      port1.onmessage = (e) => {
        promise2.resolve(e.data);
        port1.close();
        worker.postMessage("3", [channel.port1]);
      };
      port1.postMessage("2");
    };
    channel.port2.onmessage = (e) => {
      promise3.resolve(e.data);
      channel.port2.close();
      result.resolve();
    };

    assertEquals(await promise1, ["1", 1]);
    assertEquals(await promise2, true);
    assertEquals(await promise3, true);
    await result;
    worker.terminate();
  },
});

Deno.test({
  name: "worker Deno.memoryUsage",
  fn: async function () {
    const w = new Worker(
      /**
       * Source code
       * self.onmessage = function() {self.postMessage(Deno.memoryUsage())}
       */
      "data:application/typescript;base64,c2VsZi5vbm1lc3NhZ2UgPSBmdW5jdGlvbigpIHtzZWxmLnBvc3RNZXNzYWdlKERlbm8ubWVtb3J5VXNhZ2UoKSl9",
      { type: "module", name: "tsWorker" },
    );

    w.postMessage(null);

    const memoryUsagePromise = deferred();
    w.onmessage = function (evt) {
      memoryUsagePromise.resolve(evt.data);
    };

    assertEquals(
      Object.keys(
        await memoryUsagePromise as unknown as Record<string, number>,
      ),
      ["rss", "heapTotal", "heapUsed", "external"],
    );
    w.terminate();
  },
});