import { afterEach, describe, expect, test } from "bun:test";
import { mkdtempSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { runCli } from "./cli";

function captureStdout(run: () => Promise<number>): Promise<{ code: number; stdout: string }> {
  const originalWrite = process.stdout.write.bind(process.stdout);
  let stdout = "";
  process.stdout.write = ((chunk: string | Uint8Array) => {
    stdout += String(chunk);
    return true;
  }) as typeof process.stdout.write;

  return run().then((code) => {
    process.stdout.write = originalWrite;
    return { code, stdout: stdout.trim() };
  });
}

describe("cli", () => {
  const previousReport = process.env.OVERDECK_SPINE_REPORT;
  const previousObey = process.env.OVERDECK_SPINE_OBEY;
  const previousConfigDir = process.env.OVERDECK_CONFIG_DIR;

  afterEach(() => {
    if (previousReport === undefined) delete process.env.OVERDECK_SPINE_REPORT;
    else process.env.OVERDECK_SPINE_REPORT = previousReport;
    if (previousObey === undefined) delete process.env.OVERDECK_SPINE_OBEY;
    else process.env.OVERDECK_SPINE_OBEY = previousObey;
    if (previousConfigDir === undefined) delete process.env.OVERDECK_CONFIG_DIR;
    else process.env.OVERDECK_CONFIG_DIR = previousConfigDir;
  });

  test("parse-config writes one compact success line", async () => {
    const dir = mkdtempSync(join(tmpdir(), "spine-cli-parse-"));
    const path = join(dir, "build-remote.json");
    writeFileSync(path, JSON.stringify({ enabled: true, port: 2222 }));

    const { code, stdout } = await captureStdout(() => runCli(["parse-config", "--json", path]));
    expect(code).toBe(0);
    expect(stdout.split("\n")).toHaveLength(1);
    expect(JSON.parse(stdout)).toEqual({
      ok: true,
      config: expect.objectContaining({
        enabled: true,
        port: 2222,
        hosts: [],
      }),
    });
  });

  test("parse-config exits 65 for a leftover hosts key", async () => {
    const dir = mkdtempSync(join(tmpdir(), "spine-cli-leftover-hosts-"));
    const path = join(dir, "build-remote.json");
    writeFileSync(path, JSON.stringify({ enabled: true, hosts: ["debian1"] }));

    const { code, stdout } = await captureStdout(() => runCli(["parse-config", "--json", path]));
    expect(code).toBe(65);
    expect(JSON.parse(stdout)).toEqual({
      ok: false,
      error: "config-invalid-shape",
      detail: expect.any(String),
    });
  });

  test("parse-config exits 65 for invalid shape", async () => {
    const dir = mkdtempSync(join(tmpdir(), "spine-cli-invalid-shape-"));
    const path = join(dir, "build-remote.json");
    writeFileSync(path, JSON.stringify({ enabled: "nope" }));

    const { code, stdout } = await captureStdout(() => runCli(["parse-config", "--json", path]));
    expect(code).toBe(65);
    expect(JSON.parse(stdout)).toEqual({
      ok: false,
      error: "config-invalid-shape",
      detail: expect.any(String),
    });
  });

  test("report accepts stdin-only payloads and exits 0 on delivery failure", async () => {
    process.env.OVERDECK_SPINE_REPORT = "1";
    const dir = mkdtempSync(join(tmpdir(), "spine-cli-report-"));
    process.env.OVERDECK_CONFIG_DIR = dir;
    writeFileSync(join(dir, "token"), "secret-token", { mode: 0o600 });

    const originalText = Bun.stdin.text;
    Bun.stdin.text = async () => JSON.stringify({
      source: "remote-build",
      host: "debian1",
      key: "job-key",
      mirror: "repo-abc123",
      repo: "owner/repo",
      snapshot: "42",
      argv: ["/usr/bin/bun", "test"],
      attempt: 1,
      stage: "started",
      startedAt: "2026-07-20T00:00:00.000Z",
      timeoutSec: 30,
    });

    const originalFetch = globalThis.fetch;
    globalThis.fetch = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch;

    try {
      const { code, stdout } = await captureStdout(() => runCli(["report", "--json"]));
      expect(code).toBe(0);
      expect(JSON.parse(stdout)).toEqual({ ok: true });
    } finally {
      Bun.stdin.text = originalText;
      globalThis.fetch = originalFetch;
    }
  });

  test("report exits 64 for invalid stdin", async () => {
    const originalText = Bun.stdin.text;
    Bun.stdin.text = async () => "not-json";
    try {
      const { code, stdout } = await captureStdout(() => runCli(["report", "--json"]));
      expect(code).toBe(64);
      expect(JSON.parse(stdout)).toEqual({
        ok: false,
        error: "invalid-args",
        detail: expect.any(String),
      });
    } finally {
      Bun.stdin.text = originalText;
    }
  });

  test("host-eligible writes exact verdict line", async () => {
    process.env.OVERDECK_SPINE_OBEY = "1";
    const dir = mkdtempSync(join(tmpdir(), "spine-cli-eligible-"));
    process.env.OVERDECK_CONFIG_DIR = dir;
    writeFileSync(join(dir, "token"), "secret-token", { mode: 0o600 });

    const originalFetch = globalThis.fetch;
    globalThis.fetch = (async () => Response.json({ eligible: false, reason: "no-capacity" })) as unknown as typeof fetch;
    try {
      const { code, stdout } = await captureStdout(() => runCli(["host-eligible", "--json", "debian1", "bun"]));
      expect(code).toBe(0);
      expect(JSON.parse(stdout)).toEqual({ eligible: false, reason: "no-capacity" });
    } finally {
      globalThis.fetch = originalFetch;
    }
  });

  test("host-eligible exits 64 for missing args", async () => {
    const { code, stdout } = await captureStdout(() => runCli(["host-eligible", "--json", "debian1"]));
    expect(code).toBe(64);
    expect(JSON.parse(stdout)).toEqual({
      ok: false,
      error: "invalid-args",
      detail: "host and command are required",
    });
  });
});
