import { describe, expect, test } from "bun:test";
import { readLandqState } from "./landq-state";

const REPO = "/repo";
const DIR = `${REPO}/.git/harness/landq`;
const B64 = (s: string) => Buffer.from(s, "utf8").toString("base64");

function job(fields: Record<string, string>): string {
  return Object.entries(fields).map(([k, v]) => `${k} ${B64(v)}`).join("\n") + "\n";
}

function makeFs(files: Record<string, string>, dirs: string[] = [DIR]) {
  return {
    existsSyncImpl: (path: string) => path in files || dirs.includes(path),
    readFileImpl: (path: string) => {
      if (!(path in files)) throw new Error(`missing ${path}`);
      return files[path]!;
    },
  };
}

describe("readLandqState", () => {
  test("repo with no landq dir reports absent, not an error", () => {
    const result = readLandqState({
      repoRoots: [REPO],
      existsSyncImpl: () => false,
      readFileImpl: () => {
        throw new Error("should not read");
      },
      readProcLocksImpl: () => "",
      statImpl: () => {
        throw new Error("should not stat");
      },
    });
    expect(result.repos).toEqual([
      {
        repoRoot: REPO,
        project: "repo",
        status: "absent",
        queueDepth: 0,
        waiting: [],
        waitingComplete: true,
        conductorHeld: null,
        conductorHolderPid: null,
        conductorHolderAgeSeconds: null,
        conductorComplete: true,
      },
    ]);
  });

  test("waiting ticket with every field populated, sourced from the queue file", () => {
    const ticket = "ticket.11111111111111111111111111111111";
    const fs = makeFs({
      [`${DIR}/queue`]: `${ticket}\n`,
      [`${DIR}/${ticket}.job`]: job({
        branch: "wt/thing",
        gate_class: "full",
        owner_pid: "4242",
        enqueued_at: "2026-08-14T22:00:00Z",
        queue_depth: "2",
      }),
    });
    const result = readLandqState({
      repoRoots: [REPO],
      ...fs,
      statImpl: () => ({ dev: 1, ino: 99 }),
      readProcLocksImpl: () => "1: POSIX ADVISORY WRITE 4242 fe:01:99 0 0",
      nowMs: () => Date.parse("2026-08-14T22:00:30Z"),
    });
    const repo = result.repos[0]!;
    expect(repo.status).toBe("ok");
    expect(repo.queueDepth).toBe(1);
    expect(repo.waitingComplete).toBe(true);
    expect(repo.waiting).toEqual([
      {
        ticket,
        branch: "wt/thing",
        gateClass: "full",
        ownerPid: 4242,
        enqueuedAt: "2026-08-14T22:00:00Z",
        waitSeconds: 30,
        queueDepthAtArrival: 2,
        position: 1,
        ownerLockHeld: true,
        malformed: false,
      },
    ]);
  });

  test("a .job with no matching .verdict is still waiting; a resolved verdict drops it", () => {
    const waitingTicket = "ticket.22222222222222222222222222222222";
    const resolvedTicket = "ticket.33333333333333333333333333333333";
    const fs = makeFs({
      [`${DIR}/queue`]: `${waitingTicket}\n${resolvedTicket}\n`,
      [`${DIR}/${waitingTicket}.job`]: job({ branch: "wt/a", enqueued_at: "2026-08-14T22:00:00Z" }),
      [`${DIR}/${resolvedTicket}.job`]: job({ branch: "wt/b", enqueued_at: "2026-08-14T21:00:00Z" }),
      [`${DIR}/${resolvedTicket}.verdict`]: "0\n",
    });
    const result = readLandqState({
      repoRoots: [REPO],
      ...fs,
      statImpl: () => ({ dev: 1, ino: 1 }),
      readProcLocksImpl: () => "",
      nowMs: () => Date.parse("2026-08-14T22:00:30Z"),
    });
    const repo = result.repos[0]!;
    expect(repo.waiting.map((t) => t.ticket)).toEqual([waitingTicket]);
    expect(repo.queueDepth).toBe(1);
  });

  test("stale .verdict.<pid> debris is not a resolved verdict and does not affect waiting", () => {
    const ticket = "ticket.44444444444444444444444444444444";
    const fs = makeFs({
      [`${DIR}/queue`]: `${ticket}\n`,
      [`${DIR}/${ticket}.job`]: job({ branch: "wt/c", enqueued_at: "2026-08-14T22:00:00Z" }),
      [`${DIR}/${ticket}.verdict.998877`]: "",
    });
    const result = readLandqState({
      repoRoots: [REPO],
      ...fs,
      statImpl: () => ({ dev: 1, ino: 1 }),
      readProcLocksImpl: () => "",
      nowMs: () => Date.parse("2026-08-14T22:00:30Z"),
    });
    expect(result.repos[0]!.waiting.map((t) => t.ticket)).toEqual([ticket]);
  });

  test("stale .verdict files from prior days are not globbed into the roster (only queue-listed tickets count)", () => {
    const listed = "ticket.55555555555555555555555555555555";
    const orphanVerdict = "ticket.66666666666666666666666666666666";
    const fs = makeFs({
      [`${DIR}/queue`]: `${listed}\n`,
      [`${DIR}/${listed}.job`]: job({ branch: "wt/d", enqueued_at: "2026-08-14T22:00:00Z" }),
      [`${DIR}/${orphanVerdict}.verdict`]: "20\n",
    });
    const result = readLandqState({
      repoRoots: [REPO],
      ...fs,
      statImpl: () => ({ dev: 1, ino: 1 }),
      readProcLocksImpl: () => "",
      nowMs: () => Date.parse("2026-08-14T22:00:30Z"),
    });
    expect(result.repos[0]!.queueDepth).toBe(1);
    expect(result.repos[0]!.waiting.map((t) => t.ticket)).toEqual([listed]);
  });

  test("a field with an empty value (real finish-branch.sh shape: `assets_ok `) is not malformed", () => {
    const ticket = "ticket.99999999999999999999999999999999";
    // Exact shape finish-branch.sh writes: assets_ok/verified_tree carry an empty encoded
    // value, i.e. a trailing space with nothing after it — trimming the line loses the split.
    const raw = [
      `root ${B64("/home/user/Projects/overdeck")}`,
      `branch ${B64("wt/example")}`,
      "assets_ok ",
      "verified_tree ",
      `gate_class ${B64("full")}`,
      `owner_pid ${B64("4242")}`,
      `enqueued_at ${B64("2026-08-14T22:00:00Z")}`,
      `queue_depth ${B64("0")}`,
      "",
    ].join("\n");
    const fs = makeFs({
      [`${DIR}/queue`]: `${ticket}\n`,
      [`${DIR}/${ticket}.job`]: raw,
    });
    const result = readLandqState({
      repoRoots: [REPO],
      ...fs,
      statImpl: () => ({ dev: 1, ino: 1 }),
      readProcLocksImpl: () => "",
      nowMs: () => Date.parse("2026-08-14T22:00:30Z"),
    });
    const t = result.repos[0]!.waiting[0]!;
    expect(t.malformed).toBe(false);
    expect(t.branch).toBe("wt/example");
    expect(t.gateClass).toBe("full");
  });

  test("a malformed job line still surfaces the ticket as waiting, never drops it", () => {
    const ticket = "ticket.77777777777777777777777777777777";
    const fs = makeFs({
      [`${DIR}/queue`]: `${ticket}\n`,
      [`${DIR}/${ticket}.job`]: "not-a-valid-line-without-space\n",
    });
    const result = readLandqState({
      repoRoots: [REPO],
      ...fs,
      statImpl: () => ({ dev: 1, ino: 1 }),
      readProcLocksImpl: () => "",
      nowMs: () => Date.parse("2026-08-14T22:00:30Z"),
    });
    const t = result.repos[0]!.waiting[0]!;
    expect(t.ticket).toBe(ticket);
    expect(t.malformed).toBe(true);
    expect(t.branch).toBeNull();
  });

  test("conductor lock held is reported with holder pid", () => {
    const fs = makeFs({
      [`${DIR}/queue`]: "",
      [`${DIR}/conductor.lock`]: "",
    });
    const result = readLandqState({
      repoRoots: [REPO],
      ...fs,
      statImpl: (path) => (path.endsWith("conductor.lock") ? { dev: 1, ino: 500 } : { dev: 1, ino: 1 }),
      readProcLocksImpl: () => "3: POSIX ADVISORY WRITE 9001 fe:01:500 0 0",
      readProcStatImpl: (pid) => (pid === 9001 ? `${pid} (bash) S 1 1 1 0 -1 0 0 0 0 0 0 0 0 0 20 0 1 0 500 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0` : null),
      readProcUptimeImpl: () => "600.0 100.0",
      nowMs: () => Date.parse("2026-08-14T22:00:30Z"),
    });
    const repo = result.repos[0]!;
    expect(repo.conductorHeld).toBe(true);
    expect(repo.conductorHolderPid).toBe(9001);
    expect(repo.conductorComplete).toBe(true);
    expect(repo.conductorHolderAgeSeconds).toBeCloseTo(595, 0);
  });

  test("conductor.lock file present but unlocked reports not held", () => {
    const fs = makeFs({
      [`${DIR}/queue`]: "",
      [`${DIR}/conductor.lock`]: "",
    });
    const result = readLandqState({
      repoRoots: [REPO],
      ...fs,
      statImpl: () => ({ dev: 1, ino: 500 }),
      readProcLocksImpl: () => "",
      nowMs: () => Date.parse("2026-08-14T22:00:30Z"),
    });
    expect(result.repos[0]!.conductorHeld).toBe(false);
    expect(result.repos[0]!.conductorHolderPid).toBeNull();
  });

  test("/proc/locks unreadable renders as an explicit gap, never a false negative", () => {
    const fs = makeFs({
      [`${DIR}/queue`]: "",
      [`${DIR}/conductor.lock`]: "",
    });
    const result = readLandqState({
      repoRoots: [REPO],
      ...fs,
      statImpl: () => ({ dev: 1, ino: 500 }),
      readProcLocksImpl: () => null,
      nowMs: () => Date.parse("2026-08-14T22:00:30Z"),
    });
    const repo = result.repos[0]!;
    expect(repo.conductorHeld).toBeNull();
    expect(repo.conductorComplete).toBe(false);
  });

  test("never opens, locks, or writes anything under the landq dir", () => {
    const ticket = "ticket.88888888888888888888888888888888";
    const writes: string[] = [];
    const fs = makeFs({
      [`${DIR}/queue`]: `${ticket}\n`,
      [`${DIR}/${ticket}.job`]: job({ branch: "wt/e", enqueued_at: "2026-08-14T22:00:00Z" }),
      [`${DIR}/conductor.lock`]: "",
    });
    readLandqState({
      repoRoots: [REPO],
      existsSyncImpl: fs.existsSyncImpl,
      readFileImpl: (path) => {
        writes.push(`read:${path}`);
        return fs.readFileImpl(path);
      },
      statImpl: () => ({ dev: 1, ino: 1 }),
      readProcLocksImpl: () => "",
      nowMs: () => Date.parse("2026-08-14T22:00:30Z"),
    });
    expect(writes.every((entry) => entry.startsWith("read:"))).toBe(true);
  });
});
