import { describe, test } from "node:test";
import assert from "node:assert/strict";

import { validateBinding } from "../src/binding.js";
import type { LivedSpecifics, Slot } from "../src/binding.js";
import type { Bank, Specific } from "../src/types.js";

// ---- fixtures ---------------------------------------------------------------

function mkSpecific(over: Partial<Specific> & { id: string }): Specific {
  return {
    id: over.id,
    type: over.type ?? "other",
    content: over.content ?? "x",
    topicTags: over.topicTags ?? ["t"],
    provenance: over.provenance ?? { capturedAt: "2026-06-20", source: "test" },
    verification: over.verification ?? "n/a",
    status: over.status ?? "active",
  };
}

const mkBank = (specifics: Specific[]): Bank => ({ schemaVersion: 1, specifics });

const slot = (slotId: string, binding: Slot["binding"], over: Partial<Slot> = {}): Slot => ({
  slotId,
  kind: over.kind ?? "key-point",
  label: over.label ?? `label ${slotId}`,
  binding,
});

const ls = (slots: Slot[]): LivedSpecifics => ({ slots, status: "pending" });

// ---- tests ------------------------------------------------------------------

describe("validateBinding — the «vaghet avvises» gate (G4)", () => {
  test("every slot resolved (specific + abstrakt + ekstern) → PASS, coverage counted", () => {
    const bank = mkBank([mkSpecific({ id: "aaa", type: "named-case" })]);
    const res = validateBinding(
      ls([
        slot("kp1", { type: "specific", specificId: "aaa" }),
        slot("kp2", { type: "abstrakt", rationale: "bevisst abstrakt her" }),
        slot("kp3", { type: "ekstern", source: "research note 2" }),
      ]),
      bank,
    );
    assert.equal(res.verdict, "PASS");
    assert.equal(res.problems.length, 0);
    assert.deepEqual(res.coverage, {
      total: 3,
      specific: 1,
      abstrakt: 1,
      ekstern: 1,
      unresolved: 0,
    });
  });

  test("an unresolved slot → BLOCK with reason 'unresolved'", () => {
    const res = validateBinding(
      ls([
        slot("kp1", { type: "specific", specificId: "aaa" }),
        slot("kp2", { type: "unresolved" }),
      ]),
      mkBank([mkSpecific({ id: "aaa" })]),
    );
    assert.equal(res.verdict, "BLOCK");
    assert.ok(
      res.problems.some((p) => p.slotId === "kp2" && p.reason === "unresolved"),
      "kp2 should be flagged unresolved",
    );
    assert.equal(res.coverage.unresolved, 1);
  });

  test("a specific slot whose id is not in the bank → BLOCK 'dangling-specific'", () => {
    const res = validateBinding(
      ls([slot("kp1", { type: "specific", specificId: "missing" })]),
      mkBank([]),
    );
    assert.equal(res.verdict, "BLOCK");
    assert.ok(
      res.problems.some((p) => p.slotId === "kp1" && p.reason === "dangling-specific"),
      "dangling id must be caught",
    );
    // dangling still counts under `specific` in coverage (binding TYPE is specific)
    assert.equal(res.coverage.specific, 1);
  });

  test("an abstrakt escape with a blank rationale is vaghet → BLOCK 'unjustified-abstrakt'", () => {
    const res = validateBinding(ls([slot("kp1", { type: "abstrakt", rationale: "   " })]), mkBank([]));
    assert.equal(res.verdict, "BLOCK");
    assert.ok(
      res.problems.some((p) => p.slotId === "kp1" && p.reason === "unjustified-abstrakt"),
      "an unjustified abstraction must be rejected",
    );
  });

  test("ekstern needs no source (research fills it later) → PASS", () => {
    const res = validateBinding(
      ls([
        slot("a", { type: "ekstern" }),
        slot("b", { type: "abstrakt", rationale: "x" }),
        slot("c", { type: "abstrakt", rationale: "y" }),
      ]),
      mkBank([]),
    );
    assert.equal(res.verdict, "PASS");
    assert.deepEqual(res.coverage, {
      total: 3,
      specific: 0,
      abstrakt: 2,
      ekstern: 1,
      unresolved: 0,
    });
  });

  test("a slot backed by an UNVERIFIED number → PASS + warning (regel 6/7)", () => {
    const bank = mkBank([mkSpecific({ id: "num1", type: "number", verification: "unverified" })]);
    const res = validateBinding(ls([slot("kp1", { type: "specific", specificId: "num1" })]), bank);
    assert.equal(res.verdict, "PASS", "an unverified number warns, it does not block");
    assert.ok(
      res.warnings.some((w) => w.slotId === "kp1" && w.reason === "unverified-number"),
      "unverified number must surface as a warning",
    );
  });

  test("a slot backed by a VERIFIED number → PASS, no warning", () => {
    const bank = mkBank([mkSpecific({ id: "num1", type: "number", verification: "verified" })]);
    const res = validateBinding(ls([slot("kp1", { type: "specific", specificId: "num1" })]), bank);
    assert.equal(res.verdict, "PASS");
    assert.equal(res.warnings.length, 0);
  });

  test("empty slot-map → PASS, total 0 (the command decides whether ≥1 slot is required)", () => {
    const res = validateBinding(ls([]), mkBank([]));
    assert.equal(res.verdict, "PASS");
    assert.equal(res.coverage.total, 0);
    assert.equal(res.problems.length, 0);
  });

  test("multiple problems are all reported (gate is not short-circuited)", () => {
    const res = validateBinding(
      ls([
        slot("a", { type: "unresolved" }),
        slot("b", { type: "specific", specificId: "nope" }),
        slot("c", { type: "abstrakt", rationale: "" }),
      ]),
      mkBank([]),
    );
    assert.equal(res.verdict, "BLOCK");
    assert.equal(res.problems.length, 3);
  });
});
