import { describe, expect, test } from "bun:test";
import { mkdtempSync } from "node:fs";
import { join } from "node:path";
import { IncidentMutationStore } from "./incident-mutation-store";

function stores() {
  const path = join(mkdtempSync("/tmp/overdeck-incident-mutation-"), "mutations.sqlite");
  return [new IncidentMutationStore(path), new IncidentMutationStore(path)] as const;
}

describe("incident durable mutation store", () => {
  test("deletion intent survives lease loss after remote success and finalizes matching state", () => {
    const [first, second] = stores();
    const key = "task:1:42";
    first.claimLaunch("inc-delete", 42, "dispatch-delete", 1_000);
    first.claimResolution("inc-delete", 42, "hash", "{}");
    expect(first.acquire(key, "holder-a", 1_000, 10)).toBe(true);
    expect(first.claimDeletion("inc-delete", 42, key, "holder-a", 1_001)).toBe("intent");
    expect(second.acquire(key, "holder-b", 1_011, 10)).toBe(true);
    expect(first.markDeletionRemoved("inc-delete", 42, key, "holder-a", 1_011)).toBe(false);
    expect(() => first.adoptDeletion("inc-delete", 42, "intent", "holder-a", key, "holder-a", 1_011)).toThrow("deletion adoption refused");
    expect(second.adoptDeletion("inc-delete", 42, "intent", "holder-a", key, "holder-b", 1_011)).toBe("intent");
    expect(first.markDeletionRemoved("inc-delete", 42, key, "holder-a", 1_011)).toBe(false);
    expect(second.markDeletionRemoved("inc-delete", 42, key, "holder-b", 1_011)).toBe(true);
    expect(first.finalizeDeletion("inc-delete", 42, key, "holder-a", 1_011)).toBe(false);
    expect(second.finalizeDeletion("inc-delete", 42, key, "holder-b", 1_011)).toBe(true);
    expect(second.getLaunchClaim("inc-delete")).toBeNull();
    expect(second.listPendingResolutions()).toEqual([]);
    expect(second.getDeletion("inc-delete")).toMatchObject({ phase: "finalized", taskId: 42 });
    expect(second.finalizeDeletion("inc-delete", 42, key, "holder-b", 1_011)).toBe(false);
  });

  test("two store instances serialize the same lock key", () => {
    const [first, second] = stores();
    expect(first.acquire("incident:race", "holder-a", 1_000, 30_000)).toBe(true);
    expect(second.acquire("incident:race", "holder-b", 1_001, 30_000)).toBe(false);
    first.release("incident:race", "holder-a");
    expect(second.acquire("incident:race", "holder-b", 1_002, 30_000)).toBe(true);
  });

  test("expired leases can be reclaimed by another holder", () => {
    const [first, second] = stores();
    expect(first.acquire("incident:expired", "holder-a", 1_000, 10)).toBe(true);
    expect(second.acquire("incident:expired", "holder-b", 1_020, 10)).toBe(true);
  });

  test("holder-bound lease renewal extends an active lease", () => {
    const [first, second] = stores();
    expect(first.acquire("incident:renew", "holder-a", 1_000, 20)).toBe(true);
    expect(first.renewLease("incident:renew", "holder-a", 1_010, 20)).toBe(true);
    expect(second.acquire("incident:renew", "holder-b", 1_015, 20)).toBe(false);
    expect(second.acquire("incident:renew", "holder-b", 1_025, 20)).toBe(false);
    expect(second.acquire("incident:renew", "holder-b", 1_030, 20)).toBe(false);
    expect(second.acquire("incident:renew", "holder-b", 1_031, 20)).toBe(true);
  });

  test("lease renewal refuses a mismatched holder", () => {
    const [first] = stores();
    expect(first.acquire("incident:holder", "holder-a", 1_000, 30_000)).toBe(true);
    expect(first.renewLease("incident:holder", "holder-b", 1_001, 30_000)).toBe(false);
  });

  test("launch claim allows one dispatch id and replays the canonical id", () => {
    const [first, second] = stores();
    const claimed = first.claimLaunch("inc-1", 42, "dispatch-a", 1_000);
    expect(claimed).toMatchObject({ result: "claimed", dispatchId: "dispatch-a", phase: "claimed", taskId: 42 });

    const replay = second.claimLaunch("inc-1", 42, "dispatch-a", 1_001);
    expect(replay).toMatchObject({ result: "replay", dispatchId: "dispatch-a", phase: "claimed", taskId: 42 });

    const mismatchedRetry = second.claimLaunch("inc-1", 42, "dispatch-b", 1_002);
    expect(mismatchedRetry).toMatchObject({ result: "replay", dispatchId: "dispatch-a", phase: "claimed", taskId: 42 });

    const wrongTask = second.claimLaunch("inc-1", 99, "dispatch-c", 1_003);
    expect(wrongTask).toMatchObject({ result: "lost", dispatchId: "dispatch-a", taskId: 42 });
  });

  test("resolution phases persist, replay, and reject conflicting evidence", () => {
    const [first, second] = stores();
    const lockKey = "task:1:42";
    expect(first.claimResolution("inc-r", 42, "hash-a", "{\"evidence\":1}")).toMatchObject({ phase: "verified" });
    expect(first.acquire(lockKey, "holder", 1_000)).toBe(true);
    expect(first.advanceResolutionPhase("inc-r", "verified", "closed", lockKey, "holder", 1_001)).toBe(true);
    expect(second.claimResolution("inc-r", 42, "hash-a", "{\"evidence\":1}")).toMatchObject({ phase: "closed" });
    expect(second.listPendingResolutions()).toEqual([{ incidentId: "inc-r", taskId: 42, evidenceSha256: "hash-a", evidenceJson: "{\"evidence\":1}", phase: "closed", attemptCount: 0, nextAttemptAtMs: 0, lastError: null }]);
    expect(first.advanceResolutionPhase("inc-r", "closed", "learned", lockKey, "holder", 1_002)).toBe(true);
    expect(second.claimResolution("inc-r", 42, "hash-a", "{\"evidence\":1}")).toMatchObject({ phase: "learned" });
    expect(second.listPendingResolutions()).toEqual([]);
    expect(() => second.claimResolution("inc-r", 42, "hash-b", "{\"evidence\":2}")).toThrow("conflicting resolution evidence");
  });

  test("launch phase advances with holder-bound CAS", () => {
    const [store] = stores();
    const lockKey = "incident:inc-2";
    store.claimLaunch("inc-2", 7, "dispatch-2", 1_000);
    expect(store.acquire(lockKey, "holder-a", 1_000, 20)).toBe(true);
    expect(store.advanceLaunchPhase("inc-2", "claimed", "metadata", lockKey, "holder-b", 1_001)).toBe(false);
    expect(store.advanceLaunchPhase("inc-2", "claimed", "metadata", lockKey, "holder-a", 1_001)).toBe(true);
    expect(store.getLaunchClaim("inc-2")).toMatchObject({ phase: "metadata", dispatchId: "dispatch-2" });
    expect(store.advanceLaunchPhase("inc-2", "claimed", "board", lockKey, "holder-a", 1_002)).toBe(false);
    expect(store.advanceLaunchPhase("inc-2", "metadata", "board", lockKey, "holder-a", 1_021)).toBe(false);
    expect(store.getLaunchClaim("inc-2")?.phase).toBe("metadata");
  });
});
