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

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

const cert = Deno.readTextFileSync("tests/testdata/tls/localhost.crt");
const key = Deno.readTextFileSync("tests/testdata/tls/localhost.key");
const caCerts = [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")];

interface Pair {
  server: Deno.QuicConn;
  client: Deno.QuicConn;
  endpoint: Deno.QuicEndpoint;
}

function trackedStreamResourceCount(conn: Deno.QuicConn): number {
  // @ts-ignore Deno.internal is available to the unit test suite.
  return Deno[Deno.internal].getQuicStreamResourceCount(conn);
}

async function pair(opt?: Deno.QuicTransportOptions): Promise<Pair> {
  const endpoint = new Deno.QuicEndpoint({ hostname: "localhost" });
  const listener = endpoint.listen({
    cert,
    key,
    alpnProtocols: ["deno-test"],
    ...opt,
  });
  assertEquals(endpoint, listener.endpoint);

  const [server, client] = await Promise.all([
    listener.accept(),
    Deno.connectQuic({
      hostname: "localhost",
      port: endpoint.addr.port,
      caCerts,
      alpnProtocols: ["deno-test"],
      ...opt,
    }),
  ]);

  assertEquals(server.protocol, "deno-test");
  assertEquals(client.protocol, "deno-test");
  assertEquals(client.remoteAddr, endpoint.addr);
  assertEquals(server.serverName, "localhost");

  return { server, client, endpoint };
}

Deno.test("bidirectional stream", async () => {
  const { server, client, endpoint } = await pair();

  const encoded = (new TextEncoder()).encode("hi!");

  {
    const bi = await server.createBidirectionalStream({ sendOrder: 42 });
    assertEquals(bi.writable.sendOrder, 42);
    bi.writable.sendOrder = 0;
    assertEquals(bi.writable.sendOrder, 0);
    await bi.writable.getWriter().write(encoded);
  }

  {
    const { value: bi } = await client.incomingBidirectionalStreams
      .getReader()
      .read();
    const { value: data } = await bi!.readable.getReader().read();
    assertEquals(data, encoded);
  }

  client.close();
  endpoint.close();
});

Deno.test("unidirectional stream", async () => {
  const { server, client, endpoint } = await pair();

  const encoded = (new TextEncoder()).encode("hi!");

  {
    const uni = await server.createUnidirectionalStream({ sendOrder: 42 });
    assertEquals(uni.sendOrder, 42);
    uni.sendOrder = 0;
    assertEquals(uni.sendOrder, 0);
    await uni.getWriter().write(encoded);
  }

  {
    const { value: uni } = await client.incomingUnidirectionalStreams
      .getReader()
      .read();
    const { value: data } = await uni!.getReader().read();
    assertEquals(data, encoded);
  }

  endpoint.close();
  client.close();
});

Deno.test("completed streams release connection cleanup state", async () => {
  const { server, client, endpoint } = await pair();

  const bi = await server.createBidirectionalStream();
  assertEquals(trackedStreamResourceCount(server), 2);
  await Promise.all([bi.readable.cancel(), bi.writable.close()]);
  assertEquals(trackedStreamResourceCount(server), 0);

  const uni = await server.createUnidirectionalStream();
  assertEquals(trackedStreamResourceCount(server), 1);
  await uni.close();
  assertEquals(trackedStreamResourceCount(server), 0);

  await server.createBidirectionalStream();
  assertEquals(trackedStreamResourceCount(server), 2);
  server.close();
  await server.closed;
  assertEquals(trackedStreamResourceCount(server), 0);

  await client.closed;
  endpoint.close();
});

Deno.test("datagrams", async () => {
  const { server, client, endpoint } = await pair();

  const encoded = (new TextEncoder()).encode("hi!");

  await server.sendDatagram(encoded);

  const data = await client.readDatagram();
  assertEquals(data, encoded);

  endpoint.close();
  client.close();
});

Deno.test("closing", async () => {
  const { server, client } = await pair();

  server.close({ closeCode: 42, reason: "hi!" });

  assertEquals(await client.closed, { closeCode: 42, reason: "hi!" });
});

Deno.test("max concurrent streams", async () => {
  const { server, client, endpoint } = await pair({
    maxConcurrentBidirectionalStreams: 1,
    maxConcurrentUnidirectionalStreams: 1,
  });

  {
    await server.createBidirectionalStream();
    await server.createBidirectionalStream()
      .then(() => {
        throw new Error("expected failure");
      }, () => {
        // success!
      });
  }

  {
    await server.createUnidirectionalStream();
    await server.createUnidirectionalStream()
      .then(() => {
        throw new Error("expected failure");
      }, () => {
        // success!
      });
  }

  endpoint.close();
  client.close();
});

Deno.test("incoming", async () => {
  const endpoint = new Deno.QuicEndpoint({ hostname: "localhost" });
  const listener = endpoint.listen({
    cert,
    key,
    alpnProtocols: ["deno-test"],
  });

  const connect = () =>
    Deno.connectQuic({
      hostname: "localhost",
      port: endpoint.addr.port,
      caCerts,
      alpnProtocols: ["deno-test"],
    });

  const c1p = connect();
  const i1 = await listener.incoming();
  const server = await i1.accept();
  const client = await c1p;

  assertEquals(server.protocol, "deno-test");
  assertEquals(client.protocol, "deno-test");
  assertEquals(client.remoteAddr, endpoint.addr);

  endpoint.close();
  client.close();
});

Deno.test("0rtt", async () => {
  const sEndpoint = new Deno.QuicEndpoint({ hostname: "localhost" });
  const listener = sEndpoint.listen({
    cert,
    key,
    alpnProtocols: ["deno-test"],
  });

  (async () => {
    while (true) {
      let incoming;
      try {
        incoming = await listener.incoming();
      } catch (e) {
        if (e instanceof Deno.errors.BadResource) {
          break;
        }
        throw e;
      }
      const conn = incoming.accept({ zeroRtt: true });
      conn.handshake.then(() => {
        conn.close();
      });
    }
  })();

  const endpoint = new Deno.QuicEndpoint();

  const c1 = await Deno.connectQuic({
    hostname: "localhost",
    port: sEndpoint.addr.port,
    caCerts,
    alpnProtocols: ["deno-test"],
    endpoint,
  });

  await c1.closed;

  const c2 = Deno.connectQuic({
    hostname: "localhost",
    port: sEndpoint.addr.port,
    caCerts,
    alpnProtocols: ["deno-test"],
    zeroRtt: true,
    endpoint,
  });

  assert(!(c2 instanceof Promise), "0rtt should be accepted");

  await c2.closed;

  sEndpoint.close();
  endpoint.close();
});
