"""Collecteurs et rapprochement.

Les parseurs sont testés sur des fixtures HTML figées, capturées le 25 juillet
2026 sur les sites officiels : aucun test ne dépend du réseau. Une fixture qui
cesse de passer signale un changement de structure du site — c'est le but.
"""

from __future__ import annotations

from datetime import date
from pathlib import Path

import pytest
from selectolax.parser import HTMLParser

from pipeline.collecteurs.conseil_constitutionnel import (
    _index_des_colonnes,
    _lire_decision_rendue,
    _lire_ligne_instance,
)
from pipeline.collecteurs.http import ClientHttp, _cle_de_cache
from pipeline.collecteurs.legifrance import CollecteurLegifrance, _lire_titre
from pipeline.collecteurs.senat import CollecteurSenat
from pipeline.modeles import ResultatCC, Statut, TypeTexte
from pipeline.rapprochement import Issue, normaliser, rapprocher, similarite

FIXTURES = Path(__file__).parent / "fixtures"


# ─────────────────────────────────────────────────────────────────────────────
# Conseil constitutionnel
# ─────────────────────────────────────────────────────────────────────────────
@pytest.fixture(scope="module")
def affaires_dc() -> list:
    tableau = HTMLParser(
        (FIXTURES / "cc_affaires_dc.html").read_text(encoding="utf-8")
    ).css_first("table")
    colonnes = _index_des_colonnes(tableau)
    return [
        affaire
        for ligne in tableau.css("tbody tr")
        if (affaire := _lire_ligne_instance(ligne, colonnes))
    ]


def test_les_sept_affaires_dc_en_instance(affaires_dc) -> None:
    """Le corpus en annonçait sept ; le registre officiel les confirme."""
    numeros = {a.numero_affaire for a in affaires_dc}
    assert numeros == {
        "2026-907 DC",
        "2026-910 DC",
        "2026-911 DC",
        "2026-912 DC",
        "2026-913 DC",
        "2026-914 DC",
        "2026-915 DC",
    }


def test_saisine_de_la_loi_riposte(affaires_dc) -> None:
    riposte = next(a for a in affaires_dc if a.numero_affaire == "2026-915 DC")
    assert riposte.date_saisine == date(2026, 7, 24)
    assert riposte.resultat is ResultatCC.EN_INSTANCE
    assert "soixante députés" in (riposte.saisissants or "")


def test_la_programmation_militaire_recoit_son_numero(affaires_dc) -> None:
    """Le rapport la citait sans numéro d'affaire ; la collecte le fournit."""
    lpm = next(a for a in affaires_dc if a.numero_affaire == "2026-907 DC")
    assert lpm.date_saisine == date(2026, 7, 6)
    assert "programmation militaire" in (lpm.resume or "").lower()


def test_colonnes_lues_par_entete_et_non_par_position() -> None:
    """Les tableaux DC et QPC n'ont pas les mêmes colonnes."""
    tableau = HTMLParser(
        (FIXTURES / "cc_affaires_dc.html").read_text(encoding="utf-8")
    ).css_first("table")
    colonnes = _index_des_colonnes(tableau)
    assert colonnes["saisissants"] != colonnes["date_saisine"]
    assert set(colonnes) >= {"affaire", "disposition", "saisissants", "date_saisine"}


@pytest.mark.parametrize(
    ("intitule", "attendu"),
    [
        ("2026-908 DC du 23 juillet 2026 Loi organique … Conformité", ResultatCC.CONFORME),
        (
            "2026-906 DC du 23 juillet 2026 Loi … Conformité - réserve",
            ResultatCC.CONFORME_AVEC_RESERVES,
        ),
        (
            "2026-909 DC du 23 juillet 2026 Loi … Non conformité partielle",
            ResultatCC.NON_CONFORMITE_PARTIELLE,
        ),
    ],
)
def test_lecture_du_resultat_d_une_decision(intitule: str, attendu: ResultatCC) -> None:
    """« Conformité » ne contient pas « conforme » : l'ordre des motifs compte."""
    decision = _lire_decision_rendue(f"Décision n° {intitule}", None)
    assert decision is not None
    assert decision.resultat is attendu
    assert decision.date_decision == date(2026, 7, 23)


def test_decisions_rendues_de_la_fixture() -> None:
    document = HTMLParser((FIXTURES / "cc_decisions.html").read_text(encoding="utf-8"))
    decisions = [
        d
        for article in document.css("article")
        if (d := _lire_decision_rendue(" ".join(article.text(separator=" ").split()), None))
    ]
    numeros = {d.numero_affaire for d in decisions}
    assert {"2026-906 DC", "2026-908 DC", "2026-909 DC"} <= numeros


# ─────────────────────────────────────────────────────────────────────────────
# Sénat
# ─────────────────────────────────────────────────────────────────────────────
@pytest.fixture(scope="module")
def lois_senat() -> list:
    document = HTMLParser(
        (FIXTURES / "senat_lois_promulguees.html").read_text(encoding="utf-8")
    )
    collecteur = CollecteurSenat(ClientHttp(hors_ligne=True))
    textes = []
    for ancre in document.css("a"):
        libelle = " ".join(ancre.text(separator=" ", strip=True).split())
        if texte := collecteur._lire_intitule(libelle, ancre.attributes.get("href")):
            textes.append(texte)
    return textes


def test_senat_releve_les_lois_de_la_fenetre(lois_senat) -> None:
    numeros = {t.numero_officiel for t in lois_senat}
    # Quatre lois de la fenêtre de veille, aux dates connues du corpus.
    assert {"2026-651", "2026-650", "2026-630", "2026-602"} <= numeros


def test_senat_lit_dates_et_nature(lois_senat) -> None:
    par_numero = {t.numero_officiel: t for t in lois_senat}

    justice = par_numero["2026-651"]
    assert justice.date_promulgation == date(2026, 7, 23)
    assert justice.statut is Statut.PROMULGUEE
    assert justice.type is TypeTexte.LOI


def test_les_deux_sources_ne_disent_pas_la_meme_chose_du_caractere_organique(
    lois_senat,
) -> None:
    """Asymétrie réelle entre les sources, à ne pas corriger en silence.

    Légifrance intitule la loi n° 2026-650 « LOI organique n° 2026-650 … » ;
    la liste du Sénat écrit « Loi n° 2026-650 … » sans la qualifier. Le
    collecteur rapporte ce que sa source dit — c'est le rôle du rapprochement
    de ne pas laisser la source la plus pauvre écraser la plus riche. Le run de
    mise à jour ne compare d'ailleurs pas le champ `type`.
    """
    organique = par_numero_senat(lois_senat)["2026-650"]
    assert organique.type is TypeTexte.LOI, (
        "le Sénat ne qualifie pas cette loi d'organique : le collecteur ne doit "
        "pas le deviner à sa place"
    )


def par_numero_senat(lois: list) -> dict:
    return {t.numero_officiel: t for t in lois}


def test_senat_ne_confond_pas_l_objet_avec_l_etat(lois_senat) -> None:
    """Le libellé se termine par « parue » : ce n'est pas l'objet de la loi."""
    for texte in lois_senat:
        assert not texte.titre.rstrip().endswith("parue")
        assert "en cours" not in texte.titre


def test_senat_produit_une_source_primaire(lois_senat) -> None:
    for texte in lois_senat:
        assert texte.sources
        assert texte.sources[0].tier == "T1"
        assert texte.sources[0].url.startswith("https://www.senat.fr")


# ─────────────────────────────────────────────────────────────────────────────
# Légifrance
# ─────────────────────────────────────────────────────────────────────────────
def test_legifrance_indisponible_sans_identifiants(monkeypatch) -> None:
    monkeypatch.delenv("LEGIFRANCE_CLIENT_ID", raising=False)
    collecteur = CollecteurLegifrance(ClientHttp(hors_ligne=True))
    disponible, motif = collecteur.est_disponible()
    assert disponible is False
    assert "repli" in (motif or "")


def test_legifrance_lit_un_resultat_de_recherche() -> None:
    titre = {
        "id": "LEGITEXT000054249605_14-06-2026",
        "cid": "JORFTEXT000054245243",
        "title": "LOI n° 2026-491 du 12 juin 2026 visant à reconnaître la responsabilité de l'Etat",
    }
    texte = _lire_titre(titre, "2026-491")
    assert texte is not None
    assert texte.date_promulgation == date(2026, 6, 12)
    assert texte.source_url.endswith("JORFTEXT000054245243")
    assert texte.sources[0].tier == "T1"


def test_legifrance_ecarte_un_resultat_hors_sujet() -> None:
    titre = {"cid": "X", "title": "LOI n° 2026-999 du 1er janvier 2026 sans rapport"}
    assert _lire_titre(titre, "2026-491") is None


def test_legifrance_deduit_le_caractere_organique() -> None:
    titre = {
        "cid": "JORF1",
        "title": "LOI organique n° 2026-650 du 23 juillet 2026 relative aux juridictions",
    }
    texte = _lire_titre(titre, "2026-650")
    assert texte.type is TypeTexte.LOI_ORGANIQUE


# ─────────────────────────────────────────────────────────────────────────────
# Client HTTP
# ─────────────────────────────────────────────────────────────────────────────
def test_le_cache_distingue_les_corps_de_requete() -> None:
    a = _cle_de_cache("POST", "https://x.fr/search", {"q": "2026-491"}, None)
    b = _cle_de_cache("POST", "https://x.fr/search", {"q": "2026-403"}, None)
    assert a != b


def test_mode_hors_ligne_ne_touche_pas_au_reseau(tmp_path) -> None:
    client = ClientHttp(cache=tmp_path, hors_ligne=True)
    reponse = client.get("https://exemple.invalide/rien")
    assert reponse.code == 0
    assert not reponse.a_reussi


def test_le_cache_est_relu(tmp_path) -> None:
    client = ClientHttp(cache=tmp_path, hors_ligne=True)
    cle = _cle_de_cache("GET", "https://x.fr/a", None, None)
    client._ecrire_cache(cle, "contenu")
    reponse = client.get("https://x.fr/a")
    assert reponse.texte == "contenu"
    assert reponse.depuis_le_cache


# ─────────────────────────────────────────────────────────────────────────────
# Rapprochement
# ─────────────────────────────────────────────────────────────────────────────
def test_normalisation_retire_le_vocabulaire_passe_partout() -> None:
    assert "loi" not in normaliser("LOI n° 2026-491 relative au chlordécone")
    assert "chlordecone" in normaliser("LOI n° 2026-491 relative au chlordécone")


def test_similarite_reconnait_un_titre_court_dans_un_titre_long() -> None:
    court = "Loi Riposte ordre public sécurité tranquillité"
    long = (
        "Loi visant à offrir des réponses immédiates aux phénomènes troublant "
        "l'ordre public, la sécurité et la tranquillité de nos concitoyens"
    )
    assert similarite(court, long) >= 0.72


def test_le_numero_officiel_prime_sur_le_titre() -> None:
    correspondance = rapprocher(
        titre="Un intitulé totalement différent",
        numero_officiel="2026-491",
        candidats=[("loi-2026-491", "Chlordécone : responsabilité de l'État", "2026-491")],
    )
    assert correspondance.issue is Issue.RAPPROCHE
    assert correspondance.texte_id == "loi-2026-491"
    assert correspondance.score == 1.0


def test_un_texte_inconnu_est_declare_nouveau() -> None:
    correspondance = rapprocher(
        titre="Loi sur les moniteurs de ski stagiaires en zone de montagne",
        numero_officiel=None,
        candidats=[("loi-2026-491", "Chlordécone : responsabilité de l'État", "2026-491")],
    )
    assert correspondance.issue is Issue.NOUVEAU


def test_une_correspondance_douteuse_est_signalee_pas_fusionnee() -> None:
    """Entre les deux seuils, le pipeline s'abstient et demande un humain."""
    correspondance = rapprocher(
        titre="Loi relative à la protection des enfants et des mineurs",
        numero_officiel=None,
        candidats=[("pjl-protection-enfants", "PJL relatif à la protection des enfants", None)],
    )
    assert correspondance.issue in (Issue.RAPPROCHE, Issue.A_SIGNALER)
    if correspondance.issue is Issue.A_SIGNALER:
        assert "vérification humaine" in correspondance.motif


def test_aucun_candidat() -> None:
    correspondance = rapprocher(titre="X", numero_officiel="2026-1", candidats=[])
    assert correspondance.issue is Issue.NOUVEAU
