import { afterEach, describe, expect, test } from "bun:test";
import { connect, createServer, type Server, type Socket } from "node:net";
import { readdirSync } from "node:fs";
import { join } from "node:path";

const relay = join(import.meta.dir, "..", "frontdoor", "relay.mjs");
import { resolveNode } from "./resolve-node";
const node = resolveNode();
const children: Bun.Subprocess[] = [];

afterEach(() => {
  for (const child of children.splice(0)) child.kill();
});

async function freePort(): Promise<number> {
  return await new Promise((resolve, reject) => {
    const server = createServer();
    server.once("error", reject);
    server.listen(0, "127.0.0.1", () => {
      const address = server.address();
      if (!address || typeof address === "string") return reject(new Error("no TCP address"));
      server.close(() => resolve(address.port));
    });
  });
}

function activate(frontPort: number, backendPort: number, deadline = 3000): Bun.Subprocess {
  const child = Bun.spawn([
    // No "--now": that flag only controls whether the child launches before
    // the first connection or on it, and CI's runner image ships systemd 255
    // (Ubuntu 24.04), which predates "--now" and rejects it outright — the
    // listen socket then never gets bound, and every caller sees a bare
    // ECONNREFUSED with no clue why. Omitting it also matches real systemd
    // socket-activated services, which never pre-start either.
    "systemd-socket-activate", "-E", `OVERDECK_FRONTDOOR_DEADLINE_MS=${deadline}`,
    "-l", `127.0.0.1:${frontPort}`,
    node, relay, `127.0.0.1:${backendPort}`,
  ], {
    env: { ...process.env },
    stdout: "pipe", stderr: "pipe",
  });
  children.push(child);
  // Surface a dead-on-arrival child immediately: without this, a bad flag or
  // missing binary shows up only as a downstream ECONNREFUSED that gives no
  // hint the activator itself never started.
  void (async () => {
    const stderr = await new Response(child.stderr).text();
    await child.exited;
    // signalCode is set when afterEach's child.kill() ends the process — that
    // is routine cleanup, not a failure to diagnose. A non-zero exitCode
    // (option parse error, missing binary, etc.) is the real signal.
    if (child.signalCode === null && child.exitCode !== 0) {
      console.error(`systemd-socket-activate exited ${child.exitCode} before serving 127.0.0.1:${frontPort}: ${stderr}`);
    }
    // This IIFE only ever adds diagnostic context; it never gates a test's
    // pass/fail. Swallow read errors here (e.g. the stream tearing down
    // after kill) so they can't surface as a spurious "unhandled error
    // between tests" failure unrelated to what the test actually asserts.
  })().catch((error) => console.error(`frontdoor test diagnostics failed for 127.0.0.1:${frontPort}:`, error));
  return child;
}

function clientRequest(port: number, body: Buffer, halfClose = false): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];
    let attempts = 0;
    const open = () => {
      const socket = connect({ host: "127.0.0.1", port, allowHalfOpen: true });
      socket.once("connect", () => halfClose ? socket.end(body) : socket.write(body));
      socket.on("data", (chunk) => chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk));
      socket.once("end", () => { socket.end(); resolve(Buffer.concat(chunks)); });
      socket.once("error", (error: NodeJS.ErrnoException) => {
        if (error.code === "ECONNREFUSED" && attempts++ < 20) setTimeout(open, 25);
        else reject(error);
      });
    };
    open();
  });
}

function listenBackend(port: number, handler: (socket: Socket) => void): Promise<Server> {
  return new Promise((resolve, reject) => {
    const server = createServer({ allowHalfOpen: true }, handler);
    server.once("error", reject);
    server.listen(port, "127.0.0.1", () => resolve(server));
  });
}

function closeServer(server: Server): Promise<void> {
  return new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}

describe("socket-activated collector front door", () => {
  test("delayed backend preserves bytes sent before backend startup", async () => {
    const frontPort = await freePort();
    const backendPort = await freePort();
    activate(frontPort, backendPort);
    const request = Buffer.from("request-before-backend\0with-binary\xff", "latin1");
    const responsePromise = clientRequest(frontPort, request);
    await Bun.sleep(1000);
    let received = Buffer.alloc(0);
    const backend = await listenBackend(backendPort, (socket) => {
      socket.on("data", (chunk) => {
        const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
        received = Buffer.concat([received, bytes]);
        if (received.length === request.length) socket.end("delayed-response");
      });
    });
    expect((await responsePromise).equals(Buffer.from("delayed-response"))).toBe(true);
    expect(received.equals(request)).toBe(true);
    await closeServer(backend);
  }, 8000);

  test("deadline closes no earlier than 3000ms and no later than 4000ms", async () => {
    const frontPort = await freePort();
    const backendPort = await freePort();
    activate(frontPort, backendPort);
    // No --now (see activate()): the relay only execs on the first
    // connection. Node's own boot time would otherwise land inside this
    // 1000ms-wide assertion window and make it flaky under load. Warm the
    // relay with one real round trip against a backend that then goes away,
    // so the clock below times only the deadline logic, not process startup.
    const warmupBackend = await listenBackend(backendPort, (socket) => socket.end());
    await clientRequest(frontPort, Buffer.from("warmup"));
    await closeServer(warmupBackend);
    await Bun.sleep(100);
    const started = performance.now();
    await new Promise<void>((resolve, reject) => {
      const socket = connect({ host: "127.0.0.1", port: frontPort });
      socket.once("close", resolve);
      socket.once("error", (error: NodeJS.ErrnoException) => error.code === "ECONNRESET" ? undefined : reject(error));
    });
    const elapsed = performance.now() - started;
    expect(elapsed).toBeGreaterThanOrEqual(3000);
    expect(elapsed).toBeLessThanOrEqual(4000);
  }, 7000);

  test("abandoned client cancels retry and releases descriptors", async () => {
    const frontPort = await freePort();
    const backendPort = await freePort();
    const child = activate(frontPort, backendPort, 6000);
    // No --now (see activate()): the activator only execs into the relay on
    // the first connection, so a fd baseline taken before any connection
    // would measure the dormant activator, not the running relay, and every
    // post-activation sample would look like a leak. Warm it up with one
    // clean round trip so the baseline reflects the relay's true idle state.
    const warmupBackend = await listenBackend(backendPort, (socket) => socket.end());
    await clientRequest(frontPort, Buffer.from("warmup"));
    await closeServer(warmupBackend);
    await Bun.sleep(100);
    const baseline = readdirSync(`/proc/${child.pid}/fd`).length;
    const abandoningClient = Bun.spawn([node, "-e", `
      const net = require("node:net");
      const socket = net.connect(${frontPort}, "127.0.0.1");
      socket.once("connect", () => setTimeout(() => socket.resetAndDestroy(), 1000));
      socket.once("close", () => process.exit(0));
    `], { stdout: "ignore", stderr: "pipe" });
    children.push(abandoningClient);
    await abandoningClient.exited;
    await Bun.sleep(750);
    let accepts = 0;
    const backend = await listenBackend(backendPort, (peer) => { accepts++; peer.destroy(); });
    await Bun.sleep(1000);
    expect(accepts).toBe(0);
    expect(readdirSync(`/proc/${child.pid}/fd`).length).toBeLessThanOrEqual(baseline);
    await closeServer(backend);
  }, 7000);

  test("client half-close still receives the complete response", async () => {
    const frontPort = await freePort();
    const backendPort = await freePort();
    activate(frontPort, backendPort);
    const backend = Bun.spawn([node, "-e", `
      const net = require("node:net");
      const server = net.createServer({ allowHalfOpen: true }, socket => {
        const chunks = [];
        socket.on("data", chunk => chunks.push(chunk));
        socket.once("end", () => {
          if (Buffer.concat(chunks).toString() !== "half-close-request") process.exit(3);
          socket.end(Buffer.alloc(256 * 1024, 0x5a));
        });
      });
      server.listen(${backendPort}, "127.0.0.1");
    `], { stdout: "ignore", stderr: "pipe" });
    children.push(backend);
    await Bun.sleep(100);
    const halfClosedClient = Bun.spawn([node, "-e", `
      const net = require("node:net");
      const chunks = [];
      const socket = net.connect({ host: "127.0.0.1", port: ${frontPort}, allowHalfOpen: true });
      socket.once("connect", () => socket.end("half-close-request"));
      socket.on("data", chunk => chunks.push(chunk));
      socket.once("end", () => {
        const response = Buffer.concat(chunks);
        socket.end();
        process.exit(response.length === 256 * 1024 && response.every(byte => byte === 0x5a) ? 0 : 4);
      });
    `], { stdout: "ignore", stderr: "pipe" });
    children.push(halfClosedClient);
    expect(await halfClosedClient.exited).toBe(0);
    backend.kill();
  }, 7000);

  test("a fresh activation accepts after the prior relay is killed", async () => {
    const backendPort = await freePort();
    const backend = await listenBackend(backendPort, (socket) => socket.end("ok"));
    const firstPort = await freePort();
    const first = activate(firstPort, backendPort);
    expect((await clientRequest(firstPort, Buffer.from("one"))).toString()).toBe("ok");
    first.kill();
    await first.exited;
    const secondPort = await freePort();
    activate(secondPort, backendPort);
    expect((await clientRequest(secondPort, Buffer.from("two"))).toString()).toBe("ok");
    await closeServer(backend);
  }, 7000);

  test("rejects activation metadata that does not name exactly one inherited fd", async () => {
    const mismatched = Bun.spawn([node, relay, "127.0.0.1:1"], {
      env: { ...process.env, LISTEN_PID: "0", LISTEN_FDS: "2" }, stderr: "pipe",
    });
    expect(await mismatched.exited).toBe(1);
    expect(await new Response(mismatched.stderr).text()).toContain("LISTEN_PID");

    for (const count of ["0", "2"]) {
      const child = Bun.spawn(["/bin/sh", "-c", `LISTEN_PID=$$ LISTEN_FDS=${count} exec ${node} ${relay} 127.0.0.1:1`], { stderr: "pipe" });
      expect(await child.exited).toBe(1);
      expect(await new Response(child.stderr).text()).toContain("LISTEN_FDS must equal 1");
    }
  });
});
