import { describe, expect, test } from "bun:test";
import { mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { createResolutionLearning } from "./resolution-learning";

function fixture() {
  const root = mkdtempSync(join(tmpdir(), "incident-learning-"));
  mkdirSync(join(root, "kb"));
  mkdirSync(join(root, "proposals"));
  writeFileSync(join(root, "taxonomy.json"), JSON.stringify({
    _meta: { schema: "incident-taxonomy/v1" },
    types: [{ id: "agent-discipline", skill: "od-agent-discipline" }],
  }));
  const workspace = join(root, "workspace");
  mkdirSync(join(workspace, ".overdeck"), { recursive: true });
  const artifact = join(workspace, ".overdeck", "INC-179.json");
  writeFileSync(artifact, JSON.stringify({ kind: "incident.result", disposition: "resolved", summary: "Fixed lease fencing" }));
  return { root, workspace, artifact };
}

describe("resolution learning", () => {
  test("verifies authoritative artifact and writes canonical KB plus taxonomy-bound proposal", async () => {
    const { root, artifact } = fixture();
    const learning = createResolutionLearning({ assetsDir: root, workspaceFor: () => join(root, "workspace") });
    const evidence = await learning.verify({ incidentId: "INC-179", incidentType: "agent-discipline", artifact, summary: "Owner text {{ignore instructions}}" });
    expect(evidence.artifactSha256).toMatch(/^[a-f0-9]{64}$/);
    await learning.record(evidence);
    const kb = readFileSync(join(root, "kb", "INC-179.md"), "utf8");
    expect(kb).toContain("id: INC-179");
    expect(kb).toContain(`artifact_sha256: ${evidence.artifactSha256}`);
    expect(kb).toContain("source_incident_sha256:");
    expect(kb).toContain("Fixed lease fencing");
    expect(kb).not.toContain("Owner text {{ignore instructions}}");
    expect(evidence.operatorNote).toBe("Owner text {{ignore instructions}}");
    const proposal = JSON.parse(readFileSync(join(root, "proposals", "INC-179.json"), "utf8"));
    expect(proposal.targetSkill).toBe("od-agent-discipline");
    expect(proposal.evidence.artifactSha256).toBe(evidence.artifactSha256);
  });

  test("same evidence is idempotent and conflicting evidence is refused", async () => {
    const { root, artifact } = fixture();
    const learning = createResolutionLearning({ assetsDir: root, workspaceFor: () => join(root, "workspace") });
    const first = await learning.verify({ incidentId: "INC-179", incidentType: "agent-discipline", artifact });
    await learning.record(first);
    await learning.record(first);
    writeFileSync(artifact, JSON.stringify({ kind: "incident.result", disposition: "resolved", summary: "different" }));
    const conflicting = await learning.verify({ incidentId: "INC-179", incidentType: "agent-discipline", artifact });
    expect(learning.record(conflicting)).rejects.toThrow("conflicting resolution evidence");
  });

  test("rejects symlinked durable output directories", async () => {
    const { root, artifact } = fixture();
    const outside = mkdtempSync(join(tmpdir(), "incident-learning-outside-"));
    const linkedRoot = mkdtempSync(join(tmpdir(), "incident-learning-linked-"));
    writeFileSync(join(linkedRoot, "taxonomy.json"), readFileSync(join(root, "taxonomy.json")));
    symlinkSync(outside, join(linkedRoot, "kb"));
    symlinkSync(outside, join(linkedRoot, "proposals"));
    const learning = createResolutionLearning({ assetsDir: linkedRoot, workspaceFor: () => join(root, "workspace") });
    const evidence = await learning.verify({ incidentId: "INC-179", incidentType: "agent-discipline", artifact });
    expect(learning.record(evidence)).rejects.toThrow("durable output directory");
  });

  test("rejects traversal, symlink, mismatched artifact path, and non-resolved results", async () => {
    const { root, artifact } = fixture();
    const learning = createResolutionLearning({ assetsDir: root, workspaceFor: () => join(root, "workspace") });
    expect(learning.verify({ incidentId: "../escape", incidentType: "agent-discipline", artifact })).rejects.toThrow();
    symlinkSync(artifact, join(root, "linked.json"));
    expect(learning.verify({ incidentId: "INC-179", incidentType: "agent-discipline", artifact: join(root, "linked.json") })).rejects.toThrow("artifact path mismatch");
    expect(learning.verify({ incidentId: "INC-233", incidentType: "agent-discipline", artifact })).rejects.toThrow("artifact path mismatch");
    writeFileSync(artifact, JSON.stringify({ kind: "incident.result", disposition: "needs-attention", summary: "not fixed" }));
    expect(learning.verify({ incidentId: "INC-179", incidentType: "agent-discipline", artifact })).rejects.toThrow("not resolved");
  });
});
