"""
Tests unitaires pour le GroupageValidator.

Ces tests vérifient:
- L'intégration de la fonction de groupage ATIH (mock pour POC)
- La transformation CIM-10/CCAM → GHM/GHS
- La vérification des dates de réalisation CCAM (erreur bloquante 2026)
- La vérification de la version FG
- L'enregistrement de la version FG dans l'audit
"""

import uuid
from datetime import datetime

import pytest

from pipeline_mco_pmsi.models.clinical import ClinicalFact, Evidence, Qualifier, Span
from pipeline_mco_pmsi.models.coding import Code, CodingProposal
from pipeline_mco_pmsi.models.metadata import ModelVersion, StayMetadata
from pipeline_mco_pmsi.validators.groupage_validator import GroupageValidator


@pytest.fixture
def stay_metadata():
    """Fixture pour les métadonnées de séjour."""
    return StayMetadata(
        stay_id="stay_001",
        admission_date=datetime(2026, 1, 15),
        discharge_date=datetime(2026, 1, 20),
        age=45,
        sex="M",
        specialty="Chirurgie",
    )


@pytest.fixture
def model_version():
    """Fixture pour la version du modèle."""
    return ModelVersion(
        model_name="test-model",
        model_tag="v1.0",
        model_digest="a" * 64,  # SHA-256 hash (64 caractères hexadécimaux)
        quantization="q4_0",
    )


@pytest.fixture
def sample_evidence():
    """Fixture pour une preuve."""
    return Evidence(
        document_id="doc_001",
        span=Span(start=100, end=120),
        text="Gastrite aiguë",
        context="Le patient présente une gastrite aiguë confirmée par endoscopie.",
    )


@pytest.fixture
def sample_dp_code(sample_evidence):
    """Fixture pour un code DP."""
    return Code(
        code="K29.1",
        label="Gastrite aiguë",
        type="dp",
        evidence=[sample_evidence],
        confidence=0.9,
        reasoning="Diagnostic principal basé sur l'endoscopie",
        referentiel_version="2026",
    )


@pytest.fixture
def sample_das_code(sample_evidence):
    """Fixture pour un code DAS."""
    return Code(
        code="E11.9",
        label="Diabète de type 2",
        type="das",
        evidence=[sample_evidence],
        confidence=0.85,
        reasoning="Antécédent de diabète mentionné",
        referentiel_version="2026",
    )


@pytest.fixture
def sample_ccam_code_with_date(sample_evidence):
    """Fixture pour un code CCAM avec date."""
    evidence_with_date = Evidence(
        document_id="doc_001",
        span=Span(start=200, end=250),
        text="Endoscopie réalisée le 15/01/2026",
        context="Endoscopie digestive haute réalisée le 15/01/2026 sous anesthésie.",
    )
    return Code(
        code="HGQE002",
        label="Endoscopie œsogastroduodénale",
        type="ccam",
        evidence=[evidence_with_date],
        confidence=0.95,
        reasoning="Acte réalisé le 15/01/2026 selon le compte rendu opératoire",
        referentiel_version="2026",
    )


@pytest.fixture
def sample_ccam_code_without_date(sample_evidence):
    """Fixture pour un code CCAM sans date."""
    return Code(
        code="YYYY001",
        label="Acte sans date",
        type="ccam",
        evidence=[sample_evidence],
        confidence=0.8,
        reasoning="Acte mentionné dans le dossier",
        referentiel_version="2026",
    )


@pytest.fixture
def coding_proposal_complete(
    sample_dp_code, sample_das_code, sample_ccam_code_with_date, model_version
):
    """Fixture pour une proposition de codage complète."""
    return CodingProposal(
        stay_id="stay_001",
        dp=sample_dp_code,
        dr=None,
        das=[sample_das_code],
        ccam=[sample_ccam_code_with_date],
        reasoning="Codage basé sur les documents cliniques",
        model_version=model_version,
        prompt_version="v1.0",
    )


@pytest.fixture
def coding_proposal_missing_ccam_date(
    sample_dp_code, sample_das_code, sample_ccam_code_without_date, model_version
):
    """Fixture pour une proposition avec CCAM sans date."""
    return CodingProposal(
        stay_id="stay_001",
        dp=sample_dp_code,
        dr=None,
        das=[sample_das_code],
        ccam=[sample_ccam_code_without_date],
        reasoning="Codage basé sur les documents cliniques",
        model_version=model_version,
        prompt_version="v1.0",
    )


@pytest.fixture
def coding_proposal_no_dp(sample_das_code, sample_ccam_code_with_date, model_version):
    """Fixture pour une proposition sans DP."""
    return CodingProposal(
        stay_id="stay_001",
        dp=None,
        dr=None,
        das=[sample_das_code],
        ccam=[sample_ccam_code_with_date],
        reasoning="Codage incomplet",
        model_version=model_version,
        prompt_version="v1.0",
    )


class TestGroupageValidatorInitialization:
    """Tests d'initialisation du GroupageValidator."""

    def test_init_default_version(self):
        """Test l'initialisation avec la version par défaut."""
        validator = GroupageValidator()
        assert validator.groupage_version == "2026"

    def test_init_custom_version(self):
        """Test l'initialisation avec une version personnalisée."""
        validator = GroupageValidator(groupage_version="2025")
        assert validator.groupage_version == "2025"

    def test_get_version_info(self):
        """Test la récupération des informations de version."""
        validator = GroupageValidator(groupage_version="2026")
        version_info = validator.get_version_info()

        assert version_info["groupage_version"] == "2026"
        assert version_info["implementation"] == "mock_poc"
        assert "library_version" in version_info


class TestCheckCCAMDates:
    """Tests de la vérification des dates CCAM."""

    def test_ccam_with_date_in_reasoning(self, sample_ccam_code_with_date):
        """Test qu'un code CCAM avec date dans le reasoning est accepté."""
        validator = GroupageValidator()
        missing_dates = validator.check_ccam_dates([sample_ccam_code_with_date])

        assert len(missing_dates) == 0

    def test_ccam_without_date(self, sample_ccam_code_without_date):
        """Test qu'un code CCAM sans date est détecté."""
        validator = GroupageValidator()
        missing_dates = validator.check_ccam_dates([sample_ccam_code_without_date])

        assert len(missing_dates) == 1
        assert missing_dates[0] == "YYYY001"

    def test_multiple_ccam_mixed_dates(
        self, sample_ccam_code_with_date, sample_ccam_code_without_date
    ):
        """Test avec plusieurs codes CCAM, certains avec date, d'autres sans."""
        validator = GroupageValidator()
        missing_dates = validator.check_ccam_dates(
            [sample_ccam_code_with_date, sample_ccam_code_without_date]
        )

        assert len(missing_dates) == 1
        assert missing_dates[0] == "YYYY001"

    def test_empty_ccam_list(self):
        """Test avec une liste vide de codes CCAM."""
        validator = GroupageValidator()
        missing_dates = validator.check_ccam_dates([])

        assert len(missing_dates) == 0

    def test_ccam_with_date_in_evidence(self, sample_evidence):
        """Test qu'un code CCAM avec date dans les preuves est accepté."""
        evidence_with_date = Evidence(
            document_id="doc_001",
            span=Span(start=200, end=250),
            text="Intervention du 15/01/2026",
            context="Chirurgie effectuée le 15/01/2026.",
        )
        ccam_code = Code(
            code="HGQE002",
            label="Endoscopie",
            type="ccam",
            evidence=[evidence_with_date],
            confidence=0.95,
            reasoning="Acte chirurgical",
            referentiel_version="2026",
        )

        validator = GroupageValidator()
        missing_dates = validator.check_ccam_dates([ccam_code])

        assert len(missing_dates) == 0


class TestValidateGroupage:
    """Tests de la validation de groupage complète."""

    def test_validate_groupage_success(self, coding_proposal_complete, stay_metadata):
        """Test une validation de groupage réussie."""
        validator = GroupageValidator(groupage_version="2026")
        result = validator.validate_groupage(coding_proposal_complete, stay_metadata)

        assert result.stay_id == "stay_001"
        assert result.ghm is not None
        assert result.ghs is not None
        assert len(result.ccam_date_errors) == 0
        assert result.groupage_version == "2026"
        assert isinstance(result.groupage_date, datetime)

    def test_validate_groupage_missing_ccam_date(
        self, coding_proposal_missing_ccam_date, stay_metadata
    ):
        """Test la détection d'une date CCAM manquante."""
        validator = GroupageValidator(groupage_version="2026")
        result = validator.validate_groupage(
            coding_proposal_missing_ccam_date, stay_metadata
        )

        assert result.stay_id == "stay_001"
        assert len(result.ccam_date_errors) == 1
        assert result.ccam_date_errors[0] == "YYYY001"

        # Vérifier qu'une erreur bloquante est générée
        blocking_errors = [
            err for err in result.groupage_errors if err.severity == "bloquant"
        ]
        assert len(blocking_errors) >= 1
        assert any("Date de réalisation manquante" in err.message for err in blocking_errors)

    def test_validate_groupage_no_dp(self, coding_proposal_no_dp, stay_metadata):
        """Test la détection d'un DP manquant."""
        validator = GroupageValidator(groupage_version="2026")
        result = validator.validate_groupage(coding_proposal_no_dp, stay_metadata)

        assert result.stay_id == "stay_001"
        assert result.ghm is None
        assert result.ghs is None

        # Vérifier qu'une erreur bloquante est générée
        blocking_errors = [
            err for err in result.groupage_errors if err.severity == "bloquant"
        ]
        assert len(blocking_errors) >= 1
        assert any("Aucun Diagnostic Principal" in err.message for err in blocking_errors)

    def test_validate_groupage_version_mismatch(
        self, coding_proposal_complete, stay_metadata
    ):
        """Test la détection d'une version FG incorrecte."""
        # Créer un validateur avec une version différente de l'année du séjour
        validator = GroupageValidator(groupage_version="2025")

        with pytest.raises(ValueError) as exc_info:
            validator.validate_groupage(coding_proposal_complete, stay_metadata)

        assert "Version FG" in str(exc_info.value)
        assert "ne correspond pas" in str(exc_info.value)

    def test_validate_groupage_many_das(
        self, sample_dp_code, sample_das_code, sample_ccam_code_with_date, model_version, stay_metadata
    ):
        """Test avec un nombre élevé de DAS."""
        # Créer 25 DAS (au-dessus de la limite de 20)
        many_das = [sample_das_code] * 25

        proposal = CodingProposal(
            stay_id="stay_001",
            dp=sample_dp_code,
            dr=None,
            das=many_das,
            ccam=[sample_ccam_code_with_date],
            reasoning="Codage avec beaucoup de DAS",
            model_version=model_version,
            prompt_version="v1.0",
        )

        validator = GroupageValidator(groupage_version="2026")
        result = validator.validate_groupage(proposal, stay_metadata)

        # Vérifier qu'un avertissement est généré
        review_errors = [
            err for err in result.groupage_errors if err.severity == "a_revoir"
        ]
        assert len(review_errors) >= 1
        assert any("Nombre élevé de DAS" in err.message for err in review_errors)

    def test_validate_groupage_many_ccam(
        self, sample_dp_code, sample_das_code, sample_ccam_code_with_date, model_version, stay_metadata
    ):
        """Test avec un nombre élevé d'actes CCAM."""
        # Créer 35 actes CCAM (au-dessus de la limite de 30)
        many_ccam = [sample_ccam_code_with_date] * 35

        proposal = CodingProposal(
            stay_id="stay_001",
            dp=sample_dp_code,
            dr=None,
            das=[sample_das_code],
            ccam=many_ccam,
            reasoning="Codage avec beaucoup d'actes",
            model_version=model_version,
            prompt_version="v1.0",
        )

        validator = GroupageValidator(groupage_version="2026")
        result = validator.validate_groupage(proposal, stay_metadata)

        # Vérifier qu'un avertissement est généré
        review_errors = [
            err for err in result.groupage_errors if err.severity == "a_revoir"
        ]
        assert len(review_errors) >= 1
        assert any("Nombre élevé d'actes CCAM" in err.message for err in review_errors)


class TestGHMGHSGeneration:
    """Tests de la génération de GHM/GHS (mock)."""

    def test_ghm_format(self, coding_proposal_complete, stay_metadata):
        """Test que le GHM généré a le bon format."""
        validator = GroupageValidator(groupage_version="2026")
        result = validator.validate_groupage(coding_proposal_complete, stay_metadata)

        # Format GHM: 2 chiffres + 1 lettre + 2 chiffres (ex: "05K02")
        assert result.ghm is not None
        assert len(result.ghm) == 5
        assert result.ghm[:2].isdigit()
        assert result.ghm[2].isalpha()
        assert result.ghm[3:].isdigit()

    def test_ghs_generated(self, coding_proposal_complete, stay_metadata):
        """Test que le GHS est généré."""
        validator = GroupageValidator(groupage_version="2026")
        result = validator.validate_groupage(coding_proposal_complete, stay_metadata)

        assert result.ghs is not None
        assert len(result.ghs) > 0

    def test_ghm_deterministic(self, coding_proposal_complete, stay_metadata):
        """Test que le GHM est déterministe pour les mêmes codes."""
        validator = GroupageValidator(groupage_version="2026")

        result1 = validator.validate_groupage(coding_proposal_complete, stay_metadata)
        result2 = validator.validate_groupage(coding_proposal_complete, stay_metadata)

        # Le GHM devrait être identique pour les mêmes codes
        assert result1.ghm == result2.ghm
        assert result1.ghs == result2.ghs


class TestVersionVerification:
    """Tests de la vérification de version FG."""

    def test_version_matches_year(self, coding_proposal_complete):
        """Test que la version FG correspond à l'année du séjour."""
        stay_metadata = StayMetadata(
            stay_id="stay_001",
            admission_date=datetime(2026, 1, 15),
            discharge_date=datetime(2026, 1, 20),
            age=45,
            sex="M",
            specialty="Chirurgie",
        )

        validator = GroupageValidator(groupage_version="2026")
        result = validator.validate_groupage(coding_proposal_complete, stay_metadata)

        # Pas d'exception levée, la version correspond
        assert result.groupage_version == "2026"

    def test_version_mismatch_raises_error(self, coding_proposal_complete):
        """Test qu'une version FG incorrecte lève une erreur."""
        stay_metadata = StayMetadata(
            stay_id="stay_001",
            admission_date=datetime(2026, 1, 15),
            discharge_date=datetime(2026, 1, 20),
            age=45,
            sex="M",
            specialty="Chirurgie",
        )

        validator = GroupageValidator(groupage_version="2025")

        with pytest.raises(ValueError) as exc_info:
            validator.validate_groupage(coding_proposal_complete, stay_metadata)

        assert "2025" in str(exc_info.value)
        assert "2026" in str(exc_info.value)

    def test_version_recorded_in_result(self, coding_proposal_complete, stay_metadata):
        """Test que la version FG est enregistrée dans le résultat."""
        validator = GroupageValidator(groupage_version="2026")
        result = validator.validate_groupage(coding_proposal_complete, stay_metadata)

        assert result.groupage_version == "2026"
        assert isinstance(result.groupage_date, datetime)


class TestEdgeCases:
    """Tests des cas limites."""

    def test_empty_proposal(self, model_version, stay_metadata):
        """Test avec une proposition vide."""
        proposal = CodingProposal(
            stay_id="stay_001",
            dp=None,
            dr=None,
            das=[],
            ccam=[],
            reasoning="Proposition vide",
            model_version=model_version,
            prompt_version="v1.0",
        )

        validator = GroupageValidator(groupage_version="2026")
        result = validator.validate_groupage(proposal, stay_metadata)

        # Devrait générer une erreur bloquante pour DP manquant
        assert result.ghm is None
        assert result.ghs is None
        blocking_errors = [
            err for err in result.groupage_errors if err.severity == "bloquant"
        ]
        assert len(blocking_errors) >= 1

    def test_only_dp(self, sample_dp_code, model_version, stay_metadata):
        """Test avec seulement un DP."""
        proposal = CodingProposal(
            stay_id="stay_001",
            dp=sample_dp_code,
            dr=None,
            das=[],
            ccam=[],
            reasoning="Seulement DP",
            model_version=model_version,
            prompt_version="v1.0",
        )

        validator = GroupageValidator(groupage_version="2026")
        result = validator.validate_groupage(proposal, stay_metadata)

        # Devrait réussir avec seulement un DP
        assert result.ghm is not None
        assert result.ghs is not None
        assert len(result.ccam_date_errors) == 0

    def test_multiple_ccam_some_without_dates(
        self, sample_ccam_code_with_date, sample_ccam_code_without_date
    ):
        """Test avec plusieurs codes CCAM, certains sans dates."""
        validator = GroupageValidator()

        # Créer un deuxième code sans date
        ccam_without_date_2 = Code(
            code="ZZZZ999",
            label="Autre acte sans date",
            type="ccam",
            evidence=[sample_ccam_code_without_date.evidence[0]],
            confidence=0.7,
            reasoning="Acte mentionné",
            referentiel_version="2026",
        )

        missing_dates = validator.check_ccam_dates(
            [
                sample_ccam_code_with_date,
                sample_ccam_code_without_date,
                ccam_without_date_2,
            ]
        )

        assert len(missing_dates) == 2
        assert "YYYY001" in missing_dates
        assert "ZZZZ999" in missing_dates
