"""Tests unitaires pour pipeline.recueil (logique métier de la page recueil).

Les fonctions testées ici sont toutes pures (pas d'appel au VLM) :
- filter_cim10_codes
- classify_codes_dp_dr_das
- merge_codage_reco
- resolve_recueil_zones (juste lecture de config)
"""
from __future__ import annotations

from pipeline.recueil import (
    classify_codes_dp_dr_das,
    filter_cim10_codes,
    merge_codage_reco,
    resolve_recueil_zones,
)


class TestFilterCim10Codes:
    def test_codes_valides_conservés(self):
        codes = [
            {"code": "K650", "position": "1"},
            {"code": "T814", "position": "2"},
            {"code": "Z954 *", "position": "3"},
        ]
        out = filter_cim10_codes(codes)
        assert len(out) == 3
        assert out[0]["code"] == "K650"

    def test_ccam_rejeté(self):
        """Un code CCAM (4 lettres + 3 chiffres) ne doit pas passer le filtre CIM-10."""
        codes = [
            {"code": "K650", "position": ""},
            {"code": "EBFA012", "position": "1"},  # CCAM
        ]
        out = filter_cim10_codes(codes)
        assert len(out) == 1
        assert out[0]["code"] == "K650"

    def test_code_vide_rejeté(self):
        codes = [{"code": "", "position": ""}, {"code": "K650", "position": ""}]
        out = filter_cim10_codes(codes)
        assert len(out) == 1

    def test_non_dict_ignoré(self):
        codes = ["K650", None, {"code": "T814", "position": ""}]
        out = filter_cim10_codes(codes)
        assert len(out) == 1

    def test_liste_vide(self):
        assert filter_cim10_codes([]) == []
        assert filter_cim10_codes(None) == []


class TestClassifyCodesDpDrDas:
    def test_cas_nominal(self):
        """1er sans position = DP, 2e sans position = DR, puis DAS avec positions."""
        codes = [
            {"code": "K650", "position": ""},
            {"code": "T814", "position": ""},
            {"code": "Z954", "position": "2"},
            {"code": "R33",  "position": "3"},
        ]
        dp, dr, das = classify_codes_dp_dr_das(codes)
        assert dp == "K650"
        assert dr == "T814"
        assert [d["code"] for d in das] == ["Z954", "R33"]

    def test_dr_vide_non_duplique_dp(self):
        """Quand Qwen duplique le DP (parce que DR est visuellement vide),
        on doit considérer que DR est vide, pas DR = DP."""
        codes = [
            {"code": "K650", "position": ""},
            {"code": "K650", "position": ""},  # doublon
            {"code": "T814", "position": "2"},
        ]
        dp, dr, das = classify_codes_dp_dr_das(codes)
        assert dp == "K650"
        assert dr == ""  # dédupliqué
        assert len(das) == 1

    def test_seulement_dp(self):
        codes = [{"code": "K650", "position": ""}]
        dp, dr, das = classify_codes_dp_dr_das(codes)
        assert dp == "K650"
        assert dr == ""
        assert das == []

    def test_tous_avec_positions(self):
        """Si tous les codes ont une position, DP et DR sont vides, tout en DAS."""
        codes = [
            {"code": "K650", "position": "1"},
            {"code": "T814", "position": "2"},
        ]
        dp, dr, das = classify_codes_dp_dr_das(codes)
        assert dp == ""
        assert dr == ""
        assert len(das) == 2

    def test_vide(self):
        dp, dr, das = classify_codes_dp_dr_das([])
        assert (dp, dr, das) == ("", "", [])


class TestMergeCodageReco:
    def test_crop_prime_sur_passage_principal(self):
        parsed = {"codage_reco": {"dp": "", "dr": "", "das": []}}
        reco = {"dp": "K650", "dr": "T814",
                "das": [{"code": "Z954", "position": "2"}]}
        merge_codage_reco(parsed, reco)
        assert parsed["codage_reco"]["dp"] == "K650"
        assert parsed["codage_reco"]["dr"] == "T814"
        assert len(parsed["codage_reco"]["das"]) == 1

    def test_crop_vide_garde_passage_principal(self):
        """Si le crop a un champ vide mais le passage principal l'avait rempli,
        on ne dégrade pas : on garde le passage principal."""
        parsed = {"codage_reco": {"dp": "K650", "dr": "", "das": []}}
        reco = {"dp": "", "dr": "", "das": []}
        merge_codage_reco(parsed, reco)
        assert parsed["codage_reco"]["dp"] == "K650"  # préservé

    def test_codage_reco_initialement_absent(self):
        parsed = {}
        reco = {"dp": "K650", "dr": "", "das": []}
        merge_codage_reco(parsed, reco)
        assert parsed["codage_reco"]["dp"] == "K650"

    def test_trace_crop_ajoutee(self):
        parsed = {"codage_reco": {"dp": "", "dr": "", "das": []}}
        reco = {"dp": "K650", "_elapsed_s": 1.5}
        merge_codage_reco(parsed, reco)
        assert parsed["_crop_recodage"]["result"]["_elapsed_s"] == 1.5


class TestResolveRecueilZones:
    def test_fallback_constantes(self):
        """Sans config utilisateur, on a les zones par défaut."""
        reco, cb = resolve_recueil_zones()
        # 4 coords flottantes
        assert len(reco) == 4
        assert all(isinstance(v, float) for v in reco)
        # Checkbox zones
        assert len(cb.accord) == 4
        assert len(cb.desaccord) == 4
