import { Http3Server, WebTransport } from "@fails-components/webtransport";
import expect from "expect.js";
import { Server } from "engine.io";
import { Socket } from "../build/esm-debug/index.js";
import { generateWebTransportCertificate } from "./util-wt.mjs";
import { createServer } from "http";
import { TransformStream } from "stream/web";

if (typeof window === "undefined") {
  global.WebTransport = WebTransport;
  global.TransformStream = TransformStream;
}

async function setup(opts, cb) {
  const certificate = await generateWebTransportCertificate(
    [{ shortName: "CN", value: "localhost" }],
    {
      days: 13, // the total length of the validity period MUST NOT exceed two weeks (https://w3c.github.io/webtransport/#custom-certificate-requirements)
    },
  );

  const engine = new Server(opts);

  const h3Server = new Http3Server({
    port: 0,
    host: "0.0.0.0",
    secret: "changeit",
    cert: certificate.cert,
    privKey: certificate.private,
  });

  (async () => {
    try {
      const stream = await h3Server.sessionStream("/engine.io/");
      const sessionReader = stream.getReader();

      while (true) {
        const { done, value } = await sessionReader.read();
        if (done) {
          break;
        }
        engine.onWebTransportSession(value);
      }
    } catch (ex) {
      console.error("Server error", ex);
    }
  })();

  h3Server.startServer();

  await h3Server.ready;

  cb({ engine, h3Server, certificate });
}

function createHttpServer(port) {
  const httpServer = createServer();

  let retryCount = 0;

  return new Promise((resolve, reject) => {
    httpServer.listen(port, () => resolve(httpServer));

    httpServer.on("error", (e) => {
      if (e.code === "EADDRINUSE" && ++retryCount <= 3) {
        console.warn("port already in use, retrying...");
        setTimeout(() => {
          httpServer.listen(port, () => resolve(httpServer));
        }, 100);
      }
      reject(e);
    });
  });
}

function success(engine, h3server, done) {
  engine.close();
  h3server.stopServer();
  done();
}

function createSocket(port, certificate, opts) {
  return new Socket(
    `http://127.0.0.1:${port}`,
    Object.assign(
      {
        transportOptions: {
          webtransport: {
            serverCertificateHashes: [
              {
                algorithm: "sha-256",
                value: certificate.hash,
              },
            ],
          },
        },
      },
      opts,
    ),
  );
}

describe("WebTransport", () => {
  it("should allow to connect with WebTransport directly", (done) => {
    setup({}, ({ engine, h3Server, certificate }) => {
      const socket = createSocket(h3Server.port, certificate, {
        transports: ["webtransport"],
      });

      socket.on("open", () => {
        success(engine, h3Server, done);
      });
    });
  });

  it("should allow to upgrade to WebTransport", (done) => {
    setup(
      {
        transports: ["polling", "webtransport"],
      },
      async ({ engine, h3Server, certificate }) => {
        const httpServer = await createHttpServer(h3Server.port);
        engine.attach(httpServer);

        const socket = createSocket(h3Server.port, certificate, {
          transports: ["polling", "webtransport"],
        });

        socket.on("upgrade", () => {
          httpServer.close();
          success(engine, h3Server, done);
        });
      },
    );
  });

  it("should favor WebTransport over WebSocket", (done) => {
    setup(
      {
        transports: ["polling", "websocket", "webtransport"],
      },
      async ({ engine, h3Server, certificate }) => {
        const httpServer = await createHttpServer(h3Server.port);
        engine.attach(httpServer);

        const socket = createSocket(h3Server.port, certificate, {
          transports: ["polling", "websocket", "webtransport"],
        });

        socket.on("upgrade", (transport) => {
          expect(transport.name).to.eql("webtransport");

          httpServer.close();
          success(engine, h3Server, done);
        });
      },
    );
  });

  it("should send ping/pong packets", (done) => {
    setup(
      {
        pingInterval: 20,
      },
      ({ engine, h3Server, certificate }) => {
        const socket = createSocket(h3Server.port, certificate, {
          transports: ["webtransport"],
        });

        let i = 0;

        socket.on("heartbeat", () => {
          i++;

          if (i === 10) {
            success(engine, h3Server, done);
          }
        });
      },
    );
  });

  it("should handle connections closed by the server", (done) => {
    setup({}, ({ engine, h3Server, certificate }) => {
      const socket = createSocket(h3Server.port, certificate, {
        transports: ["webtransport"],
      });

      engine.on("connection", (serverSocket) => {
        serverSocket.close();
      });

      socket.on("close", (reason) => {
        expect(reason).to.eql("transport close");

        success(engine, h3Server, done);
      });
    });
  });

  it("should handle connections closed by the client", (done) => {
    setup({}, ({ engine, h3Server, certificate }) => {
      const socket = createSocket(h3Server.port, certificate, {
        transports: ["webtransport"],
      });

      engine.on("connection", (serverSocket) => {
        serverSocket.on("close", (reason) => {
          expect(reason).to.eql("transport close");

          success(engine, h3Server, done);
        });
      });

      socket.on("open", () => {
        socket.close();
      });
    });
  });

  it("should send some plaintext data (client to server)", (done) => {
    setup({}, ({ engine, h3Server, certificate }) => {
      const socket = createSocket(h3Server.port, certificate, {
        transports: ["webtransport"],
      });

      engine.on("connection", (serverSocket) => {
        serverSocket.on("message", (data) => {
          expect(data).to.eql("hello");

          success(engine, h3Server, done);
        });
      });

      socket.on("open", () => {
        socket.send("hello");
      });
    });
  });

  it("should send some plaintext data (server to client)", (done) => {
    setup({}, ({ engine, h3Server, certificate }) => {
      const socket = createSocket(h3Server.port, certificate, {
        transports: ["webtransport"],
      });

      engine.on("connection", (serverSocket) => {
        serverSocket.send("hello");
      });

      socket.on("message", (data) => {
        expect(data).to.eql("hello");

        success(engine, h3Server, done);
      });
    });
  });

  it("should send some binary data (client to server)", (done) => {
    setup({}, ({ engine, h3Server, certificate }) => {
      const socket = createSocket(h3Server.port, certificate, {
        transports: ["webtransport"],
      });

      engine.on("connection", (serverSocket) => {
        serverSocket.on("message", (data) => {
          expect(data).to.eql(Uint8Array.from([1, 2, 3]));

          success(engine, h3Server, done);
        });
      });

      socket.on("open", () => {
        socket.send(Uint8Array.from([1, 2, 3]));
      });
    });
  });

  it("should send some binary data (server to client) (as ArrayBuffer)", (done) => {
    setup({}, ({ engine, h3Server, certificate }) => {
      const socket = createSocket(h3Server.port, certificate, {
        transports: ["webtransport"],
      });

      socket.binaryType = "arraybuffer";

      engine.on("connection", (serverSocket) => {
        serverSocket.send(Uint8Array.from([1, 2, 3]));
      });

      socket.on("message", (data) => {
        expect(data).to.be.an(ArrayBuffer);
        expect(new Uint8Array(data)).to.eql(Uint8Array.of(1, 2, 3));

        success(engine, h3Server, done);
      });
    });
  });

  it("should send some binary data (server to client) (as Buffer)", (done) => {
    setup({}, ({ engine, h3Server, certificate }) => {
      const socket = createSocket(h3Server.port, certificate, {
        transports: ["webtransport"],
      });

      engine.on("connection", (serverSocket) => {
        serverSocket.send(Uint8Array.from([1, 2, 3]));
      });

      socket.on("message", (data) => {
        expect(Buffer.isBuffer(data)).to.be(true);
        expect(data).to.eql(Uint8Array.of(1, 2, 3));

        success(engine, h3Server, done);
      });
    });
  });
});
