"""Tests für app.parteien — den zentralen Parteinamen-Mapper.

Issue #55 + Roadmap #59 Phase B. Drei Schichten:

1. ``normalize_partei`` — single-string lookup (für Wahlprogramm-/
   Embedding-Lookups, wo wir nur einen Namen haben)
2. ``extract_fraktionen`` — komma-separierte Listen, MdL-mit-Klammerpartei,
   HTML-Reste; ersetzt die vier ``_normalize_fraktion()``-Methoden
3. ``display_name`` — Anzeigeform (kurz/lang)

Plus Roundtrip-Tests gegen die historisch in den Adaptern erkannten
Patterns (F.D.P., LL/PDS, BÜNDNIS 90, MINISTERIUM, etc.) damit der
Refactor garantiert nichts kaputt macht.
"""
from __future__ import annotations

import pytest

from app.parteien import (
    GOVERNMENT_KEY,
    PARTEIEN,
    all_canonical_keys,
    display_name,
    extract_fraktionen,
    normalize_partei,
)


# ─────────────────────────────────────────────────────────────────────────────
# normalize_partei — single-string lookup
# ─────────────────────────────────────────────────────────────────────────────


class TestNormalizePartei:
    @pytest.mark.parametrize(
        ("raw", "expected"),
        [
            ("CDU", "CDU"),
            ("SPD", "SPD"),
            ("FDP", "FDP"),
            ("F.D.P.", "FDP"),
            ("F. D. P.", "FDP"),
            ("AfD", "AfD"),
            ("AFD", "AfD"),
            ("Alternative für Deutschland", "AfD"),
            ("GRÜNE", "GRÜNE"),
            ("Grüne", "GRÜNE"),
            ("GRUENE", "GRÜNE"),
            ("Bündnis 90/Die Grünen", "GRÜNE"),
            ("LINKE", "LINKE"),
            ("Die Linke", "LINKE"),
            ("DIE LINKE", "LINKE"),
            ("LL/PDS", "LINKE"),
            ("BSW", "BSW"),
        ],
    )
    def test_canonical_keys_for_known_aliases(self, raw, expected):
        assert normalize_partei(raw) == expected

    def test_unknown_returns_none(self):
        assert normalize_partei("Pirateneinhornpartei XYZ") is None

    def test_empty_returns_none(self):
        assert normalize_partei("") is None
        assert normalize_partei(None) is None  # type: ignore[arg-type]

    def test_government_marker_takes_precedence(self):
        # "Antrag der Landesregierung" → Government, nicht eine zufällig
        # erkannte Partei aus dem Rest des Strings.
        assert normalize_partei("Antrag der Landesregierung") == GOVERNMENT_KEY
        assert normalize_partei("Ministerium der Finanzen") == GOVERNMENT_KEY
        assert normalize_partei("Senat von Berlin") == GOVERNMENT_KEY

    def test_ssw_only_in_sh(self):
        # SSW ist BL-spezifisch — bei explizit SH klappt es, ohne BL
        # akzeptieren wir es auch (tolerant) — die strenge BL-Constraint
        # gilt nur bei extract_fraktionen.
        assert normalize_partei("SSW", bundesland="SH") == "SSW"

    def test_csu_only_in_by(self):
        assert normalize_partei("CSU", bundesland="BY") == "CSU"


# ─────────────────────────────────────────────────────────────────────────────
# Freie-Wähler-Disambiguierung — der eigentliche Mehrwert des Mappers
# ─────────────────────────────────────────────────────────────────────────────


class TestFreieWaehlerDisambiguation:
    def test_freie_waehler_in_brandenburg_resolves_to_bvb(self):
        assert normalize_partei("FREIE WÄHLER", bundesland="BB") == "BVB-FW"

    def test_freie_waehler_in_bayern_resolves_to_fw_bayern(self):
        assert normalize_partei("FREIE WÄHLER", bundesland="BY") == "FW-BAYERN"

    def test_freie_waehler_in_saarland_resolves_to_fw_sl(self):
        assert normalize_partei("FREIE WÄHLER", bundesland="SL") == "FW-SL"

    def test_freie_waehler_in_rlp_resolves_to_generic(self):
        # In RP gehört der Landesverband zur Bundesvereinigung — der ist
        # der generische "FREIE WÄHLER"-Eintrag (kein Scope).
        assert normalize_partei("FREIE WÄHLER", bundesland="RP") == "FREIE WÄHLER"

    def test_fw_kuerzel_disambiguates(self):
        assert normalize_partei("FW", bundesland="BY") == "FW-BAYERN"
        assert normalize_partei("FW", bundesland="BB") == "BVB-FW"

    def test_freie_waehler_without_bundesland_falls_back_to_generic(self):
        # Kein BL → wir können nicht disambiguieren, der generische
        # Eintrag bleibt der Default
        assert normalize_partei("FREIE WÄHLER") == "FREIE WÄHLER"


# ─────────────────────────────────────────────────────────────────────────────
# extract_fraktionen — der Adapter-Funnel
# ─────────────────────────────────────────────────────────────────────────────


class TestExtractFraktionen:
    def test_single_simple(self):
        assert extract_fraktionen("CDU") == ["CDU"]

    def test_comma_separated(self):
        assert set(extract_fraktionen("CDU, SPD, FDP")) == {"CDU", "SPD", "FDP"}

    def test_dedupe(self):
        # CDU zweimal im Text → nur einmal im Output
        out = extract_fraktionen("CDU und CDU")
        assert out == ["CDU"]

    def test_empty(self):
        assert extract_fraktionen("") == []
        assert extract_fraktionen(None) == []  # type: ignore[arg-type]

    def test_mdl_with_paren_party(self):
        # ParlDok-Stil: "Felix Herkens (GRÜNE), Saskia Frank (GRÜNE)"
        out = extract_fraktionen("Felix Herkens (GRÜNE), Saskia Frank (GRÜNE)")
        assert out == ["GRÜNE"]

    def test_mixed_mdl_and_government(self):
        out = extract_fraktionen("Antrag der Fraktion CDU und der Landesregierung")
        assert "CDU" in out
        assert GOVERNMENT_KEY in out

    def test_fdp_with_dots(self):
        assert extract_fraktionen("F.D.P.") == ["FDP"]
        assert extract_fraktionen("Antrag der F.D.P.-Fraktion") == ["FDP"]

    def test_bsw(self):
        assert extract_fraktionen("BSW") == ["BSW"]

    def test_linke_via_ll_pds_legacy(self):
        # Historische ParlDok-Schreibweise
        assert extract_fraktionen("LL/PDS") == ["LINKE"]

    def test_gruene_via_buendnis_90(self):
        assert extract_fraktionen("BÜNDNIS 90/DIE GRÜNEN") == ["GRÜNE"]

    def test_ministerium_marker(self):
        assert extract_fraktionen("Ministerium der Finanzen") == [GOVERNMENT_KEY]

    def test_real_th_urheber(self):
        # TH liefert oft "Antrag der Fraktion der CDU"
        assert extract_fraktionen("Antrag der Fraktion der CDU") == ["CDU"]

    def test_ssw_only_when_in_sh_context(self):
        # SH-spezifisch — in einem SH-Antrag taucht SSW auf
        assert extract_fraktionen("Christian Dirschauer (SSW)", bundesland="SH") == ["SSW"]

    def test_ssw_filtered_in_other_bl(self):
        # In einem MV-Antrag würde "SSW" nichts ergeben — BL-Filter greift
        assert extract_fraktionen("SSW Beispiel", bundesland="MV") == []

    def test_freie_waehler_bb_resolves_to_bvb_in_extract(self):
        # BB-Drucksache der "FREIE WÄHLER"-Fraktion → BVB-FW
        out = extract_fraktionen("Antrag der Fraktion FREIE WÄHLER", bundesland="BB")
        assert out == ["BVB-FW"]

    def test_freie_waehler_rp_resolves_to_generic(self):
        out = extract_fraktionen("Antrag der Fraktion FREIE WÄHLER", bundesland="RP")
        assert out == ["FREIE WÄHLER"]

    def test_freie_waehler_by(self):
        out = extract_fraktionen("Antrag der Fraktion FREIE WÄHLER", bundesland="BY")
        assert out == ["FW-BAYERN"]


# ─────────────────────────────────────────────────────────────────────────────
# display_name
# ─────────────────────────────────────────────────────────────────────────────


class TestDisplayName:
    def test_short_default(self):
        assert display_name("CDU") == "CDU"
        assert display_name("GRÜNE") == "GRÜNE"

    def test_long_form(self):
        assert display_name("GRÜNE", long=True) == "BÜNDNIS 90/DIE GRÜNEN"
        assert display_name("LINKE", long=True) == "DIE LINKE"
        assert display_name("BiW", long=True) == "BÜRGER IN WUT"

    def test_government(self):
        assert display_name(GOVERNMENT_KEY) == "Landesregierung"

    def test_unknown_passthrough(self):
        # Unbekannt — kein Fehler, einfach durchreichen
        assert display_name("UnknownPartyXYZ") == "UnknownPartyXYZ"


# ─────────────────────────────────────────────────────────────────────────────
# Tabellen-Konsistenz
# ─────────────────────────────────────────────────────────────────────────────


class TestTableConsistency:
    def test_no_duplicate_canonical_keys(self):
        keys = [p.canonical for p in PARTEIEN]
        assert len(keys) == len(set(keys)), f"Duplikate: {[k for k in keys if keys.count(k) > 1]}"

    def test_all_canonical_keys_resolvable_by_themselves(self):
        # Jeder kanonische Key muss als Roh-Eingabe wieder auf sich selbst
        # auflösen — das ist die Identitäts-Invariante des Mappers
        for p in PARTEIEN:
            if p.bundesland_scope:
                # Disambiguierungs-Einträge brauchen ihren Scope mitgegeben
                assert normalize_partei(p.canonical, bundesland=p.bundesland_scope) in {
                    p.canonical,
                    # FW-BAYERN/FW-SL/BVB-FW haben ihren canonical-Key nicht
                    # als alias drin — er ist nur ein interner Schlüssel.
                    # Sie matchen über die FW-Aliase.
                    "FREIE WÄHLER", "FW-BAYERN", "FW-SL", "BVB-FW",
                }
            else:
                assert normalize_partei(p.canonical) == p.canonical

    def test_all_canonical_keys_helper_includes_government(self):
        keys = all_canonical_keys()
        assert GOVERNMENT_KEY in keys
        assert "CDU" in keys
        assert "GRÜNE" in keys
