import { resolveNode } from "./resolve-node";
import { afterEach, describe, expect, test } from "bun:test";
import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const script = join(import.meta.dir, "..", "..", "packaging", "frontdoor-migrate.sh");
const roots: string[] = [];

afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });

function fixture(config: string) {
  const root = mkdtempSync(join(tmpdir(), "overdeck-frontdoor-migrate-"));
  roots.push(root);
  const home = join(root, "home");
  const configDir = join(home, ".config", "overdeck");
  const unitDir = join(home, ".config", "systemd", "user");
  const bin = join(root, "bin");
  mkdirSync(configDir, { recursive: true });
  mkdirSync(unitDir, { recursive: true });
  mkdirSync(bin);
  writeFileSync(join(configDir, "config.toml"), config);
  writeFileSync(join(configDir, "token"), "fixture-token\n");
  writeFileSync(join(unitDir, "overdeck-collector-front.service"), "[Service]\nExecStart=__NODE__ /fixture/relay.mjs 127.0.0.1:31341\n");
  for (const [name, body] of Object.entries({
    systemctl: `#!/usr/bin/env bash
printf "%s\\n" "$*" >>"$FRONTDOOR_TEST_LOG"
if [[ "\${FRONTDOOR_FAIL_COLLECTOR_START:-}" == 1 && "$*" == *"start overdeck-collector.service"* && ! -f "$FRONTDOOR_TEST_LOG.failed" ]]; then
  touch "$FRONTDOOR_TEST_LOG.failed"
  exit 1
fi
`,
    ss: '#!/usr/bin/env bash\nprintf "LISTEN 0 128 127.0.0.1:31341 0.0.0.0:*\\n"\n',
    curl: '#!/usr/bin/env bash\nprintf "200"\n',
  })) {
    writeFileSync(join(bin, name), body);
    chmodSync(join(bin, name), 0o755);
  }
  return { root, home, configDir, unitDir, bin, log: join(root, "systemctl.log") };
}

async function run(f: ReturnType<typeof fixture>, extra: Record<string, string> = {}) {
  const child = Bun.spawn(["bash", script], {
    cwd: join(import.meta.dir, ".."),
    env: {
      ...process.env, HOME: f.home, FRONTDOOR_TEST_LOG: f.log,
      OVERDECK_FRONTDOOR_SYSTEMCTL: join(f.bin, "systemctl"),
      OVERDECK_FRONTDOOR_SS: join(f.bin, "ss"),
      OVERDECK_FRONTDOOR_CURL: join(f.bin, "curl"),
      OVERDECK_FRONTDOOR_BUN: process.execPath,
      OVERDECK_FRONTDOOR_NODE: resolveNode(),
      ...extra,
    },
    stdout: "pipe", stderr: "pipe",
  });
  const [status, stdout, stderr] = await Promise.all([
    child.exited, new Response(child.stdout).text(), new Response(child.stderr).text(),
  ]);
  return { status, stdout, stderr };
}

describe("frontdoor migration fixture", () => {
  test("precondition gates change nothing", async () => {
    const cases: Array<[string, string, Record<string, string>]> = [
      ["effective port is not 31338", "port = 4000\n", {}],
      ["collector bind is not loopback-only", 'port = 31338\ntailnetBind = true\nbindHost = "100.64.0.1"\n', {}],
      ["node >= v20 is unavailable", "port = 31338\n", { OVERDECK_FRONTDOOR_NODE: "" }],
    ];
    for (const [reason, config, env] of cases) {
      const f = fixture(config);
      const result = await run(f, env);
      expect(result.status).toBe(0);
      expect(result.stderr).toContain(reason);
      expect(readFileSync(join(f.configDir, "config.toml"), "utf8")).toBe(config);
      expect(Bun.file(f.log).size).toBe(0);
    }
  });

  test("prepends a root key, verifies with the real loader, and re-run is a no-op", async () => {
    const f = fixture('port = 31338\n[adapters.demo]\nenabled = false\n');
    const first = await run(f);
    expect(first.status).toBe(0);
    const edited = readFileSync(join(f.configDir, "config.toml"), "utf8");
    expect(edited.startsWith("bind_port = 31341\nport = 31338\n")).toBe(true);
    const calls = readFileSync(f.log, "utf8");
    const second = await run(f);
    expect(second.status).toBe(0);
    expect(second.stdout).toContain("already configured");
    expect(readFileSync(f.log, "utf8")).toBe(calls);
    expect(readFileSync(join(f.configDir, "config.toml"), "utf8")).toBe(edited);
  });

  test("a post-stop failure disables the front units, restores config, and restarts collector", async () => {
    const original = "port = 31338\n";
    const f = fixture(original);
    const result = await run(f, { FRONTDOOR_FAIL_COLLECTOR_START: "1" });
    expect(result.status).not.toBe(0);
    expect(result.stderr).toContain("start collector");
    expect(readFileSync(join(f.configDir, "config.toml"), "utf8")).toBe(original);
    const calls = readFileSync(f.log, "utf8");
    expect(calls).toContain("stop overdeck-collector-front.service overdeck-collector-front.socket");
    expect(calls).toContain("disable overdeck-collector-front.service overdeck-collector-front.socket");
    expect(calls.match(/start overdeck-collector\.service/g)?.length).toBe(2);
  });
});
