import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { LandRetirementService, successorSeatArgs } from "./land-retirement";
import { ControllerStore } from "./store";

let dir = "";
let now = 1_000_000;
let store: ControllerStore;

function receipt(ticketId = "ticket.0123456789abcdef0123456789abcdef") {
  const queueDir = join(dir, "landq");
  mkdirSync(queueDir, { recursive: true });
  return {
    requestId: "request-1", taskId: "task-1", root: "/repo", worktree: "/repo/.worktrees/task-1",
    ref: "refs/heads/task-1", commit: "a".repeat(40), artifact: "sha256:artifact", verification: "focused-green",
    pendingOperation: "land" as const, blocker: "", nextAction: "deploy exact artifact", rollback: "restore previous release",
    successorAccount: "delivery", successorModel: "sonnet", queueDir, ticketId,
  };
}

beforeEach(() => {
  dir = mkdtempSync(join(tmpdir(), "land-retirement-"));
  now = 1_000_000;
  store = new ControllerStore(join(dir, "state.sqlite"), { now: () => now });
});

afterEach(() => {
  store.close();
  rmSync(dir, { recursive: true, force: true });
});

describe("land wait retirement", () => {

  test("conducts every configured root once and records successful health", async () => {
    const roots = [join(dir, "repo-a"), join(dir, "repo-b")];
    const conducted: string[] = [];
    const service = new LandRetirementService(store, {
      now: () => now,
      conductor: () => {},
      repositoryRoots: roots,
      conductRoot: (root) => {
        conducted.push(root);
        return { ok: true, detail: "" };
      },
    });

    await service.conductConfiguredRoots();

    expect(conducted).toEqual(roots);
    expect(store.listLandConductHealth()).toEqual(roots.map((root) => ({
      root,
      lastPassAt: new Date(now).toISOString(),
      lastOk: true,
      lastDetail: "",
      consecutiveFailures: 0,
    })));
  });

  test("skips a missing configured root and continues conducting remaining roots", async () => {
    const missing = join(dir, "missing");
    const available = join(dir, "available");
    const script = join(dir, "finish-branch.sh");
    mkdirSync(available);
    const calls = join(dir, "calls");
    writeFileSync(script, `#!/bin/sh\nprintf '%s\\n' "$*" >> '${calls}'\n`);
    const lines: string[] = [];
    const service = new LandRetirementService(store, {
      now: () => now,
      conductorScript: script,
      repositoryRoots: [missing, available],
    });

    await expect(service.conductConfiguredRoots((line) => lines.push(line))).resolves.toBeUndefined();
    expect(lines[0]).toStartWith(`land-conduct: ${missing} conduct pass failed (ENOENT`);
    expect(readFileSync(calls, "utf8")).toBe(`landq-conduct ${available}\n`);
    expect(store.listLandConductHealth()).toEqual([
      {
        root: available, lastPassAt: new Date(now).toISOString(), lastOk: true,
        lastDetail: "", consecutiveFailures: 0,
      },
      {
        root: missing, lastPassAt: new Date(now).toISOString(), lastOk: false,
        lastDetail: expect.stringContaining("ENOENT"), consecutiveFailures: 1,
      },
    ]);
  });

  test("records a non-zero conduct pass without throwing", async () => {
    const root = join(dir, "repo");
    const script = join(dir, "finish-branch.sh");
    mkdirSync(root);
    writeFileSync(script, "#!/bin/sh\n");
    const lines: string[] = [];
    const service = new LandRetirementService(store, {
      now: () => now,
      conductorScript: script,
      repositoryRoots: [root],
      conductRoot: () => ({ ok: false, detail: "rc=9" }),
    });

    await expect(service.conductConfiguredRoots((line) => lines.push(line))).resolves.toBeUndefined();
    expect(lines).toEqual([`land-conduct: ${root} conduct pass failed (rc=9)`]);
    expect(store.listLandConductHealth()).toEqual([{
      root, lastPassAt: new Date(now).toISOString(), lastOk: false,
      lastDetail: "rc=9", consecutiveFailures: 1,
    }]);
  });

  test("ignores a re-entrant configured-root conduct pass", async () => {
    const root = join(dir, "repo");
    let calls = 0;
    let service: LandRetirementService;
    service = new LandRetirementService(store, {
      conductor: () => {},
      repositoryRoots: [root],
      conductRoot: () => {
        calls += 1;
        service.conductConfiguredRoots();
        return { ok: true, detail: "" };
      },
    });

    await service.conductConfiguredRoots();
    expect(calls).toBe(1);
    expect(store.listLandConductHealth()).toHaveLength(1);
  });

  test("uses fixed conductor executable and server-derived repository paths", () => {
    const allowed = join(dir, "projects");
    const root = join(allowed, "repo");
    const queueDir = join(root, ".git", "harness", "landq");
    const script = join(dir, "installed", "finish-branch.sh");
    mkdirSync(queueDir, { recursive: true });
    mkdirSync(join(dir, "installed"), { recursive: true });
    writeFileSync(script, "#!/bin/sh\n");
    const calls: unknown[][] = [];
    const inputReceipt = { ...receipt(), root, queueDir };
    new LandRetirementService(store, {
      conductorScript: script,
      repositoryRoots: [root],
      spawn: ((...args: unknown[]) => { calls.push(args); return { status: 0 }; }) as never,
    }).accept({ operationId: "land-fixed-script", receipt: inputReceipt, leaseTtlMs: 5_000 });
    expect(calls[0]?.[0]).toBe("bash");
    expect(calls[0]?.[1]).toEqual([script, "landq-conduct", root]);
  });

  test("ignores unavailable configured roots until a receipt selects one", () => {
    const unavailable = join(dir, "removed-worktree");
    const script = join(dir, "finish-branch.sh");
    writeFileSync(script, "#!/bin/sh\n");
    const service = new LandRetirementService(store, { conductorScript: script, repositoryRoots: [unavailable] });
    const inputReceipt = { ...receipt(), root: unavailable, queueDir: join(unavailable, ".git", "harness", "landq") };
    expect(() => service.accept({ operationId: "land-removed", receipt: inputReceipt, leaseTtlMs: 5_000 }))
      .toThrow();
  });

  test("reconciliation skips receipts whose configured worktree no longer exists", () => {
    const unavailable = join(dir, "removed-worktree");
    const script = join(dir, "finish-branch.sh");
    writeFileSync(script, "#!/bin/sh\n");
    const inputReceipt = { ...receipt(), root: unavailable, queueDir: join(unavailable, ".git", "harness", "landq") };
    store.acceptLandOperation({ operationId: "land-removed", receipt: inputReceipt, leaseExpiresAt: now - 1 });
    expect(() => new LandRetirementService(store, { conductorScript: script, repositoryRoots: [unavailable] }).reconcile())
      .not.toThrow();
  });

  test("rejects malicious absolute repository paths outside configured roots", () => {
    const allowed = join(dir, "projects");
    const outside = join(dir, "attacker");
    const script = join(dir, "finish-branch.sh");
    mkdirSync(join(outside, ".git", "harness", "landq"), { recursive: true });
    mkdirSync(allowed, { recursive: true });
    writeFileSync(script, "#!/bin/sh\n");
    const inputReceipt = { ...receipt(), root: outside, queueDir: join(outside, ".git", "harness", "landq") };
    expect(() => new LandRetirementService(store, { conductorScript: script, repositoryRoots: [allowed] })
      .accept({ operationId: "land-outside", receipt: inputReceipt, leaseTtlMs: 5_000 }))
      .toThrow("repository root is not configured");
  });

  test("rejects repository symlink escapes", () => {
    const allowed = join(dir, "projects");
    const outside = join(dir, "attacker");
    const linkedRoot = join(allowed, "repo");
    const script = join(dir, "finish-branch.sh");
    mkdirSync(join(outside, ".git", "harness", "landq"), { recursive: true });
    mkdirSync(allowed, { recursive: true });
    symlinkSync(outside, linkedRoot);
    writeFileSync(script, "#!/bin/sh\n");
    const inputReceipt = { ...receipt(), root: linkedRoot, queueDir: join(linkedRoot, ".git", "harness", "landq") };
    expect(() => new LandRetirementService(store, { conductorScript: script, repositoryRoots: [allowed] })
      .accept({ operationId: "land-symlink", receipt: inputReceipt, leaseTtlMs: 5_000 }))
      .toThrow("repository root must not contain symlinks");
  });

  test("rejects symlinked receipt queue paths", () => {
    const root = join(dir, "repo");
    const queueDir = join(root, ".git", "harness", "landq");
    const linkedQueue = join(root, "linked-queue");
    const script = join(dir, "finish-branch.sh");
    mkdirSync(queueDir, { recursive: true });
    symlinkSync(queueDir, linkedQueue);
    writeFileSync(script, "#!/bin/sh\n");
    const inputReceipt = { ...receipt(), root, queueDir: linkedQueue };
    expect(() => new LandRetirementService(store, { conductorScript: script, repositoryRoots: [root] })
      .accept({ operationId: "land-queue-symlink", receipt: inputReceipt, leaseTtlMs: 5_000 }))
      .toThrow("land queue directory must not contain symlinks");
  });

  test("rejects receipt queue paths not derived from repository metadata", () => {
    const allowed = join(dir, "projects");
    const root = join(allowed, "repo");
    const queueDir = join(root, ".git", "harness", "landq");
    const maliciousQueue = join(root, "attacker-queue");
    const script = join(dir, "finish-branch.sh");
    mkdirSync(queueDir, { recursive: true });
    mkdirSync(maliciousQueue);
    writeFileSync(script, "#!/bin/sh\n");
    const inputReceipt = { ...receipt(), root, queueDir: maliciousQueue };
    expect(() => new LandRetirementService(store, { conductorScript: script, repositoryRoots: [root] })
      .accept({ operationId: "land-queue", receipt: inputReceipt, leaseTtlMs: 5_000 }))
      .toThrow("land queue directory does not match repository");
  });
  test("persists immutable acceptance before returning and replays duplicate acceptance", () => {
    const service = new LandRetirementService(store, { now: () => now, conductor: () => {} });
    const input = { operationId: "land-1", receipt: receipt(), leaseTtlMs: 5_000 };
    const accepted = service.accept(input);
    const replay = service.accept(input);
    expect(accepted).toEqual(replay);
    expect(store.getLandOperation("land-1")).toEqual(accepted);
    expect(() => service.accept({ ...input, receipt: { ...input.receipt, nextAction: "different" } })).toThrow("immutable land operation receipt mismatch");
  });

  test("replays acceptance after receipt persistence before controller claim", () => {
    const input = { operationId: "land-1", receipt: receipt(), leaseTtlMs: 5_000 };
    store.acceptLandOperation({ operationId: input.operationId, receipt: input.receipt, leaseExpiresAt: now + input.leaseTtlMs });
    const accepted = new LandRetirementService(store, { now: () => now, conductor: () => {} }).accept(input);
    expect(accepted.operationId).toBe(input.operationId);
    expect(readFileSync(join(input.receipt.queueDir, `${input.receipt.ticketId}.controller`), "utf8")).toBe("land-1\n1\n");
  });

  test("reconciliation tolerates an unavailable retired worker queue", () => {
    const input = { operationId: "land-1", receipt: receipt(), leaseTtlMs: 5_000 };
    store.acceptLandOperation({ operationId: input.operationId, receipt: input.receipt, leaseExpiresAt: now + input.leaseTtlMs });
    rmSync(input.receipt.queueDir, { recursive: true });
    expect(() => new LandRetirementService(store, { now: () => now, conductor: () => {} }).reconcile()).not.toThrow();
  });

  test("rejects stale fence and creates one idempotent successor intent", () => {
    const dispatched: string[] = [];
    const service = new LandRetirementService(store, { now: () => now, conductor: () => {}, dispatch: (intent) => {
      dispatched.push(intent.intentId);
      return { identity: intent.intentId, generation: intent.claimGeneration, state: "running" };
    } });
    const accepted = service.accept({ operationId: "land-1", receipt: receipt(), leaseTtlMs: 5_000 });
    const verdict = { rc: 0, result: { status: "pushed" } };
    expect(() => store.recordLandOperationVerdict({ operationId: "land-1", ticketId: accepted.receipt.ticketId, fence: accepted.fence + 1, verdict })).toThrow("stale land operation fence");
    const completed = store.recordLandOperationVerdict({ operationId: "land-1", ticketId: accepted.receipt.ticketId, fence: accepted.fence, verdict });
    expect(completed.state).toBe("succeeded");
    expect(store.recordLandOperationVerdict({ operationId: "land-1", ticketId: accepted.receipt.ticketId, fence: accepted.fence, verdict })).toEqual(completed);
    service.reconcile();
    service.reconcile();
    expect(dispatched).toEqual([`land-successor:land-1:${accepted.fence}`]);
  });

  test("rejects stale fence after a terminal verdict", () => {
    const service = new LandRetirementService(store, { now: () => now, conductor: () => {} });
    const accepted = service.accept({ operationId: "land-1", receipt: receipt(), leaseTtlMs: 5_000 });
    const verdict = { rc: 0, result: { status: "pushed" } };
    store.recordLandOperationVerdict({ operationId: "land-1", ticketId: accepted.receipt.ticketId, fence: accepted.fence, verdict });
    expect(() => store.recordLandOperationVerdict({ operationId: "land-1", ticketId: accepted.receipt.ticketId, fence: accepted.fence - 1, verdict })).toThrow("stale land operation fence");
  });

  test("restart reconciliation consumes a persisted verdict", () => {
    const launched: string[] = [];
    const accepted = new LandRetirementService(store, { now: () => now, conductor: () => {} }).accept({ operationId: "land-1", receipt: receipt(), leaseTtlMs: 5_000 });
    writeFileSync(join(accepted.receipt.queueDir, `${accepted.receipt.ticketId}.verdict`), '0\n{"status":"pushed"}\n');
    store = store.reopen(join(dir, "state.sqlite"), { now: () => now });
    new LandRetirementService(store, { now: () => now, conductor: () => {}, dispatch: (intent) => {
      launched.push(intent.intentId);
      return { identity: intent.intentId, generation: intent.claimGeneration, state: "running" };
    } }).reconcile();
    expect(store.getLandOperation("land-1")?.state).toBe("succeeded");
    expect(launched).toEqual([`land-successor:land-1:${accepted.fence}`]);
  });

  test("restart reconciliation records a failed persisted verdict without dispatch", () => {
    const launched: string[] = [];
    const accepted = new LandRetirementService(store, { now: () => now, conductor: () => {} }).accept({ operationId: "land-1", receipt: receipt(), leaseTtlMs: 5_000 });
    writeFileSync(join(accepted.receipt.queueDir, `${accepted.receipt.ticketId}.verdict`), '20\n{"status":"push-failed"}\n');
    store = store.reopen(join(dir, "state.sqlite"), { now: () => now });
    new LandRetirementService(store, { now: () => now, conductor: () => {}, dispatch: (intent) => {
      launched.push(intent.intentId);
      return { identity: intent.intentId, generation: intent.claimGeneration, state: "running" };
    } }).reconcile();
    expect(store.getLandOperation("land-1")?.state).toBe("failed");
    expect(launched).toEqual([]);
  });

  test("fences successor seat dispatch to durable receipt", () => {
    const accepted = new LandRetirementService(store, { now: () => now, conductor: () => {} }).accept({ operationId: "land-1", receipt: receipt(), leaseTtlMs: 5_000 });
    const verdict = { rc: 0, result: { status: "pushed" } };
    store.recordLandOperationVerdict({ operationId: "land-1", ticketId: accepted.receipt.ticketId, fence: accepted.fence, verdict });
    const intent = store.claimPendingLandSuccessorDispatches()[0];
    expect(intent).toBeDefined();
    expect(successorSeatArgs(intent!)).toEqual([
      "launch", "--host", "auto", "--account", "delivery", "--project", accepted.receipt.worktree,
      "--model", "sonnet", "--dispatch-key", `land-successor:land-1:${accepted.fence}`, "--", "-p",
      `Resume land operation land-1 at fence ${accepted.fence} with dispatch key land-successor:land-1:${accepted.fence}. Reconstruct durable receipt from controller state, then deploy exact artifact.`,
    ]);
  });

  test("expires lease monotonically and restarts conductor with next fence", () => {
    const fences: number[] = [];
    const service = new LandRetirementService(store, { now: () => now, conductor: (operation) => fences.push(operation.fence) });
    service.accept({ operationId: "land-1", receipt: receipt(), leaseTtlMs: 1_000 });
    now += 1_001;
    service.reconcile();
    expect(fences).toEqual([1, 2]);
    expect(readFileSync(join(receipt().queueDir, `${receipt().ticketId}.controller`), "utf8")).toBe("land-1\n2\n");
    expect(store.getLandOperation("land-1")?.fence).toBe(2);
  });

  test("claims one successor across duplicate reconcilers", () => {
    const accepted = new LandRetirementService(store, { now: () => now, conductor: () => {} }).accept({ operationId: "land-1", receipt: receipt(), leaseTtlMs: 5_000 });
    store.recordLandOperationVerdict({ operationId: "land-1", ticketId: accepted.receipt.ticketId, fence: accepted.fence, verdict: { rc: 0, result: { status: "pushed" } } });
    const duplicate = new ControllerStore(join(dir, "state.sqlite"), { now: () => now });
    expect(store.claimPendingLandSuccessorDispatches()).toHaveLength(1);
    expect(duplicate.claimPendingLandSuccessorDispatches()).toEqual([]);
    duplicate.close();
  });

  test("reclaims expired successor dispatches and rejects stale generations", () => {
    const accepted = new LandRetirementService(store, { now: () => now, conductor: () => {} }).accept({ operationId: "land-1", receipt: receipt(), leaseTtlMs: 5_000 });
    store.recordLandOperationVerdict({ operationId: "land-1", ticketId: accepted.receipt.ticketId, fence: accepted.fence, verdict: { rc: 0, result: { status: "pushed" } } });
    const stale = store.claimPendingLandSuccessorDispatches(1_000)[0]!;
    now += 1_001;
    const current = store.claimPendingLandSuccessorDispatches(1_000)[0]!;
    expect(current.claimGeneration).toBe(stale.claimGeneration + 1);
    expect(store.markLandSuccessorDispatched(stale)).toBe(false);
    expect(store.markLandSuccessorDispatched(current)).toBe(true);
  });

  test("retries a claim that crashed before external acceptance", () => {
    const accepted = new LandRetirementService(store, { now: () => now, conductor: () => {} }).accept({ operationId: "land-1", receipt: receipt(), leaseTtlMs: 5_000 });
    store.recordLandOperationVerdict({ operationId: "land-1", ticketId: accepted.receipt.ticketId, fence: accepted.fence, verdict: { rc: 0, result: { status: "pushed" } } });
    store.claimPendingLandSuccessorDispatches(1_000);
    now += 1_001;
    const dispatched: string[] = [];
    new LandRetirementService(store, { now: () => now, conductor: () => {}, dispatch: (intent) => {
      dispatched.push(intent.dispatchKey);
      return { identity: intent.intentId, generation: intent.claimGeneration, state: "running" };
    } }).reconcile();
    expect(dispatched).toEqual([`land-successor:land-1:${accepted.fence}`]);
  });

  test("rejects non-running and invalid receiver states", () => {
    const accepted = new LandRetirementService(store, { now: () => now, conductor: () => {} }).accept({ operationId: "land-1", receipt: receipt(), leaseTtlMs: 5_000 });
    store.recordLandOperationVerdict({ operationId: "land-1", ticketId: accepted.receipt.ticketId, fence: accepted.fence, verdict: { rc: 0, result: { status: "pushed" } } });
    const responses = [
      { identity: "dispatch", generation: 1, state: "accepted" },
      { identity: "dispatch", generation: 1, state: "failed" },
      { identity: "dispatch", generation: 1, state: "arbitrary" },
      { identity: "dispatch", generation: 0, state: "running" },
      { identity: "dispatch", generation: -1, state: "running" },
      { identity: "dispatch", generation: 1, state: "running" },
    ];
    for (const response of responses) {
      new LandRetirementService(store, { now: () => now, conductor: () => {}, dispatch: () => response }).reconcile();
      now += 60_001;
    }
    expect(store.claimPendingLandSuccessorDispatches()).toEqual([]);
  });

  test("recovers after receiver acceptance without a second accepted successor", () => {
    const accepted = new LandRetirementService(store, { now: () => now, conductor: () => {} }).accept({ operationId: "land-1", receipt: receipt(), leaseTtlMs: 5_000 });
    store.recordLandOperationVerdict({ operationId: "land-1", ticketId: accepted.receipt.ticketId, fence: accepted.fence, verdict: { rc: 0, result: { status: "pushed" } } });
    const intent = store.claimPendingLandSuccessorDispatches(1_000)[0]!;
    let receiverAccepts = 0;
    const invokeSeat = (_dispatch: typeof intent) => {
      receiverAccepts += 1;
      return { identity: intent.intentId, generation: intent.claimGeneration, state: "running" };
    };
    expect(invokeSeat(intent)).not.toBeNull();
    expect(store.markLandSuccessorDispatched(intent)).toBe(true);
    store = store.reopen(join(dir, "state.sqlite"), { now: () => now + 1_001 });
    now += 1_001;
    new LandRetirementService(store, { now: () => now, conductor: () => {}, dispatch: invokeSeat }).reconcile();
    expect(receiverAccepts).toBe(1);
  });
});
