// Copyright 2018-2026 the Deno authors. MIT license.

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

import {
  assert,
  assertEquals,
  assertThrows,
  delay,
  fail,
} from "./test_util.ts";

const servePort = 4248;
const serveUrl = `ws://localhost:${servePort}/`;

Deno.test({ permissions: "none" }, function websocketPermissionless() {
  assertThrows(
    () => new WebSocket("ws://localhost"),
    Deno.errors.NotCapable,
  );
});

Deno.test(async function websocketConstructorTakeURLObjectAsParameter() {
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const ws = new WebSocket(new URL("ws://localhost:4242/"));
  assertEquals(ws.url, "ws://localhost:4242/");
  ws.onerror = (e) => reject(e);
  ws.onopen = () => ws.close();
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test(async function websocketH2SendSmallPacket() {
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const ws = new WebSocket(new URL("wss://localhost:4249/"));
  assertEquals(ws.url, "wss://localhost:4249/");
  let messageCount = 0;
  ws.onerror = (e) => reject(e);
  ws.onopen = () => {
    ws.send("a".repeat(16));
    ws.send("a".repeat(16));
    ws.send("a".repeat(16));
  };
  ws.onmessage = () => {
    if (++messageCount == 3) {
      ws.close();
    }
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

// Regression test: the WebSocket-over-HTTP/2 client must disable HTTP/2 server
// push (SETTINGS_ENABLE_PUSH = 0). Leaving push enabled exposes an h2
// state-machine assertion that a malicious server can trip via a crafted
// PUSH_PROMISE sequence, aborting the whole process. The test server attempts a
// server push and reports the outcome as its first message: a safe client makes
// h2 reject the push ("push-rejected").
Deno.test(async function websocketH2DisablesServerPush() {
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const ws = new WebSocket(new URL("wss://localhost:4266/"));
  ws.onerror = (e) => reject(e);
  ws.onmessage = (m) => {
    try {
      assertEquals(m.data, "push-rejected");
    } finally {
      ws.close();
    }
  };
  ws.onclose = () => resolve();
  await promise;
});

Deno.test(async function websocketH2SendLargePacket() {
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const ws = new WebSocket(new URL("wss://localhost:4249/"));
  assertEquals(ws.url, "wss://localhost:4249/");
  let messageCount = 0;
  ws.onerror = (e) => reject(e);
  ws.onopen = () => {
    ws.send("a".repeat(65000));
    ws.send("a".repeat(65000));
    ws.send("a".repeat(65000));
  };
  ws.onmessage = () => {
    if (++messageCount == 3) {
      ws.close();
    }
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test(async function websocketSendLargePacket() {
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const ws = new WebSocket(new URL("wss://localhost:4243/"));
  assertEquals(ws.url, "wss://localhost:4243/");
  ws.onerror = (e) => reject(e);
  ws.onopen = () => {
    ws.send("a".repeat(65000));
  };
  ws.onmessage = () => {
    ws.close();
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test(async function websocketSendLargeBinaryPacket() {
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const ws = new WebSocket(new URL("wss://localhost:4243/"));
  ws.binaryType = "arraybuffer";
  assertEquals(ws.url, "wss://localhost:4243/");
  ws.onerror = (e) => reject(e);
  ws.onopen = () => {
    ws.send(new Uint8Array(65000));
  };
  ws.onmessage = (msg: MessageEvent) => {
    assertEquals(msg.data.byteLength, 65000);
    ws.close();
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test(async function websocketSendLargeBlobPacket() {
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const ws = new WebSocket(new URL("wss://localhost:4243/"));
  ws.binaryType = "arraybuffer";
  assertEquals(ws.url, "wss://localhost:4243/");
  ws.onerror = (e) => reject(e);
  ws.onopen = () => {
    ws.send(new Blob(["a".repeat(65000)]));
  };
  ws.onmessage = (msg: MessageEvent) => {
    assertEquals(msg.data.byteLength, 65000);
    ws.close();
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

// https://github.com/denoland/deno/pull/17762
// https://github.com/denoland/deno/issues/17761
Deno.test(async function websocketPingPong() {
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4245/");
  assertEquals(ws.url, "ws://localhost:4245/");
  ws.onerror = (e) => reject(e);
  ws.onmessage = (e) => {
    ws.send(e.data);
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
  ws.close();
});

// TODO(mmastrac): This requires us to ignore bad certs
// Deno.test(async function websocketSecureConnect() {
//   const { promise, resolve } = Promise.withResolvers<void>();
//   const ws = new WebSocket("wss://localhost:4243/");
//   assertEquals(ws.url, "wss://localhost:4243/");
//   ws.onerror = (error) => {
//     console.log(error);
//     fail();
//   };
//   ws.onopen = () => ws.close();
//   ws.onclose = () => {
//     resolve();
//   };
//   await promise;
// });

// https://github.com/denoland/deno/issues/18700
Deno.test(
  { sanitizeOps: false, sanitizeResources: false },
  async function websocketWriteLock() {
    const ac = new AbortController();
    const listeningDeferred = Promise.withResolvers<void>();

    const server = Deno.serve({
      handler: (req) => {
        const { socket, response } = Deno.upgradeWebSocket(req);
        socket.onopen = function () {
          setTimeout(() => socket.send("Hello"), 500);
        };
        socket.onmessage = function (e) {
          assertEquals(e.data, "Hello");
          ac.abort();
        };
        return response;
      },
      signal: ac.signal,
      onListen: () => listeningDeferred.resolve(),
      hostname: "localhost",
      port: servePort,
    });

    await listeningDeferred.promise;
    const deferred = Promise.withResolvers<void>();
    const ws = new WebSocket(serveUrl);
    assertEquals(ws.url, serveUrl);
    ws.onerror = () => fail();
    ws.onmessage = (e) => {
      assertEquals(e.data, "Hello");
      setTimeout(() => {
        ws.send(e.data);
      }, 1000);
      deferred.resolve();
    };
    ws.onclose = () => {
      deferred.resolve();
    };

    await Promise.all([deferred.promise, server.finished]);
    ws.close();
  },
);

// https://github.com/denoland/deno/issues/18775
Deno.test({
  sanitizeOps: false,
  sanitizeResources: false,
}, async function websocketDoubleClose() {
  const deferred = Promise.withResolvers<void>();

  const ac = new AbortController();
  const listeningDeferred = Promise.withResolvers<void>();

  const server = Deno.serve({
    handler: (req) => {
      const { response, socket } = Deno.upgradeWebSocket(req);
      let called = false;
      socket.onopen = () => socket.send("Hello");
      socket.onmessage = () => {
        assert(!called);
        called = true;
        socket.send("bye");
        socket.close();
      };
      socket.onclose = () => ac.abort();
      socket.onerror = () => fail();
      return response;
    },
    signal: ac.signal,
    onListen: () => listeningDeferred.resolve(),
    hostname: "localhost",
    port: servePort,
  });

  await listeningDeferred.promise;

  const ws = new WebSocket(serveUrl);
  assertEquals(ws.url, serveUrl);
  ws.onerror = () => fail();
  ws.onmessage = (m: MessageEvent) => {
    if (m.data == "Hello") ws.send("bye");
  };
  ws.onclose = () => {
    deferred.resolve();
  };
  await Promise.all([deferred.promise, server.finished]);
});

// https://github.com/denoland/deno/issues/19483
Deno.test({
  sanitizeOps: false,
  sanitizeResources: false,
}, async function websocketCloseFlushes() {
  const deferred = Promise.withResolvers<void>();

  const ac = new AbortController();
  const listeningDeferred = Promise.withResolvers<void>();

  const server = Deno.serve({
    handler: (req) => {
      const { response, socket } = Deno.upgradeWebSocket(req);
      socket.onopen = () => socket.send("Hello");
      socket.onmessage = () => {
        socket.send("Bye");
        socket.close(1000);
      };
      socket.onclose = () => ac.abort();
      socket.onerror = () => fail();
      return response;
    },
    signal: ac.signal,
    onListen: () => listeningDeferred.resolve(),
    hostname: "localhost",
    port: servePort,
  });

  await listeningDeferred.promise;

  const ws = new WebSocket(serveUrl);
  assertEquals(ws.url, serveUrl);
  let seenBye = false;
  ws.onerror = () => fail();
  ws.onmessage = ({ data }) => {
    if (data == "Hello") {
      ws.send("Hello!");
    } else {
      assertEquals(data, "Bye");
      seenBye = true;
    }
  };
  ws.onclose = (e) => {
    assertEquals(e.code, 1000);
    deferred.resolve();
  };
  await Promise.all([deferred.promise, server.finished]);

  assert(seenBye);
});

// https://github.com/denoland/deno/issues/19283
Deno.test({
  sanitizeOps: false,
  sanitizeResources: false,
}, async function websocketUrlCredentialsAsBasicAuth() {
  const deferred = Promise.withResolvers<void>();
  const ac = new AbortController();
  const listeningDeferred = Promise.withResolvers<void>();

  let seenAuth: string | null = null;
  const server = Deno.serve({
    handler: (req) => {
      seenAuth = req.headers.get("authorization");
      if (seenAuth !== "Basic " + btoa("user:p@ss")) {
        return new Response("unauthorized", { status: 401 });
      }
      const { response, socket } = Deno.upgradeWebSocket(req);
      socket.onopen = () => socket.close();
      socket.onclose = () => ac.abort();
      return response;
    },
    signal: ac.signal,
    onListen: () => listeningDeferred.resolve(),
    hostname: "localhost",
    port: servePort,
  });

  await listeningDeferred.promise;

  // Percent-encoded userinfo (p%40ss decodes to p@ss).
  const ws = new WebSocket(`ws://user:p%40ss@localhost:${servePort}/`);
  // Credentials are stripped from the URL exposed via `url`.
  assertEquals(ws.url, serveUrl);
  ws.onerror = () => fail();
  ws.onclose = () => deferred.resolve();

  await Promise.all([deferred.promise, server.finished]);
  assertEquals(seenAuth, "Basic " + btoa("user:p@ss"));
});

Deno.test(
  { sanitizeOps: false },
  function websocketConstructorWithPrototypePollution() {
    const originalSymbolIterator = Array.prototype[Symbol.iterator];
    try {
      Array.prototype[Symbol.iterator] = () => {
        throw Error("unreachable");
      };
      assertThrows(() => {
        new WebSocket(
          new URL("ws://localhost:4242/"),
          // Allow `Symbol.iterator` to be called in WebIDL conversion to `sequence<DOMString>`
          // deno-lint-ignore no-explicit-any
          ["soap", "soap"].values() as any,
        );
      }, DOMException);
    } finally {
      Array.prototype[Symbol.iterator] = originalSymbolIterator;
    }
  },
);

Deno.test({
  sanitizeOps: false,
  sanitizeResources: false,
}, async function websocketTlsSocketWorks() {
  const cert = await Deno.readTextFile("tests/testdata/tls/localhost.crt");
  const key = await Deno.readTextFile("tests/testdata/tls/localhost.key");

  const messages: string[] = [],
    errors: { server?: Event; client?: Event }[] = [];
  const promise = new Promise((okay, nope) => {
    const ac = new AbortController();
    const server = Deno.serve({
      handler: (req) => {
        const { response, socket } = Deno.upgradeWebSocket(req);
        socket.onopen = () => socket.send("ping");
        socket.onmessage = (e) => {
          messages.push(e.data);
          socket.close();
        };
        socket.onerror = (e) => errors.push({ server: e });
        socket.onclose = () => ac.abort();
        return response;
      },
      signal: ac.signal,
      hostname: "localhost",
      port: servePort,
      cert,
      key,
    });
    setTimeout(() => {
      const ws = new WebSocket(`wss://localhost:${servePort}`);
      ws.onmessage = (e) => {
        messages.push(e.data);
        ws.send("pong");
      };
      ws.onerror = (e) => {
        errors.push({ client: e });
        nope();
      };
      ws.onclose = () => okay(server.finished);
    }, 1000);
  });

  const finished = await promise;

  assertEquals(errors, []);
  assertEquals(messages, ["ping", "pong"]);

  await finished;
});

// https://github.com/denoland/deno/issues/15340
Deno.test(
  async function websocketServerFieldInit() {
    const ac = new AbortController();
    const listeningDeferred = Promise.withResolvers<void>();

    const server = Deno.serve({
      handler: (req) => {
        const { socket, response } = Deno.upgradeWebSocket(req, {
          idleTimeout: 0,
        });
        socket.onopen = function () {
          assert(typeof socket.url == "string");
          assert(socket.readyState == WebSocket.OPEN);
          assert(socket.protocol == "");
          assert(socket.binaryType == "arraybuffer");
          socket.close();
        };
        socket.onclose = () => ac.abort();
        return response;
      },
      signal: ac.signal,
      onListen: () => listeningDeferred.resolve(),
      hostname: "localhost",
      port: servePort,
    });

    await listeningDeferred.promise;
    const deferred = Promise.withResolvers<void>();
    const ws = new WebSocket(serveUrl);
    assertEquals(ws.url, serveUrl);
    ws.onerror = () => fail();
    ws.onclose = () => {
      deferred.resolve();
    };

    await Promise.all([deferred.promise, server.finished]);
  },
);

Deno.test(
  { sanitizeOps: false },
  async function websocketServerGetsGhosted() {
    const ac = new AbortController();
    const listeningDeferred = Promise.withResolvers<void>();

    const server = Deno.serve({
      handler: (req) => {
        const { socket, response } = Deno.upgradeWebSocket(req, {
          idleTimeout: 2,
        });
        socket.onerror = () => socket.close();
        socket.onclose = () => ac.abort();
        return response;
      },
      signal: ac.signal,
      onListen: () => listeningDeferred.resolve(),
      hostname: "localhost",
      port: servePort,
    });

    await listeningDeferred.promise;
    const r = await fetch("http://localhost:4545/ghost_ws_client");
    assertEquals(r.status, 200);
    await r.body?.cancel();

    await server.finished;
  },
);

Deno.test("invalid scheme", () => {
  assertThrows(() => new WebSocket("foo://localhost:4242"));
});

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

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

Deno.test("invalid server", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:2121");
  let err = false;
  ws.onerror = (e) => {
    assert("error" in e);
    err = true;
  };
  ws.onclose = () => {
    if (err) {
      resolve();
    } else {
      fail();
    }
  };
  ws.onopen = () => fail();
  await promise;
});

Deno.test("connect & close", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  ws.onerror = () => fail();
  ws.onopen = () => {
    ws.close();
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test("connect & abort", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  ws.close();
  let err = false;
  ws.onerror = () => {
    err = true;
  };
  ws.onclose = () => {
    if (err) {
      resolve();
    } else {
      fail();
    }
  };
  ws.onopen = () => fail();
  await promise;
});

Deno.test("connect & close custom valid code", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  ws.onerror = () => fail();
  ws.onopen = () => ws.close(1000);
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test("connect & close custom invalid code", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  ws.onerror = () => fail();
  ws.onopen = () => {
    assertThrows(() => ws.close(1001));
    ws.close();
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test("connect & close custom valid reason", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  ws.onerror = () => fail();
  ws.onopen = () => ws.close(1000, "foo");
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test("connect & close custom invalid reason", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  ws.onerror = () => fail();
  ws.onopen = () => {
    assertThrows(() => ws.close(1000, "".padEnd(124, "o")));
    ws.close();
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test("echo string", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  ws.onerror = () => fail();
  ws.onopen = () => ws.send("foo");
  ws.onmessage = (e) => {
    assertEquals(e.data, "foo");
    ws.close();
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test("echo string tls", async () => {
  const deferred1 = Promise.withResolvers<void>();
  const deferred2 = Promise.withResolvers<void>();
  const ws = new WebSocket("wss://localhost:4243");
  ws.onerror = () => fail();
  ws.onopen = () => ws.send("foo");
  ws.onmessage = (e) => {
    assertEquals(e.data, "foo");
    ws.close();
    deferred1.resolve();
  };
  ws.onclose = () => {
    deferred2.resolve();
  };
  await deferred1.promise;
  await deferred2.promise;
});

Deno.test("websocket error", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("wss://localhost:4242");
  ws.onopen = () => fail();
  ws.onerror = (err) => {
    assert(err instanceof ErrorEvent);
    assertEquals(
      err.message,
      "NetworkError: failed to connect to WebSocket: received corrupt message of type InvalidContentType",
    );
    resolve();
  };
  await promise;
});

Deno.test("echo blob with binaryType blob", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  const blob = new Blob(["foo"]);
  ws.onerror = () => fail();
  ws.onopen = () => ws.send(blob);
  ws.onmessage = (e) => {
    e.data.text().then((actual: string) => {
      blob.text().then((expected) => {
        assertEquals(actual, expected);
      });
    });
    ws.close();
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test("echo blob with binaryType arraybuffer", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  ws.binaryType = "arraybuffer";
  const blob = new Blob(["foo"]);
  ws.onerror = () => fail();
  ws.onopen = () => ws.send(blob);
  ws.onmessage = (e) => {
    blob.arrayBuffer().then((expected) => {
      assertEquals(e.data, expected);
    });
    ws.close();
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test("echo uint8array with binaryType blob", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  const uint = new Uint8Array([102, 111, 111]);
  ws.onerror = () => fail();
  ws.onopen = () => ws.send(uint);
  ws.onmessage = (e) => {
    e.data.arrayBuffer().then((actual: ArrayBuffer) => {
      assertEquals(actual, uint.buffer);
    });
    ws.close();
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test("echo uint8array with binaryType arraybuffer", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  ws.binaryType = "arraybuffer";
  const uint = new Uint8Array([102, 111, 111]);
  ws.onerror = () => fail();
  ws.onopen = () => ws.send(uint);
  ws.onmessage = (e) => {
    assertEquals(e.data, uint.buffer);
    ws.close();
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test("echo arraybuffer with binaryType blob", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  const buffer = new ArrayBuffer(3);
  ws.onerror = () => fail();
  ws.onopen = () => ws.send(buffer);
  ws.onmessage = (e) => {
    e.data.arrayBuffer().then((actual: ArrayBuffer) => {
      assertEquals(actual, buffer);
    });
    ws.close();
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test("echo arraybuffer with binaryType arraybuffer", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  ws.binaryType = "arraybuffer";
  const buffer = new ArrayBuffer(3);
  ws.onerror = () => fail();
  ws.onopen = () => ws.send(buffer);
  ws.onmessage = (e) => {
    assertEquals(e.data, buffer);
    ws.close();
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test("echo blob mixed with string", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  ws.binaryType = "arraybuffer";
  const blob = new Blob(["foo"]);
  ws.onerror = () => fail();
  ws.onopen = () => {
    ws.send(blob);
    ws.send("bar");
  };
  const messages: (ArrayBuffer | string)[] = [];
  ws.onmessage = (e) => {
    messages.push(e.data);
    if (messages.length === 2) {
      assertEquals(messages[0], new Uint8Array([102, 111, 111]).buffer);
      assertEquals(messages[1], "bar");
      ws.close();
    }
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test("Event Handlers order", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  const arr: number[] = [];
  ws.onerror = () => fail();
  ws.addEventListener("message", () => arr.push(1));
  ws.onmessage = () => fail();
  ws.addEventListener("message", () => {
    arr.push(3);
    ws.close();
    assertEquals(arr, [1, 2, 3]);
  });
  ws.onmessage = () => arr.push(2);
  ws.onopen = () => ws.send("Echo");
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

Deno.test("Close without frame", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4244");
  ws.onerror = () => fail();
  ws.onclose = (e) => {
    assertEquals(e.code, 1005);
    resolve();
  };
  await promise;
});

Deno.test("Close connection", async () => {
  const ac = new AbortController();
  const listeningDeferred = Promise.withResolvers<void>();

  const server = Deno.serve({
    handler: (req) => {
      const { socket, response } = Deno.upgradeWebSocket(req);
      socket.onmessage = function (e) {
        socket.close(1008);
        assertEquals(e.data, "Hello");
      };
      socket.onclose = () => {
        ac.abort();
      };
      socket.onerror = () => fail();
      return response;
    },
    signal: ac.signal,
    onListen: () => listeningDeferred.resolve(),
    hostname: "localhost",
    port: servePort,
  });

  await listeningDeferred.promise;

  const conn = await Deno.connect({ port: servePort, hostname: "localhost" });
  await conn.write(
    new TextEncoder().encode(
      "GET / HTTP/1.1\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n",
    ),
  );

  // Write a 2 text frame saying "Hello"
  await conn.write(new Uint8Array([0x81, 0x05]));
  await conn.write(new TextEncoder().encode("Hello"));

  // We are a bad client so we won't acknowledge the close frame
  await conn.write(new Uint8Array([0x81, 0x05]));
  await conn.write(new TextEncoder().encode("Hello"));

  await server.finished;
  conn.close();
});

Deno.test("send to a closed socket", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const ws = new WebSocket("ws://localhost:4242");
  const blob = new Blob(["foo"]);
  ws.onerror = () => fail();
  ws.onopen = () => {
    ws.close();
    ws.send(blob);
  };
  ws.onclose = () => {
    resolve();
  };
  await promise;
});

// https://github.com/denoland/deno/issues/25126
Deno.test("websocket close ongoing handshake", async () => {
  // First try to close without any delay
  {
    const { promise, resolve } = Promise.withResolvers<void>();
    let gotError1 = false;
    const ws = new WebSocket("ws://localhost:4264");
    ws.onopen = () => fail();
    ws.onerror = (e) => {
      assertEquals((e as ErrorEvent).error.code, "EINTR");
      gotError1 = true;
    };
    ws.onclose = () => resolve();
    ws.close();
    await promise;
    assert(gotError1);
  }

  await delay(50); // Wait a bit before trying again.

  {
    const { promise: promise2, resolve: resolve2 } = Promise.withResolvers<
      void
    >();
    const ws2 = new WebSocket("ws://localhost:4264");
    ws2.onopen = () => fail();
    let gotError2 = false;
    ws2.onerror = (e) => {
      assertEquals((e as ErrorEvent).error.code, "EINTR");
      gotError2 = true;
    };
    ws2.onclose = () => resolve2();
    await delay(50); // wait a bit this time before calling close
    ws2.close();
    await promise2;
    assert(gotError2);
  }
});

// Regression test: non-ASCII bytes in Sec-WebSocket-Protocol header
// should not cause a panic.
Deno.test(
  { sanitizeOps: false, sanitizeResources: false },
  async function websocketNonAsciiProtocolHeader() {
    const port = 14248;
    const listener = Deno.listen({ port, hostname: "127.0.0.1" });

    const serverTask = (async () => {
      const conn = await listener.accept();
      const buf = new Uint8Array(4096);
      let total = 0;
      while (true) {
        const n = await conn.read(buf.subarray(total));
        if (n === null) break;
        total += n;
        const text = new TextDecoder().decode(buf.subarray(0, total));
        if (text.includes("\r\n\r\n")) break;
      }

      // Parse Sec-WebSocket-Key from the request
      const request = new TextDecoder().decode(buf.subarray(0, total));
      const keyMatch = request.match(/sec-websocket-key:\s*(\S+)/i);
      assert(keyMatch !== null, "Expected Sec-WebSocket-Key in: " + request);
      const key = keyMatch[1];

      // Compute accept key
      const hash = await crypto.subtle.digest(
        "SHA-1",
        new TextEncoder().encode(key + "258EAFA5-E914-47DA-95CA-5AB5DF85B1E3"),
      );
      const accept = btoa(
        String.fromCharCode(...new Uint8Array(hash)),
      );

      // Build response with non-ASCII bytes in Sec-WebSocket-Protocol
      const headerPart =
        `HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\nSec-WebSocket-Protocol: `;
      const headerPartBytes = new TextEncoder().encode(headerPart);
      // 0xFF is non-ASCII
      const nonAscii = new Uint8Array([0xFF, 0xFE]);
      const ending = new TextEncoder().encode("\r\n\r\n");

      const response = new Uint8Array(
        headerPartBytes.length + nonAscii.length + ending.length,
      );
      response.set(headerPartBytes, 0);
      response.set(nonAscii, headerPartBytes.length);
      response.set(ending, headerPartBytes.length + nonAscii.length);
      await conn.write(response);

      // Send a close frame
      await conn.write(new Uint8Array([0x88, 0x02, 0x03, 0xE8]));

      // Give it a moment then clean up
      await delay(100);
      conn.close();
    })();

    const { promise, resolve, reject } = Promise.withResolvers<void>();
    const ws = new WebSocket(`ws://127.0.0.1:${port}`);
    ws.onerror = () => {};
    ws.onopen = () => {
      // If we get here without panicking, the fix works.
      ws.close();
    };
    ws.onclose = () => resolve();
    setTimeout(() => reject(new Error("timeout")), 5000);

    await promise;
    await serverTask;
    listener.close();
  },
);

function createOnErrorCb(ac: AbortController): (err: unknown) => Response {
  return (err) => {
    console.error(err);
    ac.abort();
    return new Response("Internal server error", { status: 500 });
  };
}

function onListen(
  resolve: (value: void | PromiseLike<void>) => void,
): (_addr: Deno.Addr) => void {
  return () => {
    resolve();
  };
}

Deno.test("WebSocket headers", async () => {
  const ac = new AbortController();
  const listeningDeferred = Promise.withResolvers<void>();
  const doneDeferred = Promise.withResolvers<void>();
  await using server = Deno.serve({
    handler: (request) => {
      assertEquals(request.headers.get("Authorization"), "Bearer foo");
      const {
        response,
        socket,
      } = Deno.upgradeWebSocket(request);
      socket.onerror = (e) => {
        console.error(e);
        fail();
      };
      socket.onmessage = (m) => {
        socket.send(m.data);
        socket.close(1001);
      };
      socket.onclose = () => doneDeferred.resolve();
      return response;
    },
    port: servePort,
    signal: ac.signal,
    onListen: onListen(listeningDeferred.resolve),
    onError: createOnErrorCb(ac),
  });

  await listeningDeferred.promise;
  const def = Promise.withResolvers<void>();
  const ws = new WebSocket(`ws://localhost:${servePort}`, {
    headers: {
      "Authorization": "Bearer foo",
    },
  });
  ws.onmessage = (m) => assertEquals(m.data, "foo");
  ws.onerror = (e) => {
    console.error(e);
    fail();
  };
  ws.onclose = () => def.resolve();
  ws.onopen = () => ws.send("foo");

  await def.promise;
  await doneDeferred.promise;
  ac.abort();
  await server.finished;
});

Deno.test("WebSocket with custom http client with unix proxy", {
  ignore: Deno.build.os === "windows",
}, async () => {
  const socketPath = Deno.makeTempFileSync();
  Deno.removeSync(socketPath);
  const ac = new AbortController();
  const listeningDeferred = Promise.withResolvers<void>();
  const doneDeferred = Promise.withResolvers<void>();
  await using server = Deno.serve({
    handler: (request) => {
      const { response, socket } = Deno.upgradeWebSocket(request);
      socket.onerror = (e) => {
        console.error(e);
        fail();
      };
      socket.onmessage = (m) => {
        socket.send(m.data);
        socket.close(1001);
      };
      socket.onclose = () => doneDeferred.resolve();
      return response;
    },
    signal: ac.signal,
    onListen: onListen(listeningDeferred.resolve),
    onError: createOnErrorCb(ac),
    transport: "unix",
    path: socketPath,
  });

  await listeningDeferred.promise;
  const def = Promise.withResolvers<void>();
  using client = Deno.createHttpClient({
    proxy: {
      transport: "unix",
      path: socketPath,
    },
  });
  const ws = new WebSocket(`ws://localhost:8000/`, { client });
  ws.onmessage = (m) => assertEquals(m.data, "foo");
  ws.onerror = (e) => {
    console.error(e);
    fail();
  };
  ws.onclose = () => def.resolve();
  ws.onopen = () => ws.send("foo");

  await def.promise;
  await doneDeferred.promise;
  ac.abort();
  await server.finished;
});

Deno.test("WebSocket with custom http client with vsock proxy", {
  ignore: Deno.build.os !== "linux",
}, async () => {
  const socketPath = Deno.makeTempFileSync();
  Deno.removeSync(socketPath);
  const ac = new AbortController();
  const listeningDeferred = Promise.withResolvers<void>();
  const doneDeferred = Promise.withResolvers<void>();
  await using server = Deno.serve({
    handler: (request) => {
      const { response, socket } = Deno.upgradeWebSocket(request);
      socket.onerror = (e) => {
        console.error(e);
        fail();
      };
      socket.onmessage = (m) => {
        socket.send(m.data);
        socket.close(1001);
      };
      socket.onclose = () => doneDeferred.resolve();
      return response;
    },
    signal: ac.signal,
    onListen: onListen(listeningDeferred.resolve),
    onError: createOnErrorCb(ac),
    transport: "vsock",
    cid: -1,
    port: 4242,
  });

  await listeningDeferred.promise;
  const def = Promise.withResolvers<void>();
  using client = Deno.createHttpClient({
    proxy: {
      transport: "vsock",
      cid: 1,
      port: 4242,
    },
  });
  const ws = new WebSocket(`ws://localhost:8000/`, { client });
  ws.onmessage = (m) => assertEquals(m.data, "foo");
  ws.onerror = (e) => {
    console.error(e);
    fail();
  };
  ws.onclose = () => def.resolve();
  ws.onopen = () => ws.send("foo");

  await def.promise;
  await doneDeferred.promise;
  ac.abort();
  await server.finished;
});

Deno.test("WebSocket custom host header", async () => {
  const ac = new AbortController();
  const listeningDeferred = Promise.withResolvers<void>();
  const doneDeferred = Promise.withResolvers<void>();
  await using server = Deno.serve({
    handler: (request) => {
      assertEquals(request.headers.get("host"), "example.com");
      assertEquals(request.url, "http://example.com/");
      const {
        response,
        socket,
      } = Deno.upgradeWebSocket(request);
      socket.onerror = (e) => {
        console.error(e);
        fail();
      };
      socket.onmessage = (m) => {
        socket.send(m.data);
        socket.close(1001);
      };
      socket.onclose = () => doneDeferred.resolve();
      return response;
    },
    port: servePort,
    signal: ac.signal,
    onListen: onListen(listeningDeferred.resolve),
    onError: createOnErrorCb(ac),
  });

  await listeningDeferred.promise;
  const def = Promise.withResolvers<void>();
  using client = Deno.createHttpClient({ allowHost: true });
  const ws = new WebSocket(`ws://localhost:${servePort}`, {
    headers: {
      "Host": "example.com",
    },
    client,
  });
  ws.onmessage = (m) => assertEquals(m.data, "foo");
  ws.onerror = (e) => {
    console.error(e);
    fail();
  };
  ws.onclose = () => def.resolve();
  ws.onopen = () => ws.send("foo");

  await def.promise;
  await doneDeferred.promise;
  ac.abort();
  await server.finished;
});

Deno.test("WebSocket close with reason but no code doesn't send 1005", async () => {
  const ac = new AbortController();
  const listeningDeferred = Promise.withResolvers<void>();

  const server = Deno.serve({
    handler: (request) => {
      const { response, socket } = Deno.upgradeWebSocket(request);
      socket.onerror = () => fail();
      socket.onopen = () => {
        socket.close(undefined, "A");
      };
      socket.onclose = () => ac.abort();
      return response;
    },
    port: servePort,
    signal: ac.signal,
    onListen: () => listeningDeferred.resolve(),
    hostname: "localhost",
    onError: createOnErrorCb(ac),
  });

  await listeningDeferred.promise;

  const conn = await Deno.connect({ port: servePort, hostname: "localhost" });
  await conn.write(
    new TextEncoder().encode(
      "GET / HTTP/1.1\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n",
    ),
  );

  const response = new Uint8Array(2048);
  let totalRead = 0;

  let n = await conn.read(response);
  assert(n !== null && n > 0);
  totalRead += n;
  const headerEnd = new TextDecoder().decode(response.subarray(0, totalRead))
    .indexOf("\r\n\r\n");
  assert(headerEnd > 0);

  const frameStart = headerEnd + 4;

  // Keep reading until we have the WebSocket frame header and payload
  while (totalRead < frameStart + 3) {
    n = await conn.read(response.subarray(totalRead));
    assert(n !== null && n > 0);
    totalRead += n;
  }

  // [FIN+opcode][length][payload]
  assertEquals(response[frameStart], 0x88);
  const payloadLength = response[frameStart + 1] & 0x7F;

  // Payload should just be "A" (1 byte), NOT 1005 code (2 bytes) + "A"
  assertEquals(payloadLength, 1);
  assertEquals(response[frameStart + 2], "A".charCodeAt(0));

  conn.close();
  await server.finished;
});
