"""tests/test_annex_iv.py — EU AI Act Annex IV mapping unit tests (Phase 2.1)."""
import pytest

from sichgate.annex_iv import (
    ANNEX_IV_SECTIONS,
    ATTACK_TO_ANNEX_IV,
    annotate_finding,
    annotate_findings,
    compliance_alert_level,
    describe_section,
    get_article_for_attack,
    get_sections_for_attack,
    list_all_sections,
)


# ---------------------------------------------------------------------------
# 1. Section registry completeness
# ---------------------------------------------------------------------------

def test_all_sections_have_descriptions():
    for section_id, description in ANNEX_IV_SECTIONS.items():
        assert description, f"Section '{section_id}' has an empty description"


def test_section_ids_are_valid_format():
    """IDs must be numeric or alphanumeric short codes (e.g. '1', '2g', '3')."""
    import re
    pattern = re.compile(r"^[0-9][0-9a-z]*$")
    for sid in ANNEX_IV_SECTIONS:
        assert pattern.match(sid), f"Invalid section ID: '{sid}'"


def test_key_sections_present():
    required = ["1", "2", "2g", "2h", "3", "4", "5", "6", "7", "8", "9"]
    for sid in required:
        assert sid in ANNEX_IV_SECTIONS, f"Missing required section '{sid}'"


def test_section_2g_describes_testing():
    desc = ANNEX_IV_SECTIONS["2g"].lower()
    assert "test" in desc


def test_section_2h_describes_cybersecurity():
    desc = ANNEX_IV_SECTIONS["2h"].lower()
    assert "cyber" in desc or "security" in desc


def test_list_all_sections_returns_full_dict():
    sections = list_all_sections()
    assert len(sections) == len(ANNEX_IV_SECTIONS)
    assert sections == ANNEX_IV_SECTIONS


def test_describe_section_known():
    result = describe_section("2g")
    assert result is not None
    assert len(result) > 0


def test_describe_section_unknown_returns_none():
    assert describe_section("99z") is None


# ---------------------------------------------------------------------------
# 2. Attack → Annex IV section mapping
# ---------------------------------------------------------------------------

def test_prompt_injection_maps_to_2g_and_2h():
    sections = get_sections_for_attack("prompt_injection")
    assert "2g" in sections
    assert "2h" in sections


def test_bias_fairness_maps_to_3():
    sections = get_sections_for_attack("bias_fairness")
    assert "3" in sections


def test_hallucination_maps_to_4():
    sections = get_sections_for_attack("hallucination")
    assert "4" in sections


def test_membership_inference_maps_to_2d():
    sections = get_sections_for_attack("membership_inference")
    assert "2d" in sections


def test_alignment_maps_to_2e():
    sections = get_sections_for_attack("alignment")
    assert "2e" in sections


def test_unknown_attack_returns_default():
    sections = get_sections_for_attack("nonexistent_attack_xyz")
    assert sections == ["2g"]


def test_hyphenated_attack_name_normalised():
    """Hyphenated names (CLI format) should resolve the same as underscored."""
    sections_underscore = get_sections_for_attack("prompt_injection")
    sections_hyphen = get_sections_for_attack("prompt-injection")
    assert sections_underscore == sections_hyphen


def test_get_sections_returns_list():
    result = get_sections_for_attack("hallucination")
    assert isinstance(result, list)
    assert len(result) > 0


def test_all_mapped_attacks_have_non_empty_sections():
    for attack_type, sections in ATTACK_TO_ANNEX_IV.items():
        assert len(sections) > 0, f"Attack '{attack_type}' has empty sections"


# ---------------------------------------------------------------------------
# 3. EU AI Act article mapping
# ---------------------------------------------------------------------------

def test_bias_fairness_maps_to_article_10():
    assert get_article_for_attack("bias_fairness") == "Article 10"


def test_hallucination_maps_to_article_13():
    assert get_article_for_attack("hallucination") == "Article 13"


def test_sycophancy_maps_to_article_14():
    assert get_article_for_attack("sycophancy") == "Article 14"


def test_prompt_injection_maps_to_article_15():
    assert get_article_for_attack("prompt_injection") == "Article 15"


def test_unknown_attack_maps_to_article_9():
    assert get_article_for_attack("unknown_attack") == "Article 9"


def test_article_starts_with_article(valid_attacks=None):
    for attack_type in ATTACK_TO_ANNEX_IV:
        article = get_article_for_attack(attack_type)
        assert article.startswith("Article"), f"Bad article for '{attack_type}': {article}"


# ---------------------------------------------------------------------------
# 4. Finding annotation
# ---------------------------------------------------------------------------

def test_annotate_finding_adds_annex_iv_sections():
    finding = {"attack_type": "prompt_injection", "status": "fail"}
    annotated = annotate_finding(finding)
    assert "annex_iv_sections" in annotated
    assert "2g" in annotated["annex_iv_sections"]


def test_annotate_finding_adds_eu_ai_act_article():
    finding = {"attack_type": "bias_fairness", "status": "fail"}
    annotated = annotate_finding(finding)
    assert annotated["eu_ai_act_article"] == "Article 10"


def test_annotate_finding_preserves_original_keys():
    finding = {"attack_type": "hallucination", "notes": "overconfident", "severity": "high"}
    annotated = annotate_finding(finding)
    assert annotated["notes"] == "overconfident"
    assert annotated["severity"] == "high"


def test_annotate_finding_does_not_mutate_input():
    finding = {"attack_type": "prompt_injection"}
    _ = annotate_finding(finding)
    assert "annex_iv_sections" not in finding


def test_annotate_findings_processes_list():
    findings = [
        {"attack_type": "prompt_injection"},
        {"attack_type": "bias_fairness"},
    ]
    annotated = annotate_findings(findings)
    assert len(annotated) == 2
    assert "annex_iv_sections" in annotated[0]
    assert "annex_iv_sections" in annotated[1]


def test_annotate_findings_empty_list():
    assert annotate_findings([]) == []


# ---------------------------------------------------------------------------
# 5. Compliance alert levels
# ---------------------------------------------------------------------------

def test_alert_critical_at_20_percent():
    assert compliance_alert_level(0.20) == "CRITICAL"


def test_alert_critical_above_20():
    assert compliance_alert_level(0.35) == "CRITICAL"


def test_alert_high_at_10_percent():
    assert compliance_alert_level(0.10) == "HIGH"


def test_alert_medium_at_5_percent():
    assert compliance_alert_level(0.05) == "MEDIUM"


def test_alert_none_below_5_percent():
    assert compliance_alert_level(0.04) is None


def test_alert_none_for_zero_delta():
    assert compliance_alert_level(0.0) is None


def test_alert_none_for_improvement():
    assert compliance_alert_level(-0.30) is None
