/**
 * WP-14 — health / diagnostics / migration / parent guard / startup recovery.
 */

import assert from "node:assert/strict";
import { mkdir, rm, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { after, before, describe, it } from "node:test";

import { SwitchboardPaths } from "../../paths/SwitchboardPaths.js";
import { DoorbellMailbox } from "../../store/DoorbellMailbox.js";
import { RunStore } from "../../store/RunStore.js";
import { HostClaimStore } from "../../store/HostClaimStore.js";
import type { DoorbellMessage } from "../../contracts/DoorbellMessage.js";
import type { RunRecord } from "../../contracts/RunRecord.js";
import {
  SwitchboardHealth,
  SwitchboardDiagnostics,
  SwitchboardStartupRecovery,
  migrateSwitchboardState,
  evaluateParentLink,
  detectParentCycle,
  rejectParentOrThrow,
} from "../index.js";

async function mkPersist(): Promise<{ root: string; paths: SwitchboardPaths }> {
  const root = join(
    tmpdir(),
    `wp14-${Date.now()}-${Math.random().toString(16).slice(2)}`,
  );
  const persistDir = join(root, ".codeflowmu", "state");
  await mkdir(persistDir, { recursive: true });
  await mkdir(join(root, "fcop"), { recursive: true });
  return { root, paths: new SwitchboardPaths({ persistDir }) };
}

describe("WP-14 recovery and migration", () => {
  let root = "";
  let paths!: SwitchboardPaths;

  before(async () => {
    const env = await mkPersist();
    root = env.root;
    paths = env.paths;
  });

  after(async () => {
    if (root) await rm(root, { recursive: true, force: true });
  });

  it("migrates layout and never rewrites FCoP ledger", async () => {
    const persistDir = join(root, ".codeflowmu", "state");
    const result = await migrateSwitchboardState({ persistDir });
    assert.equal(result.fcopLedgerRewritten, false);
    assert.equal(result.toVersion, 1);
    assert.equal(result.migrated, true);

    const again = await migrateSwitchboardState({ persistDir });
    assert.equal(again.migrated, false);
  });

  it("health reports claimed-without-run and fcop readable", async () => {
    const box = new DoorbellMailbox(paths);
    const msg: DoorbellMessage = {
      schema: "codeflowmu.doorbell/v1",
      doorbellId: "db-h1",
      recipientWorkerId: "DEV-01",
      topic: "task.ready",
      sourceFactRefs: ["TASK-1"],
      summary: "health",
      priority: "P1",
      dedupeKey: "dk-h1",
      createdAt: "2026-08-05T12:00:00.000Z",
    };
    await box.enqueue(msg);
    await box.claim("DEV-01", "db-h1");

    const report = await new SwitchboardHealth().check({
      paths,
      workerIds: ["DEV-01"],
      projectRoot: root,
    });
    assert.equal(report.productionActiveEnabled, false);
    assert.ok(
      report.findings.some((f) => f.code === "doorbell.claimed_orphaned"),
    );
    assert.ok(report.findings.some((f) => f.code === "fcop.readable"));
  });

  it("diagnostics repairs starting→delivery_unknown with audit", async () => {
    const env = await mkPersist();
    const runs = new RunStore(env.paths);
    const run: RunRecord = {
      schema: "codeflowmu.run/v1",
      runId: "run-start-1",
      workerId: "DEV-01",
      doorbellId: "db-s1",
      bindingId: "bind-1",
      hostId: "host-local",
      deliveryAttemptId: "att-1",
      state: "starting",
      sourceFactRefs: ["TASK-1"],
      continuationMode: "same_session",
      startedAt: "2026-08-05T12:00:00.000Z",
    };
    await runs.put(run);

    const diag = await new SwitchboardDiagnostics().diagnose({
      paths: env.paths,
      workerIds: ["DEV-01"],
      applyRepairs: true,
    });
    assert.ok(
      diag.audits.some(
        (a) =>
          a.action === "mark_delivery_unknown" && a.result === "applied",
      ),
    );
    const settled = await runs.list("DEV-01", "settled");
    assert.equal(settled[0]?.state, "delivery_unknown");
    await rm(env.root, { recursive: true, force: true });
  });

  it("startup recovery initializes version and recovers orphans", async () => {
    const env = await mkPersist();
    const box = new DoorbellMailbox(env.paths);
    await box.enqueue({
      schema: "codeflowmu.doorbell/v1",
      doorbellId: "db-or",
      recipientWorkerId: "DEV-01",
      topic: "task.ready",
      sourceFactRefs: ["TASK-1"],
      summary: "orphan",
      priority: "P2",
      dedupeKey: "dk-or",
      createdAt: "2026-08-05T12:00:00.000Z",
    });
    const pendingDir = env.paths.mailboxDir("DEV-01", "pending");
    const { writeFile } = await import("node:fs/promises");
    await writeFile(join(pendingDir, "junk.md.tmp"), "partial", "utf8");

    const result = await new SwitchboardStartupRecovery().run({
      paths: env.paths,
      workerIds: ["DEV-01"],
      projectRoot: env.root,
      applyRepairs: true,
    });
    assert.equal(result.productionActiveEnabled, false);
    assert.equal(result.initialized, true);
    assert.ok(
      result.diagnostics.audits.some(
        (a) => a.action === "recover_mailbox_orphans",
      ),
    );
    await rm(env.root, { recursive: true, force: true });
  });

  it("releases expired claims with audit", async () => {
    const env = await mkPersist();
    const claims = new HostClaimStore(env.paths);
    await claims.acquire({
      schema: "codeflowmu.host-claim/v1",
      hostId: "host-local",
      sessionKey: "sess-expired",
      workerId: "DEV-01",
      acquiredAt: "2026-08-01T00:00:00.000Z",
      expiresAt: "2026-08-02T00:00:00.000Z",
    });
    const diag = await new SwitchboardDiagnostics().diagnose({
      paths: env.paths,
      workerIds: ["DEV-01"],
      hostIds: ["host-local"],
      applyRepairs: true,
      now: () => new Date("2026-08-05T00:00:00.000Z"),
    });
    assert.ok(
      diag.audits.some((a) => a.action === "release_expired_claim"),
    );
    assert.equal(await claims.get("host-local", "sess-expired"), null);
    await rm(env.root, { recursive: true, force: true });
  });

  it("rejects abnormal parent links and detects cycles", () => {
    const known = ["TASK-20260805-001", "TASK-20260805-002"];
    assert.equal(
      evaluateParentLink({
        taskId: "TASK-20260805-002",
        parent: "TASK-20260805-001",
        knownTaskIds: known,
      }).accepted,
      true,
    );
    assert.equal(
      evaluateParentLink({
        taskId: "TASK-20260805-003",
        parent: "TASK-20990101-999",
        knownTaskIds: known,
      }).code,
      "parent_missing",
    );
    assert.equal(
      evaluateParentLink({
        taskId: "TASK-20260805-001",
        parent: "TASK-20260805-001",
        knownTaskIds: known,
      }).code,
      "parent_self_reference",
    );
    assert.equal(
      evaluateParentLink({
        taskId: "TASK-20260805-003",
        parent: "TASK-20260805-001",
        knownTaskIds: known,
        closedTaskIds: ["TASK-20260805-001"],
      }).code,
      "closed_parent",
    );
    assert.throws(() =>
      rejectParentOrThrow({
        taskId: "TASK-20260805-003",
        parent: "TASK-20990101-999",
        knownTaskIds: known,
      }),
    );
    const cycle = detectParentCycle({
      "TASK-A": "TASK-B",
      "TASK-B": "TASK-A",
    });
    assert.equal(cycle.hasCycle, true);
  });

  it("no dual-delivery: diagnostics never enable Active", async () => {
    const report = await new SwitchboardHealth().check({
      paths,
      workerIds: ["DEV-01"],
    });
    assert.equal(report.productionActiveEnabled, false);
    const diag = await new SwitchboardDiagnostics().diagnose({
      paths,
      workerIds: ["DEV-01"],
      applyRepairs: false,
    });
    assert.equal(diag.productionActiveEnabled, false);
  });
});
