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

import {
  assert,
  assertEquals,
  assertNotEquals,
  assertRejects,
  assertThrows,
  unreachable,
} from "../../../../test_util/std/assert/mod.ts";

Deno.test("fragment", () => {
  assertThrows(() => new WebSocketStream("ws://localhost:4242/#"));
  assertThrows(() => new WebSocketStream("ws://localhost:4242/#foo"));
});

Deno.test("duplicate protocols", () => {
  assertThrows(() =>
    new WebSocketStream("ws://localhost:4242", {
      protocols: ["foo", "foo"],
    })
  );
});

Deno.test("connect & close custom valid code", async () => {
  const ws = new WebSocketStream("ws://localhost:4242");
  await ws.opened;
  ws.close({ code: 1000 });
  await ws.closed;
});

Deno.test("connect & close custom invalid reason", async () => {
  const ws = new WebSocketStream("ws://localhost:4242");
  await ws.opened;
  assertThrows(() => ws.close({ code: 1000, reason: "".padEnd(124, "o") }));
  ws.close();
  await ws.closed;
});

Deno.test("echo string", async () => {
  const ws = new WebSocketStream("ws://localhost:4242");
  const { readable, writable } = await ws.opened;
  await writable.getWriter().write("foo");
  const res = await readable.getReader().read();
  assertEquals(res.value, "foo");
  ws.close();
  await ws.closed;
});

Deno.test("echo string tls", async () => {
  const ws = new WebSocketStream("wss://localhost:4243");
  const { readable, writable } = await ws.opened;
  await writable.getWriter().write("foo");
  const res = await readable.getReader().read();
  assertEquals(res.value, "foo");
  ws.close();
  await ws.closed;
});

Deno.test("websocket error", async () => {
  const ws = new WebSocketStream("wss://localhost:4242");
  await Promise.all([
    assertRejects(
      () => ws.opened,
      Deno.errors.UnexpectedEof,
      "tls handshake eof",
    ),
    assertRejects(
      () => ws.closed,
      Deno.errors.UnexpectedEof,
      "tls handshake eof",
    ),
  ]);
});

Deno.test("echo uint8array", async () => {
  const ws = new WebSocketStream("ws://localhost:4242");
  const { readable, writable } = await ws.opened;
  const uint = new Uint8Array([102, 111, 111]);
  await writable.getWriter().write(uint);
  const res = await readable.getReader().read();
  assertEquals(res.value, uint);
  ws.close();
  await ws.closed;
});

Deno.test("aborting immediately throws an AbortError", async () => {
  const controller = new AbortController();
  const wss = new WebSocketStream("ws://localhost:4242", {
    signal: controller.signal,
  });
  controller.abort();
  await assertRejects(
    () => wss.opened,
    (error: Error) => {
      assert(error instanceof DOMException);
      assertEquals(error.name, "AbortError");
    },
  );
  await assertRejects(
    () => wss.closed,
    (error: Error) => {
      assert(error instanceof DOMException);
      assertEquals(error.name, "AbortError");
    },
  );
});

Deno.test("aborting immediately with a reason throws that reason", async () => {
  const controller = new AbortController();
  const wss = new WebSocketStream("ws://localhost:4242", {
    signal: controller.signal,
  });
  const abortReason = new Error();
  controller.abort(abortReason);
  await assertRejects(
    () => wss.opened,
    (error: Error) => assertEquals(error, abortReason),
  );
  await assertRejects(
    () => wss.closed,
    (error: Error) => assertEquals(error, abortReason),
  );
});

Deno.test("aborting immediately with a primitive as reason throws that primitive", async () => {
  const controller = new AbortController();
  const wss = new WebSocketStream("ws://localhost:4242", {
    signal: controller.signal,
  });
  controller.abort("Some string");
  await wss.opened.then(
    () => unreachable(),
    (e) => assertEquals(e, "Some string"),
  );
  await wss.closed.then(
    () => unreachable(),
    (e) => assertEquals(e, "Some string"),
  );
});

Deno.test("headers", async () => {
  const listener = Deno.listen({ port: 4512 });
  const promise = (async () => {
    const conn = await listener.accept();
    const httpConn = Deno.serveHttp(conn);
    const { request, respondWith } = (await httpConn.nextRequest())!;
    assertEquals(request.headers.get("x-some-header"), "foo");
    const { response, socket } = Deno.upgradeWebSocket(request);
    socket.onopen = () => socket.close();
    const p = new Promise<void>((resolve) => {
      socket.onopen = () => socket.close();
      socket.onclose = () => resolve();
    });
    await respondWith(response);
    await p;
  })();

  const ws = new WebSocketStream("ws://localhost:4512", {
    headers: [["x-some-header", "foo"]],
  });
  await ws.opened;
  await promise;
  await ws.closed;
  listener.close();
});

Deno.test("forbidden headers", async () => {
  const forbiddenHeaders = [
    "sec-websocket-accept",
    "sec-websocket-extensions",
    "sec-websocket-key",
    "sec-websocket-protocol",
    "sec-websocket-version",
    "upgrade",
    "connection",
  ];

  const listener = Deno.listen({ port: 4512 });
  const promise = (async () => {
    const conn = await listener.accept();
    const httpConn = Deno.serveHttp(conn);
    const { request, respondWith } = (await httpConn.nextRequest())!;
    for (const [key] of request.headers) {
      assertNotEquals(key, "foo");
    }
    const { response, socket } = Deno.upgradeWebSocket(request);
    const p = new Promise<void>((resolve) => {
      socket.onopen = () => socket.close();
      socket.onclose = () => resolve();
    });
    await respondWith(response);
    await p;
  })();

  const ws = new WebSocketStream("ws://localhost:4512", {
    headers: forbiddenHeaders.map((header) => [header, "foo"]),
  });
  await ws.opened;
  await promise;
  await ws.closed;
  listener.close();
});

Deno.test("sync close with empty stream", async () => {
  const listener = Deno.listen({ port: 4512 });
  const promise = (async () => {
    const conn = await listener.accept();
    const httpConn = Deno.serveHttp(conn);
    const { request, respondWith } = (await httpConn.nextRequest())!;
    const { response, socket } = Deno.upgradeWebSocket(request);
    const p = new Promise<void>((resolve) => {
      socket.onopen = () => {
        socket.send("first message");
        socket.send("second message");
      };
      socket.onclose = () => resolve();
    });
    await respondWith(response);
    await p;
  })();

  const ws = new WebSocketStream("ws://localhost:4512");
  const { readable } = await ws.opened;
  const reader = readable.getReader();
  const firstMessage = await reader.read();
  assertEquals(firstMessage.value, "first message");
  const secondMessage = await reader.read();
  assertEquals(secondMessage.value, "second message");
  ws.close({ code: 1000 });
  await ws.closed;
  await promise;
  listener.close();
});

Deno.test("sync close with unread messages in stream", async () => {
  const listener = Deno.listen({ port: 4512 });
  const promise = (async () => {
    const conn = await listener.accept();
    const httpConn = Deno.serveHttp(conn);
    const { request, respondWith } = (await httpConn.nextRequest())!;
    const { response, socket } = Deno.upgradeWebSocket(request);
    const p = new Promise<void>((resolve) => {
      socket.onopen = () => {
        socket.send("first message");
        socket.send("second message");
        socket.send("third message");
        socket.send("fourth message");
      };
      socket.onclose = () => resolve();
    });
    await respondWith(response);
    await p;
  })();

  const ws = new WebSocketStream("ws://localhost:4512");
  const { readable } = await ws.opened;
  const reader = readable.getReader();
  const firstMessage = await reader.read();
  assertEquals(firstMessage.value, "first message");
  const secondMessage = await reader.read();
  assertEquals(secondMessage.value, "second message");
  ws.close({ code: 1000 });
  await ws.closed;
  await promise;
  listener.close();
});

Deno.test("async close with empty stream", async () => {
  const listener = Deno.listen({ port: 4512 });
  const promise = (async () => {
    const conn = await listener.accept();
    const httpConn = Deno.serveHttp(conn);
    const { request, respondWith } = (await httpConn.nextRequest())!;
    const { response, socket } = Deno.upgradeWebSocket(request);
    const p = new Promise<void>((resolve) => {
      socket.onopen = () => {
        socket.send("first message");
        socket.send("second message");
      };
      socket.onclose = () => resolve();
    });
    await respondWith(response);
    await p;
  })();

  const ws = new WebSocketStream("ws://localhost:4512");
  const { readable } = await ws.opened;
  const reader = readable.getReader();
  const firstMessage = await reader.read();
  assertEquals(firstMessage.value, "first message");
  const secondMessage = await reader.read();
  assertEquals(secondMessage.value, "second message");
  setTimeout(() => {
    ws.close({ code: 1000 });
  }, 0);
  await ws.closed;
  await promise;
  listener.close();
});

Deno.test("async close with unread messages in stream", async () => {
  const listener = Deno.listen({ port: 4512 });
  const promise = (async () => {
    const conn = await listener.accept();
    const httpConn = Deno.serveHttp(conn);
    const { request, respondWith } = (await httpConn.nextRequest())!;
    const { response, socket } = Deno.upgradeWebSocket(request);
    const p = new Promise<void>((resolve) => {
      socket.onopen = () => {
        socket.send("first message");
        socket.send("second message");
        socket.send("third message");
        socket.send("fourth message");
      };
      socket.onclose = () => resolve();
    });
    await respondWith(response);
    await p;
  })();

  const ws = new WebSocketStream("ws://localhost:4512");
  const { readable } = await ws.opened;
  const reader = readable.getReader();
  const firstMessage = await reader.read();
  assertEquals(firstMessage.value, "first message");
  const secondMessage = await reader.read();
  assertEquals(secondMessage.value, "second message");
  setTimeout(() => {
    ws.close({ code: 1000 });
  }, 0);
  await ws.closed;
  await promise;
  listener.close();
});