"""Prüft das Glossar gegen Katalog und amtlichen Gesetzestext.

Dieselbe Zusage wie bei den Erklärungen: Was als Fundstelle dasteht, muss im
Gesetz nachweisbar sein. Die Prüfung der Fundstellen kommt deshalb wörtlich
aus `pruefe_erklaerungen.py` – zwei Kopien derselben Regel würden mit der
Zeit auseinanderlaufen, und dann gälte für das Glossar ein anderer Maßstab
als für die Erklärungen.

Zusätzlich glossarspezifisch geprüft
------------------------------------
1. **Eindeutigkeit** – kein Begriff zweimal.
2. **Wortformen** – gültige Zeichenketten. Ein Eintrag ohne Formen ist
   zulässig: Er erscheint dann nur in der Glossaransicht und an keiner Frage.
   Das ist bei Abkürzungen normal, die nur in den Erklärungen vorkommen.
3. **Querverweise** – jedes `siehe` zeigt auf einen Eintrag, den es gibt.
4. **Bezug zum Text** – kommt eine Wortform in den Fragen oder Erklärungen
   überhaupt vor? Das ist kein Fehler, aber ein Hinweis: Ein Glossar aus
   Wörtern, die niemand liest, hilft niemandem. Durchsucht werden beide,
   denn Abkürzungen wie WBK oder NWR stehen nur in den Erklärungen.
5. **Form** – Länge von `begriff`, `kurz`, `text` und `ohneFundstelleGrund`,
   kein Leerraum am Rand, `art` ist `begriff` oder `abkuerzung`, keine
   unbekannten Felder.
6. **Redliche Lücken** – ein Eintrag ohne Fundstelle muss begründen, warum es
   keine gibt; eine Begründung neben vorhandenen Fundstellen ist umgekehrt ein
   Widerspruch. Damit ist das Fehlen eine Aussage und keine Nachlässigkeit.
7. **Gesetzesstand** – der im Kopf vermerkte Stand jedes Gesetzes muss zum
   Index passen. Sonst wäre gegen etwas anderes geprüft worden, als
   ausgewiesen ist.

Die Punkte 5 bis 7 standen bis Fassung 0.21.0 nicht hier, obwohl das Skript sie
prüfte. Sie sind nachgetragen, weil der Selbsttest unten zusagt, *jede* hier
genannte Regel zu verletzen – eine unvollständige Liste machte diese Zusage
wieder wertlos.

Nicht geprüft wird, ob der Kopf die Pflichtfelder überhaupt trägt; bei den
Erklärungen tut das `alles_pruefen` dort, hier nicht.

Selbsttest
----------
`--selbsttest` prüft die Prüfung: Jede der oben genannten Regeln wird einmal
absichtlich verletzt, und der Lauf besteht nur, wenn jede Verletzung auch
beanstandet wird. Dazu die Gegenprobe – der ausgelieferte Bestand darf keinen
Fehlalarm auslösen. Eine Prüfung, die nichts beanstandet, ist von einer
funktionierenden nicht zu unterscheiden.

Genau das war hier der Fall: Die Fahne stand im Modulkopf, `main()` hat
`sys.argv` aber nie gelesen. Der Aufruf mit `--selbsttest` fuhr stumm dieselbe
Bestandsprüfung und meldete Rückgabewert 0 – wer ihn in eine Prüfkette hängte,
führte dieselbe Prüfung zweimal aus und glaubte, die Prüfung sei geprüft.

Die geerbte Fundstellenprüfung deckt `pruefe_erklaerungen.py --selbsttest`
ab; hier stehen dafür nur zwei Fälle, die belegen, dass sie von einem
Glossareintrag aus überhaupt erreicht wird.

Aufruf
------
    python data-pipeline/pruefe_glossar.py                # Bestand prüfen
    python data-pipeline/pruefe_glossar.py --selbsttest   # Prüfung prüfen
"""

from __future__ import annotations

import io
import json
import re
import sys
from pathlib import Path

from pruefe_erklaerungen import GESETZE, KATALOG, Pruefung

WURZEL = Path(__file__).resolve().parent.parent
GLOSSAR = WURZEL / 'content' / 'glossar.json'
ERKLAERUNGEN_DATEI = WURZEL / 'content' / 'erklaerungen.json'

MIN_KURZ, MAX_KURZ = 15, 400
MIN_TEXT, MAX_TEXT = 80, 2000
ARTEN = ('begriff', 'abkuerzung')

#: Zeichen, die in einem deutschen Wort vorkommen – wie in shared/glossar.ts.
WORTZEICHEN = 'A-Za-zÄÖÜäöüßẞ0-9'


def wortform_kommt_vor(form: str, korpus: str) -> bool:
    """Kommt die Wortform als eigenes Wort im Katalog vor?

    Dieselbe Wortgrenze wie in der Anwendung: `\\b` taugt nicht, weil es nur
    ASCII-Wortzeichen kennt und in „Schießstätte" an jedem ß eine Grenze
    sähe.
    """
    muster = rf'(?<![{WORTZEICHEN}]){re.escape(form)}(?![{WORTZEICHEN}])'
    return re.search(muster, korpus, re.IGNORECASE) is not None


def korpus_bilden() -> str:
    """Alles, was der Nutzer liest: Katalog und Erklärungen.

    Abkürzungen wie WBK, NWR oder VDMA kommen im amtlichen Katalog nicht vor,
    sehr wohl aber in den Erklärungen. Für WCAG 3.1.4 zählt, was angezeigt
    wird, nicht nur der Fragenwortlaut.
    """
    katalog = json.load(io.open(KATALOG, encoding='utf-8'))
    teile: list[str] = []
    for frage in katalog['fragen']:
        teile.append(frage['frage']['text'])
        muster = frage.get('musterantwort')
        if muster:
            teile.append(muster['text'])
        for option in frage.get('optionen') or []:
            teile.append(option['inhalt']['text'])

    if ERKLAERUNGEN_DATEI.exists():
        erklaerungen = json.load(io.open(ERKLAERUNGEN_DATEI, encoding='utf-8'))
        for eintrag in (erklaerungen.get('zuFrage') or {}).values():
            teile.append(eintrag.get('kurz', ''))
            teile.append(eintrag.get('text', ''))
            teile.append(eintrag.get('merksatz', '') or '')

    return ' '.join(teile)


class Glossarpruefung(Pruefung):
    """Erbt die Fundstellenprüfung und ergänzt, was nur das Glossar betrifft."""

    def __init__(self, katalog: dict, gesetze: dict) -> None:
        super().__init__(katalog, gesetze)
        #: Wortformen ohne Vorkommen. Kein Fehler, nur ein Hinweis.
        self.ohne_vorkommen: list[str] = []
        #: Eintraege ohne Wortformen – sie erscheinen nur in der Glossaransicht.
        self.nur_im_glossar: list[str] = []

    def text_feld(self, wo: str, feld: str, wert: object, unten: int, oben: int) -> None:
        if not isinstance(wert, str) or not wert.strip():
            self.melden(wo, feld, 'fehlt oder ist leer')
            return
        if len(wert) < unten:
            self.melden(wo, feld, f'zu kurz ({len(wert)} Zeichen, mindestens {unten})')
        if len(wert) > oben:
            self.melden(wo, feld, f'zu lang ({len(wert)} Zeichen, höchstens {oben})')
        if wert != wert.strip():
            self.melden(wo, feld, 'beginnt oder endet mit Leerraum')

    def eintrag_pruefen(self, eintrag: dict, bekannt: set[str], korpus: str) -> None:
        begriff = eintrag.get('begriff')
        wo = str(begriff) if isinstance(begriff, str) and begriff else '(ohne Begriff)'

        erlaubt = {
            'begriff', 'art', 'kurz', 'text', 'varianten',
            'fundstellen', 'ohneFundstelleGrund', 'siehe',
        }
        for feld in set(eintrag) - erlaubt:
            self.melden(wo, feld, 'Unbekanntes Feld')

        self.text_feld(wo, 'begriff', begriff, 2, 60)
        self.text_feld(wo, 'kurz', eintrag.get('kurz'), MIN_KURZ, MAX_KURZ)

        if 'text' in eintrag:
            self.text_feld(wo, 'text', eintrag['text'], MIN_TEXT, MAX_TEXT)

        art = eintrag.get('art')
        if art not in ARTEN:
            self.melden(wo, 'art', f'muss {" oder ".join(ARTEN)} sein, ist: {art!r}')

        varianten = eintrag.get('varianten')
        if not isinstance(varianten, list):
            self.melden(wo, 'varianten', 'fehlt oder ist keine Liste')
        elif not varianten:
            # Kein Fehler: Der Eintrag steht dann nur im Glossar und wird
            # an keiner Frage angezeigt. Das ist bei Abkuerzungen normal, die
            # nur in den Erklaerungen vorkommen.
            self.nur_im_glossar.append(wo)
        else:
            for form in varianten:
                if not isinstance(form, str) or not form.strip():
                    self.melden(wo, 'varianten', f'ungültige Wortform: {form!r}')
                elif not wortform_kommt_vor(form, korpus):
                    self.ohne_vorkommen.append(f'{wo}: „{form}"')

        for verweis in eintrag.get('siehe') or []:
            if verweis not in bekannt:
                self.melden(wo, 'siehe', f'verweist auf „{verweis}“ – diesen Eintrag gibt es nicht')

        fundstellen = eintrag.get('fundstellen')
        if not isinstance(fundstellen, list):
            self.melden(wo, 'fundstellen', 'fehlt oder ist keine Liste')
            return

        for index, fundstelle in enumerate(fundstellen):
            if not isinstance(fundstelle, dict):
                self.melden(wo, f'fundstelle[{index}]', 'ist kein Objekt')
                continue
            self.fundstelle_pruefen(wo, index, fundstelle)

        grund = eintrag.get('ohneFundstelleGrund')
        if not fundstellen:
            if grund is None:
                self.melden(
                    wo, 'fundstellen', 'Ohne Fundstelle muss „ohneFundstelleGrund" gesetzt sein'
                )
            else:
                self.text_feld(wo, 'ohneFundstelleGrund', grund, 25, 400)
        elif grund is not None:
            self.melden(wo, 'ohneFundstelleGrund', 'gesetzt, obwohl Fundstellen vorhanden sind')

    def alles_pruefen(self, daten: dict, korpus: str) -> None:  # type: ignore[override]
        meta = daten.get('meta')
        if not isinstance(meta, dict):
            self.melden('(meta)', 'meta', 'fehlt')
        else:
            for kuerzel, stand in (meta.get('gesetzesstand') or {}).items():
                if kuerzel not in self.gesetze:
                    self.melden('(meta)', 'gesetzesstand', f'Unbekanntes Gesetz: {kuerzel}')
                elif self.gesetze[kuerzel]['stand'] != stand:
                    self.melden(
                        '(meta)',
                        'gesetzesstand',
                        f'{kuerzel}: vermerkt „{stand}", Index sagt '
                        f'„{self.gesetze[kuerzel]["stand"]}"',
                    )

        eintraege = daten.get('eintraege')
        if not isinstance(eintraege, list):
            self.melden('(datei)', 'eintraege', 'fehlt oder ist keine Liste')
            return

        gesehen: set[str] = set()
        bekannt = {
            e['begriff'] for e in eintraege if isinstance(e, dict) and isinstance(e.get('begriff'), str)
        }

        for eintrag in eintraege:
            if not isinstance(eintrag, dict):
                self.melden('(datei)', 'eintraege', 'Eintrag ist kein Objekt')
                continue
            begriff = eintrag.get('begriff')
            if isinstance(begriff, str):
                if begriff in gesehen:
                    self.melden(begriff, 'begriff', 'kommt mehrfach vor')
                gesehen.add(begriff)
            self.eintrag_pruefen(eintrag, bekannt, korpus)


# ── Selbsttest ───────────────────────────────────────────────────────────────

#: Ein gültiger Eintrag als Ausgangspunkt; jeder Fall unten ändert genau eines.
#:
#: „Schießstätte" ist mit Bedacht gewählt: Das Wort steht im Katalog und trägt
#: ein ß. Liefe die Wortsuche wieder über `\\b`, sähe sie dort eine Wortgrenze
#: und fände die Form nicht mehr – die glückliche Bahn am Ende schlägt dann an.
GUELTIGER_EINTRAG: dict = {
    'begriff': 'Schießstätte',
    'art': 'begriff',
    'kurz': 'Anlage, in der geschossen wird und für deren Betrieb es einer Erlaubnis bedarf.',
    'text': (
        'Dieser Eintrag ist Prüfmaterial, kein Glossarinhalt: An ihm ist nichts zu '
        'beanstanden, damit jeder Befund unten von der absichtlich eingebauten '
        'Abweichung stammt und nicht vom Rest des Eintrags.'
    ),
    'varianten': ['Schießstätte'],
    'fundstellen': [{'gesetz': 'StGB', 'norm': '§ 32', 'absatz': '1'}],
}

#: Ein zweiter gültiger Eintrag. Er verweist auf den ersten und belegt damit
#: die erlaubte Seite zweier Regeln, die sonst nur von ihrer Fehlerseite her
#: geprüft wären: ein `siehe` auf einen vorhandenen Eintrag und eine Lücke bei
#: den Fundstellen, die begründet ist. Ohne `text` – der ist zulässigerweise
#: freiwillig.
ZWEITER_EINTRAG: dict = {
    'begriff': 'Schießstand',
    'art': 'begriff',
    'kurz': 'Einzelne Schießbahn innerhalb einer Schießstätte.',
    'varianten': ['Schießstand'],
    'siehe': ['Schießstätte'],
    'fundstellen': [],
    'ohneFundstelleGrund': 'Ein technischer Begriff ohne eigene gesetzliche Bestimmung.',
}

#: Eine gültige Glossardatei als Ausgangspunkt für die Fälle, die erst im
#: Zusammenhang der ganzen Datei entstehen können.
GUELTIGE_DATEI: dict = {
    'meta': {
        'version': 1,
        'stand': '2026-01-01',
        'hinweis': 'Prüfmaterial des Selbsttests, kein ausgelieferter Inhalt.',
        'gesetzesstand': {},
    },
    'eintraege': [GUELTIGER_EINTRAG, ZWEITER_EINTRAG],
}

#: Bewusst fehlerhafte Einträge: Name, Abweichung vom gültigen Eintrag, Feld
#: und erwartete Meldung. Jeder Fall muss beanstandet werden.
#:
#: Anders als in `pruefe_erklaerungen.py` gehört das Feld zur Erwartung. Hier
#: teilen sich mehrere Regeln denselben Wortlaut – „fehlt oder ist keine Liste"
#: gilt für `varianten` wie für `fundstellen`, „zu kurz" für vier Felder. Eine
#: Erwartung nur auf den Meldungstext wäre schon erfüllt, wenn eine ganz andere
#: Regel angeschlagen hätte.
SELBSTTEST_EINTRAG: list[tuple[str, dict, str, str]] = [
    # ── Form der Textfelder ──
    ('Begriff fehlt', {'begriff': None}, 'begriff', 'fehlt oder ist leer'),
    ('Begriff zu kurz', {'begriff': 'A'}, 'begriff', 'zu kurz'),
    ('Begriff zu lang', {'begriff': 'Schießstätte' * 6}, 'begriff', 'zu lang'),
    ('Kurztext besteht nur aus Leerraum', {'kurz': '   '}, 'kurz', 'fehlt oder ist leer'),
    ('Kurztext zu kurz', {'kurz': 'Zu knapp.'}, 'kurz', 'zu kurz'),
    ('Kurztext zu lang', {'kurz': 'x' * (MAX_KURZ + 1)}, 'kurz', 'zu lang'),
    (
        'Kurztext mit Leerraum am Rand',
        {'kurz': ' Anlage, in der geschossen wird und die eine Erlaubnis braucht.'},
        'kurz',
        'beginnt oder endet mit Leerraum',
    ),
    ('Volltext zu kurz', {'text': 'Zu wenig Text.'}, 'text', 'zu kurz'),
    ('Volltext zu lang', {'text': 'x' * (MAX_TEXT + 1)}, 'text', 'zu lang'),
    # ── Art ──
    ('unbekannte Art', {'art': 'fachwort'}, 'art', 'muss begriff oder abkuerzung sein'),
    ('Art fehlt', {'art': None}, 'art', 'muss begriff oder abkuerzung sein'),
    # ── Wortformen ──
    (
        'Wortformen sind keine Liste',
        {'varianten': 'Schießstätte'},
        'varianten',
        'fehlt oder ist keine Liste',
    ),
    ('leere Wortform', {'varianten': ['']}, 'varianten', 'ungültige Wortform'),
    (
        'Wortform ist keine Zeichenkette',
        {'varianten': [42]},
        'varianten',
        'ungültige Wortform',
    ),
    # ── Querverweise ──
    (
        'Verweis auf einen Eintrag, den es nicht gibt',
        {'siehe': ['Handfeuerlöscher']},
        'siehe',
        'diesen Eintrag gibt es nicht',
    ),
    # ── Fundstellen und redliche Lücken ──
    (
        'Fundstellen sind keine Liste',
        {'fundstellen': '§ 12 WaffG'},
        'fundstellen',
        'fehlt oder ist keine Liste',
    ),
    (
        'Fundstelle ist kein Objekt',
        {'fundstellen': ['§ 12 WaffG']},
        'fundstelle[0]',
        'ist kein Objekt',
    ),
    (
        'Fundstelle fehlt ohne Begründung',
        {'fundstellen': []},
        'fundstellen',
        'ohneFundstelleGrund',
    ),
    (
        'Begründung der Lücke zu kurz',
        {'fundstellen': [], 'ohneFundstelleGrund': 'Steht nirgends.'},
        'ohneFundstelleGrund',
        'zu kurz',
    ),
    (
        'Begründung trotz vorhandener Fundstelle',
        {'ohneFundstelleGrund': 'Zu diesem Begriff gibt es keine gesetzliche Bestimmung.'},
        'ohneFundstelleGrund',
        'obwohl Fundstellen vorhanden',
    ),
    # ── Unbekanntes Feld ──
    ('unbekanntes Feld', {'quelle': 'irgendwoher'}, 'quelle', 'Unbekanntes Feld'),
    # ── Die geerbte Fundstellenprüfung greift auch hier ──
    #
    # Nicht deren Regeln werden hier geprüft – das tut
    # `pruefe_erklaerungen.py --selbsttest`, der seine Fallzahl selbst
    # ausgibt –, sondern nur, dass ein Glossareintrag sie überhaupt erreicht.
    # (Hier stand bis Fassung 0.24.1 „mit 22 Fällen“. Die Zahl war überholt,
    # und sie stand ausgerechnet an einer Stelle, an der niemand nachrechnet;
    # eine Zahl in Prosa läuft ohne Wache immer wieder auseinander.) Wäre der
    # Aufruf in
    # `eintrag_pruefen` verlorengegangen, bliebe jede erfundene Fundstelle im
    # Glossar unbemerkt, während der Selbsttest der Erklärungen weiter grün
    # meldete.
    (
        'erfundener Paragraf in einer Fundstelle',
        {'fundstellen': [{'gesetz': 'WaffG', 'norm': '§ 999'}]},
        'fundstelle[0]',
        'gibt es im WaffG nicht',
    ),
    (
        'Absatz als Zahl statt als Zeichenkette',
        {'fundstellen': [{'gesetz': 'StGB', 'norm': '§ 32', 'absatz': 1}]},
        'fundstelle[0]',
        'muss eine Zeichenkette sein',
    ),
]

#: Fälle, die keinen Fehler darstellen, aber sichtbar werden müssen: Name,
#: Abweichung, Name der Hinweisliste, erwarteter Eintrag darin. Geprüft wird
#: beides – dass der Hinweis erscheint und dass er *keine* Beanstandung
#: auslöst. Ein Hinweis, der als Fehler zählt, hielte die Auslieferung an;
#: einer, der nirgends auftaucht, ist keiner.
SELBSTTEST_HINWEIS: list[tuple[str, dict, str, str]] = [
    (
        'Wortform, die nur als Wortanfang vorkommt',
        {'varianten': ['Schießstätt']},
        'ohne_vorkommen',
        'Schießstätt',
    ),
    (
        'Wortform, die im ganzen Text nicht vorkommt',
        {'varianten': ['Handfeuerlöscher']},
        'ohne_vorkommen',
        'Handfeuerlöscher',
    ),
    (
        'Eintrag ohne jede Wortform',
        {'varianten': []},
        'nur_im_glossar',
        'Schießstätte',
    ),
]

#: Fehler, die erst im Zusammenhang der ganzen Datei entstehen.
SELBSTTEST_DATEI: list[tuple[str, dict, str, str]] = [
    ('Kopf fehlt', {'meta': None}, 'meta', 'fehlt'),
    (
        'Gesetzesstand nennt ein unbekanntes Gesetz',
        {'meta': {'gesetzesstand': {'BGB': 'Neugefasst durch Bek. v. 2.1.2002 I 42'}}},
        'gesetzesstand',
        'Unbekanntes Gesetz',
    ),
    (
        'vermerkter Gesetzesstand weicht vom Index ab',
        {'meta': {'gesetzesstand': {'WaffG': 'Stand von vorgestern'}}},
        'gesetzesstand',
        'Index sagt',
    ),
    ('Einträge fehlen', {'eintraege': None}, 'eintraege', 'fehlt oder ist keine Liste'),
    (
        'Eintrag ist kein Objekt',
        {'eintraege': ['Schießstätte']},
        'eintraege',
        'Eintrag ist kein Objekt',
    ),
    (
        'Begriff kommt zweimal vor',
        {'eintraege': [GUELTIGER_EINTRAG, GUELTIGER_EINTRAG]},
        'begriff',
        'kommt mehrfach vor',
    ),
]


def melde_ergebnis(name: str, getroffen: bool, erwartet: str, gemeldet: str) -> int:
    """Gibt einen Fall aus und antwortet mit 1, wenn er durchgelassen wurde."""
    if getroffen:
        print(f'  erkannt:      {name}')
        return 0
    print(f'  DURCHGELASSEN: {name}')
    print(f'                 erwartet: {erwartet}')
    print(f'                 gemeldet: {gemeldet}')
    return 1


def befunde_lesbar(pruefung: Glossarpruefung) -> str:
    if not pruefung.befunde:
        return '(nichts beanstandet)'
    return '; '.join(f'{b.feld}: {b.meldung}' for b in pruefung.befunde)


def selbsttest(katalog: dict, gesetze: dict, korpus: str) -> int:
    """Prüft die Prüfung.

    Jede Regel wird einmal absichtlich verletzt – an echten Daten: Korpus,
    Katalog und Gesetzesindex sind dieselben wie im Bestandslauf. Am Ende
    steht die Gegenprobe, denn eine Prüfung, die alles beanstandet, wäre
    genauso wertlos wie eine, die nichts beanstandet.
    """
    fehlgeschlagen = 0
    gelaufen = 0
    bekannt = {GUELTIGER_EINTRAG['begriff'], ZWEITER_EINTRAG['begriff']}

    print('Einzelne Einträge:')
    for name, aenderung, feld, erwartet in SELBSTTEST_EINTRAG:
        pruefung = Glossarpruefung(katalog, gesetze)
        pruefung.eintrag_pruefen({**GUELTIGER_EINTRAG, **aenderung}, bekannt, korpus)
        getroffen = any(b.feld == feld and erwartet in b.meldung for b in pruefung.befunde)
        fehlgeschlagen += melde_ergebnis(
            name, getroffen, f'{feld}: {erwartet}', befunde_lesbar(pruefung)
        )
        gelaufen += 1

    print('\nHinweise – kein Fehler, aber sie müssen sichtbar werden:')
    for name, aenderung, liste, erwartet in SELBSTTEST_HINWEIS:
        pruefung = Glossarpruefung(katalog, gesetze)
        pruefung.eintrag_pruefen({**GUELTIGER_EINTRAG, **aenderung}, bekannt, korpus)
        gesammelt: list[str] = getattr(pruefung, liste)
        getroffen = any(erwartet in eintrag for eintrag in gesammelt) and not pruefung.befunde
        gemeldet = ', '.join(gesammelt) or '(kein Hinweis)'
        if pruefung.befunde:
            gemeldet += f' – dazu beanstandet: {befunde_lesbar(pruefung)}'
        fehlgeschlagen += melde_ergebnis(
            name, getroffen, f'{liste}: {erwartet}, ohne Beanstandung', gemeldet
        )
        gelaufen += 1

    print('\nDie Datei als Ganzes:')
    for name, aenderung, feld, erwartet in SELBSTTEST_DATEI:
        pruefung = Glossarpruefung(katalog, gesetze)
        pruefung.alles_pruefen({**GUELTIGE_DATEI, **aenderung}, korpus)
        getroffen = any(b.feld == feld and erwartet in b.meldung for b in pruefung.befunde)
        fehlgeschlagen += melde_ergebnis(
            name, getroffen, f'{feld}: {erwartet}', befunde_lesbar(pruefung)
        )
        gelaufen += 1

    print('\nDie Gegenprobe:')

    # Auch die Hinweislisten müssen leer bleiben. Wäre die Wortsuche kaputt,
    # fände sie „Schießstätte" nicht mehr – und kein einziger Fall oben würde
    # das bemerken, weil ein fehlender Treffer dort ja gerade erwartet wird.
    pruefung = Glossarpruefung(katalog, gesetze)
    pruefung.alles_pruefen(GUELTIGE_DATEI, korpus)
    sauber = not pruefung.befunde and not pruefung.ohne_vorkommen and not pruefung.nur_im_glossar
    gemeldet = befunde_lesbar(pruefung)
    if pruefung.ohne_vorkommen:
        gemeldet += f' – ohne Vorkommen: {", ".join(pruefung.ohne_vorkommen)}'
    if pruefung.nur_im_glossar:
        gemeldet += f' – ohne Wortform: {", ".join(pruefung.nur_im_glossar)}'
    fehlgeschlagen += melde_ergebnis(
        'gültige Einträge bleiben unbeanstandet und ohne Hinweis',
        sauber,
        'kein Befund, kein Hinweis',
        gemeldet,
    )
    gelaufen += 1

    # Und der ausgelieferte Bestand selbst: Regeln, die nur an erfundenen
    # Beispielen greifen, aber die echte Datei anhalten würden, sind keine.
    if GLOSSAR.exists():
        daten = json.load(io.open(GLOSSAR, encoding='utf-8'))
        pruefung = Glossarpruefung(katalog, gesetze)
        pruefung.alles_pruefen(daten, korpus)
        fehlgeschlagen += melde_ergebnis(
            f'der ausgelieferte Bestand löst keinen Fehlalarm aus '
            f'({len(daten.get("eintraege") or [])} Einträge)',
            not pruefung.befunde,
            'keine Beanstandung',
            befunde_lesbar(pruefung),
        )
        gelaufen += 1
    else:
        # Kein Fehlschlag – ohne Glossar läuft auch die Anwendung. Aber es
        # muss dastehen, sonst zählte der Lauf einen Fall mit, den er nicht
        # gefahren hat.
        print(f'  ENTFÄLLT:     kein Glossar unter {GLOSSAR.relative_to(WURZEL)}')

    if fehlgeschlagen:
        print(f'\nSelbsttest fehlgeschlagen: {fehlgeschlagen} von {gelaufen} Fällen.')
        return 1

    print(f'\nSelbsttest bestanden: {gelaufen} Fälle.')
    return 0


def main(argv: list[str] | None = None) -> int:
    argumente = list(sys.argv[1:] if argv is None else argv)
    nur_selbsttest = '--selbsttest' in argumente
    if nur_selbsttest:
        argumente.remove('--selbsttest')

    # Ein vertippter Schalter darf nicht stillschweigend die Bestandsprüfung
    # fahren. Genau so blieb „--selbsttest" hier wirkungslos: Die Fahne stand
    # im Modulkopf, main() las sys.argv nie, und der Aufruf meldete
    # Rückgabewert 0 für eine Prüfung, die niemand angefordert hatte.
    if argumente:
        print(f'Unbekanntes Argument: {" ".join(argumente)}')
        print('Aufruf: python data-pipeline/pruefe_glossar.py [--selbsttest]')
        return 2

    if nur_selbsttest:
        katalog = json.load(io.open(KATALOG, encoding='utf-8'))
        gesetze = json.load(io.open(GESETZE, encoding='utf-8'))
        return selbsttest(katalog, gesetze, korpus_bilden())

    if not GLOSSAR.exists():
        print(f'Kein Glossar gefunden: {GLOSSAR.relative_to(WURZEL)}')
        print('Das ist zulässig – die Anwendung läuft auch ohne.')
        return 0

    katalog = json.load(io.open(KATALOG, encoding='utf-8'))
    gesetze = json.load(io.open(GESETZE, encoding='utf-8'))
    daten = json.load(io.open(GLOSSAR, encoding='utf-8'))

    pruefung = Glossarpruefung(katalog, gesetze)
    pruefung.alles_pruefen(daten, korpus_bilden())

    eintraege = daten.get('eintraege') or []
    begriffe = [e for e in eintraege if isinstance(e, dict) and e.get('art') == 'begriff']
    kuerzel = [e for e in eintraege if isinstance(e, dict) and e.get('art') == 'abkuerzung']

    print(f'Einträge:               {len(eintraege)}')
    print(f'davon Begriffe:         {len(begriffe)}')
    print(f'davon Abkürzungen:      {len(kuerzel)}')
    print(f'Wortformen:             {sum(len(e.get("varianten") or []) for e in eintraege)}')
    # Dieselbe ehrliche Zählung wie in pruefe_erklaerungen.py: Der Sammelzähler
    # dort wies auch Fundstellen als „geprüft" aus, von denen nur die Existenz
    # der Norm nachgewiesen war. Das Glossar erbt die Prüfung – und die Zählung.
    print(f'geprüfte Fundstellen:   {pruefung.voll_geprueft + pruefung.nur_existenz}')
    print(f'  vollständig geprüft:  {pruefung.voll_geprueft}  (jede Angabe nachgewiesen)')
    print(
        f'  nur Existenz geprüft: {pruefung.nur_existenz}'
        '  (Feinangabe maschinell nicht prüfbar)'
    )

    if pruefung.nur_im_glossar:
        print(f'\nNur in der Glossaransicht, an keiner Frage ({len(pruefung.nur_im_glossar)}):')
        print(f'  {", ".join(pruefung.nur_im_glossar)}')
        print('  (Kein Fehler – diese Einträge nennen keine Wortform.)')

    if pruefung.ohne_vorkommen:
        print(f'\nWortformen ohne Vorkommen ({len(pruefung.ohne_vorkommen)}):')
        for eintrag in pruefung.ohne_vorkommen[:25]:
            print(f'  {eintrag}')
        if len(pruefung.ohne_vorkommen) > 25:
            print(f'  … und {len(pruefung.ohne_vorkommen) - 25} weitere')
        print('  (Kein Fehler – aber solche Formen finden nie einen Treffer.)')

    if pruefung.befunde:
        print(f'\n{len(pruefung.befunde)} Beanstandungen:\n')
        for befund in pruefung.befunde[:100]:
            print(f'  {befund}')
        if len(pruefung.befunde) > 100:
            print(f'  … und {len(pruefung.befunde) - 100} weitere')
        return 1

    print('\nAlle Fundstellen im amtlichen Gesetzestext nachgewiesen.')
    return 0


if __name__ == '__main__':
    sys.stdout.reconfigure(encoding='utf-8', errors='replace')  # type: ignore[union-attr]
    raise SystemExit(main())
