"""
Tests pour le Vérificateur.

Ces tests vérifient que le Vérificateur:
- Utilise un prompt différent du Codeur (Exigence 4.1)
- Détecte les erreurs sensibles DIM (Exigences 4.2, 4.3, 4.4)
- Génère des vetos pour contradictions bloquantes (Exigence 4.5)
- Marque "à_revoir" pour contradictions non-bloquantes (Exigence 4.6)
- Fournit des alternatives (Exigence 4.6)
"""

import hashlib
from datetime import datetime
from unittest.mock import MagicMock

import pytest

from pipeline_mco_pmsi.models.clinical import (
    ClinicalFact,
    Evidence,
    Qualifier,
    Span,
)
from pipeline_mco_pmsi.models.coding import (
    Code,
    CodingProposal,
    DIMError,
)
from pipeline_mco_pmsi.models.metadata import ModelVersion, StayMetadata
from pipeline_mco_pmsi.verifiers.verificateur import Verificateur


@pytest.fixture
def mock_rag_engine():
    """Crée un mock du RAG Engine."""
    return MagicMock()


@pytest.fixture
def verificateur(mock_rag_engine):
    """Crée une instance du Vérificateur avec un RAG Engine mocké."""
    return Verificateur(
        rag_engine=mock_rag_engine,
        model_name="mock-llm",
        model_version="1.0.0",
    )


@pytest.fixture
def stay_metadata():
    """Crée des métadonnées de séjour pour les tests."""
    return StayMetadata(
        stay_id="stay_001",
        admission_date=datetime(2024, 1, 1),
        discharge_date=datetime(2024, 1, 5),
        specialty="Chirurgie",
        unit="Bloc opératoire",
        age=45,
        sex="M",
    )


def create_evidence(doc_id="doc_001", start=100, end=120, text="Appendicite aiguë"):
    """Helper pour créer une preuve."""
    return Evidence(
        document_id=doc_id,
        span=Span(start=start, end=end),
        text=text,
        context=f"Le patient présente {text}",
    )


def create_qualifier(certainty="affirmé", markers=None, confidence=0.95):
    """Helper pour créer un qualificateur."""
    return Qualifier(
        certainty=certainty,
        markers=markers or [],
        confidence=confidence,
    )


def create_fact(
    fact_id="f_001",
    fact_type="diagnostic",
    text="Appendicite aiguë",
    certainty="affirmé",
    temporality="actuel",
    evidence=None,
):
    """Helper pour créer un fait clinique."""
    if evidence is None:
        evidence = create_evidence(text=text)
    
    return ClinicalFact(
        fact_id=fact_id,
        type=fact_type,
        text=text,
        qualifier=create_qualifier(certainty=certainty),
        temporality=temporality,
        evidence=evidence,
        confidence=0.90,
    )


def create_code(
    code="K35.8",
    label="Appendicite aiguë",
    code_type="dp",
    evidence=None,
    confidence=0.85,
):
    """Helper pour créer un code."""
    if evidence is None:
        evidence = [create_evidence(text=label)]
    
    return Code(
        code=code,
        label=label,
        type=code_type,
        evidence=evidence,
        confidence=confidence,
        reasoning=f"Code {code_type} proposé pour {label}",
        referentiel_version="2026",
    )


def create_proposal(
    stay_id="stay_001",
    dp=None,
    dr=None,
    das=None,
    ccam=None,
    prompt_version="codeur-1.0.0",
):
    """Helper pour créer une proposition de codage."""
    model_version = ModelVersion(
        model_name="mock-llm",
        model_tag="1.0.0",
        model_digest=hashlib.sha256(b"mock-llm:1.0.0").hexdigest(),
        quantization=None,
    )
    
    return CodingProposal(
        stay_id=stay_id,
        dp=dp,
        dr=dr,
        das=das or [],
        ccam=ccam or [],
        reasoning="Proposition de codage basée sur les faits cliniques",
        model_version=model_version,
        prompt_version=prompt_version,
    )


# ============================================================================
# Tests d'initialisation
# ============================================================================

def test_verificateur_initialization(verificateur):
    """Test l'initialisation du Vérificateur."""
    assert verificateur.model_name == "mock-llm"
    assert verificateur.model_version_str == "1.0.0"
    assert verificateur.VERIFICATEUR_PROMPT_VERSION == "verificateur-1.0.0"
    assert verificateur.model_digest is not None


def test_verificateur_prompt_version_different_from_codeur(verificateur):
    """
    Test que le Vérificateur utilise un prompt différent du Codeur.
    
    Exigence 4.1: Le Vérificateur DOIT utiliser un prompt différent du Codeur.
    """
    codeur_prompt_version = "codeur-1.0.0"
    assert verificateur.VERIFICATEUR_PROMPT_VERSION != codeur_prompt_version


# ============================================================================
# Tests de verify_proposal()
# ============================================================================

def test_verify_proposal_accepts_valid_proposal(verificateur):
    """
    Test que le Vérificateur accepte une proposition valide.
    
    Exigence 4.8: Quand le Vérificateur confirme la proposition,
    le système doit accepter la proposition.
    """
    # Créer une proposition valide
    evidence = create_evidence()
    fact = create_fact(evidence=evidence)
    dp = create_code(evidence=[evidence])
    proposal = create_proposal(dp=dp)
    
    # Vérifier
    result = verificateur.verify_proposal(proposal, [fact])
    
    # Assertions
    assert result.decision == "accept"
    assert len(result.dim_errors) == 0
    assert len(result.contradictions) == 0
    assert result.stay_id == "stay_001"
    assert result.prompt_version == "verificateur-1.0.0"


def test_verify_proposal_rejects_same_prompt_version(verificateur):
    """
    Test que le Vérificateur rejette une proposition avec le même prompt.
    
    Exigence 4.1: Le Vérificateur DOIT utiliser un prompt différent du Codeur.
    """
    evidence = create_evidence()
    fact = create_fact(evidence=evidence)
    dp = create_code(evidence=[evidence])
    
    # Créer une proposition avec le même prompt que le Vérificateur
    proposal = create_proposal(
        dp=dp,
        prompt_version="verificateur-1.0.0"  # MÊME prompt!
    )
    
    # Vérifier que cela lève une erreur
    with pytest.raises(ValueError, match="DOIT utiliser un prompt différent"):
        verificateur.verify_proposal(proposal, [fact])


# ============================================================================
# Tests de detect_dim_errors() - Diagnostics niés
# ============================================================================

def test_detect_dim_errors_negated_diagnostic(verificateur):
    """
    Test la détection de diagnostic nié codé comme affirmé.
    
    Exigence 4.2, 19.1: Le Vérificateur DOIT détecter les diagnostics niés
    codés comme affirmés.
    """
    # Créer un fait nié
    evidence = create_evidence(text="Pas d'appendicite")
    fact = create_fact(
        text="Pas d'appendicite",
        certainty="nié",
        evidence=evidence,
    )
    
    # Créer un code basé sur ce fait nié
    dp = create_code(
        code="K35.8",
        label="Appendicite",
        evidence=[evidence],
    )
    proposal = create_proposal(dp=dp)
    
    # Détecter les erreurs
    errors = verificateur.detect_dim_errors(proposal, [fact])
    
    # Assertions
    assert len(errors) == 1
    assert errors[0].error_type == "negated_as_affirmed"
    assert errors[0].severity == "bloquant"
    assert "K35.8" in errors[0].affected_codes


def test_detect_dim_errors_suspected_as_dp(verificateur):
    """
    Test la détection de diagnostic suspecté codé comme DP.
    
    Exigence 19.2: Le Vérificateur DOIT détecter la transformation
    de suspicion en certitude.
    """
    # Créer un fait suspecté
    evidence = create_evidence(text="Appendicite possible")
    fact = create_fact(
        text="Appendicite possible",
        certainty="suspecté",
        evidence=evidence,
    )
    
    # Créer un DP basé sur ce fait suspecté
    dp = create_code(
        code="K35.8",
        label="Appendicite",
        code_type="dp",
        evidence=[evidence],
    )
    proposal = create_proposal(dp=dp)
    
    # Détecter les erreurs
    errors = verificateur.detect_dim_errors(proposal, [fact])
    
    # Assertions
    assert len(errors) == 1
    assert errors[0].error_type == "suspected_as_certain"
    assert errors[0].severity == "bloquant"


# ============================================================================
# Tests de detect_dim_errors() - Antécédents
# ============================================================================

def test_detect_dim_errors_history_as_dp(verificateur):
    """
    Test la détection d'antécédent codé comme DP.
    
    Exigence 4.4, 19.4: Le Vérificateur DOIT détecter les antécédents
    codés comme épisode actuel.
    """
    # Créer un fait antécédent
    evidence = create_evidence(text="Antécédent d'appendicectomie")
    fact = create_fact(
        text="Antécédent d'appendicectomie",
        temporality="antecedent",
        evidence=evidence,
    )
    
    # Créer un DP basé sur cet antécédent
    dp = create_code(
        code="Z90.4",
        label="Absence d'appendice",
        code_type="dp",
        evidence=[evidence],
    )
    proposal = create_proposal(dp=dp)
    
    # Détecter les erreurs
    errors = verificateur.detect_dim_errors(proposal, [fact])
    
    # Assertions
    assert len(errors) == 1
    assert errors[0].error_type == "history_as_current"
    assert errors[0].severity == "bloquant"


# ============================================================================
# Tests de detect_dim_errors() - Actes CCAM
# ============================================================================

def test_detect_dim_errors_ccam_without_evidence(verificateur):
    """
    Test la détection d'acte CCAM sans preuve explicite.
    
    Exigence 4.3, 19.3: Le Vérificateur DOIT détecter les actes CCAM
    sans preuve explicite.
    """
    # Créer un fait diagnostic (pas un acte)
    evidence = create_evidence(text="Appendicite")
    fact = create_fact(
        fact_type="diagnostic",  # Pas un acte!
        text="Appendicite",
        evidence=evidence,
    )
    
    # Créer un code CCAM basé sur ce fait diagnostic
    ccam = create_code(
        code="HHFA001",
        label="Appendicectomie",
        code_type="ccam",
        evidence=[evidence],
    )
    proposal = create_proposal(ccam=[ccam])
    
    # Détecter les erreurs
    errors = verificateur.detect_dim_errors(proposal, [fact])
    
    # Assertions
    assert len(errors) == 1
    assert errors[0].error_type == "act_without_evidence"
    assert errors[0].severity == "bloquant"


def test_detect_dim_errors_ccam_with_valid_evidence(verificateur):
    """
    Test qu'un acte CCAM avec preuve valide n'est pas signalé.
    """
    # Créer un fait acte
    evidence = create_evidence(text="Appendicectomie réalisée")
    fact = create_fact(
        fact_type="acte",  # Type correct
        text="Appendicectomie réalisée",
        evidence=evidence,
    )
    
    # Créer un code CCAM basé sur ce fait acte
    ccam = create_code(
        code="HHFA001",
        label="Appendicectomie",
        code_type="ccam",
        evidence=[evidence],
    )
    proposal = create_proposal(ccam=[ccam])
    
    # Détecter les erreurs
    errors = verificateur.detect_dim_errors(proposal, [fact])
    
    # Assertions
    assert len(errors) == 0


# ============================================================================
# Tests de detect_dim_errors() - Inversions DP/DAS
# ============================================================================

def test_detect_dim_errors_dp_das_inversion(verificateur):
    """
    Test la détection d'inversion DP/DAS.
    
    Exigence 19.5: Le Vérificateur DOIT détecter les inversions
    grossières d'assignation DP/DAS.
    """
    # Créer des faits
    evidence_dp = create_evidence(text="Diabète")
    fact_dp = create_fact(fact_id="f_001", text="Diabète", evidence=evidence_dp)
    
    evidence_das = create_evidence(start=200, end=220, text="Appendicite aiguë")
    fact_das = create_fact(fact_id="f_002", text="Appendicite aiguë", evidence=evidence_das)
    
    # Créer un DP avec faible confiance et un DAS avec forte confiance
    dp = create_code(
        code="E11.9",
        label="Diabète",
        code_type="dp",
        evidence=[evidence_dp],
        confidence=0.60,  # Faible
    )
    das = create_code(
        code="K35.8",
        label="Appendicite aiguë",
        code_type="das",
        evidence=[evidence_das],
        confidence=0.95,  # Forte (> DP + 0.1)
    )
    proposal = create_proposal(dp=dp, das=[das])
    
    # Détecter les erreurs
    errors = verificateur.detect_dim_errors(proposal, [fact_dp, fact_das])
    
    # Assertions
    assert len(errors) == 1
    assert errors[0].error_type == "dp_das_inversion"
    assert errors[0].severity == "a_revoir"
    assert "E11.9" in errors[0].affected_codes
    assert "K35.8" in errors[0].affected_codes


# ============================================================================
# Tests de décision (accept/veto/review)
# ============================================================================

def test_verify_proposal_veto_on_blocking_error(verificateur):
    """
    Test que le Vérificateur génère un veto pour une erreur bloquante.
    
    Exigence 4.5: Quand le Vérificateur détecte une contradiction bloquante,
    le système doit opposer un veto.
    """
    # Créer un fait nié
    evidence = create_evidence(text="Pas d'appendicite")
    fact = create_fact(
        text="Pas d'appendicite",
        certainty="nié",
        evidence=evidence,
    )
    
    # Créer un code basé sur ce fait nié (erreur bloquante)
    dp = create_code(evidence=[evidence])
    proposal = create_proposal(dp=dp)
    
    # Vérifier
    result = verificateur.verify_proposal(proposal, [fact])
    
    # Assertions
    assert result.decision == "veto"
    assert len(result.dim_errors) == 1
    assert result.dim_errors[0].severity == "bloquant"


def test_verify_proposal_review_on_non_blocking_error(verificateur):
    """
    Test que le Vérificateur marque "à_revoir" pour une erreur non-bloquante.
    
    Exigence 4.6: Quand le Vérificateur détecte une contradiction non-bloquante,
    le système doit marquer le code comme "à_revoir".
    """
    # Créer des faits pour inversion DP/DAS
    evidence_dp = create_evidence(text="Diabète")
    fact_dp = create_fact(fact_id="f_001", text="Diabète", evidence=evidence_dp)
    
    evidence_das = create_evidence(start=200, end=220, text="Appendicite")
    fact_das = create_fact(fact_id="f_002", text="Appendicite", evidence=evidence_das)
    
    # Créer une inversion DP/DAS (erreur non-bloquante)
    dp = create_code(
        code="E11.9",
        label="Diabète",
        code_type="dp",
        evidence=[evidence_dp],
        confidence=0.60,
    )
    das = create_code(
        code="K35.8",
        label="Appendicite",
        code_type="das",
        evidence=[evidence_das],
        confidence=0.95,
    )
    proposal = create_proposal(dp=dp, das=[das])
    
    # Vérifier
    result = verificateur.verify_proposal(proposal, [fact_dp, fact_das])
    
    # Assertions
    assert result.decision == "review"
    assert len(result.dim_errors) == 1
    assert result.dim_errors[0].severity == "a_revoir"


# ============================================================================
# Tests d'alternatives
# ============================================================================

def test_verify_proposal_provides_alternatives(verificateur):
    """
    Test que le Vérificateur fournit des alternatives.
    
    Exigence 4.6: Le Vérificateur doit fournir des alternatives.
    """
    # Créer un fait nié pour le DP
    evidence_dp = create_evidence(text="Pas d'appendicite")
    fact_dp = create_fact(
        fact_id="f_001",
        text="Pas d'appendicite",
        certainty="nié",
        evidence=evidence_dp,
    )
    
    # Créer un fait valide pour le DAS
    evidence_das = create_evidence(start=200, end=220, text="Gastrite")
    fact_das = create_fact(
        fact_id="f_002",
        text="Gastrite",
        evidence=evidence_das,
    )
    
    # Créer une proposition avec DP invalide et DAS valide
    dp = create_code(
        code="K35.8",
        label="Appendicite",
        code_type="dp",
        evidence=[evidence_dp],
    )
    das = create_code(
        code="K29.7",
        label="Gastrite",
        code_type="das",
        evidence=[evidence_das],
        confidence=0.90,
    )
    proposal = create_proposal(dp=dp, das=[das])
    
    # Vérifier
    result = verificateur.verify_proposal(proposal, [fact_dp, fact_das])
    
    # Assertions
    assert result.decision == "veto"
    assert len(result.alternatives) > 0
    # L'alternative devrait être le DAS comme nouveau DP
    assert result.alternatives[0].code == "K29.7"
    assert result.alternatives[0].type == "dp"


# ============================================================================
# Tests de raisonnement
# ============================================================================

def test_verify_proposal_generates_reasoning(verificateur):
    """
    Test que le Vérificateur génère un raisonnement détaillé.
    """
    evidence = create_evidence()
    fact = create_fact(evidence=evidence)
    dp = create_code(evidence=[evidence])
    proposal = create_proposal(dp=dp)
    
    # Vérifier
    result = verificateur.verify_proposal(proposal, [fact])
    
    # Assertions
    assert result.reasoning is not None
    assert len(result.reasoning) > 0
    assert "Vérification indépendante" in result.reasoning
    assert "verificateur-1.0.0" in result.reasoning
    assert "codeur-1.0.0" in result.reasoning


# ============================================================================
# Tests de cas complexes
# ============================================================================

def test_verify_proposal_multiple_errors(verificateur):
    """
    Test la détection de plusieurs erreurs simultanées.
    """
    # Créer plusieurs faits avec erreurs
    evidence1 = create_evidence(text="Pas d'appendicite")
    fact1 = create_fact(
        fact_id="f_001",
        text="Pas d'appendicite",
        certainty="nié",
        evidence=evidence1,
    )
    
    evidence2 = create_evidence(start=200, end=220, text="Diabète ancien")
    fact2 = create_fact(
        fact_id="f_002",
        text="Diabète ancien",
        temporality="antecedent",
        evidence=evidence2,
    )
    
    # Créer des codes avec erreurs
    dp = create_code(
        code="K35.8",
        label="Appendicite",
        code_type="dp",
        evidence=[evidence1],
    )
    das = create_code(
        code="E11.9",
        label="Diabète",
        code_type="das",
        evidence=[evidence2],
    )
    proposal = create_proposal(dp=dp, das=[das])
    
    # Vérifier
    result = verificateur.verify_proposal(proposal, [fact1, fact2])
    
    # Assertions
    assert result.decision == "veto"
    assert len(result.dim_errors) >= 1  # Au moins l'erreur du DP nié


def test_verify_proposal_no_errors_with_das_only(verificateur):
    """
    Test qu'une proposition sans DP mais avec DAS valides est acceptée.
    """
    evidence = create_evidence(text="Diabète")
    fact = create_fact(text="Diabète", evidence=evidence)
    
    das = create_code(
        code="E11.9",
        label="Diabète",
        code_type="das",
        evidence=[evidence],
    )
    proposal = create_proposal(dp=None, das=[das])
    
    # Vérifier
    result = verificateur.verify_proposal(proposal, [fact])
    
    # Assertions
    assert result.decision == "accept"
    assert len(result.dim_errors) == 0
