// @vitest-environment node
/**
 * Der echte Inhalt durch den echten Lader.
 *
 * Zweimal ist derselbe Fehler passiert: Die Vorprüfung in `data-pipeline/`
 * war laxer als der Lader in `src/main/`, der Inhalt kam durch die Prüfung,
 * und die **gepackte** Anwendung startete mit einem leeren Bestand – obwohl
 * die Datei vollständig im Paket lag. Beim ersten Mal waren es 450 Angaben
 * als Zahl statt als Zeichenkette, beim zweiten Mal neun Glossareinträge ohne
 * Wortform.
 *
 * Aufgefallen ist beides erst im Rauchtest gegen das gepackte Programm – also
 * spät und nur, weil es diesen Test gibt. Hier fällt es früher auf: Diese
 * Datei schickt die ausgelieferten Inhalte durch dieselben Prüfungen, die der
 * Hauptprozess anwendet, und zwar bei jedem `npm test`.
 *
 * Die Ladefunktionen selbst brauchen Electron (für den Pfad). Geprüft wird
 * deshalb die reine Validierung – genau der Teil, der bisher versagt hat.
 */

import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

import { describe, expect, it } from 'vitest';

import { GESETZE } from '../src/shared/erklaerungen';
import { begriffssucheBauen } from '../src/shared/glossar';

const wurzel = fileURLToPath(new URL('..', import.meta.url));

function inhalt(name: string): unknown {
  return JSON.parse(readFileSync(join(wurzel, '..', 'content', name), 'utf8'));
}

/**
 * Die Prüfregeln der Lader, ohne deren Electron-Abhängigkeit.
 *
 * Die **Muster** sind bewusst eine eigene, knappe Nachbildung: Sie stehen in
 * `main/erklaerungen.ts` und `main/glossar.ts`, und die importieren `electron`,
 * das hier nicht läuft. Laufen sie auseinander, ist das genau der Befund, den
 * dieser Test sucht.
 *
 * Die **Gesetzesliste** dagegen wird eingelesen und nicht nachgebaut. Sie ist
 * kein Muster, sondern eine Aufzählung, deren Nachbildung bis Fassung 0.24.1
 * hier zeichengleich ein zweites Mal stand: Ein achtes Gesetz in
 * `shared/erklaerungen.ts` hätte diesen Test dann rot gemacht, obwohl nichts
 * falsch wäre – und der bequemste Weg zu Grün wäre gewesen, die Liste hier
 * nachzuziehen, statt zu prüfen. `shared/erklaerungen.ts` ist frei von
 * Electron und lässt sich anstandslos einlesen.
 */
const NORM_MUSTER = /^(§ \d+[a-z]?|Anlage( [0-9IVX]+)?)$/u;
const GLIEDERUNG_MUSTER = /^[0-9]{1,3}[a-z]?$/u;
const BUCHSTABE_MUSTER = /^[a-z]{1,2}$/u;

interface RohFundstelle {
  readonly gesetz?: unknown;
  readonly norm?: unknown;
  readonly absatz?: unknown;
  readonly nummer?: unknown;
  readonly buchstabe?: unknown;
  readonly satz?: unknown;
  readonly stelle?: unknown;
}

/** Alle Verstöße einer Fundstelle gegen die Laderegeln. */
function fundstellenfehler(f: RohFundstelle, wo: string): string[] {
  const fehler: string[] = [];

  for (const [feld, wert] of Object.entries(f)) {
    if (wert !== undefined && typeof wert !== 'string') {
      fehler.push(`${wo}.${feld} ist ${typeof wert}, muss Zeichenkette sein`);
    }
  }
  if (typeof f.gesetz !== 'string' || !(GESETZE as readonly string[]).includes(f.gesetz)) {
    fehler.push(`${wo}.gesetz unbekannt: ${String(f.gesetz)}`);
  }
  if (typeof f.norm !== 'string' || !NORM_MUSTER.test(f.norm)) {
    fehler.push(`${wo}.norm ungültig: ${String(f.norm)}`);
  }
  for (const feld of ['absatz', 'nummer', 'satz'] as const) {
    const wert = f[feld];
    if (typeof wert === 'string' && !GLIEDERUNG_MUSTER.test(wert)) {
      fehler.push(`${wo}.${feld} ungültig: ${wert}`);
    }
  }
  if (typeof f.buchstabe === 'string' && !BUCHSTABE_MUSTER.test(f.buchstabe)) {
    fehler.push(`${wo}.buchstabe ungültig: ${f.buchstabe}`);
  }
  return fehler;
}

describe('Ausgelieferte Erklärungen', () => {
  /* Die Felder stehen hier als optional, weil geparstes JSON nichts
     garantiert – genau das prüft diese Datei ja. Ein Typ, der mehr
     behauptet, würde die Prüfung wegoptimieren. */
  const daten = inhalt('erklaerungen.json') as {
    meta: { version?: number };
    zuFrage: Record<
      string,
      {
        kurz?: string;
        text?: string;
        fundstellen?: RohFundstelle[];
        ohneFundstelleGrund?: string;
      }
    >;
  };

  it('hat einen brauchbaren Kopf', () => {
    expect(Number.isInteger(daten.meta.version)).toBe(true);
    expect(daten.meta.version ?? 0).toBeGreaterThanOrEqual(1);
  });

  it('erfüllt in jeder Fundstelle die Regeln des Laders', () => {
    const fehler = Object.entries(daten.zuFrage).flatMap(([id, e]) =>
      (e.fundstellen ?? []).flatMap((f, i) =>
        fundstellenfehler(f, `${id}.fundstellen[${String(i)}]`),
      ),
    );

    expect(fehler, 'Der Lader würde den gesamten Bestand verwerfen.').toEqual([]);
  });

  it('begründet jede fehlende Fundstelle', () => {
    const ohne = Object.entries(daten.zuFrage)
      .filter(([, e]) => (e.fundstellen ?? []).length === 0 && !e.ohneFundstelleGrund)
      .map(([id]) => id);

    expect(ohne).toEqual([]);
  });
});

describe('Ausgeliefertes Glossar', () => {
  const daten = inhalt('glossar.json') as {
    meta: { version?: number };
    eintraege: {
      begriff: string;
      art: string;
      kurz: string;
      varianten?: string[];
      fundstellen?: RohFundstelle[];
      ohneFundstelleGrund?: string;
      siehe?: string[];
    }[];
  };

  it('erfüllt in jeder Fundstelle die Regeln des Laders', () => {
    const fehler = daten.eintraege.flatMap((e) =>
      (e.fundstellen ?? []).flatMap((f, i) =>
        fundstellenfehler(f, `${e.begriff}.fundstellen[${String(i)}]`),
      ),
    );

    expect(fehler, 'Der Lader würde das gesamte Glossar verwerfen.').toEqual([]);
  });

  it('nennt zu jedem Eintrag Art, Begriff und Bedeutung', () => {
    for (const eintrag of daten.eintraege) {
      expect(['begriff', 'abkuerzung']).toContain(eintrag.art);
      expect(eintrag.begriff.trim().length).toBeGreaterThan(1);
      expect(eintrag.kurz.trim().length).toBeGreaterThan(14);
      expect(Array.isArray(eintrag.varianten)).toBe(true);
    }
  });

  it('führt keinen Begriff doppelt', () => {
    const begriffe = daten.eintraege.map((e) => e.begriff);

    expect(begriffe).toHaveLength(new Set(begriffe).size);
  });

  it('lässt jeden Querverweis auf einen vorhandenen Eintrag zeigen', () => {
    const bekannt = new Set(daten.eintraege.map((e) => e.begriff));
    const offen = daten.eintraege.flatMap((e) =>
      (e.siehe ?? []).filter((v) => !bekannt.has(v)).map((v) => `${e.begriff} → ${v}`),
    );

    expect(offen).toEqual([]);
  });

  it('findet mit den hinterlegten Wortformen tatsächlich Begriffe', () => {
    /* Der Sinn des Glossars an der Frage. Fände die Suche nichts, wäre die
       ganze Zuordnung tot – und das fiele in keinem anderen Test auf. */
    const suche = begriffssucheBauen(daten as never);
    const gefunden = suche.imText(
      'Wer eine Schusswaffe außerhalb des befriedeten Besitztums führt, braucht einen Waffenschein.',
    );

    expect(gefunden.map((e) => e.begriff)).toContain('Führen');
    expect(gefunden.length).toBeGreaterThan(2);
  });
});
