/**
 * Die mitgelieferten Normtexte.
 *
 * Zwei Sorten Prüfung: die Auflösungsregel an gebauten Fällen – dort lässt
 * sich die Mehrdeutigkeit gezielt herstellen – und der **ganze ausgelieferte
 * Bestand** gegen alle 2123 Fundstellen der Erklärungen und des Glossars.
 * Letzteres ist die eigentliche Zusage: Wer „Im Gesetz nachlesen“ liest, soll
 * etwas vorfinden.
 */

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

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

import type { Erklaerungen, Fundstelle } from '../src/shared/erklaerungen';
import type { Glossar } from '../src/shared/glossar';
import {
  auszugBilden,
  blockFinden,
  NORMTEXTE_LEER,
  zeigtAufAbbildung,
  type Anlagenblock,
  type Normtexte,
} from '../src/shared/normtexte';

const WURZEL = join(process.cwd(), '..');

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

/* Gelesen wird der ausgelieferte Bestand selbst, nicht eine Attrappe: Die
   Zusage lautet, dass jede der 2123 Fundstellen etwas vorfindet – und das
   lässt sich nur an der echten Datei prüfen. */
const NORMTEXTE = inhalt('normtexte.json') as Normtexte;
const ERKLAERUNGEN = inhalt('erklaerungen.json') as Erklaerungen;
const GLOSSAR = inhalt('glossar.json') as Glossar;

const ALLE_FUNDSTELLEN: readonly Fundstelle[] = [
  ...Object.values(ERKLAERUNGEN.zuFrage).flatMap((eintrag) => eintrag.fundstellen),
  ...GLOSSAR.eintraege.flatMap((eintrag) => eintrag.fundstellen),
];

function block(marke: string | null, pfad: string[], text = 'Text'): Anlagenblock {
  return { marke, pfad, text };
}

describe('blockFinden', () => {
  const bloecke: Anlagenblock[] = [
    block(null, []),
    block('Abschnitt 1', []),
    block('Unterabschnitt 1', ['Abschnitt 1']),
    block('1.1', ['Abschnitt 1', 'Unterabschnitt 1'], 'Schusswaffen sind …'),
    block('1.3.1.3', ['Abschnitt 1', 'Unterabschnitt 1'], 'Tiefe Nummer'),
    block('Unterabschnitt 3', ['Abschnitt 1']),
    block('1.1', ['Abschnitt 1', 'Unterabschnitt 3'], 'Etwas ganz anderes'),
    block('Abschnitt 2', []),
    block('1.1', ['Abschnitt 2'], 'Und noch etwas anderes'),
  ];

  it('trifft die Nummer im richtigen Unterabschnitt', () => {
    /* Der Kern der Sache: „1.1“ kommt hier dreimal vor, in Anlage 1 des WaffG
       sogar viermal. Ohne den Pfad spränge die Fundstelle unbemerkt an die
       falsche Stelle des Gesetzes. */
    expect(blockFinden(bloecke, 'Abschnitt 1 Unterabschnitt 1 Nr. 1.1')?.text).toBe(
      'Schusswaffen sind …',
    );
    expect(blockFinden(bloecke, 'Abschnitt 1 Unterabschnitt 3 Nr. 1.1')?.text).toBe(
      'Etwas ganz anderes',
    );
    expect(blockFinden(bloecke, 'Abschnitt 2 Nr. 1.1')?.text).toBe('Und noch etwas anderes');
  });

  it('nimmt die tiefste genannte Nummer', () => {
    expect(blockFinden(bloecke, 'Abschnitt 1 Unterabschnitt 1 Nr. 1.3.1.3')?.marke).toBe('1.3.1.3');
  });

  it('findet einen Abschnitt auch ohne Nummer', () => {
    expect(blockFinden(bloecke, 'Abschnitt 2')?.marke).toBe('Abschnitt 2');
    expect(blockFinden(bloecke, 'Abschnitt 1 Unterabschnitt 3')?.marke).toBe('Unterabschnitt 3');
  });

  it('gibt bei Mehrdeutigkeit lieber nichts zurück', () => {
    // „Nr. 1.1“ allein passt auf drei Blöcke. Einen davon zu wählen hieße,
    // zwei Drittel der Leser an die falsche Stelle zu schicken.
    expect(blockFinden(bloecke, 'Nr. 1.1')).toBeNull();
  });

  it('gibt bei fehlendem Treffer nichts zurück', () => {
    expect(blockFinden(bloecke, 'Abschnitt 9 Nr. 4.2')).toBeNull();
    expect(blockFinden(bloecke, 'Abbildung 5b')).toBeNull();
    expect(blockFinden(bloecke, '')).toBeNull();
  });
});

describe('zeigtAufAbbildung', () => {
  it('erkennt die Abbildungen der Anlage II', () => {
    expect(zeigtAufAbbildung('Abbildung 5b')).toBe(true);
    expect(zeigtAufAbbildung('Abbildung 3 – Ortszeichen der zuständigen Behörden')).toBe(true);
  });

  it('hält gewöhnliche Stellen auseinander', () => {
    expect(zeigtAufAbbildung('Abschnitt 1 Unterabschnitt 1 Nr. 1.1')).toBe(false);
  });
});

describe('auszugBilden', () => {
  it('liefert den zitierten Absatz', () => {
    const auszug = auszugBilden(NORMTEXTE, { gesetz: 'WaffG', norm: '§ 1', absatz: '1' });

    expect(auszug.art).toBe('absatz');
    expect(auszug.art === 'absatz' ? auszug.text : '').toContain('Umgang mit Waffen oder Munition');
  });

  it('setzt eine Norm ohne Absatzangabe aus ihren Absätzen zusammen', () => {
    const auszug = auszugBilden(NORMTEXTE, { gesetz: 'WaffG', norm: '§ 1' });

    expect(auszug.art).toBe('norm');
    expect(auszug.art === 'norm' ? auszug.text : '').toContain('(1)');
  });

  it('meldet ehrlich, wenn nichts vorliegt', () => {
    expect(auszugBilden(NORMTEXTE_LEER, { gesetz: 'WaffG', norm: '§ 1' }).art).toBe('fehlt');
    expect(auszugBilden(NORMTEXTE, { gesetz: 'StGB', norm: '§ 173' }).art).toBe('fehlt');
  });

  it('zeigt bei einer Anlage den getroffenen Block', () => {
    const auszug = auszugBilden(NORMTEXTE, {
      gesetz: 'WaffG',
      norm: 'Anlage 1',
      stelle: 'Abschnitt 1 Unterabschnitt 1 Nr. 1.1',
    });

    expect(auszug.art).toBe('block');
    expect(auszug.art === 'block' ? auszug.block.text : '').toContain('Schusswaffen sind');
  });

  it('zeigt die ganze Anlage, wenn die Stelle eine Abbildung ist', () => {
    const auszug = auszugBilden(NORMTEXTE, {
      gesetz: 'BeschussV',
      norm: 'Anlage II',
      stelle: 'Abbildung 5b',
    });

    expect(auszug.art).toBe('anlage');
    expect(auszug.art === 'anlage' ? auszug.abbildung : false).toBe(true);
    expect(auszug.art === 'anlage' ? auszug.bloecke.length : 0).toBeGreaterThan(0);
  });
});

describe('Der Gesetzesabzug sagt, wann er gezogen wurde', () => {
  /*
    Die Wache, die gefehlt hat.

    `content/gesetze/index.json` führte zu jedem Gesetz die amtliche
    Standangabe („Zuletzt geändert durch …“) – aber nirgends stand, wann
    jemand zuletzt nachgesehen hat, ob es dabei geblieben ist. Alle 1854
    Fundstellen werden bei jedem Lauf gegen genau diesen Abzug geprüft; ist
    er veraltet, meldet `pruefe_erklaerungen.py` weiter grün, gegen eine
    Vorschrift, die es so nicht mehr gibt. Am 01.09.2026 lagen zwischen dem
    ältesten Abzug (Beschussgesetz, 09.09.2020) und dem jüngsten
    (Sprengstoffgesetz, 23.07.2026) fast sechs Jahre.

    Geprüft wird hier ausdrücklich **nicht** das Alter. Ob eine Vorschrift
    sich geändert hat, kann nur ein Mensch nachsehen, und eine Prüfung, die
    allein durch Zeitablauf rot wird, meldet an ihrem Tag keinen Befund,
    sondern eine Störung – dieselbe Bauart, die in `selbstsicherung.test.ts`
    schon einmal zugeschlagen hat. Geprüft wird, dass die Angabe **da** ist;
    hinsehen muss `docs/veroeffentlichen.md`.
  */
  const index = JSON.parse(
    readFileSync(join(WURZEL, 'content', 'gesetze', 'index.json'), 'utf8'),
  ) as { gesetze: Record<string, { stand?: string; abzug?: string }> };

  it('trägt zu jedem Gesetz ein Abzugsdatum', () => {
    const ohne = Object.entries(index.gesetze)
      .filter(([, gesetz]) => gesetz.abzug === undefined)
      .map(([kuerzel]) => kuerzel);

    expect(ohne, 'Neu erzeugen mit: python data-pipeline/gesetze_index.py').toEqual([]);
  });

  it('schreibt das Abzugsdatum als ISO-Datum', () => {
    for (const [kuerzel, gesetz] of Object.entries(index.gesetze)) {
      expect(gesetz.abzug, `Abzugsdatum von ${kuerzel}`).toMatch(/^\d{4}-\d{2}-\d{2}$/u);
    }
  });

  it('trägt zu jedem Gesetz auch die amtliche Standangabe', () => {
    /* Beides zusammen ergibt erst die Auskunft: Der Abzug sagt, wann
       nachgesehen wurde, die Standangabe, was dabei zu sehen war. */
    for (const [kuerzel, gesetz] of Object.entries(index.gesetze)) {
      expect((gesetz.stand ?? '').length, `Standangabe von ${kuerzel}`).toBeGreaterThan(10);
    }
  });

  it('wird von der Veröffentlichungsliste tatsächlich angesehen', () => {
    /* Ohne diesen Fall wäre die Angabe da und niemand sähe sie an. Der
       Schritt in der Liste ist der einzige Ort, an dem aus dem Datum eine
       Handlung wird. */
    const liste = readFileSync(join(WURZEL, 'docs', 'veroeffentlichen.md'), 'utf8');

    expect(liste).toContain('Gesetzesstand ansehen');
    expect(liste).toContain('gesetze_index.py');
  });
});

describe('Der ausgelieferte Bestand', () => {
  it('enthält nur die zitierten Normen', () => {
    /* Der Index führt die sieben Gesetze vollständig – 914 Normen, darunter
       katalogfremde wie § 173 StGB. Ausgeliefert wird der Auszug. */
    const zitiert = new Set(ALLE_FUNDSTELLEN.map((f) => `${f.gesetz}|${f.norm}`));
    const geliefert = Object.entries(NORMTEXTE.gesetze).flatMap(([kuerzel, gesetz]) =>
      Object.keys(gesetz.normen).map((norm) => `${kuerzel}|${norm}`),
    );

    expect(geliefert.length).toBeGreaterThan(0);
    for (const norm of geliefert) {
      expect(zitiert.has(norm)).toBe(true);
    }
  });

  it('kennt jede Norm, auf die eine Fundstelle zeigt', () => {
    const fehlend = ALLE_FUNDSTELLEN.filter(
      (f) => NORMTEXTE.gesetze[f.gesetz]?.normen[f.norm] === undefined,
    ).map((f) => `${f.gesetz} ${f.norm}`);

    expect([...new Set(fehlend)]).toEqual([]);
  });

  it('kennt jeden zitierten Absatz', () => {
    /* Die eigentliche Zusage: 1501 Paragrafenzitate, und keines davon läuft
       ins Leere. Ohne diesen Test bliebe eine Lücke unbemerkt, bis sie
       jemandem beim Lernen begegnet. */
    const fehlend = ALLE_FUNDSTELLEN.filter((f) => {
      const norm = NORMTEXTE.gesetze[f.gesetz]?.normen[f.norm];
      return (
        norm !== undefined &&
        !norm.istAnlage &&
        f.absatz !== undefined &&
        norm.absaetze?.[f.absatz] === undefined
      );
    }).map((f) => `${f.gesetz} ${f.norm} Abs. ${f.absatz ?? ''}`);

    expect([...new Set(fehlend)]).toEqual([]);
  });

  it('löst mindestens neun von zehn Anlagenzitaten eindeutig auf', () => {
    /* Kein Anspruch auf hundert Prozent, und das mit Grund: Die Abbildungen
       der Anlage II BeschussV stehen im amtlichen XML nur als
       Bildunterschrift. Was nicht auflöst, zeigt die ganze Anlage und sagt,
       warum – falsch wird dabei nichts. */
    const anlagenzitate = ALLE_FUNDSTELLEN.filter(
      (f) => NORMTEXTE.gesetze[f.gesetz]?.normen[f.norm]?.istAnlage === true,
    );
    const getroffen = anlagenzitate.filter(
      (f) => auszugBilden(NORMTEXTE, f).art === 'block',
    ).length;

    expect(anlagenzitate.length).toBeGreaterThan(600);
    expect(getroffen / anlagenzitate.length).toBeGreaterThan(0.9);
  });

  it('liefert zu jeder Fundstelle etwas Anzeigbares', () => {
    const leer = ALLE_FUNDSTELLEN.filter((f) => auszugBilden(NORMTEXTE, f).art === 'fehlt');

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

  it('steht auf demselben Gesetzesstand wie Erklärungen und Glossar', () => {
    /* Die eine Falle des Bauwegs: Nach einer Gesetzesänderung wird der Index
       neu erzeugt und die Fundstellen werden gegen ihn geprüft – wer dann
       vergisst, `normtexte_bauen.py` laufen zu lassen, liefert den Wortlaut
       des alten Standes zu Fundstellen des neuen aus. Von außen sähe das
       tadellos aus: Jede Norm da, jeder Absatz da, nur eben von gestern. */
    expect(NORMTEXTE.meta.gesetzesstand).toEqual(ERKLAERUNGEN.meta.gesetzesstand);
    expect(NORMTEXTE.meta.gesetzesstand).toEqual(GLOSSAR.meta.gesetzesstand);
  });

  it('nennt zu jedem Gesetz den Änderungsstand', () => {
    // Ein Gesetzestext ohne Fassungsangabe ist eine Behauptung über heute,
    // die morgen falsch sein kann.
    for (const [kuerzel, gesetz] of Object.entries(NORMTEXTE.gesetze)) {
      expect(gesetz.stand.length, kuerzel).toBeGreaterThan(0);
      expect(NORMTEXTE.meta.gesetzesstand[kuerzel]).toBe(gesetz.stand);
    }
  });

  it('hält den Wortlaut der Anlagen verlustfrei', () => {
    /* Die Pipeline rechnet das beim Bauen nach; hier steht die Zusage noch
       einmal am ausgelieferten Bestand. Ein beschnittenes Gesetzeszitat wäre
       der schlimmste Fehler, den diese Anwendung machen kann – deshalb wird
       nirgends geschnitten, nur zerlegt. */
    for (const gesetz of Object.values(NORMTEXTE.gesetze)) {
      for (const [name, norm] of Object.entries(gesetz.normen)) {
        if (!norm.istAnlage) {
          continue;
        }
        const bloecke = norm.bloecke ?? [];
        expect(bloecke.length, name).toBeGreaterThan(0);
        // Jeder Block beginnt mit seiner eigenen Marke – die Marke bleibt im
        // Text stehen, sie wird nicht in ein Feld herausgezogen.
        for (const block of bloecke) {
          if (block.marke !== null) {
            expect(block.text.trimStart().startsWith(block.marke), `${name} ${block.marke}`).toBe(
              true,
            );
          }
        }
      }
    }
  });
});
