"""TDD — Moteur de qualification des décisions.

Source de vérité exécutable des règles métier du tunnel "Décider".

Règles testées :
  R1   within_mandate → individual + consultation_avis
  R2   within_mandate → aucune modalité de vote + consignation Observatoire
  R4   2 ≤ affected_count ≤ small_group_max → individual recommandé, collectif disponible
  R5   small_group_max < affected_count ≤ collective_wot_min → collective recommandé
  R6   affected_count > collective_wot_min → collective recommandé (WoT applicable, non obligatoire)
  R7   is_structural → recommend_onchain + raison explicite
  R8   is_structural=False → recommend_onchain=False

  GARDE-FOUS (invariants internes qui ne doivent jamais régresser) :
  G1   decision_type est toujours dans l'enum autorisé
  G2   individual n'expose jamais de modalités de vote
  G3   collective expose au moins une modalité
  G4   les seuils sont lus depuis QualificationConfig (configurables)
"""

from __future__ import annotations

import pytest

from app.engine.qualifier import (
    DecisionType,
    QualificationConfig,
    QualificationInput,
    QualificationResult,
    qualify,
)

DEFAULT_CONFIG = QualificationConfig()


# ---------------------------------------------------------------------------
# R1 — within_mandate → individual + consultation_avis
# ---------------------------------------------------------------------------


def test_r1_within_mandate_gives_individual():
    result = qualify(QualificationInput(within_mandate=True), DEFAULT_CONFIG)
    assert result.decision_type == DecisionType.INDIVIDUAL


def test_r1_within_mandate_gives_consultation_avis():
    result = qualify(QualificationInput(within_mandate=True), DEFAULT_CONFIG)
    assert result.process == "consultation_avis"


def test_r1_within_mandate_overrides_large_affected_count():
    """Même si de nombreuses personnes sont concernées, un mandat impose individual."""
    result = qualify(
        QualificationInput(within_mandate=True, affected_count=500),
        DEFAULT_CONFIG,
    )
    assert result.decision_type == DecisionType.INDIVIDUAL
    assert result.process == "consultation_avis"


# ---------------------------------------------------------------------------
# R2 — within_mandate → aucune modalité de vote + consignation Observatoire
# ---------------------------------------------------------------------------


def test_r2_within_mandate_no_vote_modalities():
    """Le mandataire décide seul après consultation — pas de vote collectif."""
    result = qualify(QualificationInput(within_mandate=True), DEFAULT_CONFIG)
    assert result.recommended_modalities == []


def test_r2_within_mandate_records_in_observatory():
    """Une décision dans un mandat doit être consignée dans l'Observatoire."""
    result = qualify(QualificationInput(within_mandate=True), DEFAULT_CONFIG)
    assert result.record_in_observatory is True


def test_r2_out_of_mandate_does_not_force_observatory():
    """Hors mandat, la consignation dans l'Observatoire n'est pas imposée."""
    result = qualify(
        QualificationInput(within_mandate=False, affected_count=10),
        DEFAULT_CONFIG,
    )
    assert result.record_in_observatory is False


# ---------------------------------------------------------------------------
# R4 — 2 ≤ affected_count ≤ small_group_max → individual recommandé
# ---------------------------------------------------------------------------


def test_r4_small_group_recommends_individual():
    for count in range(2, DEFAULT_CONFIG.small_group_max + 1):
        result = qualify(
            QualificationInput(within_mandate=False, affected_count=count),
            DEFAULT_CONFIG,
        )
        assert result.decision_type == DecisionType.INDIVIDUAL, (
            f"affected_count={count} devrait recommander individual"
        )


def test_r4_small_group_collective_is_available():
    result = qualify(
        QualificationInput(within_mandate=False, affected_count=3),
        DEFAULT_CONFIG,
    )
    assert result.collective_available is True


def test_r4_small_group_confidence_is_recommended():
    result = qualify(
        QualificationInput(within_mandate=False, affected_count=3),
        DEFAULT_CONFIG,
    )
    assert result.confidence == "recommended"


# ---------------------------------------------------------------------------
# R5 — small_group_max < affected_count ≤ collective_wot_min → collective recommandé
# ---------------------------------------------------------------------------


def test_r5_medium_group_recommends_collective():
    mid = DEFAULT_CONFIG.small_group_max + 1
    result = qualify(
        QualificationInput(within_mandate=False, affected_count=mid),
        DEFAULT_CONFIG,
    )
    assert result.decision_type == DecisionType.COLLECTIVE


def test_r5_medium_group_confidence_is_recommended():
    mid = DEFAULT_CONFIG.small_group_max + 1
    result = qualify(
        QualificationInput(within_mandate=False, affected_count=mid),
        DEFAULT_CONFIG,
    )
    assert result.confidence == "recommended"


# ---------------------------------------------------------------------------
# R6 — affected_count > collective_wot_min → collective recommandé (pas obligatoire)
# ---------------------------------------------------------------------------


def test_r6_large_group_recommends_collective():
    result = qualify(
        QualificationInput(within_mandate=False, affected_count=DEFAULT_CONFIG.collective_wot_min + 1),
        DEFAULT_CONFIG,
    )
    assert result.decision_type == DecisionType.COLLECTIVE


def test_r6_large_group_confidence_is_recommended_not_required():
    """Au-delà du seuil WoT, le vote collectif est recommandé — pas imposé."""
    result = qualify(
        QualificationInput(within_mandate=False, affected_count=DEFAULT_CONFIG.collective_wot_min + 1),
        DEFAULT_CONFIG,
    )
    assert result.confidence == "recommended"


def test_r6_large_group_includes_vote_wot_modality():
    result = qualify(
        QualificationInput(within_mandate=False, affected_count=DEFAULT_CONFIG.collective_wot_min + 1),
        DEFAULT_CONFIG,
    )
    assert "vote_wot" in result.recommended_modalities


# ---------------------------------------------------------------------------
# R7 — is_structural=True → recommend_onchain + raison
# ---------------------------------------------------------------------------


def test_r7_structural_recommends_onchain():
    result = qualify(
        QualificationInput(within_mandate=False, affected_count=100, is_structural=True),
        DEFAULT_CONFIG,
    )
    assert result.recommend_onchain is True


def test_r7_structural_provides_onchain_reason():
    result = qualify(
        QualificationInput(within_mandate=False, affected_count=100, is_structural=True),
        DEFAULT_CONFIG,
    )
    assert result.onchain_reason is not None and len(result.onchain_reason) > 0


def test_r7_structural_within_mandate_can_also_recommend_onchain():
    """Même une décision dans un mandat peut être gravée si structurante."""
    result = qualify(
        QualificationInput(within_mandate=True, is_structural=True),
        DEFAULT_CONFIG,
    )
    assert result.recommend_onchain is True


# ---------------------------------------------------------------------------
# R8 — is_structural=False → recommend_onchain=False
# ---------------------------------------------------------------------------


def test_r8_non_structural_never_recommends_onchain():
    for count in [2, 3, 10, 100]:
        result = qualify(
            QualificationInput(within_mandate=False, affected_count=count, is_structural=False),
            DEFAULT_CONFIG,
        )
        assert result.recommend_onchain is False, (
            f"affected_count={count} non structurant : on-chain ne doit pas être proposé"
        )


# ---------------------------------------------------------------------------
# GARDE-FOUS internes (régressions silencieuses)
# ---------------------------------------------------------------------------


def test_g1_decision_type_always_valid():
    valid_types = set(DecisionType)
    for inp in [
        QualificationInput(within_mandate=True),
        QualificationInput(within_mandate=False, affected_count=2),
        QualificationInput(within_mandate=False, affected_count=10),
        QualificationInput(within_mandate=False, affected_count=100),
    ]:
        result = qualify(inp, DEFAULT_CONFIG)
        assert result.decision_type in valid_types


def test_g2_individual_never_has_vote_modalities():
    for inp in [
        QualificationInput(within_mandate=True),
        QualificationInput(within_mandate=False, affected_count=2),
        QualificationInput(within_mandate=False, affected_count=3),
    ]:
        result = qualify(inp, DEFAULT_CONFIG)
        if result.decision_type == DecisionType.INDIVIDUAL:
            assert result.recommended_modalities == [], (
                f"Individual ne doit pas exposer de modalités : {inp}"
            )


def test_g3_collective_has_at_least_one_modality():
    result = qualify(
        QualificationInput(within_mandate=False, affected_count=20),
        DEFAULT_CONFIG,
    )
    assert result.decision_type == DecisionType.COLLECTIVE
    assert len(result.recommended_modalities) >= 1


def test_g4_custom_config_overrides_thresholds():
    """Les seuils viennent de QualificationConfig — pas de constantes hardcodées."""
    custom = QualificationConfig(small_group_max=2, collective_wot_min=10)
    result = qualify(
        QualificationInput(within_mandate=False, affected_count=3),
        custom,
    )
    assert result.decision_type == DecisionType.COLLECTIVE


def test_g4_default_thresholds_are_stable():
    cfg = QualificationConfig()
    assert cfg.small_group_max == 5
    assert cfg.collective_wot_min == 50
