/**
 * WP-11 — work inheritance: hot / warm / cold recovery + sufficiency.
 */

import assert from "node:assert/strict";
import { promises as fs } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { after, before, describe, it } from "node:test";
import {
  ContextProjectionBuilder,
  RecoveryPlanner,
  WorkFactReader,
} from "../index.js";

async function mkProject(): Promise<string> {
  const root = join(
    tmpdir(),
    `wp11-inherit-${Date.now()}-${Math.random().toString(16).slice(2)}`,
  );
  await fs.mkdir(join(root, "fcop", "_lifecycle", "active"), {
    recursive: true,
  });
  await fs.mkdir(join(root, "fcop", "reports"), { recursive: true });
  await fs.mkdir(join(root, "fcop", "issues"), { recursive: true });
  await fs.mkdir(join(root, ".codeflowmu", "state"), { recursive: true });

  const taskId = "TASK-20260805-099";
  const taskBody = `---
protocol: fcop
sender: PM
recipient: DEV
task_id: ${taskId}
thread_key: thread-wp11-demo
parent: TASK-20260804-001
references: "['TASK-20260804-001', 'TASK-20260805-014']"
state: active
---

# DEV implement WP-11 fixture

Do the work. Do not invent facts.
`;
  await fs.writeFile(
    join(root, "fcop", "_lifecycle", "active", `${taskId}-PM-to-DEV.md`),
    taskBody,
    "utf8",
  );

  const report = `---
protocol: fcop
source_task_id: TASK-20260805-014
task_id: TASK-20260805-014
reporter: QA
recipient: PM
status: done
thread_key: thread-wp11-demo
---

## 结论

**PASS** — WP-10 closed. Ready for WP-11.
`;
  await fs.writeFile(
    join(root, "fcop", "reports", "REPORT-20260805-029-QA-to-PM.md"),
    report,
    "utf8",
  );

  // Related report that mentions the fixture task id (simulates thread continuity).
  await fs.writeFile(
    join(root, "fcop", "reports", "REPORT-20260805-099-DEV-to-PM.md"),
    `---
protocol: fcop
source_task_id: ${taskId}
task_id: ${taskId}
reporter: DEV
recipient: PM
status: done
thread_key: thread-wp11-demo
---

## 结论

done — fixture DEV report for cold handoff.
`,
    "utf8",
  );

  // Hidden thinking must never be loaded.
  await fs.mkdir(join(root, "fcop", "reports", ".thinking"), {
    recursive: true,
  });
  await fs.writeFile(
    join(root, "fcop", "reports", ".thinking", "sdk.thinking.jsonl"),
    '{"secret":"should-never-appear","path":"C:\\\\Users\\\\Administrator\\\\secret"}\n',
    "utf8",
  );

  return root;
}

describe("WP-11 WorkFactReader", () => {
  let root = "";

  before(async () => {
    root = await mkProject();
  });

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

  it("reads TASK/REPORT facts and never loads hidden thinking", async () => {
    const reader = new WorkFactReader({ projectRoot: root });
    const facts = await reader.readTaskFacts("TASK-20260805-099");

    assert.equal(facts.taskId, "TASK-20260805-099");
    assert.equal(facts.threadKey, "thread-wp11-demo");
    assert.equal(facts.parent, "TASK-20260804-001");
    assert.notEqual(facts.taskPath, "UNKNOWN");
    assert.ok(facts.reports.length >= 1);
    assert.ok(
      facts.reports.every((r) => !r.path.includes(".thinking")),
      "hidden thinking paths must be excluded",
    );
    assert.ok(
      !JSON.stringify(facts).includes("should-never-appear"),
      "thinking payload must not leak",
    );
    assert.ok(
      !JSON.stringify(facts).includes("Administrator"),
      "absolute user paths must be redacted from excerpts",
    );
  });

  it("writes UNKNOWN for missing task instead of inventing", async () => {
    const reader = new WorkFactReader({ projectRoot: root });
    const facts = await reader.readTaskFacts("TASK-20990101-000");
    assert.equal(facts.taskPath, "UNKNOWN");
    assert.ok(facts.unknowns.some((u) => /not found/i.test(u)));
  });
});

describe("WP-11 RecoveryPlanner hot/warm/cold", () => {
  let root = "";
  let persistDir = "";

  before(async () => {
    root = await mkProject();
    persistDir = join(root, ".codeflowmu", "state");
  });

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

  it("hot: Session healthy → delta pack", async () => {
    const facts = await new WorkFactReader({ projectRoot: root }).readTaskFacts(
      "TASK-20260805-099",
    );
    const plan = await new RecoveryPlanner().plan({
      facts,
      workerId: "DEV-01",
      runId: "run-hot-1",
      sessionHealth: "healthy",
      hostHealth: "available",
      persistDir,
    });
    assert.equal(plan.tier, "hot");
    assert.equal(plan.action, "resume_hot");
    assert.equal(plan.canHandoff, true);
    assert.ok(plan.projection);
    const ctx = await fs.readFile(plan.projection!.contextPath, "utf8");
    assert.match(ctx, /tier: hot/);
    assert.match(ctx, /TASK-20260805-099/);
  });

  it("warm: Session recoverable → summary pack", async () => {
    const facts = await new WorkFactReader({ projectRoot: root }).readTaskFacts(
      "TASK-20260805-099",
    );
    const plan = await new RecoveryPlanner().plan({
      facts,
      workerId: "DEV-01",
      runId: "run-warm-1",
      sessionHealth: "recoverable",
      hostHealth: "available",
      persistDir,
    });
    assert.equal(plan.tier, "warm");
    assert.equal(plan.action, "resume_warm");
    assert.ok(plan.projection?.sufficient);
  });

  it("cold: Session lost — file facts still enough to hand off", async () => {
    const facts = await new WorkFactReader({ projectRoot: root }).readTaskFacts(
      "TASK-20260805-099",
    );
    // Simulate: InMemory Host Session deleted; chat log inaccessible.
    // Only file facts remain — RecoveryPlanner must still produce cold pack.
    const plan = await new RecoveryPlanner().plan({
      facts,
      workerId: "DEV-01",
      runId: "run-cold-1",
      sessionHealth: "lost",
      hostHealth: "available",
      persistDir,
      notes: ["session deleted; chat inaccessible"],
    });
    assert.equal(plan.tier, "cold");
    assert.equal(plan.action, "cold_same_host");
    assert.equal(plan.canHandoff, true);
    assert.ok(plan.projection?.sufficient);
    const ctx = await fs.readFile(plan.projection!.contextPath, "utf8");
    assert.match(ctx, /Hidden thinking streams are never read/);
    assert.match(ctx, /thread-wp11-demo/);
    assert.ok(plan.projection!.manifest.entries.some((e) => e.kind === "task"));
  });

  it("cold insufficient materials → explicit failed (no invent)", async () => {
    const emptyRoot = join(root, "empty-proj");
    await fs.mkdir(join(emptyRoot, "fcop", "_lifecycle", "active"), {
      recursive: true,
    });
    await fs.mkdir(join(emptyRoot, "fcop", "reports"), { recursive: true });
    // Task without reports / without thread continuity for cold.
    await fs.writeFile(
      join(
        emptyRoot,
        "fcop",
        "_lifecycle",
        "active",
        "TASK-20990101-001-PM-to-DEV.md",
      ),
      `---
task_id: TASK-20990101-001
thread_key: orphan-thread
parent: UNKNOWN
---

# orphan
`,
      "utf8",
    );
    const facts = await new WorkFactReader({
      projectRoot: emptyRoot,
    }).readTaskFacts("TASK-20990101-001");
    const plan = await new RecoveryPlanner().plan({
      facts,
      workerId: "DEV-01",
      runId: "run-cold-fail",
      sessionHealth: "lost",
      hostHealth: "available",
      persistDir: join(emptyRoot, ".codeflowmu", "state"),
    });
    assert.equal(plan.action, "failed");
    assert.equal(plan.canHandoff, false);
    assert.match(plan.failure ?? "", /insufficient/i);
  });

  it("Host unavailable without alternate → needs_human / failed", async () => {
    const facts = await new WorkFactReader({ projectRoot: root }).readTaskFacts(
      "TASK-20260805-099",
    );
    const plan = new RecoveryPlanner().decide({
      facts,
      workerId: "DEV-01",
      runId: "run-no-host",
      sessionHealth: "lost",
      hostHealth: "unavailable",
      alternateHostAvailable: false,
    });
    assert.equal(plan.action, "failed");
    assert.equal(plan.canHandoff, false);
    assert.match(plan.failure ?? "", /no replacement Host/i);
  });

  it("Host unavailable with alternate → needs_human (no auto migrate)", async () => {
    const facts = await new WorkFactReader({ projectRoot: root }).readTaskFacts(
      "TASK-20260805-099",
    );
    const plan = new RecoveryPlanner().decide({
      facts,
      workerId: "DEV-01",
      runId: "run-needs-choice",
      sessionHealth: "lost",
      hostHealth: "unavailable",
      alternateHostAvailable: true,
    });
    assert.equal(plan.action, "cold_needs_host_choice");
    assert.equal(plan.canHandoff, false);
    assert.match(plan.failure ?? "", /needs_human/i);
  });

  it("projection pack can be deleted and regenerated", async () => {
    const facts = await new WorkFactReader({ projectRoot: root }).readTaskFacts(
      "TASK-20260805-099",
    );
    const builder = new ContextProjectionBuilder();
    const first = await builder.build({
      facts,
      workerId: "DEV-01",
      runId: "run-rebuild",
      tier: "cold",
      persistDir,
    });
    assert.ok(first.sufficient);
    await builder.deletePack(persistDir, "run-rebuild");
    await assert.rejects(() => fs.access(first.contextPath));
    const second = await builder.build({
      facts,
      workerId: "DEV-01",
      runId: "run-rebuild",
      tier: "cold",
      persistDir,
    });
    assert.ok(second.sufficient);
    const ctx = await fs.readFile(second.contextPath, "utf8");
    assert.match(ctx, /Context Projection — cold/);
  });

  it("tracks multi-session/run chain via distinct runIds", async () => {
    const facts = await new WorkFactReader({ projectRoot: root }).readTaskFacts(
      "TASK-20260805-099",
    );
    const planner = new RecoveryPlanner();
    const a = await planner.plan({
      facts,
      workerId: "DEV-01",
      runId: "run-s1",
      sessionHealth: "lost",
      hostHealth: "available",
      persistDir,
    });
    const b = await planner.plan({
      facts,
      workerId: "DEV-01",
      runId: "run-s2",
      sessionHealth: "lost",
      hostHealth: "available",
      persistDir,
    });
    assert.notEqual(a.projection!.runId, b.projection!.runId);
    assert.equal(a.projection!.manifest.workerId, "DEV-01");
    assert.equal(b.projection!.manifest.runId, "run-s2");
  });
});
