import { describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync } from "node:fs";
import { Database } from "bun:sqlite";
import { createHash } from "node:crypto";
import { join } from "node:path";
import { RequestEvidenceConflictError, RequestNotFoundError, RequestsStore, type RequestRow } from "./requests-store";

const TEST_TMP_ROOT = process.env.TMPDIR ?? join(process.env.HOME ?? "/tmp", ".cache", "overdeck", "tests", "requests-store");
mkdirSync(TEST_TMP_ROOT, { recursive: true });
function testDbPath(): string { return join(mkdtempSync(join(TEST_TMP_ROOT, "case-")), "requests.sqlite"); }
function store() { return new RequestsStore(testDbPath()); }
function row(id: string, updated_at = "2026-08-14T00:00:00.000Z"): RequestRow {
  return { id, title: `Title ${id}`, project: "overdeck", state: "asked", priority: "HIGH", origin: "owner", asked_at: "2026-08-13T00:00:00.000Z", original_body: null, original_body_format: null, intake_source: "owner", intake_source_event_id: id, updated_at, worker: null, detail: null, proof_url: null, plan_ref: null, factory_run_id: null, session_name: null, session_id: null, announced_at: null, receipt_trail: [], transition_trail: [] };
}
const PENDING_OWNER_DECISIONS = [
  "Remaining: owner decision to clear pi_spawn remote - engineering precondition met.",
  "Pending owner approval for the rollout target.",
  "The owner must choose which account is charged.",
  "The owner needs to decide which account to use.",
  "We need the owner's approval.",
  "Pending owner approval by 2026-08-20.",
  "Pending owner approval is not resolved.",
  "Pending owner approval has not yet been decided.",
  "Owner decided the implementation scope, but rollout is pending owner approval.",
  "Owner decision visibility is pending owner approval.",
  "This is the owner's call.",
];
const DECISION_VISIBILITY_PROSE = [
  "Pending owner decisions must appear in the Needs-you column as blocked_needs_owner rows - a decision buried in a shipped row detail was invisible and stalled step 3 for hours",
  "Implement and deliver the board defect so genuine owner decisions reliably appear under Needs you, without fabricating clearance gates.",
  "Pending owner decision checks require owner approval markers to be visible.",
  "Pending owner decisions should be visible in the Needs-you column.",
];

describe("RequestsStore", () => {
  test("creates, gets, lists, and updates rows", () => {
    const requests = store();
    requests.create(row("req-a"));
    expect(requests.get("req-a")).toEqual(row("req-a"));
    const updated = requests.update("req-a", { state: "in_flight", worker: "worker-a", priority: "FIRE", factory_run_id: "adw-req-a" });
    expect(updated).toMatchObject({ id: "req-a", state: "in_flight", worker: "worker-a", priority: "FIRE", factory_run_id: "adw-req-a" });
    expect(updated.updated_at).not.toBe(row("req-a").updated_at);
    expect(requests.list()).toHaveLength(1);
    requests.close();
  });

  test("migrates an existing requests sqlite additively with nullable session attribution", () => {
    const path = testDbPath();
    const legacy = new Database(path);
    legacy.exec("CREATE TABLE requests (id TEXT PRIMARY KEY, title TEXT NOT NULL, project TEXT NOT NULL, state TEXT NOT NULL, priority TEXT NOT NULL, asked_at TEXT NOT NULL, updated_at TEXT NOT NULL, worker TEXT, detail TEXT, proof_url TEXT, plan_ref TEXT)");
    legacy.close();
    const requests = new RequestsStore(path);
    expect(requests.create({ ...row("attributed"), session_name: "memory-organizer-fluttering-owl", session_id: "session-full-id", factory_run_id: "adw-explicit" })).toMatchObject({ session_name: "memory-organizer-fluttering-owl", session_id: "session-full-id", factory_run_id: "adw-explicit" });
    requests.close();
  });

  test("migrates Slice 1 request events with stable source and producer identities", () => {
    const path = testDbPath();
    const legacy = new Database(path);
    legacy.exec(`CREATE TABLE request_events (
      id TEXT PRIMARY KEY, request_id TEXT NOT NULL, kind TEXT NOT NULL,
      occurred_at TEXT NOT NULL, recorded_at TEXT NOT NULL, summary TEXT NOT NULL,
      actor_type TEXT NOT NULL, actor_id TEXT, actor_display_name TEXT,
      session_id TEXT, host_id TEXT, account_id TEXT, source_id TEXT NOT NULL,
      source_record_id TEXT, payload TEXT NOT NULL,
      UNIQUE(source_id, source_record_id)
    )`);
    legacy.query(`INSERT INTO request_events
      (id, request_id, kind, occurred_at, recorded_at, summary, actor_type, source_id, source_record_id, payload)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
      .run("event-old", "request-old", "progress", "2026-08-17T00:00:00.000Z", "2026-08-17T00:00:00.000Z", "Recorded before Slice 2.", "unknown", "legacy-source", "record-old", "{}");
    legacy.close();

    const requests = new RequestsStore(path);
    requests.close();
    const migrated = new Database(path);
    expect(migrated.query<{ source_event_id: string; producer_id: string }, []>(
      "SELECT source_event_id, producer_id FROM request_events WHERE id = 'event-old'",
    ).get()).toEqual({ source_event_id: "record-old", producer_id: "legacy-source" });
    migrated.close();
  });

  test("rebuilds the closed state constraint once while preserving rows, child facts, and indexes", () => {
    const path = testDbPath();
    const legacy = new Database(path);
    legacy.exec(`
      CREATE TABLE requests (
        id TEXT PRIMARY KEY, title TEXT NOT NULL, project TEXT NOT NULL,
        state TEXT NOT NULL CHECK (state IN ('asked', 'in_flight', 'blocked_needs_owner', 'shipped')),
        priority TEXT NOT NULL, origin TEXT NOT NULL DEFAULT 'owner', asked_at TEXT NOT NULL,
        updated_at TEXT NOT NULL, worker TEXT, detail TEXT, proof_url TEXT, plan_ref TEXT,
        session_name TEXT, session_id TEXT, announced_at INTEGER
      );
      CREATE UNIQUE INDEX requests_plan_ref_unique ON requests(plan_ref) WHERE plan_ref IS NOT NULL;
      CREATE TABLE receipt_trail (id TEXT PRIMARY KEY, request_id TEXT NOT NULL REFERENCES requests(id) ON DELETE CASCADE, at TEXT NOT NULL, kind TEXT NOT NULL, line TEXT NOT NULL, meta TEXT NOT NULL);
      CREATE TABLE request_transitions (id TEXT PRIMARY KEY, request_id TEXT NOT NULL REFERENCES requests(id) ON DELETE CASCADE, at TEXT NOT NULL, from_state TEXT NOT NULL, to_state TEXT NOT NULL, actor TEXT NOT NULL, reason TEXT);
      INSERT INTO requests VALUES ('legacy', 'Legacy work', 'overdeck', 'in_flight', 'HIGH', 'owner', '2026-08-13T00:00:00.000Z', '2026-08-14T00:00:00.000Z', 'worker-a', NULL, NULL, 'legacy-key', 'old-session', 'session-legacy', NULL);
      INSERT INTO receipt_trail VALUES ('receipt-old', 'legacy', '2026-08-14T00:00:00.000Z', 'progress', 'Still working.', '{}');
      INSERT INTO request_transitions VALUES ('transition-old', 'legacy', '2026-08-14T00:00:00.000Z', 'asked', 'in_flight', 'worker-a', NULL);
    `);
    legacy.close();

    const requests = new RequestsStore(path);
    expect(requests.get("legacy")).toMatchObject({
      state: "in_flight",
      plan_ref: "legacy-key",
      session_name: "old-session",
      session_id: "session-legacy",
      receipt_trail: [{ id: "receipt-old" }],
      transition_trail: [{ id: "transition-old" }],
    });
    expect(requests.orphanSession("session-legacy", 1)).toHaveLength(1);
    expect(requests.get("legacy")).toMatchObject({ state: "orphaned" });
    expect(requests.transition("legacy", { state: "canceled", actor: "owner", reason: "No longer needed." })).toMatchObject({ state: "canceled" });
    requests.close();

    const migrated = new Database(path);
    const index = migrated.query<{ name: string }, []>("SELECT name FROM sqlite_master WHERE type='index' AND name='requests_plan_ref_unique'").get();
    const childFk = migrated.query<{ table: string }, []>("PRAGMA foreign_key_list(receipt_trail)").get();
    expect(index?.name).toBe("requests_plan_ref_unique");
    expect(childFk?.table).toBe("requests");
    migrated.close();
  });

  test("refuses migration when legacy rows duplicate a non-null plan_ref", () => {
    const path = testDbPath();
    const legacy = new Database(path);
    legacy.exec("CREATE TABLE requests (id TEXT PRIMARY KEY, title TEXT NOT NULL, project TEXT NOT NULL, state TEXT NOT NULL, priority TEXT NOT NULL, asked_at TEXT NOT NULL, updated_at TEXT NOT NULL, worker TEXT, detail TEXT, proof_url TEXT, plan_ref TEXT)");
    legacy.exec(`INSERT INTO requests VALUES
      ('req-a', 'A', 'overdeck', 'asked', 'HIGH', '2026-08-13T00:00:00.000Z', '2026-08-14T00:00:00.000Z', NULL, NULL, NULL, 'shared-key'),
      ('req-b', 'B', 'overdeck', 'asked', 'HIGH', '2026-08-13T00:00:00.000Z', '2026-08-14T00:00:00.000Z', NULL, NULL, NULL, 'shared-key')`);
    legacy.close();
    expect(() => new RequestsStore(path)).toThrow("cannot enforce unique requests.plan_ref: duplicate non-null plan_ref values: shared-key (2)");
  });

  test("enforces a unique non-null plan_ref while allowing keyless rows", () => {
    const requests = store();
    requests.create({ ...row("req-a"), plan_ref: "work-key" });
    expect(() => requests.create({ ...row("req-b"), plan_ref: "work-key" })).toThrow("duplicate request plan_ref: work-key");
    requests.create(row("req-c"));
    requests.create(row("req-d"));
    expect(requests.list()).toHaveLength(3);
    requests.close();
  });

  test("records a claim only when a real worker claim is supplied", () => {
    const requests = store();
    requests.create({ ...row("req-a"), worker: "codex", worker_host: "buildbox-1" });
    expect(requests.get("req-a")?.receipt_trail).toMatchObject([
      { request_id: "req-a", at: "2026-08-14T00:00:00.000Z", kind: "claimed", line: "Work started on this request.", meta: { worker: "codex", host: "buildbox-1" } },
    ]);
    requests.create(row("req-empty"));
    expect(requests.get("req-empty")?.receipt_trail).toEqual([]);
    requests.close();
  });

  test("rejects duplicate ids, missing updates, and invalid state", () => {
    const requests = store();
    requests.create(row("req-a"));
    expect(() => requests.create(row("req-a"))).toThrow("duplicate request id");
    expect(() => requests.update("missing", { priority: "LOW" })).toThrow(RequestNotFoundError);
    expect(() => requests.create({ ...row("bad"), state: "nope" as never })).toThrow("invalid request state");
    expect(() => requests.create({ ...row("bad-origin"), origin: "nope" as never })).toThrow("invalid request origin");
    requests.close();
  });

  test("accepts an agent-judgement origin, distinct from owner and agent-incident", () => {
    const requests = store();
    requests.create({ ...row("req-judgement"), origin: "agent-judgement" });
    expect(requests.get("req-judgement")).toMatchObject({ origin: "agent-judgement" });
    requests.close();
  });

  test("lists newest updates first", () => {
    const requests = store();
    requests.create(row("older", "2026-08-12T00:00:00.000Z"));
    requests.create(row("newer", "2026-08-13T00:00:00.000Z"));
    expect(requests.list().map((request) => request.id)).toEqual(["newer", "older"]);
    requests.close();
  });

  test("markAnnounced persists the announcement timestamp", () => {
    const requests = store();
    requests.create(row("req-a"));
    requests.markAnnounced("req-a", 1_723_654_321_000);
    expect(requests.get("req-a")).toMatchObject({ announced_at: 1_723_654_321_000 });
    requests.close();
  });

  test("orphans only exact-session in-flight rows when the canonical ledger proves dirty work", () => {
    const requests = store();
    requests.create({ ...row("matched"), state: "in_flight", session_id: "session-dead", worker: "worker-a" });
    requests.create({ ...row("same-session-blocked"), state: "blocked_needs_owner", session_id: "session-dead", detail: "Choose a release." });
    requests.create({ ...row("different-session"), state: "in_flight", session_id: "session-live" });
    requests.create({ ...row("unassociated"), state: "in_flight", session_id: null });

    expect(requests.orphanSession("session-dead", 3)).toHaveLength(1);
    expect(requests.get("matched")).toMatchObject({
      state: "orphaned",
      detail: "Worker session stopped with 3 uncommitted files.",
      receipt_trail: expect.arrayContaining([expect.objectContaining({ kind: "worker-lost", line: "Worker session stopped with 3 uncommitted files.", meta: { worker: "session ledger", session_id: "session-dead" } })]),
      transition_trail: expect.arrayContaining([expect.objectContaining({ from_state: "in_flight", to_state: "orphaned", actor: "session ledger" })]),
    });
    expect(requests.get("same-session-blocked")?.state).toBe("blocked_needs_owner");
    expect(requests.get("different-session")?.state).toBe("in_flight");
    expect(requests.get("unassociated")?.state).toBe("in_flight");
    expect(requests.orphanSession("session-dead", 3)).toEqual([]);
    expect(requests.orphanSession("session-live", 0)).toEqual([]);
    requests.close();
  });

  test("validates and records the owner-blocking lifecycle", () => {
    const requests = store();
    requests.create({ ...row("req-a"), plan_ref: "task-lifecycle" });
    const blocked = requests.transition("req-a", { state: "blocked_needs_owner", actor: "codex@buildbox", reason: "Please provide the registry token." });
    expect(blocked).toMatchObject({ state: "blocked_needs_owner", detail: "Please provide the registry token." });
    expect(blocked.transition_trail).toMatchObject([
      { from_state: "asked", to_state: "blocked_needs_owner", actor: "codex@buildbox", reason: "Please provide the registry token." },
    ]);
    expect(blocked.transition_trail?.[0]?.at).toMatch(/^\d{4}-\d{2}-\d{2}T/);
    requests.transition("req-a", { state: "in_flight", actor: "owner" });
    const shipped = requests.transition("req-a", { state: "shipped", actor: "lander" });
    expect(shipped).toMatchObject({ state: "shipped", detail: null });
    requests.close();
  });

  test("keeps every pending owner decision on a blocked row across every direct board writer", () => {
    const requests = store();
    for (const [index, decision] of PENDING_OWNER_DECISIONS.entries()) {
      expect(() => requests.create({ ...row(`created-${index}`), title: decision })).toThrow("pending owner decision must be blocked_needs_owner");
    }
    const decision = PENDING_OWNER_DECISIONS[0]!;
    expect(requests.create({ ...row("blocked"), state: "blocked_needs_owner", detail: decision })).toMatchObject({ state: "blocked_needs_owner" });
    expect(requests.update("blocked", { state: "in_flight", detail: null })).toMatchObject({ state: "in_flight", detail: null });
    requests.create(row("updated"));
    expect(() => requests.update("updated", { detail: decision })).toThrow("pending owner decision must be blocked_needs_owner");
    expect(requests.transition("updated", { state: "blocked_needs_owner", actor: "collector", reason: decision })).toMatchObject({ state: "blocked_needs_owner", detail: decision });
    const fire = requests.claimFire({ id: "fire-pending", title: "incident", project: "overdeck", worker: null, detail: decision, now: "2026-08-15T00:00:00.000Z" });
    expect(fire).toMatchObject({ claimed: true, row: { state: "blocked_needs_owner", detail: decision } });
    expect(requests.create({ ...row("resolved"), detail: "Owner's call, decided 2026-08-16: proceed." })).toMatchObject({ state: "asked" });
    expect(requests.create({ ...row("resolved-pending"), detail: "Pending owner approval, resolved 2026-08-16." })).toMatchObject({ state: "asked" });
    expect(requests.create({ ...row("resolved-remaining"), detail: "Remaining: owner decision to approve rollout — decided 2026-08-16." })).toMatchObject({ state: "asked" });
    for (const [index, prose] of DECISION_VISIBILITY_PROSE.entries()) {
      expect(requests.create({ ...row(`meta-${index}`), detail: prose })).toMatchObject({ state: "asked" });
    }
    requests.close();
  });

  test("keeps same-millisecond transition facts in insertion order", () => {
    const path = testDbPath();
    let requests = new RequestsStore(path);
    requests.create(row("req-a"));
    requests.close();

    const db = new Database(path);
    const at = "2026-08-14T00:00:00.000Z";
    db.query("INSERT INTO request_transitions (id, request_id, at, from_state, to_state, actor, reason) VALUES (?, ?, ?, ?, ?, ?, ?)")
      .run("transition-z", "req-a", at, "asked", "in_flight", "first", null);
    db.query("INSERT INTO request_transitions (id, request_id, at, from_state, to_state, actor, reason) VALUES (?, ?, ?, ?, ?, ?, ?)")
      .run("transition-a", "req-a", at, "in_flight", "blocked_needs_owner", "second", "reason");
    db.close();

    requests = new RequestsStore(path);
    expect(requests.get("req-a")?.transition_trail).toMatchObject([
      { from_state: "asked", to_state: "in_flight", actor: "first", reason: null },
      { from_state: "in_flight", to_state: "blocked_needs_owner", actor: "second", reason: "reason" },
    ]);
    requests.close();
  });

  test("rejects every transition outside the lifecycle matrix and requires reasons for exceptional exits", () => {
    const requests = store();
    for (const [from, invalid] of Object.entries({
      asked: ["asked", "orphaned", "shipped"],
      in_flight: ["asked", "in_flight", "orphaned"],
      blocked_needs_owner: ["asked", "blocked_needs_owner", "orphaned", "shipped"],
      orphaned: ["asked", "blocked_needs_owner", "orphaned", "shipped"],
      shipped: ["asked", "in_flight", "blocked_needs_owner", "orphaned", "shipped", "canceled"],
      canceled: ["asked", "in_flight", "blocked_needs_owner", "orphaned", "shipped", "canceled"],
    })) {
      for (const to of invalid) {
        const id = `${from}-${to}`;
        if (from === "orphaned") {
          requests.create({ ...row(id), state: "in_flight", session_id: `session-${id}` });
          requests.orphanSession(`session-${id}`, 1);
        } else if (from === "canceled") {
          requests.create(row(id));
          requests.transition(id, { state: "canceled", actor: "owner", reason: "No longer needed." });
        } else {
          requests.create({ ...row(id), state: from as RequestRow["state"] });
        }
        expect(() => requests.transition(id, { state: to as RequestRow["state"], actor: "tester", reason: "reason" })).toThrow(`invalid request transition: ${from} -> ${to}`);
      }
    }
    requests.create({ ...row("reason-required-blocked"), state: "in_flight" });
    expect(() => requests.transition("reason-required-blocked", { state: "blocked_needs_owner", actor: "tester", reason: "   " })).toThrow("blocked reason is required");
    requests.create(row("reason-required-canceled"));
    expect(() => requests.transition("reason-required-canceled", { state: "canceled", actor: "tester" })).toThrow("canceled reason is required");
    expect(() => requests.create({ ...row("canonical-create"), state: "orphaned" })).toThrow("orphaned requests require canonical session evidence");
    expect(() => requests.create({ ...row("canceled-create"), state: "canceled" })).toThrow("canceled requests require a reasoned transition");
    requests.create({ ...row("canonical-update"), state: "in_flight" });
    expect(() => requests.update("canonical-update", { state: "orphaned" })).toThrow("orphaned requests require canonical session evidence");
    expect(() => requests.update("canonical-update", { state: "canceled" })).toThrow("canceled requests require a reasoned transition");
    requests.close();
  });

  test("claimFire: a fresh id is created and claimed", () => {
    const requests = store();
    const result = requests.claimFire({ id: "fire-x", title: "deploy-local:actions-gateway-config-missing", project: "overdeck", worker: "worker-a", detail: null, now: "2026-08-15T00:00:00.000Z" });
    expect(result.claimed).toBe(true);
    expect(result.row).toMatchObject({ id: "fire-x", state: "asked", origin: "agent-incident", worker: "worker-a" });
    requests.close();
  });

  test("claimFire: a second claim against the same open incident is blocked, not duplicated", () => {
    const requests = store();
    requests.claimFire({ id: "fire-x", title: "sig", project: "overdeck", worker: "worker-a", detail: null, now: "2026-08-15T00:00:00.000Z" });
    const second = requests.claimFire({ id: "fire-x", title: "sig", project: "overdeck", worker: "worker-b", detail: null, now: "2026-08-15T00:05:00.000Z" });
    expect(second.claimed).toBe(false);
    expect(second.row).toMatchObject({ id: "fire-x", worker: "worker-a" });
    expect(requests.list()).toHaveLength(1);
    requests.close();
  });

  test("claimFire: a shipped (resolved) incident is reopened by a recurring fire, not blocked forever", () => {
    const requests = store();
    requests.claimFire({ id: "fire-x", title: "sig", project: "overdeck", worker: "worker-a", detail: null, now: "2026-08-15T00:00:00.000Z" });
    requests.update("fire-x", { state: "shipped" });
    const reopened = requests.claimFire({ id: "fire-x", title: "sig", project: "overdeck", worker: "worker-b", detail: "recurred", now: "2026-08-15T01:00:00.000Z" });
    expect(reopened.claimed).toBe(true);
    expect(reopened.row).toMatchObject({ id: "fire-x", state: "asked", worker: "worker-b", detail: "recurred" });
    requests.close();
  });

  test("preserves the exact original ask and projects typed events without legacy duplicates", () => {
    const requests = store();
    const original = "  Keep my spacing.\n\nDo not replace this with the title.  ";
    requests.create({
      ...row("story"),
      original_body: original,
      original_body_format: "markdown",
      intake_source: "claude-hook",
      intake_source_event_id: "hook-event-1",
      worker: "claude",
      session_id: "session-story",
    });
    requests.transition("story", { state: "in_flight", actor: "claude" });

    const story = requests.getStory("story");
    expect(story?.originalRequest).toEqual({ body: original, format: "markdown", source: "claude-hook" });
    expect(story?.events.map((event) => event.kind)).toEqual(["intake", "claimed", "state_transition"]);
    expect(story?.events.find((event) => event.kind === "claimed")?.sessionId).toBe("session-story");
    expect(story?.events.every((event) => !event.legacy)).toBe(true);
    expect(story?.coverage).toContainEqual({ fact: "original_request", status: "complete", sourceId: "claude-hook" });
    expect(story?.coverage).toContainEqual({ fact: "activity", status: "complete", sourceId: "request-events" });
    requests.close();
  });

  test("does not infer the original ask from mutable detail", () => {
    const requests = store();
    requests.create({ ...row("detail-is-not-intake"), detail: "Current execution detail" });
    const story = requests.getStory("detail-is-not-intake");
    expect(story?.originalRequest.body).toBeNull();
    expect(story?.coverage).toContainEqual(expect.objectContaining({ fact: "original_request", status: "unavailable" }));
    requests.close();
  });

  test("keeps legacy title-only intake unavailable and marks legacy activity partial", () => {
    const path = testDbPath();
    const legacy = new Database(path);
    legacy.exec(`
      CREATE TABLE requests (
        id TEXT PRIMARY KEY, title TEXT NOT NULL, project TEXT NOT NULL,
        state TEXT NOT NULL CHECK (state IN ('asked', 'in_flight', 'blocked_needs_owner', 'shipped')),
        priority TEXT NOT NULL, origin TEXT NOT NULL DEFAULT 'owner', asked_at TEXT NOT NULL,
        updated_at TEXT NOT NULL, worker TEXT, detail TEXT, proof_url TEXT, plan_ref TEXT,
        session_name TEXT, session_id TEXT, announced_at INTEGER
      );
      CREATE TABLE receipt_trail (id TEXT PRIMARY KEY, request_id TEXT NOT NULL REFERENCES requests(id), at TEXT NOT NULL, kind TEXT NOT NULL, line TEXT NOT NULL, meta TEXT NOT NULL);
      CREATE TABLE request_transitions (id TEXT PRIMARY KEY, request_id TEXT NOT NULL REFERENCES requests(id), at TEXT NOT NULL, from_state TEXT NOT NULL, to_state TEXT NOT NULL, actor TEXT NOT NULL, reason TEXT);
      INSERT INTO requests VALUES ('legacy-story', 'Only a summary', 'overdeck', 'asked', 'HIGH', 'owner', '2026-08-13T00:00:00.000Z', '2026-08-14T00:00:00.000Z', NULL, NULL, NULL, NULL, NULL, NULL, NULL);
      INSERT INTO receipt_trail VALUES ('receipt-legacy', 'legacy-story', '2026-08-14T00:00:00.000Z', 'progress', 'Old fact', '{}');
    `);
    legacy.close();

    const requests = new RequestsStore(path);
    const story = requests.getStory("legacy-story");
    expect(story?.originalRequest.body).toBeNull();
    expect(story?.events).toMatchObject([{ kind: "legacy_receipt", summary: "Old fact", legacy: true }]);
    expect(story?.coverage).toContainEqual(expect.objectContaining({ fact: "original_request", status: "unavailable" }));
    expect(story?.coverage).toContainEqual(expect.objectContaining({ fact: "activity", status: "partial" }));
    requests.close();
  });

  test("rolls back an owner answer when its typed evidence cannot be recorded", () => {
    const path = testDbPath();
    let requests = new RequestsStore(path);
    requests.create({ ...row("answer-atomic"), state: "blocked_needs_owner", detail: "Choose one." });
    requests.close();
    const db = new Database(path);
    db.exec("CREATE TRIGGER reject_owner_answer BEFORE INSERT ON request_events WHEN NEW.kind = 'owner_answer' BEGIN SELECT RAISE(ABORT, 'reject owner answer'); END");
    db.close();

    requests = new RequestsStore(path);
    expect(() => requests.answer("answer-atomic", "Option A")).toThrow("reject owner answer");
    expect(requests.get("answer-atomic")).toMatchObject({ state: "blocked_needs_owner", detail: "Choose one.", receipt_trail: [], transition_trail: [] });
    expect(requests.getStory("answer-atomic")?.events.map((event) => event.kind)).toEqual(["intake"]);
    requests.close();
  });

  test("keeps the story available and marks coverage partial when a stored payload is unreadable", () => {
    const path = testDbPath();
    let requests = new RequestsStore(path);
    requests.create({ ...row("corrupt-event"), original_body: "Exact ask" });
    requests.close();
    const db = new Database(path);
    db.query("UPDATE request_events SET payload = 'not-json' WHERE request_id = ?").run("corrupt-event");
    db.close();

    requests = new RequestsStore(path);
    const story = requests.getStory("corrupt-event");
    expect(story?.events[0]?.payload).toEqual({ recordUnreadable: true });
    expect(story?.coverage).toContainEqual(expect.objectContaining({ fact: "activity", status: "partial", reason: "At least one recorded activity detail could not be read." }));
    requests.close();
  });

  test("ingests durable evidence once and applies its projection atomically", () => {
    const requests = store();
    const intake = {
      schemaVersion: 1 as const,
      requestId: "durable-request",
      sourceId: "claude-task-hook",
      sourceEventId: "session-a:task-1:000-intake",
      producerId: "session-a",
      occurredAt: "2026-08-18T00:00:00.000Z",
      event: {
        kind: "intake" as const,
        summary: "Request recorded from the session task list.",
        actor: { type: "agent" as const, id: "session-a" },
        sessionId: "session-a",
        payload: { taskId: "1" },
      },
      createRequest: {
        title: "Keep lifecycle evidence",
        project: "overdeck",
        priority: "NORMAL",
        origin: "agent-judgement" as const,
        originalBody: "The exact\nrequest body",
        originalBodyFormat: "plain_text" as const,
        sessionId: "session-a",
      },
    };
    expect(requests.appendEvidence(intake)).toEqual({
      requestId: "durable-request",
      sourceId: "claude-task-hook",
      sourceEventId: "session-a:task-1:000-intake",
      replayed: false,
    });
    expect(requests.appendEvidence(intake).replayed).toBe(true);

    const transition = {
      schemaVersion: 1 as const,
      requestId: "durable-request",
      sourceId: "claude-task-hook",
      sourceEventId: "session-a:task-1:001-start",
      producerId: "session-a",
      occurredAt: "2026-08-18T00:01:00.000Z",
      event: {
        kind: "state_transition" as const,
        summary: "Session task started.",
        actor: { type: "agent" as const, id: "session-a" },
        sessionId: "session-a",
        payload: { fromState: "asked", toState: "in_flight" },
      },
      projection: { expectedState: "asked", state: "in_flight", worker: "Agent", sessionId: "session-a" },
    };
    expect(requests.appendEvidence(transition).replayed).toBe(false);
    expect(requests.get("durable-request")).toMatchObject({ state: "in_flight", worker: "Agent" });
    const story = requests.getStory("durable-request");
    expect(story?.events).toHaveLength(2);
    expect(story?.events[0]).toMatchObject({
      kind: "intake",
      summary: "Request recorded from the session task list.",
      actor: { type: "agent", id: "session-a" },
      sessionId: "session-a",
      sourceId: "claude-task-hook",
      sourceEventId: "session-a:task-1:000-intake",
      producerId: "session-a",
      payload: { taskId: "1" },
    });
    expect(requests.appendEvidence(transition).replayed).toBe(true);
    expect(requests.getStory("durable-request")?.events).toHaveLength(2);
    requests.close();
  });

  test("rejects conflicting intake identities instead of inferring recurrence", () => {
    const requests = store();
    requests.create({ ...row("existing"), original_body: "First ask", intake_source: "source-a", intake_source_event_id: "event-a" });
    const conflicting = {
      schemaVersion: 1 as const,
      requestId: "existing",
      sourceId: "source-a",
      sourceEventId: "different-event",
      producerId: "producer-a",
      occurredAt: "2026-08-18T00:00:00.000Z",
      event: { kind: "intake" as const, summary: "Another ask.", actor: { type: "owner" as const }, payload: {} },
      createRequest: {
        title: "Same-looking ask",
        project: "overdeck",
        priority: "NORMAL",
        origin: "owner" as const,
        originalBody: "First ask",
        originalBodyFormat: "plain_text" as const,
      },
    };
    expect(() => requests.appendEvidence(conflicting)).toThrow(RequestEvidenceConflictError);
    expect(requests.getStory("existing")?.events).toHaveLength(1);
    requests.close();
  });

  test("records exact work links and bounded content-addressed attachments without linking similar requests", () => {
    const requests = store();
    requests.create({ ...row("linked-work"), title: "Build reports", original_body: "Build reports" });
    requests.create({ ...row("similar-unlinked"), title: "Build reports", original_body: "Build reports" });
    const diff = Buffer.from("diff --git a/a.ts b/a.ts\n+record exact evidence\n");
    const digest = `sha256:${createHash("sha256").update(diff).digest("hex")}`;

    requests.appendEvidence({
      schemaVersion: 1,
      requestId: "linked-work",
      sourceId: "factory-trace",
      sourceEventId: "run-42:linked",
      producerId: "factory-run-42",
      occurredAt: "2026-08-18T00:05:00.000Z",
      event: {
        kind: "run_linked",
        summary: "Work run linked.",
        actor: { type: "machinery", displayName: "Factory" },
        sessionId: "session-42",
        hostId: "debian2",
        accountId: "claude11-alex",
        payload: { runId: "run-42" },
      },
      links: [{
        kind: "factory_run",
        targetSource: "factory-trace",
        targetId: "run-42",
        ownerUrl: "/factory?run=run-42",
        metadata: { status: "running" },
      }],
      attachments: [{
        digest,
        contentBase64: diff.toString("base64"),
        byteCount: diff.byteLength,
        mediaType: "text/x-diff",
        redactionStatus: "redacted",
        truncated: false,
      }],
    });

    const linked = requests.getStory("linked-work");
    expect(linked?.links).toEqual([expect.objectContaining({
      kind: "factory_run",
      targetSource: "factory-trace",
      targetId: "run-42",
      sourceEventId: "run-42:linked",
      metadata: { status: "running" },
    })]);
    expect(linked?.attachments).toEqual([expect.objectContaining({
      digest,
      byteCount: diff.byteLength,
      mediaType: "text/x-diff",
      status: "available",
    })]);
    expect(requests.getEvidenceAttachment("linked-work", digest)?.content).toEqual(diff);
    expect(linked?.coverage).toContainEqual({ fact: "work_evidence", status: "complete", sourceId: "request-evidence-links" });

    const unlinked = requests.getStory("similar-unlinked");
    expect(unlinked?.links).toEqual([]);
    expect(unlinked?.attachments).toEqual([]);
    expect(unlinked?.coverage).toContainEqual({
      fact: "work_evidence",
      status: "unavailable",
      sourceId: "request-evidence-links",
      reason: "No exact work run, gate, tool, or change link was recorded for this request.",
    });
    expect(requests.getEvidenceAttachment("similar-unlinked", digest)).toBeNull();
    requests.close();
  });

  test("migrates legacy evidence-link constraints before recording delivery lineage", () => {
    const path = testDbPath();
    const legacy = new Database(path);
    legacy.exec(`CREATE TABLE request_evidence_links (
      request_id TEXT NOT NULL,
      source_id TEXT NOT NULL,
      source_event_id TEXT NOT NULL,
      kind TEXT NOT NULL CHECK (kind IN ('factory_run', 'phase', 'gate', 'tool', 'change', 'log')),
      target_source TEXT NOT NULL,
      target_id TEXT NOT NULL,
      occurred_at TEXT NOT NULL,
      owner_url TEXT,
      metadata TEXT NOT NULL,
      PRIMARY KEY (source_id, source_event_id, kind, target_source, target_id)
    )`);
    legacy.close();

    const requests = new RequestsStore(path);
    requests.create(row("delivery-migration"));
    requests.appendEvidence({
      schemaVersion: 1,
      requestId: "delivery-migration",
      sourceId: "delivery-machinery",
      sourceEventId: "delivery-migration:submitted",
      producerId: "lander",
      occurredAt: "2026-08-18T00:00:00.000Z",
      event: {
        kind: "land_submitted",
        summary: "Change submitted for landing.",
        actor: { type: "machinery" },
        payload: {},
      },
      links: [
        { kind: "branch", targetSource: "git", targetId: "wt/delivery" },
        { kind: "land_submission", targetSource: "land-queue", targetId: "ticket-1" },
      ],
    });
    expect(requests.getStory("delivery-migration")?.links.map((link) => link.kind)).toEqual([
      "branch",
      "land_submission",
    ]);
    requests.close();
  });

  test("rejects request evidence whose attachment digest does not match its content", () => {
    const requests = store();
    requests.create({ ...row("bad-attachment"), original_body: "Record a diff" });
    expect(() => requests.appendEvidence({
      schemaVersion: 1,
      requestId: "bad-attachment",
      sourceId: "factory-trace",
      sourceEventId: "change-bad",
      producerId: "factory-run-bad",
      occurredAt: "2026-08-18T00:05:00.000Z",
      event: { kind: "change_recorded", summary: "Change recorded.", actor: { type: "machinery" }, payload: {} },
      links: [{ kind: "change", targetSource: "factory-trace", targetId: "change-bad" }],
      attachments: [{
        digest: `sha256:${"0".repeat(64)}`,
        contentBase64: Buffer.from("different").toString("base64"),
        byteCount: 9,
        mediaType: "text/x-diff",
        redactionStatus: "redacted",
        truncated: false,
      }],
    })).toThrow("invalid request evidence attachment digest");
    expect(requests.getStory("bad-attachment")?.events.map((event) => event.kind)).toEqual(["intake"]);
    requests.close();
  });

  test("projects pending and quarantined producer health into the request story", () => {
    const requests = store();
    requests.create({ ...row("health"), original_body: "Track delivery", intake_source: "claude-task-hook" });
    requests.recordEvidenceSourceHealth({
      schemaVersion: 1,
      requestId: "health",
      sourceId: "claude-task-hook",
      producerId: "session-a",
      reportedAt: "1900-01-01T00:00:00.000Z",
      queueCount: 2,
      oldestQueuedAt: "2026-08-18T00:00:00.000Z",
      quarantineCount: 0,
      lastAcknowledgedAt: null,
    });
    expect(requests.getStory("health")?.coverage).toContainEqual({
      fact: "activity_delivery",
      status: "pending_delivery",
      sourceId: "claude-task-hook",
      reason: "2 recorded updates are waiting for delivery.",
    });
    requests.recordEvidenceSourceHealth({
      schemaVersion: 1,
      requestId: "health",
      sourceId: "claude-task-hook",
      producerId: "session-a",
      reportedAt: "2026-08-18T00:02:00.000Z",
      queueCount: 0,
      oldestQueuedAt: null,
      quarantineCount: 1,
      lastAcknowledgedAt: null,
    });
    expect(requests.getStory("health")?.coverage).toContainEqual({
      fact: "activity_delivery",
      status: "partial",
      sourceId: "claude-task-hook",
      reason: "1 recorded update is quarantined after validation failed.",
    });
    requests.close();
  });

  test("a fresh producer cannot hide another stale producer for the same source", () => {
    const path = testDbPath();
    let requests = new RequestsStore(path);
    requests.create({ ...row("multi-producer-health"), original_body: "Track every producer" });
    for (const producerId of ["session-old", "session-fresh"]) {
      requests.recordEvidenceSourceHealth({
        schemaVersion: 1,
        requestId: "multi-producer-health",
        sourceId: "claude-task-hook",
        producerId,
        reportedAt: "2026-08-18T00:00:00.000Z",
        queueCount: 0,
        oldestQueuedAt: null,
        quarantineCount: 0,
        lastAcknowledgedAt: null,
      });
    }
    requests.close();
    const db = new Database(path);
    db.query("UPDATE request_evidence_source_health SET reported_at = ? WHERE producer_id = ?")
      .run("1900-01-01T00:00:00.000Z", "session-old");
    db.close();

    requests = new RequestsStore(path);
    expect(requests.getStory("multi-producer-health")?.coverage).toContainEqual({
      fact: "activity_delivery",
      status: "stale",
      sourceId: "claude-task-hook",
      reason: "This evidence source has not reported recently.",
    });
    requests.close();
  });

  test("records automated fire intake exactly and only when the claim succeeds", () => {
    const requests = store();
    const first = requests.claimFire({ id: "fire-story", title: "Summary", project: "overdeck", worker: null, detail: "Exact incident detail", now: "2026-08-15T00:00:00.000Z" });
    const blocked = requests.claimFire({ id: "fire-story", title: "Changed summary", project: "overdeck", worker: null, detail: "Must not overwrite", now: "2026-08-15T00:01:00.000Z" });
    expect(first.claimed).toBe(true);
    expect(blocked.claimed).toBe(false);
    expect(requests.getStory("fire-story")?.originalRequest.body).toBe("Exact incident detail");
    expect(requests.getStory("fire-story")?.events.map((event) => event.kind)).toEqual(["intake"]);
    requests.close();
  });
});
