/**
 * WP-06 Shadow Doorbell / Run / Event recorder.
 */

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

import { InMemorySdkAdapter } from "../../registry/AgentSdkAdapter.ts";
import { SwitchboardPaths } from "../paths/SwitchboardPaths.ts";
import {
  ShadowDispatchRecorder,
  writeShadowAlignmentReport,
  type LegacyDispatchFact,
} from "../shadow/index.ts";
import { ShadowDispatchRecorder as FromPublic } from "../index.ts";

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

function fact(
  dispatchId: string,
  overrides: Partial<LegacyDispatchFact> = {},
): LegacyDispatchFact {
  return {
    dispatchId,
    workerId: "DEV-01",
    summary: `legacy dispatch ${dispatchId}`,
    dedupeKey: `dk-${dispatchId}`,
    sourceFactRefs: [`TASK-${dispatchId}`],
    hostId: "host-cursor",
    bindingId: "bind-dev-1",
    taskId: `TASK-${dispatchId}`,
    events: [
      { type: "host.assistant", payload: { text: "shadow-only" } },
      { type: "host.run.completed", payload: { ok: true } },
    ],
    ...overrides,
  };
}

describe("WP-06 Shadow Doorbell/Run/Event", () => {
  it("records Doorbell/Run/Session/Event/Evidence without sdk.send", async () => {
    const { paths } = await tempPersist();
    const sdk = new InMemorySdkAdapter();
    const sendCalls: unknown[] = [];
    const originalSend = sdk.send.bind(sdk);
    sdk.send = async (...args: Parameters<typeof sdk.send>) => {
      sendCalls.push(args);
      return originalSend(...args);
    };

    const recorder = new ShadowDispatchRecorder({ paths, mode: "shadow" });
    const result = await recorder.recordFromLegacyDispatch(fact("d1"));

    assert.equal(result.invokedSdkSend, false);
    assert.equal(recorder.sdkSendAttempts, 0);
    assert.equal(sendCalls.length, 0);
    assert.equal(result.evidence.openedLiveSession, false);
    assert.match(result.deliveryAttemptId, /^attempt-d1$/);

    const doorbellMd = await readFile(result.enqueue.path, "utf-8");
    assert.match(doorbellMd, /codeflowmu\.doorbell\/v1/);
    const runJson = await readFile(result.runPath, "utf-8");
    assert.match(runJson, /"deliveryAttemptId": "attempt-d1"/);
    const claimJson = await readFile(result.sessionClaimPath, "utf-8");
    assert.match(claimJson, /"opens_live_session": "false"/);
    assert.equal(result.eventPaths.length, 2);
    const evidence = JSON.parse(await readFile(result.evidencePath, "utf-8"));
    assert.equal(evidence.invokedSdkSend, false);
  });

  it("dedupes by dispatchId and dedupeKey (no double doorbell / no double delivery)", async () => {
    const { paths } = await tempPersist();
    const recorder = new ShadowDispatchRecorder({ paths });
    const a = await recorder.recordFromLegacyDispatch(fact("d2"));
    const b = await recorder.recordFromLegacyDispatch(fact("d2"));
    assert.equal(b.duplicate, true);
    assert.equal(a.doorbellId, b.doorbellId);
    assert.equal(a.runId, b.runId);

    const align = await recorder.buildAlignment(["DEV-01"]);
    assert.equal(align.legacyDispatchCount, 1);
    assert.equal(align.shadowDoorbellCount, 1);
    assert.equal(align.aligned, true);
  });

  it("alignment matrix: doorbell count equals legacy dispatch count", async () => {
    const { paths } = await tempPersist();
    const recorder = new ShadowDispatchRecorder({ paths });
    await recorder.recordFromLegacyDispatch(fact("a1"));
    await recorder.recordFromLegacyDispatch(fact("a2"));
    await recorder.recordFromLegacyDispatch(
      fact("a3", { workerId: "QA-01", bindingId: "bind-qa" }),
    );

    const align = await recorder.buildAlignment(["DEV-01", "QA-01"]);
    assert.equal(align.legacyDispatchCount, 3);
    assert.equal(align.shadowDoorbellCount, 3);
    assert.equal(align.aligned, true);
    assert.equal(align.rows.every((r) => r.aligned), true);

    const reportPath = await writeShadowAlignmentReport(paths, align, "20260805");
    const report = await readFile(reportPath, "utf-8");
    assert.match(report, /Shadow Alignment Report/);
    assert.match(report, /a1/);
    assert.match(report, /a3/);
  });

  it("survives reopen (runtime restart) and mode=off rollback", async () => {
    const { paths } = await tempPersist();
    const r1 = new ShadowDispatchRecorder({ paths });
    await r1.recordFromLegacyDispatch(fact("restart-1"));

    const r2 = ShadowDispatchRecorder.reopen(paths, "shadow");
    const align = await r2.buildAlignment(["DEV-01"]);
    assert.equal(align.legacyDispatchCount, 1);
    assert.equal(align.aligned, true);

    r2.setMode("off");
    await assert.rejects(
      () => r2.recordFromLegacyDispatch(fact("restart-2")),
      /mode=off/,
    );
    // Evidence from before rollback still readable.
    const still = await r2.buildAlignment(["DEV-01"]);
    assert.equal(still.legacyDispatchCount, 1);
    assert.equal(still.mode, "off");
  });

  it("exports ShadowDispatchRecorder from switchboard public surface", () => {
    assert.equal(typeof FromPublic, "function");
  });

  it("path helpers expose events/evidence/alignment under switchboard", () => {
    const paths = new SwitchboardPaths({
      persistDir: join("D:", "proj", ".codeflowmu", "state"),
    });
    assert.ok(paths.eventFile("run-1", "evt-1").includes(`${join("events", "run-1")}`));
    assert.ok(paths.evidenceFile("attempt-1").includes("evidence"));
    assert.ok(paths.alignmentLedgerFile().endsWith("ALIGNMENT.json"));
  });
});
