import { describe, it, expect } from "vitest";
// ── Pariteitsbewaking: kenmerken-motor ↔ API-/MCP-schema ↔ parser ────────────
// In juli 2026 bleek dat de motor 21 deterministische kenmerken kende, maar
// het OpenAPI-/MCP-schema er slechts 13 toonde en de routes er nog minder
// parseerden — agents konden de nieuwste kenmerken dus nooit aanleveren en
// hypotheses bleven onnodig open. Sindsdien is KENMERKEN_HANDLEIDING de enige
// bron: het schema wordt eruit gegenereerd (kenmerkenSchemaEigenschappen) en
// de routes valideren ermee (parseKenmerken). Deze test bewaakt die keten.
import { KENMERKEN_HANDLEIDING, parseKenmerken, kenmerkenNaarKeuzes, heldereTaalKeuzes } from "./kenmerken";
import { bouwOpenApiSpec } from "@/lib/openapi";
import { ontwerpUitQuery } from "@/lib/modeldocumenten/ontwerpUitQuery";
import { seedModellen } from "@/data/modeldocumenten";

/** Eén voorbeeldwaarde per kenmerk: booleans true, enums hun eerste waarde. */
function volledigVoorbeeld(): Record<string, unknown> {
  return Object.fromEntries(
    KENMERKEN_HANDLEIDING.kenmerken.map((k) => [k.naam, k.type === "boolean" ? true : k.toegelaten![0]])
  );
}

interface SchemaEigenschap {
  type?: string;
  enum?: string[];
  description?: string;
  properties?: Record<string, SchemaEigenschap>;
}

function bodyEigenschappen(pad: string): Record<string, SchemaEigenschap> {
  const spec = bouwOpenApiSpec() as unknown as {
    paths: Record<string, { post?: { requestBody?: { content: Record<string, { schema: { properties: Record<string, SchemaEigenschap> } }> } } }>;
  };
  const props = spec.paths[pad]?.post?.requestBody?.content["application/json"]?.schema.properties;
  expect(props, `body-schema van ${pad} ontbreekt`).toBeDefined();
  return props!;
}

describe("Kenmerken-pariteit motor ↔ schema ↔ parser", () => {
  it("de handleiding heeft voor elk enum-kenmerk toegelaten waarden", () => {
    for (const k of KENMERKEN_HANDLEIDING.kenmerken) {
      if (k.type === "enum") {
        expect(k.toegelaten?.length, `${k.naam} mist toegelaten waarden`).toBeGreaterThan(0);
      }
      expect(k.effect.length, `${k.naam} mist een effect-omschrijving`).toBeGreaterThan(10);
    }
  });

  it("parseKenmerken aanvaardt élk kenmerk uit de handleiding (volledige round-trip, geen problemen)", () => {
    const { kenmerken, problemen } = parseKenmerken(volledigVoorbeeld());
    expect(problemen).toEqual([]);
    expect(Object.keys(kenmerken).sort()).toEqual(KENMERKEN_HANDLEIDING.kenmerken.map((k) => k.naam as string).sort());
  });

  it("de motor-kenmerken die in juli 2026 uit de handleiding bleken te ontbreken, zijn aanleverbaar én sturen effectief een keuze", () => {
    // Regressie: overstromingVeilig en gezinswoningVanToepassing bestonden in
    // de motor (heldereTaalKeuzes) maar niet in de handleiding — een agent
    // kreeg "Onbekend kenmerk" en de hypothese bleef onnodig open. De
    // handleiding is nu een Record over álle Ontwerpkenmerken-sleutels
    // (compilefout bij nieuwe scheefgroei); dit bewaakt het runtime-gedrag.
    const r = parseKenmerken({ overstromingVeilig: true, gezinswoningVanToepassing: false, gewest: "Vlaanderen" });
    expect(r.problemen).toEqual([]);
    const keuzes = heldereTaalKeuzes(r.kenmerken);
    expect(keuzes.some((k) => k.onderdeelId === "ht-overstroming-rooilijn-erfgoed" && k.variantId === "vlaanderen-pg-score-erfgoed-geen-overstroming")).toBe(true);
    expect(keuzes.some((k) => k.onderdeelId === "ht-gezinswoning" && k.opnemen === false)).toBe(true);
  });

  it("parseKenmerken meldt onbekende sleutels en foute waarden expliciet (nooit stil negeren)", () => {
    const r = parseKenmerken({ typfoutKenmerk: true, gewest: "Limburg", metKrediet: "misschien" });
    expect(r.problemen.some((p) => /onbekend kenmerk "typfoutKenmerk"/i.test(p))).toBe(true);
    expect(r.problemen.some((p) => /"gewest"/.test(p))).toBe(true);
    expect(r.problemen.some((p) => /"metKrediet" moet een boolean/.test(p))).toBe(true);
    expect(Object.keys(r.kenmerken)).toEqual([]);
    // Tekstuele booleans van agents die alles als string sturen, zijn wél geldig.
    expect(parseKenmerken({ metKrediet: "true" }).kenmerken.metKrediet).toBe(true);
    // Legitiem vreemde sleutels (andere body-velden) kunnen genegeerd worden.
    expect(parseKenmerken({ modeldocumentId: "x" }, ["modeldocumentId"]).problemen).toEqual([]);
  });

  it("genereerOntwerpUitKenmerken toont in zijn schema ALLE kenmerken van de motor", () => {
    const props = bodyEigenschappen("/api/modeldocumenten/{id}/ontwerp");
    const schemaKenmerken = props.kenmerken?.properties ?? {};
    for (const k of KENMERKEN_HANDLEIDING.kenmerken) {
      const eigenschap = schemaKenmerken[k.naam as string];
      expect(eigenschap, `schema mist kenmerk "${k.naam}"`).toBeDefined();
      if (k.type === "enum") expect(eigenschap.enum).toEqual(k.toegelaten);
      else expect(eigenschap.type).toBe("boolean");
    }
  });

  it("genereerWerkdossierAlsWord aanvaardt dezelfde kenmerken als top-level parameters", () => {
    const props = bodyEigenschappen("/api/dossier/word");
    for (const k of KENMERKEN_HANDLEIDING.kenmerken) {
      expect(props[k.naam as string], `werkdossier-schema mist kenmerk "${k.naam}"`).toBeDefined();
    }
    // En de bevindingen-parameter (geïdentificeerde parameters + zekerheidsgraad
    // voor de afwerkende Word-agent) is gedeclareerd, mét het formaat erin.
    expect(props.bevindingenJson).toBeDefined();
    expect(props.bevindingenJson.description).toMatch(/zekerheid/i);
  });

  it("de GET-varianten declareren elk kenmerk als queryparameter (haalModeldocument + AlsWord)", () => {
    const spec = bouwOpenApiSpec() as unknown as {
      paths: Record<string, Record<string, { parameters?: { name: string; in: string }[] }>>;
    };
    for (const [pad, methode] of [
      ["/api/modeldocumenten/{id}", "get"],
      ["/api/modeldocumenten/{id}/word", "get"],
    ] as const) {
      const namen = new Set((spec.paths[pad]?.[methode]?.parameters ?? []).map((p) => p.name));
      for (const k of KENMERKEN_HANDLEIDING.kenmerken) {
        expect(namen.has(k.naam as string), `${pad} mist queryparameter "${k.naam}"`).toBe(true);
      }
    }
  });

  it("het query-pad past de kenmerken effectief toe (epcAanwezig kiest de EPC-hypothese op het gouden pad)", async () => {
    const model = seedModellen.find((m) => m.id === "verkoopovereenkomst-heldere-taal")!;
    const query = new URLSearchParams({ gewest: "Vlaanderen", goedType: "woning", epcAanwezig: "true" });
    const { ontwerp } = await ontwerpUitQuery(model, query);
    const sectie = ontwerp.slice(ontwerp.search(/DE ENERGIEPRESTATIE/));
    expect(sectie).toContain("Er is een EPC voor het woongebouw opgesteld");
    expect(sectie.slice(0, sectie.indexOf("##", 5))).not.toContain("[KIES -");
  });

  it("het query-pad meldt typfouten in parameternamen als 'genegeerdeParameters'", async () => {
    const model = seedModellen.find((m) => m.id === "verkoopovereenkomst-heldere-taal")!;
    const query = new URLSearchParams({ prijs_cijfers: "x", zeker_geen_bestaande_parameter: "y" });
    const r = await ontwerpUitQuery(model, query);
    expect(r.genegeerdeParameters).toEqual(["zeker_geen_bestaande_parameter"]);
  });

  it("de motor verwerkt het volledige kenmerkenpakket zonder fouten (sanity)", () => {
    const { kenmerken } = parseKenmerken(volledigVoorbeeld());
    const keuzes = [...kenmerkenNaarKeuzes(kenmerken), ...heldereTaalKeuzes(kenmerken)];
    expect(keuzes.length).toBeGreaterThan(5);
    // Deterministisch: tweemaal dezelfde kenmerken → exact dezelfde keuzes.
    expect([...kenmerkenNaarKeuzes(kenmerken), ...heldereTaalKeuzes(kenmerken)]).toEqual(keuzes);
  });
});
