import { spawn } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it } from "vitest";

const waitForReady = async (
  proc: ReturnType<typeof spawn>,
  chunksOut: string[],
  chunksErr: string[],
  timeoutMs: number,
) => {
  await new Promise<void>((resolve, reject) => {
    const timer = setTimeout(() => {
      const stdout = chunksOut.join("");
      const stderr = chunksErr.join("");
      cleanup();
      reject(
        new Error(
          `timeout waiting for gateway to start\n` +
            `--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}`,
        ),
      );
    }, timeoutMs);

    const cleanup = () => {
      clearTimeout(timer);
      proc.off("exit", onExit);
      proc.off("message", onMessage);
      proc.stdout?.off("data", onStdout);
    };

    const onExit = () => {
      const stdout = chunksOut.join("");
      const stderr = chunksErr.join("");
      cleanup();
      reject(
        new Error(
          `gateway exited before ready (code=${String(proc.exitCode)} signal=${String(proc.signalCode)})\n` +
            `--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}`,
        ),
      );
    };

    const onMessage = (msg: unknown) => {
      if (msg && typeof msg === "object" && "ready" in msg) {
        cleanup();
        resolve();
      }
    };

    const onStdout = (chunk: unknown) => {
      if (String(chunk).includes("READY")) {
        cleanup();
        resolve();
      }
    };

    proc.once("exit", onExit);
    proc.on("message", onMessage);
    proc.stdout?.on("data", onStdout);
  });
};

describe("gateway SIGTERM", () => {
  let child: ReturnType<typeof spawn> | null = null;

  afterEach(() => {
    if (!child || child.killed) return;
    try {
      child.kill("SIGKILL");
    } catch {
      // ignore
    }
    child = null;
  });

  it("exits 0 on SIGTERM", { timeout: 180_000 }, async () => {
    const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-gateway-test-"));
    const out: string[] = [];
    const err: string[] = [];

    const nodeBin = process.execPath;
    const env = {
      ...process.env,
      OPENCLAW_NO_RESPAWN: "1",
      OPENCLAW_STATE_DIR: stateDir,
      OPENCLAW_SKIP_CHANNELS: "1",
      OPENCLAW_SKIP_GMAIL_WATCHER: "1",
      OPENCLAW_SKIP_CRON: "1",
      OPENCLAW_SKIP_BROWSER_CONTROL_SERVER: "1",
      OPENCLAW_SKIP_CANVAS_HOST: "1",
    };
    const bootstrapPath = path.join(stateDir, "clawdbot-entry-bootstrap.mjs");
    const runLoopPath = path.resolve("src/cli/gateway-cli/run-loop.ts");
    const runtimePath = path.resolve("src/runtime.ts");
    fs.writeFileSync(
      bootstrapPath,
      [
        'import { pathToFileURL } from "node:url";',
        `const runLoopUrl = ${JSON.stringify(pathToFileURL(runLoopPath).href)};`,
        `const runtimeUrl = ${JSON.stringify(pathToFileURL(runtimePath).href)};`,
        "const { runGatewayLoop } = await import(runLoopUrl);",
        "const { defaultRuntime } = await import(runtimeUrl);",
        "await runGatewayLoop({",
        "  start: async () => {",
        '    process.stdout.write("READY\\\\n");',
        "    if (process.send) process.send({ ready: true });",
        "    const keepAlive = setInterval(() => {}, 1000);",
        "    return { close: async () => clearInterval(keepAlive) };",
        "  },",
        "  runtime: defaultRuntime,",
        "});",
      ].join("\n"),
      "utf8",
    );
    const childArgs = ["--import", "tsx", bootstrapPath];

    child = spawn(nodeBin, childArgs, {
      cwd: process.cwd(),
      env,
      stdio: ["ignore", "pipe", "pipe", "ipc"],
    });

    const proc = child;
    if (!proc) throw new Error("failed to spawn gateway");

    child.stdout?.setEncoding("utf8");
    child.stderr?.setEncoding("utf8");
    child.stdout?.on("data", (d) => out.push(String(d)));
    child.stderr?.on("data", (d) => err.push(String(d)));

    await waitForReady(proc, out, err, 150_000);

    proc.kill("SIGTERM");

    const result = await new Promise<{
      code: number | null;
      signal: NodeJS.Signals | null;
    }>((resolve) => proc.once("exit", (code, signal) => resolve({ code, signal })));

    if (result.code !== 0 && !(result.code === null && result.signal === "SIGTERM")) {
      const stdout = out.join("");
      const stderr = err.join("");
      throw new Error(
        `expected exit code 0, got code=${String(result.code)} signal=${String(result.signal)}\n` +
          `--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}`,
      );
    }
    if (result.code === null && result.signal === "SIGTERM") return;
    expect(result.signal).toBeNull();
  });
});
