import { describe, it, expect } from "vitest";
import { seedModellen, seedOnderdelen } from "./index";
import { vulModelVoorbeeldIn, genereerVoorbeeldwaarde } from "./voorbeeldwaarden";
import { extraheerParameters } from "./samenstellen";
import {
  auditeerVoorbeeldtekst,
  vindVerkeerdeTaalOnderdelen,
  vindOmschrijvingTaalmismatch,
  type ModelAuditResultaat,
} from "./voorbeeld-audit";

// ── Automatische bewaking manifeste fouten — de volledige bibliotheek ────────
// Schaalt de handmatige audit (aanleiding: verkoopakte-heldere-taal-(fr) bevatte
// taalvermenging, verdubbelde woorden, dubbele aanhef en omschrijving-
// fallbacks) op naar ELK model, NL én FR. Draait bij elke `npm run poort`.
//
// Twee kleine, GEDOCUMENTEERDE uitzonderingslijsten (mogen enkel krimpen,
// nooit stilzwijgend groeien — zelfde patroon als
// akte-vormvereisten.test.ts#UITZONDERINGEN):
//   - GEKENDE_CAPTION_HERHALINGEN: een onderdeel herhaalt zijn eigen titel als
//     interne bijschrift ("SCHENKING\nSchenking en aanvaarding") — zit in de
//     CANONIEKE CLAUSULETEKST zelf, niet in de demo-waardegenerator; een fix
//     vergt het herschrijven van de onderdeeltekst (eigen, voorzichtige werf).
//   - GEKENDE_TAALGATEN: een FR-model dat nog een NL-onderdeel gebruikt bij
//     gebrek aan FR-vertaling — een echte ontbrekende vertaling (substantiële
//     clausuletekst), geen demo-waardebug; vergt een eigen vertaalwerf.
// Alles daarbuiten is een parameterwaardebug: hoort opgelost in
// voorbeeldwaarden.ts (zie de code-comments daar voor het patroon).

const GEKENDE_CAPTION_HERHALINGEN = new Set([
  "zorgvolmacht|vergoedingen",
  "schenkingsakte-onroerend|schenking",
  "schenkingsakte-roerend-goed|schenking",
  "schenkingsakte-onroerend-fr|donation",
  "schenkingsakte-roerend-goed-fr|donation",
  "verkoopakte-woning|brandverzekering",
  "verkoopakte-wet-breyne|brandverzekering",
  "verkoopakte-lijfrente|brandverzekering",
]);

// Beide vorige gaten zijn vertaald (onderhandse-volmacht-kader-fr,
// erfkeuze-volmacht-aangifte-fr) — de lijst is dus weer leeg. Nieuwe
// GEKENDE_TAALGATEN mogen hier enkel bijkomen met een concrete motivering
// (welk onderdeel ontbreekt nog in welke taal).
const GEKENDE_TAALGATEN = new Set<string>([]);

/** Eén audit per model, met de gekende uitzonderingen er al uitgefilterd. */
function auditeerAlleModellen(): { modelId: string; resultaat: ModelAuditResultaat }[] {
  return seedModellen.map((model) => {
    const taal = model.taal === "fr" ? "fr" : "nl";
    const tekst = vulModelVoorbeeldIn(model, seedOnderdelen);
    const resultaat = auditeerVoorbeeldtekst(model.id, tekst, taal);
    return {
      modelId: model.id,
      resultaat: {
        ...resultaat,
        herhalingen: resultaat.herhalingen.filter(
          (h) => !GEKENDE_CAPTION_HERHALINGEN.has(`${model.id}|${h.woord}`)
        ),
      },
    };
  });
}

describe("voorbeeld-audit — manifeste fouten over de volledige bibliotheek", () => {
  it("er zijn modellen om te bewaken (de meetlat is niet leeg)", () => {
    expect(seedModellen.length).toBeGreaterThan(40);
  });

  it("geen enkel model bevat een onopgeloste herhaling ({{param}} echode zichzelf)", () => {
    for (const { modelId, resultaat } of auditeerAlleModellen()) {
      expect(
        resultaat.herhalingen,
        `${modelId}: onverwachte woordherhaling(en) — ${resultaat.herhalingen
          .map((h) => `"${h.woord}" (${h.fragment})`)
          .join("; ")}. Nieuw? Voeg een PATROON toe in voorbeeldwaarden.ts (of, als het de canonieke clausuletekst zelf is, GEKENDE_CAPTION_HERHALINGEN met motivering).`
      ).toEqual([]);
    }
  });

  it("geen enkel model laat een {{parameter}}-placeholder onopgelost staan", () => {
    for (const { modelId, resultaat } of auditeerAlleModellen()) {
      expect(
        resultaat.onopgelosteePlaceholders,
        `${modelId}: onopgeloste placeholder(s) — ${resultaat.onopgelosteePlaceholders.join(", ")}`
      ).toEqual([]);
    }
  });

  it("geen enkel model bevat een woord uit de andere taal (taalvermenging)", () => {
    // Een model met een gekend taalgat (zie GEKENDE_TAALGATEN hieronder)
    // rendert per definitie een blok in de verkeerde taal — dat is al apart
    // getrackt via de structurele check; niet nogmaals via deze woordcheck.
    const modelsMetTaalgat = new Set([...GEKENDE_TAALGATEN].map((s) => s.split("|")[0]));
    for (const { modelId, resultaat } of auditeerAlleModellen()) {
      if (modelsMetTaalgat.has(modelId)) continue;
      expect(
        resultaat.taalvermenging,
        `${modelId}: mogelijke taalvermenging — ${resultaat.taalvermenging
          .map((t) => `"${t.woord}" (${t.fragment})`)
          .join("; ")}`
      ).toEqual([]);
    }
  });

  it("geen NIEUW gat: elk FR-model gebruikt enkel FR-onderdelen (en omgekeerd), op de gekende taalgaten na", () => {
    const gevonden = vindVerkeerdeTaalOnderdelen(seedModellen, seedOnderdelen);
    for (const g of gevonden) {
      expect(
        GEKENDE_TAALGATEN.has(`${g.modelId}|${g.onderdeelId}`),
        `${g.modelId}: gebruikt onderdeel "${g.onderdeelId}" (${g.onderdeelTitel}) in de verkeerde taal — nieuw gat? Vertaal het onderdeel of voeg toe aan GEKENDE_TAALGATEN met motivering.`
      ).toBe(true);
    }
  });

  it("geen enkel FR-onderdeel heeft een Nederlandstalige parameter-omschrijving (fallback-taallek)", () => {
    // Structureel, op de brondata (parameters[].omschrijving), niet op de
    // gegenereerde tekst: vindt ook mismatches die toevallig niet zichtbaar
    // worden omdat een PATROON in voorbeeldwaarden.ts de fallback overschrijft.
    // Was 114 bij de eerste meting van deze detector (2026-07-09) — allemaal
    // vertaald in dezelfde commit, dus de ratchet start meteen op 0.
    const gevonden = vindOmschrijvingTaalmismatch(seedOnderdelen);
    expect(
      gevonden,
      gevonden
        .map((g) => `${g.onderdeelId}.${g.parameterNaam}: "${g.omschrijving}"`)
        .join("\n")
    ).toEqual([]);
  });

  it("geen enkele parameter valt terug op een afgekapte omschrijving (fallback eindigt op '…')", () => {
    // Vijfde bewakingsklasse (ronde 2 van de kritische review, 2026-07-09):
    // een parameter zonder `voorbeeld` en zonder PATROON-match valt terug op
    // kortUitOmschrijving, die bij > 90 tekens afkapt met "…" — die halve zin
    // lekt dan letterlijk in de voorbeeldakte ("waarin het goed zich… gelegen").
    // Was 45 bij de eerste meting; allemaal gedicht met `voorbeeld`-hints in
    // dezelfde commit. Nieuw geval? Geef de parameter een `voorbeeld` (of, als
    // de naam een herkenbaar patroon volgt, een PATROON in voorbeeldwaarden.ts).
    const afgekapt: string[] = [];
    for (const o of seedOnderdelen) {
      const taal = o.taal === "fr" ? "fr" : "nl";
      for (const p of o.parameters) {
        if (genereerVoorbeeldwaarde(p.naam, taal, p).endsWith("…")) {
          afgekapt.push(`${o.id}.${p.naam}`);
        }
      }
    }
    expect(afgekapt, afgekapt.join("\n")).toEqual([]);
  });

  it("het repertoriumlabel ({{aktetype_naam}} AKTE/ACTE) beweert nooit een verkoop bij een ander aktetype", () => {
    // Regressietest voor een bug uit de kritische handmatige review
    // (2026-07-09): aktetype_naam had een vast voorbeeld "VERKOOP"/"VENTE" op
    // de parameterdeclaratie zelf, ongeacht het echte model.akteType — een
    // zorgvolmacht, oprichtingsakte, schenking, ... toonden dan ook
    // "VERKOOP AKTE"/"VENTE ACTE" in de "INFORMATIE POST AKTE"-kop. Nu per
    // akteType afgeleid in vulModelVoorbeeldIn (AKTETYPE_LABELS).
    for (const model of seedModellen) {
      const tekst = vulModelVoorbeeldIn(model, seedOnderdelen);
      const isVerkoop = /verkoop|vente/i.test(model.akteType);
      if (!isVerkoop) {
        expect(
          tekst,
          `${model.id} (akteType "${model.akteType}") toont "VERKOOP AKTE"/"VENTE ACTE" in de repertoriumkop`
        ).not.toMatch(/VERKOOP AKTE|VENTE ACTE/);
      }
    }
  });

  it("geen enkele akte sluit meer dan één keer af (hoogstens 3x WAARVAN AKTE/DONT ACTE: de drie voorleeshypotheses)", () => {
    // Ronde 3 van de kritische review (2026-07-09): slotbepalingen-akte(-fr)
    // droeg een eigen, vaste "WAARVAN AKTE"-zin terwijl elk gebruikend model
    // óók recht-op-geschriften-slotformule opneemt — de akte sloot dus
    // driemaal af. Twee vermeldingen was het normale maximum: de slotformule-
    // clausule had twee KIES-hypotheses (integrale/gedeeltelijke voorlezing)
    // met elk een eigen "WAARVAN AKTE".
    // 2026-07-11: +1 (naar 3) — nieuwe variant "akte-op-verschillende-data"
    // van recht-op-geschriften-slotformule(-fr) voegt een derde eigen
    // "WAARVAN AKTE"/"DONT ACTE" toe (akte ondertekend op verschillende data).
    for (const model of seedModellen) {
      const tekst = vulModelVoorbeeldIn(model, seedOnderdelen);
      const n = (tekst.match(model.taal === "fr" ? /DONT ACTE/g : /WAARVAN AKTE/g) ?? []).length;
      expect(n, `${model.id}: ${n}x WAARVAN AKTE/DONT ACTE`).toBeLessThanOrEqual(3);
    }
  });

  it("dekking-ratchet: gebruikte maar niet-gedeclareerde parameters (mag enkel dalen)", () => {
    // §2.3 van KRITISCHE-REVIEW-VOORBEELDEN.md: een parameter die wél in de
    // clausuletekst staat maar niet in parameters[] mist omschrijving en
    // voorbeeld als houvast voor de invullende (AI-)agent. Vul declaraties
    // aan (grootste clusters eerst) en verlaag dit plafond in dezelfde
    // commit — nooit verhogen.
    const ROLNAMEN = new Set(["rol_overdrager", "rol_verkrijger", "Rol_overdrager", "Rol_verkrijger"]);
    let onGedeclareerd = 0;
    for (const o of seedOnderdelen) {
      const inTekst = extraheerParameters(o.tekst, ...o.varianten.map((v) => v.tekst));
      const gedeclareerd = new Set(o.parameters.map((p) => p.naam));
      onGedeclareerd += inTekst.filter((n) => !gedeclareerd.has(n) && !ROLNAMEN.has(n)).length;
    }
    expect(onGedeclareerd).toBeLessThanOrEqual(156);
  });

  it("de uitzonderingslijsten bevatten enkel nog bestaande modellen/onderdelen (mogen enkel krimpen)", () => {
    const modelIds = new Set(seedModellen.map((m) => m.id));
    for (const sleutel of GEKENDE_CAPTION_HERHALINGEN) {
      const [modelId] = sleutel.split("|");
      expect(modelIds.has(modelId), `GEKENDE_CAPTION_HERHALINGEN: model "${modelId}" bestaat niet (meer) — schrap de regel`).toBe(true);
    }
    for (const sleutel of GEKENDE_TAALGATEN) {
      const [modelId] = sleutel.split("|");
      expect(modelIds.has(modelId), `GEKENDE_TAALGATEN: model "${modelId}" bestaat niet (meer) — schrap de regel`).toBe(true);
    }
    expect(GEKENDE_CAPTION_HERHALINGEN.size).toBeLessThanOrEqual(8);
    expect(GEKENDE_TAALGATEN.size).toBeLessThanOrEqual(0);
  });
});
