import * as childProcess from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path";
import * as readline from "node:readline";
import { fileURLToPath } from "node:url";

import {
  addRequestListenerTyped,
  parserFromSchema,
} from "../src/communicator.js";
import { create, TestDataSchema } from "../src/packets/message_common_pb.js";
import { NodeWsClient } from "../src/node_ws_client.js";
import { NodeWsServer } from "../src/node_ws_server.js";

const __filename = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(__filename), "../..");
const dartDir = path.join(repoRoot, "dart");
const CERT_PATH = path.join(repoRoot, "typescript/test/certs/server.crt");
const KEY_PATH = path.join(repoRoot, "typescript/test/certs/server.key");

const HOST = "127.0.0.1";
const WS_PORT = 29898;
const WSS_PORT = 29899;
const WS_PATH = "/";
const PROCESS_TIMEOUT_MS = 60_000;
const STREAM_JOIN_TIMEOUT_MS = 5_000;
const SERVER_OBSERVATION_MS = 500;

async function main(): Promise<void> {
  console.log(`INFO typeName ts=${TestDataSchema.typeName}`);
  try {
    await runWebSuites();
    console.log("PASS scenario=ws-send-push");
    console.log("PASS scenario=ws-request-response");
    console.log("PASS scenario=wss-send-push");
    console.log("PASS scenario=wss-request-response");
    console.log("PASS all typescript-server/dart-web-client crosstests passed");
  } catch (err) {
    console.error(`FAIL crosstest error=${errorMessage(err)}`);
    process.exitCode = 1;
  }
}

async function runWebSuites(): Promise<void> {
  const wsReceived = createReceivedSignal();
  const wssReceived = createReceivedSignal();
  const wsServer = createWebServer(WS_PORT, wsReceived.resolve);
  const wssServer = createWebServer(WSS_PORT, wssReceived.resolve, serverTlsOptions());

  await wsServer.start();
  await wssServer.start();
  try {
    await runDartBrowserTest("test/browser_ws_typescript_test.dart");
    const wsOk = await withTimeout(wsReceived.promise, SERVER_OBSERVATION_MS, "WS web send-push server did not receive expected data");
    if (!wsOk) {
      throw new Error("WS web send-push server received unexpected data");
    }
    const wssOk = await withTimeout(wssReceived.promise, SERVER_OBSERVATION_MS, "WSS web send-push server did not receive expected data");
    if (!wssOk) {
      throw new Error("WSS web send-push server received unexpected data");
    }
  } finally {
    await Promise.allSettled([wssServer.stop(), wsServer.stop()]);
  }
}

function createWebServer(
  port: number,
  resolveReceived: (value: boolean) => void,
  tlsOptions?: { cert: Buffer; key: Buffer },
): NodeWsServer {
  const server = new NodeWsServer(HOST, port, WS_PATH, (ws) => new NodeWsClient(ws, 0, 0, parserMap()), tlsOptions);
  server.onClientConnected = (client) => {
    addRequestListenerTyped(client.communicator, TestDataSchema, (req) => {
      console.log(`SERVER_RECEIVED index=${req.index} message=${req.message}`);
      if (req.index === 101 && req.message === "fire from dart web client") {
        resolveReceived(true);
        void client.communicator.send(
          create(TestDataSchema, {
            index: 200,
            message: "push from typescript server",
          }),
        );
      }
      return create(TestDataSchema, {
        index: req.index * 2,
        message: `echo: ${req.message}`,
      });
    });
  };
  return server;
}

async function runDartBrowserTest(testPath: string): Promise<void> {
  const child = childProcess.spawn("dart", ["test", "-p", "chrome", testPath], {
    cwd: dartDir,
    detached: process.platform !== "win32",
    stdio: ["ignore", "pipe", "pipe"],
  });

  if (child.stdout === null || child.stderr === null) {
    throw new Error("dart browser test stdio is not piped");
  }

  const stdoutDone = collectLines(child.stdout, (line) => console.log(line));
  const stderrDone = collectLines(child.stderr, (line) => console.error(line));

  let timedOut = false;
  const timer = setTimeout(() => {
    timedOut = true;
    killProcessTree(child);
  }, PROCESS_TIMEOUT_MS);

  let exitCode: number;
  try {
    exitCode = await new Promise<number>((resolve, reject) => {
      child.once("error", reject);
      child.once("close", (code) => resolve(code ?? -1));
    });
  } finally {
    clearTimeout(timer);
  }

  await withTimeout(
    Promise.all([stdoutDone, stderrDone]),
    STREAM_JOIN_TIMEOUT_MS,
    "dart browser test streams did not finish",
  );
  if (timedOut) {
    throw new Error("dart browser test timed out");
  }
  if (exitCode !== 0) {
    throw new Error(`dart browser test failed exitCode=${exitCode}`);
  }
}

function collectLines(stream: NodeJS.ReadableStream, onLine: (line: string) => void): Promise<void> {
  const rl = readline.createInterface({ input: stream });
  return new Promise((resolve, reject) => {
    rl.on("line", onLine);
    rl.once("close", resolve);
    rl.once("error", reject);
  });
}

function parserMap() {
  return new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]);
}

function createReceivedSignal(): {
  promise: Promise<boolean>;
  resolve: (value: boolean) => void;
} {
  let done = false;
  let resolvePromise: (value: boolean) => void;
  const promise = new Promise<boolean>((resolve) => {
    resolvePromise = resolve;
  });
  return {
    promise,
    resolve: (value: boolean) => {
      if (!done) {
        done = true;
        resolvePromise(value);
      }
    },
  };
}

function serverTlsOptions(): { cert: Buffer; key: Buffer } {
  return {
    cert: fs.readFileSync(CERT_PATH),
    key: fs.readFileSync(KEY_PATH),
  };
}

function killProcessTree(child: childProcess.ChildProcess): void {
  if (process.platform !== "win32" && child.pid !== undefined) {
    try {
      process.kill(-child.pid, "SIGKILL");
      return;
    } catch {
      // fallthrough to direct kill
    }
  }
  child.kill("SIGKILL");
}

async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
  let timer: ReturnType<typeof setTimeout> | undefined;
  try {
    return await Promise.race([
      promise,
      new Promise<T>((_, reject) => {
        timer = setTimeout(() => reject(new Error(label)), timeoutMs);
      }),
    ]);
  } finally {
    if (timer !== undefined) {
      clearTimeout(timer);
    }
  }
}

function errorMessage(err: unknown): string {
  return err instanceof Error ? err.message : String(err);
}

void main();
