import { describe, expect, test } from "bun:test";
import { chmod, mkdtemp, mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { createIncidentDispatchLauncher, IncidentLaunchError, decodeIncidentRunnerRequest, type IncidentAgentRequest } from "./incident-launcher";

async function fixture() {
  const dir = await mkdtemp(join(tmpdir(), "incident-launcher-"));
  const root = join(dir, "workspaces"); const repo = join(dir, "repo"); const runner = join(repo, "runner.ts"); const wrapper = join(repo, "wrapper.sh");
  await Promise.all([mkdir(root), mkdir(repo)]); await Promise.all([writeFile(runner, ""), writeFile(wrapper, ""), chmod(repo, 0o700)]); await chmod(wrapper, 0o700);
  const request: IncidentAgentRequest = {
    incidentId: "123e4567-e89b-42d3-a456-426614174000", taskId: 42, dispatchId: "dispatch-1", brief: "Persisted incident brief",
    workspace: join(root, "123e4567-e89b-42d3-a456-426614174000"), wrapper, wrapperModel: "gpt-5.6-sol-high", account: "work",
    accountMode: "profile", permissionMode: "safe", timeoutSeconds: 600, initialRevision: 1, priority: "P1",
  };
  return { root, repo, runner, request };
}

describe("incident dispatch launcher", () => {
  test("reports activity only for one exactly identified incident unit", async () => {
    const f = await fixture();
    for (const state of ["active", "inactive", "failed"] as const) {
      const launcher = createIncidentDispatchLauncher({ workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner, exec: async () => ({ exitCode: 0, stdout: `ActiveState=${state}\nEnvironment=OVERDECK_DISPATCH_ID=${f.request.dispatchId}\n`, stderr: "" }) });
      await expect(launcher.isActive!({ incidentId: f.request.incidentId, dispatchId: f.request.dispatchId })).resolves.toBe(state === "active");
    }
    const mismatch = createIncidentDispatchLauncher({ workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner, exec: async () => ({ exitCode: 0, stdout: "ActiveState=inactive\nEnvironment=OVERDECK_DISPATCH_ID=other\n", stderr: "" }) });
    await expect(mismatch.isActive!({ incidentId: f.request.incidentId, dispatchId: f.request.dispatchId })).rejects.toMatchObject({ message: "service identity mismatch" });
  });

  test("rejects duplicate identity even when the unit is already inactive or failed", async () => {
    const f = await fixture();
    for (const state of ["inactive", "failed"] as const) {
      const environment = `OVERDECK_DISPATCH_ID=${f.request.dispatchId} OVERDECK_DISPATCH_ID=${f.request.dispatchId}`;
      const launcher = createIncidentDispatchLauncher({ workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner, exec: async () => ({ exitCode: 0, stdout: `ActiveState=${state}\nEnvironment=${environment}\n`, stderr: "" }) });
      await expect(launcher.stop!({ incidentId: f.request.incidentId, dispatchId: f.request.dispatchId })).rejects.toMatchObject({ message: "service identity mismatch" });
    }
  });

  test("stops only an exactly identified unit and verifies it became inactive", async () => {
    const f = await fixture(); const calls: string[][] = []; let stopped = false;
    const launcher = createIncidentDispatchLauncher({ workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner, exec: async (argv) => {
      calls.push(argv);
      if (argv.includes("show")) return { exitCode: 0, stdout: `ActiveState=active\nEnvironment="OTHER=xOVERDECK_DISPATCH_ID=dispatch-1" "OVERDECK_DISPATCH_ID=dispatch-1"\n`, stderr: "" };
      if (argv.includes("stop")) { stopped = true; return { exitCode: 0, stdout: "", stderr: "" }; }
      if (argv.includes("is-active")) return { exitCode: stopped ? 3 : 0, stdout: stopped ? "inactive\n" : "active\n", stderr: "" };
      return { exitCode: 1, stdout: "", stderr: "unexpected" };
    } });
    await expect(launcher.stop!({ incidentId: f.request.incidentId, dispatchId: f.request.dispatchId })).resolves.toBeUndefined();
    expect(calls.some((argv) => argv.includes("stop"))).toBe(true);
    expect(calls.at(-1)).toContain("is-active");
  });

  test("accepts a legacy inactive unit without dispatch environment but rejects conflicting identity", async () => {
    const f = await fixture();
    const legacy = createIncidentDispatchLauncher({ workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner, exec: async () => ({ exitCode: 0, stdout: "ActiveState=failed\nEnvironment=OVERDECK_DEPLOY_DIR=/deploy\n", stderr: "" }) });
    await expect(legacy.isActive!({ incidentId: f.request.incidentId, dispatchId: f.request.dispatchId })).resolves.toBe(false);
    await expect(legacy.stop!({ incidentId: f.request.incidentId, dispatchId: f.request.dispatchId })).resolves.toBeUndefined();

    const conflicting = createIncidentDispatchLauncher({ workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner, exec: async () => ({ exitCode: 0, stdout: "ActiveState=failed\nEnvironment=OVERDECK_DISPATCH_ID=other-dispatch\n", stderr: "" }) });
    await expect(conflicting.isActive!({ incidentId: f.request.incidentId, dispatchId: f.request.dispatchId })).rejects.toBeInstanceOf(IncidentLaunchError);
    await expect(conflicting.stop!({ incidentId: f.request.incidentId, dispatchId: f.request.dispatchId })).rejects.toBeInstanceOf(IncidentLaunchError);
  });

  test("fails closed when stop inspection fails, identity is only a substring, or inactivity cannot be verified", async () => {
    const f = await fixture();
    for (const mode of ["inspect", "substring", "still-active"] as const) {
      const launcher = createIncidentDispatchLauncher({ workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner, exec: async (argv) => {
        if (argv.includes("show")) return mode === "inspect" ? { exitCode: 1, stdout: "", stderr: "denied" } : { exitCode: 0, stdout: `ActiveState=active\nEnvironment=OTHER=xOVERDECK_DISPATCH_ID=${f.request.dispatchId}\n`, stderr: "" };
        if (argv.includes("stop")) return { exitCode: 0, stdout: "", stderr: "" };
        return { exitCode: 0, stdout: "active\n", stderr: "" };
      } });
      await expect(launcher.stop!({ incidentId: f.request.incidentId, dispatchId: f.request.dispatchId })).rejects.toBeInstanceOf(IncidentLaunchError);
    }
  });

  test("provisions one isolated worktree and admits one non-restarting bounded unit", async () => {
    const f = await fixture(); const calls: string[][] = [];
    const launcher = createIncidentDispatchLauncher({ workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner, bunExecutable: "/usr/bin/bun",
      now: () => new Date("2026-08-09T12:00:00.000Z"), exec: async (argv) => { calls.push(argv); return { exitCode: 0, stdout: "", stderr: "" }; } });
    await expect(launcher.launch(f.request)).resolves.toEqual({ dispatchId: "dispatch-1", unit: "overdeck-incident-123e4567-e89b-42d3-a456-426614174000.service", acceptedAt: "2026-08-09T12:00:00.000Z" });
    expect(calls[0]).toEqual(["/usr/bin/git", "-C", f.repo, "worktree", "add", "--detach", f.request.workspace, "HEAD"]);
    expect(calls[2]?.filter((arg) => !arg.startsWith("--setenv=") && !arg.startsWith("--working-directory=")).slice(0, 13)).toEqual(["/usr/bin/systemd-run", "--user", "--unit=overdeck-incident-123e4567-e89b-42d3-a456-426614174000.service", "--service-type=exec", "--collect", "--property=Restart=no", "--property=RuntimeMaxSec=630s", "--property=TimeoutStopSec=30s", "--property=MemoryMax=2G", "--property=TasksMax=512", "--property=LimitFSIZE=16M", "--quiet", "/usr/bin/bun"]);
    expect(decodeIncidentRunnerRequest(calls[2]!.at(-1)!)).toEqual(f.request);
  });

  test("refuses an active unit whose persisted identity or authoritative properties mismatch", async () => {
    const f = await fixture();
    await mkdir(f.request.workspace);
    const launcher = createIncidentDispatchLauncher({
      workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner, bunExecutable: "/usr/bin/bun",
      exec: async (argv) => {
        if (argv.includes("is-active")) return { exitCode: 0, stdout: "active\n", stderr: "" };
        if (argv.includes("--property=ExecStart")) return { exitCode: 0, stdout: "ExecStart={ path=/usr/bin/bun ; argv[]=/usr/bin/bun run /wrong/runner wrong-request ; }\nWorkingDirectory=/wrong\n", stderr: "" };
        if (argv.includes("overdeck.dispatchId")) return { exitCode: 0, stdout: "other-dispatch\n", stderr: "" };
        return { exitCode: 0, stdout: "", stderr: "" };
      },
    });
    await expect(launcher.launch(f.request)).rejects.toMatchObject({ stage: "provision", message: "active service identity mismatch" });
  });

  test("adopts an already active canonical unit without provisioning or launching twice", async () => {
    const f = await fixture(); const calls: string[][] = [];
    await mkdir(f.request.workspace);
    const launcher = createIncidentDispatchLauncher({
      workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner, bunExecutable: "/usr/bin/bun",
      now: () => new Date("2026-08-09T12:00:00.000Z"),
      exec: async (argv) => {
        calls.push(argv);
        if (argv.includes("is-active")) return { exitCode: 0, stdout: "active\n", stderr: "" };
        if (argv.includes("show")) {
          const encoded = Buffer.from(JSON.stringify(f.request), "utf8").toString("base64url");
          return { exitCode: 0, stdout: `Environment=OVERDECK_DISPATCH_ID=dispatch-1\nWorkingDirectory=${f.request.workspace}\nExecStart=/usr/bin/bun run ${f.runner} ${encoded}\n`, stderr: "" };
        }
        return { exitCode: 0, stdout: "", stderr: "" };
      },
    });

    await expect(launcher.launch(f.request)).resolves.toEqual({
      dispatchId: "dispatch-1",
      unit: "overdeck-incident-123e4567-e89b-42d3-a456-426614174000.service",
      acceptedAt: "2026-08-09T12:00:00.000Z",
    });
    expect(calls).toHaveLength(2);
  });

  test("adopts an exact inactive worktree after validating its revision", async () => {
    const f = await fixture(); const calls: string[][] = [];
    await mkdir(f.request.workspace);
    const launcher = createIncidentDispatchLauncher({ workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner,
      exec: async (argv) => {
        calls.push(argv);
        if (argv[0] === "/usr/bin/systemctl") return { exitCode: 3, stdout: "inactive\n", stderr: "" };
        if (argv.includes("rev-parse") && argv.includes("--show-toplevel")) return { exitCode: 0, stdout: `${f.request.workspace}\n`, stderr: "" };
        if (argv.includes("rev-parse")) return { exitCode: 0, stdout: "abc123\n", stderr: "" };
        if (argv.includes("list")) return { exitCode: 0, stdout: `worktree ${f.request.workspace}\nHEAD abc123\ndetached\n\n`, stderr: "" };
        if (argv.includes("status")) return { exitCode: 0, stdout: "", stderr: "" };
        if (argv.includes("--get")) return { exitCode: 0, stdout: "dispatch-1\n", stderr: "" };
        return { exitCode: 0, stdout: "", stderr: "" };
      } });
    await expect(launcher.launch(f.request)).resolves.toMatchObject({ dispatchId: "dispatch-1" });
    expect(calls.some((argv) => argv.includes("worktree") && argv.includes("add"))).toBe(false);
    expect(calls.at(-1)?.[0]).toBe("/usr/bin/systemd-run");
  });

  test("refuses an inactive worktree with dirty or dispatch-mismatched contents", async () => {
    const f = await fixture(); await mkdir(f.request.workspace);
    const launcher = createIncidentDispatchLauncher({ workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner,
      exec: async (argv) => {
        if (argv.includes("is-active")) return { exitCode: 3, stdout: "inactive\n", stderr: "" };
        if (argv.includes("rev-parse") && argv.includes("--show-toplevel")) return { exitCode: 0, stdout: `${f.repo}\n`, stderr: "" };
        if (argv.includes("rev-parse")) return { exitCode: 0, stdout: "abc123\n", stderr: "" };
        if (argv.includes("list")) return { exitCode: 0, stdout: `worktree ${f.request.workspace}\nHEAD abc123\ndetached\n\n`, stderr: "" };
        if (argv.includes("status")) return { exitCode: 0, stdout: "?? hostile.sh\n", stderr: "" };
        return { exitCode: 0, stdout: "other-dispatch\n", stderr: "" };
      } });
    await expect(launcher.launch(f.request)).rejects.toMatchObject({ stage: "provision" });
  });

  test("refuses a mismatched inactive worktree with an explicit recoverable error", async () => {
    const f = await fixture(); await mkdir(f.request.workspace);
    const launcher = createIncidentDispatchLauncher({ workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner,
      exec: async (argv) => {
        if (argv[0] === "/usr/bin/systemctl") return { exitCode: 3, stdout: "inactive\n", stderr: "" };
        if (argv.includes("rev-parse")) return { exitCode: 0, stdout: "expected\n", stderr: "" };
        if (argv.includes("list")) return { exitCode: 0, stdout: `worktree ${f.request.workspace}\nHEAD wrong\ndetached\n\n`, stderr: "" };
        return { exitCode: 0, stdout: "", stderr: "" };
      } });
    await expect(launcher.launch(f.request)).rejects.toMatchObject({
      stage: "provision",
      message: `existing workspace mismatch; recover or remove ${f.request.workspace}`,
    });
  });

  test("refuses invalid ids and workspace paths before spawning", async () => {
    const f = await fixture(); const calls: string[][] = [];
    const launcher = createIncidentDispatchLauncher({ workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner, exec: async (argv) => { calls.push(argv); return { exitCode: 0, stdout: "", stderr: "" }; } });
    await expect(launcher.launch({ ...f.request, incidentId: "../escape" })).rejects.toBeInstanceOf(IncidentLaunchError);
    await expect(launcher.launch({ ...f.request, workspace: join(f.root, "wrong") })).rejects.toBeInstanceOf(IncidentLaunchError);
    expect(calls).toEqual([]);
  });

  test("refuses a tampered unsafe runner request before provisioning", async () => {
    const f = await fixture(); const calls: string[][] = [];
    const launcher = createIncidentDispatchLauncher({ workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner, exec: async (argv) => { calls.push(argv); return { exitCode: 0, stdout: "", stderr: "" }; } });
    await expect(launcher.launch({ ...f.request, permissionMode: "unsafe" } as unknown as IncidentAgentRequest)).rejects.toMatchObject({ stage: "provision" });
    expect(calls).toEqual([]);
    const encoded = Buffer.from(JSON.stringify({ ...f.request, permissionMode: "unsafe" }), "utf8").toString("base64url");
    expect(() => decodeIncidentRunnerRequest(encoded)).toThrow();
  });

  test("reports provisioning and exec admission refusal without retry", async () => {
    const f = await fixture(); let calls = 0;
    const provision = createIncidentDispatchLauncher({ workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner, exec: async () => ({ exitCode: ++calls === 1 ? 1 : 0, stdout: "", stderr: "no worktree" }) });
    await expect(provision.launch(f.request)).rejects.toMatchObject({ stage: "provision" }); expect(calls).toBe(1);
    calls = 0; const startCalls: string[][] = [];
    const start = createIncidentDispatchLauncher({ workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner, exec: async (argv) => { startCalls.push(argv); return { exitCode: ++calls === 3 ? 1 : 0, stdout: "", stderr: "unit refused" }; } });
    await expect(start.launch(f.request)).rejects.toMatchObject({ stage: "start", message: "unit refused" });
    expect(calls).toBe(4);
    expect(startCalls[3]).toEqual(["/usr/bin/timeout", "--signal=TERM", "--kill-after=2s", "10s", "/usr/bin/git", "-C", f.repo, "worktree", "remove", f.request.workspace]);
  });

  test("preserves a recovery path when bounded start-refusal cleanup fails", async () => {
    const f = await fixture(); let calls = 0;
    const launcher = createIncidentDispatchLauncher({
      workspaceRoot: f.root, sourceRepo: f.repo, runnerEntry: f.runner,
      exec: async () => {
        calls += 1;
        if (calls === 3) return { exitCode: 1, stdout: "", stderr: "unit refused" };
        if (calls === 4) return { exitCode: 124, stdout: "", stderr: "cleanup timed out" };
        return { exitCode: 0, stdout: "", stderr: "" };
      },
    });
    await expect(launcher.launch(f.request)).rejects.toMatchObject({
      stage: "start",
      message: `unit refused; workspace cleanup refused: cleanup timed out; recover at ${f.request.workspace}`,
    });
    expect(calls).toBe(4);
  });
});
