import { afterEach, describe, expect, it } from "vitest";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { ProcessManager } from "./process-manager.js";

async function waitForOutput(
  manager: ProcessManager,
  id: string,
  predicate: (output: string) => boolean,
): Promise<string> {
  let combined = "";
  for (let i = 0; i < 50; i += 1) {
    const result = await manager.readOutput(id);
    combined += result.output;
    if (predicate(combined)) return combined;
    await new Promise((resolve) => setTimeout(resolve, 100));
  }
  throw new Error(`Timed out waiting for output. Saw:\n${combined}`);
}

async function waitForProcessExit(pid: number): Promise<void> {
  for (let i = 0; i < 50; i += 1) {
    if (!isProcessAlive(pid)) return;
    await new Promise((resolve) => setTimeout(resolve, 100));
  }
  throw new Error(`Process ${pid} was still alive after shutdown.`);
}

function isProcessAlive(pid: number): boolean {
  try {
    process.kill(pid, 0);
    return true;
  } catch {
    return false;
  }
}

function parseGrandchildPid(output: string): number {
  const match = output.match(/GRANDCHILD_READY (\d+)/);
  if (!match) throw new Error(`No grandchild pid in output:\n${output}`);
  return Number(match[1]);
}

describe("ProcessManager dev-server lifecycle repro", () => {
  let manager: ProcessManager;

  afterEach(() => {
    manager?.shutdownAll();
  });

  it("starts, reads, and stops a long-running Node HTTP server through the worker background path", async () => {
    manager = new ProcessManager();
    const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gg-dev-server-repro-"));
    const fixture = path.join(tmpDir, "dev-server.mjs");
    await fs.writeFile(
      fixture,
      `import http from 'node:http';\n` +
        `const server = http.createServer((_req, res) => res.end('ok'));\n` +
        `server.listen(0, '127.0.0.1', () => {\n` +
        `  const address = server.address();\n` +
        `  console.log('DEV_SERVER_READY ' + address.port);\n` +
        `});\n` +
        `const interval = setInterval(() => console.log('DEV_SERVER_TICK'), 250);\n` +
        `process.on('SIGTERM', () => {\n` +
        `  console.log('DEV_SERVER_SIGTERM');\n` +
        `  clearInterval(interval);\n` +
        `  server.close(() => process.exit(0));\n` +
        `});\n`,
    );

    const started = await manager.start(`${process.execPath} ${fixture}`, tmpDir);
    expect(started.pid).toBeGreaterThan(0);
    expect(started.logFile).toMatch(/\.log$/);

    const initial = await waitForOutput(manager, started.id, (output) =>
      output.includes("DEV_SERVER_READY"),
    );
    expect(initial).toContain("DEV_SERVER_READY");

    const fromStart = await manager.readOutput(started.id, true);
    expect(fromStart.isRunning).toBe(true);
    expect(fromStart.exitCode).toBeNull();
    expect(fromStart.output).toContain("DEV_SERVER_READY");

    const stopped = await manager.stop(started.id);
    expect(stopped).toBe(`Process ${started.id} stopped`);

    const final = await manager.readOutput(started.id, true);
    expect(final.isRunning).toBe(false);
    expect(final.exitCode).not.toBeNull();
    expect(final.output).toContain("DEV_SERVER_SIGTERM");
  }, 15_000);

  const posixIt = process.platform === "win32" ? it.skip : it;

  posixIt(
    "kills the whole detached process group on POSIX/WSL shutdown",
    async () => {
      manager = new ProcessManager();
      const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gg-posix-process-group-"));
      const childFixture = path.join(tmpDir, "grandchild.mjs");
      const parentFixture = path.join(tmpDir, "parent.mjs");

      await fs.writeFile(
        childFixture,
        `console.log('GRANDCHILD_READY ' + process.pid);\n` + `setInterval(() => {}, 1000);\n`,
      );
      await fs.writeFile(
        parentFixture,
        `import { spawn } from 'node:child_process';\n` +
          `const child = spawn(process.execPath, [${JSON.stringify(childFixture)}], { stdio: ['ignore', 'inherit', 'inherit'] });\n` +
          `console.log('PARENT_READY ' + process.pid + ' child=' + child.pid);\n` +
          `setInterval(() => {}, 1000);\n`,
      );

      const started = await manager.start(
        `${JSON.stringify(process.execPath)} ${JSON.stringify(parentFixture)}`,
        tmpDir,
      );
      const output = await waitForOutput(manager, started.id, (text) =>
        text.includes("GRANDCHILD_READY"),
      );
      const grandchildPid = parseGrandchildPid(output);
      expect(isProcessAlive(grandchildPid)).toBe(true);

      manager.shutdownAll();

      await waitForProcessExit(grandchildPid);
      const final = await manager.readOutput(started.id, true);
      expect(final.isRunning).toBe(false);
      expect(final.output).toContain("PARENT_READY");
      expect(final.output).toContain("GRANDCHILD_READY");
    },
    15_000,
  );
});
