"""Tests for Framework Decomposition Engine.

Covers:
- Registry loading
- Routing classification (atomic / compound / framework_container)
- Framework + domain matching
- Subcontrol selection
- Decomposition into sub-obligations
- Quality rules (warnings, errors)
- Inference helpers
"""

import pytest

from compliance.services.framework_decomposition import (
    classify_routing,
    decompose_framework_container,
    get_registry,
    registry_stats,
    reload_registry,
    DecomposedObligation,
    FrameworkDecompositionResult,
    RoutingResult,
    _detect_framework,
    _has_framework_keywords,
    _infer_action,
    _infer_object,
    _is_compound_obligation,
    _match_domain,
    _select_subcontrols,
)


# ---------------------------------------------------------------------------
# REGISTRY TESTS
# ---------------------------------------------------------------------------


class TestRegistryLoading:

    def test_registry_loads_successfully(self):
        reg = get_registry()
        assert len(reg) >= 3

    def test_nist_in_registry(self):
        reg = get_registry()
        assert "NIST_SP800_53" in reg

    def test_owasp_asvs_in_registry(self):
        reg = get_registry()
        assert "OWASP_ASVS" in reg

    def test_csa_ccm_in_registry(self):
        reg = get_registry()
        assert "CSA_CCM" in reg

    def test_nist_has_domains(self):
        reg = get_registry()
        nist = reg["NIST_SP800_53"]
        assert len(nist["domains"]) >= 5

    def test_nist_ac_has_subcontrols(self):
        reg = get_registry()
        nist = reg["NIST_SP800_53"]
        ac = next(d for d in nist["domains"] if d["domain_id"] == "AC")
        assert len(ac["subcontrols"]) >= 5

    def test_registry_stats(self):
        stats = registry_stats()
        assert stats["frameworks"] >= 3
        assert stats["total_domains"] >= 10
        assert stats["total_subcontrols"] >= 30

    def test_reload_registry(self):
        reg = reload_registry()
        assert len(reg) >= 3


# ---------------------------------------------------------------------------
# ROUTING TESTS
# ---------------------------------------------------------------------------


class TestClassifyRouting:

    def test_atomic_simple_obligation(self):
        result = classify_routing(
            obligation_text="Multi-Faktor-Authentifizierung muss implementiert werden",
            action_raw="implementieren",
            object_raw="MFA",
        )
        assert result.routing_type == "atomic"

    def test_framework_container_ccm_ais(self):
        result = classify_routing(
            obligation_text="Die CCM-Praktiken fuer Application and Interface Security (AIS) muessen implementiert werden",
            action_raw="implementieren",
            object_raw="CCM-Praktiken fuer AIS",
        )
        assert result.routing_type == "framework_container"
        assert result.framework_ref == "CSA_CCM"
        assert result.framework_domain == "AIS"

    def test_framework_container_nist_800_53(self):
        result = classify_routing(
            obligation_text="Kontrollen gemaess NIST SP 800-53 umsetzen",
            action_raw="umsetzen",
            object_raw="Kontrollen gemaess NIST SP 800-53",
        )
        assert result.routing_type == "framework_container"
        assert result.framework_ref == "NIST_SP800_53"

    def test_framework_container_owasp_asvs(self):
        result = classify_routing(
            obligation_text="OWASP ASVS Anforderungen muessen implementiert werden",
            action_raw="implementieren",
            object_raw="OWASP ASVS Anforderungen",
        )
        assert result.routing_type == "framework_container"
        assert result.framework_ref == "OWASP_ASVS"

    def test_compound_obligation(self):
        result = classify_routing(
            obligation_text="Richtlinie erstellen und Schulungen durchfuehren",
            action_raw="erstellen und durchfuehren",
            object_raw="Richtlinie",
        )
        assert result.routing_type == "compound"

    def test_no_split_phrase_not_compound(self):
        result = classify_routing(
            obligation_text="Richtlinie dokumentieren und pflegen",
            action_raw="dokumentieren und pflegen",
            object_raw="Richtlinie",
        )
        assert result.routing_type == "atomic"

    def test_framework_keywords_in_object(self):
        result = classify_routing(
            obligation_text="Massnahmen umsetzen",
            action_raw="umsetzen",
            object_raw="Framework-Praktiken und Kontrollen",
        )
        assert result.routing_type == "framework_container"

    def test_bsi_grundschutz_detected(self):
        result = classify_routing(
            obligation_text="BSI IT-Grundschutz Massnahmen umsetzen",
            action_raw="umsetzen",
            object_raw="BSI IT-Grundschutz Massnahmen",
        )
        assert result.routing_type == "framework_container"


# ---------------------------------------------------------------------------
# FRAMEWORK DETECTION TESTS
# ---------------------------------------------------------------------------


class TestFrameworkDetection:

    def test_detect_csa_ccm_with_domain(self):
        result = _detect_framework(
            "CCM-Praktiken fuer AIS implementieren",
            "CCM-Praktiken",
        )
        assert result.routing_type == "framework_container"
        assert result.framework_ref == "CSA_CCM"
        assert result.framework_domain == "AIS"

    def test_detect_nist_without_domain(self):
        result = _detect_framework(
            "NIST SP 800-53 Kontrollen implementieren",
            "Kontrollen",
        )
        assert result.routing_type == "framework_container"
        assert result.framework_ref == "NIST_SP800_53"

    def test_no_framework_in_simple_text(self):
        result = _detect_framework(
            "Passwortrichtlinie dokumentieren",
            "Passwortrichtlinie",
        )
        assert result.routing_type == "atomic"

    def test_csa_ccm_iam_domain(self):
        result = _detect_framework(
            "CSA CCM Identity and Access Management Kontrollen",
            "IAM-Kontrollen",
        )
        assert result.routing_type == "framework_container"
        assert result.framework_ref == "CSA_CCM"
        assert result.framework_domain == "IAM"


# ---------------------------------------------------------------------------
# DOMAIN MATCHING TESTS
# ---------------------------------------------------------------------------


class TestDomainMatching:

    def test_match_ais_by_id(self):
        reg = get_registry()
        ccm = reg["CSA_CCM"]
        domain_id, title = _match_domain("AIS-Kontrollen implementieren", ccm)
        assert domain_id == "AIS"

    def test_match_by_full_title(self):
        reg = get_registry()
        ccm = reg["CSA_CCM"]
        domain_id, title = _match_domain(
            "Application and Interface Security Massnahmen", ccm,
        )
        assert domain_id == "AIS"

    def test_match_nist_incident_response(self):
        reg = get_registry()
        nist = reg["NIST_SP800_53"]
        domain_id, title = _match_domain(
            "Vorfallreaktionsverfahren gemaess NIST IR", nist,
        )
        assert domain_id == "IR"

    def test_no_match_generic_text(self):
        reg = get_registry()
        nist = reg["NIST_SP800_53"]
        domain_id, title = _match_domain("etwas Allgemeines", nist)
        assert domain_id is None


# ---------------------------------------------------------------------------
# SUBCONTROL SELECTION TESTS
# ---------------------------------------------------------------------------


class TestSubcontrolSelection:

    def test_keyword_based_selection(self):
        subcontrols = [
            {"subcontrol_id": "SC-1", "title": "X", "keywords": ["api", "schnittstelle"], "object_hint": ""},
            {"subcontrol_id": "SC-2", "title": "Y", "keywords": ["backup", "sicherung"], "object_hint": ""},
        ]
        selected = _select_subcontrols("API-Schnittstellen schuetzen", subcontrols)
        assert len(selected) == 1
        assert selected[0]["subcontrol_id"] == "SC-1"

    def test_no_keyword_match_returns_empty(self):
        subcontrols = [
            {"subcontrol_id": "SC-1", "keywords": ["backup"], "title": "Backup", "object_hint": ""},
        ]
        selected = _select_subcontrols("Passwort aendern", subcontrols)
        assert selected == []

    def test_title_match_boosts_score(self):
        subcontrols = [
            {"subcontrol_id": "SC-1", "title": "Password Security", "keywords": ["passwort"], "object_hint": ""},
            {"subcontrol_id": "SC-2", "title": "Network Security", "keywords": ["netzwerk"], "object_hint": ""},
        ]
        selected = _select_subcontrols("Password Security muss implementiert werden", subcontrols)
        assert len(selected) >= 1
        assert selected[0]["subcontrol_id"] == "SC-1"


# ---------------------------------------------------------------------------
# DECOMPOSITION TESTS
# ---------------------------------------------------------------------------


class TestDecomposeFrameworkContainer:

    def test_decompose_ccm_ais(self):
        result = decompose_framework_container(
            obligation_candidate_id="OBL-001",
            parent_control_id="COMP-001",
            obligation_text="Die CCM-Praktiken fuer AIS muessen implementiert werden",
            framework_ref="CSA_CCM",
            framework_domain="AIS",
        )
        assert result.release_state == "decomposed"
        assert result.framework_ref == "CSA_CCM"
        assert result.framework_domain == "AIS"
        assert len(result.decomposed_obligations) >= 3
        assert len(result.matched_subcontrols) >= 3

    def test_decomposed_obligations_have_ids(self):
        result = decompose_framework_container(
            obligation_candidate_id="OBL-001",
            parent_control_id="COMP-001",
            obligation_text="CCM-Praktiken fuer AIS",
            framework_ref="CSA_CCM",
            framework_domain="AIS",
        )
        for d in result.decomposed_obligations:
            assert d.obligation_candidate_id.startswith("OBL-001-AIS-")
            assert d.parent_control_id == "COMP-001"
            assert d.source_ref_law == "Cloud Security Alliance CCM v4"
            assert d.routing_type == "atomic"
            assert d.release_state == "decomposed"

    def test_decomposed_have_action_and_object(self):
        result = decompose_framework_container(
            obligation_candidate_id="OBL-002",
            parent_control_id="COMP-002",
            obligation_text="CSA CCM AIS Massnahmen implementieren",
            framework_ref="CSA_CCM",
            framework_domain="AIS",
        )
        for d in result.decomposed_obligations:
            assert d.action_raw, f"{d.subcontrol_id} missing action_raw"
            assert d.object_raw, f"{d.subcontrol_id} missing object_raw"

    def test_unknown_framework_returns_unmatched(self):
        result = decompose_framework_container(
            obligation_candidate_id="OBL-003",
            parent_control_id="COMP-003",
            obligation_text="XYZ-Framework Controls",
            framework_ref="NONEXISTENT",
            framework_domain="ABC",
        )
        assert result.release_state == "unmatched"
        assert any("framework_not_matched" in i for i in result.issues)
        assert len(result.decomposed_obligations) == 0

    def test_unknown_domain_falls_back_to_full(self):
        result = decompose_framework_container(
            obligation_candidate_id="OBL-004",
            parent_control_id="COMP-004",
            obligation_text="CSA CCM Kontrollen implementieren",
            framework_ref="CSA_CCM",
            framework_domain=None,
        )
        # Should still decompose (falls back to keyword match or all domains)
        assert result.release_state in ("decomposed", "unmatched")

    def test_nist_incident_response_decomposition(self):
        result = decompose_framework_container(
            obligation_candidate_id="OBL-010",
            parent_control_id="COMP-010",
            obligation_text="NIST SP 800-53 Vorfallreaktionsmassnahmen implementieren",
            framework_ref="NIST_SP800_53",
            framework_domain="IR",
        )
        assert result.release_state == "decomposed"
        assert len(result.decomposed_obligations) >= 3
        sc_ids = [d.subcontrol_id for d in result.decomposed_obligations]
        assert any("IR-" in sc for sc in sc_ids)

    def test_confidence_high_with_full_match(self):
        result = decompose_framework_container(
            obligation_candidate_id="OBL-005",
            parent_control_id="COMP-005",
            obligation_text="CSA CCM AIS",
            framework_ref="CSA_CCM",
            framework_domain="AIS",
        )
        assert result.decomposition_confidence >= 0.7

    def test_confidence_low_without_framework(self):
        result = decompose_framework_container(
            obligation_candidate_id="OBL-006",
            parent_control_id="COMP-006",
            obligation_text="Unbekannte Massnahmen",
            framework_ref=None,
            framework_domain=None,
        )
        assert result.decomposition_confidence <= 0.3


# ---------------------------------------------------------------------------
# COMPOUND DETECTION TESTS
# ---------------------------------------------------------------------------


class TestCompoundDetection:

    def test_compound_verb(self):
        assert _is_compound_obligation(
            "erstellen und schulen",
            "Richtlinie erstellen und Schulungen durchfuehren",
        )

    def test_no_split_phrase(self):
        assert not _is_compound_obligation(
            "dokumentieren und pflegen",
            "Richtlinie dokumentieren und pflegen",
        )

    def test_no_split_define_and_maintain(self):
        assert not _is_compound_obligation(
            "define and maintain",
            "Define and maintain a security policy",
        )

    def test_single_verb_not_compound(self):
        assert not _is_compound_obligation(
            "implementieren",
            "MFA implementieren",
        )

    def test_empty_action_not_compound(self):
        assert not _is_compound_obligation("", "something")


# ---------------------------------------------------------------------------
# FRAMEWORK KEYWORD TESTS
# ---------------------------------------------------------------------------


class TestFrameworkKeywords:

    def test_two_keywords_detected(self):
        assert _has_framework_keywords("Framework-Praktiken implementieren")

    def test_single_keyword_not_enough(self):
        assert not _has_framework_keywords("Praktiken implementieren")

    def test_no_keywords(self):
        assert not _has_framework_keywords("MFA einrichten")


# ---------------------------------------------------------------------------
# INFERENCE HELPER TESTS
# ---------------------------------------------------------------------------


class TestInferAction:

    def test_infer_implementieren(self):
        assert _infer_action("Massnahmen muessen implementiert werden") == "implementieren"

    def test_infer_dokumentieren(self):
        assert _infer_action("Richtlinie muss dokumentiert werden") == "dokumentieren"

    def test_infer_testen(self):
        assert _infer_action("System wird getestet") == "testen"

    def test_infer_ueberwachen(self):
        assert _infer_action("Logs werden ueberwacht") == "ueberwachen"

    def test_infer_default(self):
        assert _infer_action("etwas passiert") == "implementieren"


class TestInferObject:

    def test_infer_from_muessen_pattern(self):
        result = _infer_object("Zugriffsrechte muessen ueberprueft werden")
        assert "ueberprueft" in result or "Zugriffsrechte" in result

    def test_infer_fallback(self):
        result = _infer_object("Einfacher Satz ohne Modalverb")
        assert len(result) > 0
