import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, mkdir, writeFile, readFile, readdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { AtomicFile } from "../atomic/AtomicFile.ts";
import {
  DoorbellMailbox,
  HostClaimStore,
  RunStore,
  SwitchboardVersionStore,
  WorkerBindingStore,
} from "../store/index.ts";
import { SwitchboardPaths } from "../paths/SwitchboardPaths.ts";
import type { DoorbellMessage } from "../contracts/DoorbellMessage.ts";
import type { RunRecord } from "../contracts/RunRecord.ts";
import type { WorkerBindingRevision } from "../contracts/WorkerBinding.ts";

async function tempPersist(): Promise<{ root: string; paths: SwitchboardPaths }> {
  const root = await mkdtemp(join(tmpdir(), "sb-wp02-"));
  const persistDir = join(root, ".codeflowmu", "state");
  await mkdir(persistDir, { recursive: true });
  return { root, paths: new SwitchboardPaths({ persistDir }) };
}

function sampleDoorbell(
  workerId: string,
  doorbellId: string,
  dedupeKey: string,
): DoorbellMessage {
  return {
    schema: "codeflowmu.doorbell/v1",
    doorbellId,
    recipientWorkerId: workerId,
    topic: "task.ready",
    sourceFactRefs: ["TASK-20260804-015"],
    summary: "WP-02 test doorbell",
    priority: "P1",
    dedupeKey,
    createdAt: "2026-08-04T10:00:00.000Z",
  };
}

describe("WP-02 AtomicFile", () => {
  it("atomically writes JSON and survives orphan tmp crash injection", async () => {
    const dir = await mkdtemp(join(tmpdir(), "atomic-"));
    const target = join(dir, "state.json");
    await AtomicFile.writeJson(target, { ok: true, n: 1 });
    assert.deepEqual(await AtomicFile.readJson(target), { ok: true, n: 1 });

    // Crash injection: half-written staging file must not replace committed target.
    await writeFile(join(dir, "state.json.tmp.deadbeef"), "{corrupt", "utf-8");
    await writeFile(join(dir, "state.json.tmp"), "partial", "utf-8");
    const before = await readFile(target, "utf-8");
    const recovered = await AtomicFile.recoverOrphanTmp(dir);
    assert.ok(recovered.removed.length >= 2);
    assert.equal(await readFile(target, "utf-8"), before);
    assert.deepEqual(await AtomicFile.readJson(target), { ok: true, n: 1 });
  });
});

describe("WP-02 stores and mailbox", () => {
  it("initializes version store without DB/broker", async () => {
    const { paths } = await tempPersist();
    const versions = new SwitchboardVersionStore(paths);
    const v = await versions.ensureInitialized();
    assert.equal(v.schema, "codeflowmu.switchboard-version/v1");
    assert.equal(v.layoutVersion, 1);
    const again = await versions.read();
    assert.equal(again?.layoutVersion, 1);
  });

  it("appends readable WorkerBinding revisions", async () => {
    const { paths } = await tempPersist();
    const store = new WorkerBindingStore(paths);
    const rev: WorkerBindingRevision = {
      schema: "codeflowmu.worker-binding/v1",
      bindingId: "bind-1",
      revision: 1,
      workerId: "DEV-01",
      host: { hostId: "host-local", externalSessionId: "sess-1" },
      state: "ready",
      createdAt: "2026-08-04T10:00:00.000Z",
      createdBy: "runtime",
    };
    const path = await store.append(rev);
    assert.match(path, /BINDING-/);
    const latest = await store.latest("DEV-01");
    assert.equal(latest?.bindingId, "bind-1");
    assert.equal(latest?.revision, 1);
  });

  it("isolates mailboxes per worker and dedupes by dedupeKey", async () => {
    const { paths } = await tempPersist();
    const box = new DoorbellMailbox(paths);
    const a = await box.enqueue(sampleDoorbell("DEV-01", "d1", "dk-1"));
    const b = await box.enqueue(sampleDoorbell("QA-01", "d2", "dk-1"));
    assert.equal(a.status, "enqueued");
    assert.equal(b.status, "enqueued");

    const dup = await box.enqueue(sampleDoorbell("DEV-01", "d3", "dk-1"));
    assert.equal(dup.status, "duplicate");

    const devPending = await box.list("DEV-01", "pending");
    const qaPending = await box.list("QA-01", "pending");
    assert.equal(devPending.length, 1);
    assert.equal(qaPending.length, 1);
    assert.equal(devPending[0]?.doorbellId, "d1");
    assert.equal(qaPending[0]?.doorbellId, "d2");

    await box.claim("DEV-01", "d1");
    assert.equal((await box.list("DEV-01", "pending")).length, 0);
    assert.equal((await box.list("DEV-01", "claimed")).length, 1);
    await box.settle("DEV-01", "d1");
    assert.equal((await box.list("DEV-01", "settled")).length, 1);
  });

  it("stores runs and host claims as file truth", async () => {
    const { paths } = await tempPersist();
    const runs = new RunStore(paths);
    const claims = new HostClaimStore(paths);

    const run: RunRecord = {
      schema: "codeflowmu.run/v1",
      runId: "run-1",
      workerId: "DEV-01",
      doorbellId: "d1",
      bindingId: "bind-1",
      hostId: "host-local",
      deliveryAttemptId: "attempt-1",
      state: "running",
      sourceFactRefs: [],
      continuationMode: "same_session",
      startedAt: "2026-08-04T10:00:00.000Z",
    };
    await runs.put(run);
    assert.equal((await runs.list("DEV-01", "active")).length, 1);
    await runs.put({
      ...run,
      state: "completed",
      settledAt: "2026-08-04T10:01:00.000Z",
    });
    assert.equal((await runs.list("DEV-01", "active")).length, 0);
    assert.equal((await runs.list("DEV-01", "settled")).length, 1);

    await claims.acquire({
      schema: "codeflowmu.host-claim/v1",
      hostId: "host-local",
      sessionKey: "sess-1",
      workerId: "DEV-01",
      runId: "run-1",
      acquiredAt: "2026-08-04T10:00:00.000Z",
    });
    const got = await claims.get("host-local", "sess-1");
    assert.equal(got?.workerId, "DEV-01");
    assert.equal(await claims.release("host-local", "sess-1"), true);
  });

  it("recovers mailbox orphan tmp without dropping committed doorbells", async () => {
    const { paths } = await tempPersist();
    const box = new DoorbellMailbox(paths);
    await box.enqueue(sampleDoorbell("DEV-01", "keep-me", "dk-keep"));
    const pendingDir = paths.mailboxDir("DEV-01", "pending");
    await writeFile(join(pendingDir, "orphan.md.tmp.abc"), "half", "utf-8");
    const removed = await box.recoverOrphans("DEV-01");
    assert.ok(removed.some((n) => n.includes("orphan")));
    const names = await readdir(pendingDir);
    assert.ok(names.some((n) => n.startsWith("keep-me")));
    assert.ok(!names.some((n) => n.includes(".tmp")));
  });
});
