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

import { RULES } from "../src/rules.js";
import {
  ratify,
  ratifyFile,
  parseManifest,
  defaultContractPath,
} from "../src/ratify.js";

const kinds = (r: { issues: { kind: string }[] }) => r.issues.map((i) => i.kind);

/** A minimal contract carrying the required section headings + a manifest fence. */
function contractWith(manifestRows: string[], opts: { bRows?: number } = {}): string {
  const bRows = opts.bRows ?? 10;
  const bTable = Array.from({ length: bRows }, (_, i) => `| ${i + 1} | regel ${i + 1} | hvorfor | flagg |`).join("\n");
  return [
    "## B. De ti reglene",
    "| # | Regel | Hvorfor | Flagg |",
    "|---|---|---|---|",
    bTable,
    "",
    "## C1. Regelsjekk-gate",
    "## C2. Redaktør-pass",
    "## E. Akkumulering",
    "<!-- ratify:begin -->",
    "| rule-id | ref | severity | proveniens |",
    "|---------|-----|----------|------------|",
    ...manifestRows,
    "<!-- ratify:end -->",
  ].join("\n");
}

/** One manifest row per rule, transcribing id/ref/severity exactly. */
const validRows = RULES.map((r) => `| ${r.id} | ${r.ref} | ${r.severity} | prov |`);

describe("ratify — rules.ts ↔ §E-manifest binding", () => {
  test("a manifest that mirrors RULES exactly is in sync", () => {
    const r = ratify(contractWith(validRows));
    assert.equal(r.ok, true, `unexpected issues: ${JSON.stringify(r.issues)}`);
    assert.equal(r.issues.length, 0);
    assert.equal(r.ruleCount, RULES.length);
    assert.equal(r.manifestCount, RULES.length);
  });

  test("a missing manifest fence is a hard failure", () => {
    const r = ratify("## B. regler\nno manifest here at all\n");
    assert.equal(r.ok, false);
    assert.ok(kinds(r).includes("missing-from-manifest"));
    assert.equal(r.manifestCount, 0);
  });

  test("a rule with no manifest row → missing-from-manifest", () => {
    // drop the first rule's row
    const rows = validRows.slice(1);
    const r = ratify(contractWith(rows));
    assert.equal(r.ok, false);
    const missing = r.issues.filter((i) => i.kind === "missing-from-manifest");
    assert.equal(missing.length, 1);
    assert.equal(missing[0].id, RULES[0].id);
  });

  test("a manifest row with no rule → extra-in-manifest", () => {
    const rows = [...validRows, "| ghost-rule | §B-2 | warn | spøkelse |"];
    const r = ratify(contractWith(rows));
    assert.equal(r.ok, false);
    const extra = r.issues.filter((i) => i.kind === "extra-in-manifest");
    assert.equal(extra.length, 1);
    assert.equal(extra[0].id, "ghost-rule");
  });

  test("a ref that differs from rules.ts → ref-mismatch", () => {
    const rows = validRows.map((row, i) =>
      i === 0 ? `| ${RULES[0].id} | §B-99 | ${RULES[0].severity} | x |` : row,
    );
    const r = ratify(contractWith(rows));
    assert.equal(r.ok, false);
    assert.ok(r.issues.some((i) => i.kind === "ref-mismatch" && i.id === RULES[0].id));
  });

  test("a severity that differs from rules.ts → severity-mismatch", () => {
    const flip = RULES[0].severity === "block" ? "warn" : "block";
    const rows = validRows.map((row, i) =>
      i === 0 ? `| ${RULES[0].id} | ${RULES[0].ref} | ${flip} | x |` : row,
    );
    const r = ratify(contractWith(rows));
    assert.equal(r.ok, false);
    assert.ok(r.issues.some((i) => i.kind === "severity-mismatch" && i.id === RULES[0].id));
  });

  test("a §B-N ref pointing past the table → dangling-ref", () => {
    // §B only has 5 rows here, but RULES cites §B-10 (B10-serie-tese)
    const r = ratify(contractWith(validRows, { bRows: 5 }));
    assert.equal(r.ok, false);
    assert.ok(r.issues.some((i) => i.kind === "dangling-ref"));
  });

  test("a duplicate manifest id is flagged", () => {
    const rows = [...validRows, validRows[0]];
    const r = ratify(contractWith(rows));
    assert.equal(r.ok, false);
    assert.ok(r.issues.some((i) => i.kind === "extra-in-manifest" && i.detail.includes("duplisert")));
  });
});

describe("parseManifest", () => {
  test("returns null when the fence is absent", () => {
    assert.equal(parseManifest("no markers"), null);
  });

  test("skips header + separator, keeps data rows", () => {
    const rows = parseManifest(contractWith(validRows));
    assert.ok(rows);
    assert.equal(rows!.length, RULES.length);
    assert.equal(rows![0].id, RULES[0].id);
    assert.equal(rows![0].severity, RULES[0].severity);
  });
});

describe("ratifyFile — real contract integration", () => {
  const real = defaultContractPath();
  // Skips on a fresh clone / CI where maskinrommet is absent; runs on KTG's machine.
  test("the live skrivekontrakt.md §E-manifest ratifies clean", { skip: !existsSync(real) }, () => {
    const r = ratifyFile(real);
    assert.equal(r.ok, true, `drift vs live contract: ${JSON.stringify(r.issues, null, 2)}`);
    assert.equal(r.ruleCount, RULES.length);
  });

  test("a non-existent contract path fails cleanly (no throw)", () => {
    const r = ratifyFile("/nonexistent/skrivekontrakt.md");
    assert.equal(r.ok, false);
    assert.equal(r.contractPath, "/nonexistent/skrivekontrakt.md");
  });
});
