"""
Applicability Use Case Tests — Demo test package for control assignment.

Loads 24 use cases from demo_cases.yaml and validates structure,
consistency, and (when DB available) actual control assignment.

Categories:
  A. Standard (6) — Branchen-Default korrekt?
  B. Scope schlaegt Branche (6) — Sonderfaelle additiv?
  C. Falsche Freunde / Negativ (6) — Keine Falschzuweisung?
  D. Eskalation (6) — Unsicherheit erkannt?

Run: pytest tests/test_applicability_use_cases.py -v
"""

import os
import pytest
import yaml

# Load demo cases from YAML
CASES_PATH = os.path.join(os.path.dirname(__file__), "demo_cases.yaml")
with open(CASES_PATH) as f:
    DEMO_CASES = yaml.safe_load(f)

CASE_IDS = [c["id"] for c in DEMO_CASES]


# ---------------------------------------------------------------------------
# Structure Tests (always run)
# ---------------------------------------------------------------------------

class TestDemoCaseStructure:
    """Verify demo cases are well-formed."""

    def test_case_count(self):
        assert len(DEMO_CASES) == 24, f"Expected 24 cases, got {len(DEMO_CASES)}"

    def test_category_distribution(self):
        cats = [c.get("category", "unknown") for c in DEMO_CASES]
        assert cats.count("standard") == 6
        assert cats.count("scope_beats_sector") == 6
        assert cats.count("false_friends") == 6
        assert cats.count("escalation") == 6

    @pytest.mark.parametrize("case", DEMO_CASES, ids=CASE_IDS)
    def test_required_fields(self, case):
        assert case.get("id"), "Missing id"
        assert case.get("title"), "Missing title"
        assert case.get("category"), "Missing category"
        assert case.get("goal"), "Missing goal"
        assert case.get("company_profile"), "Missing company_profile"
        assert case.get("facts"), "Missing facts"
        assert case.get("expected"), "Missing expected"

    @pytest.mark.parametrize("case", DEMO_CASES, ids=CASE_IDS)
    def test_expected_has_escalation(self, case):
        expected = case["expected"]
        assert "escalation_expected" in expected, f"{case['id']}: Missing escalation_expected"

    @pytest.mark.parametrize("case", DEMO_CASES, ids=CASE_IDS)
    def test_escalation_has_reason(self, case):
        expected = case["expected"]
        if expected.get("escalation_expected"):
            assert expected.get("escalation_reason"), \
                f"{case['id']}: escalation_expected=true but no escalation_reason"

    @pytest.mark.parametrize("case", DEMO_CASES, ids=CASE_IDS)
    def test_no_overlap_include_exclude(self, case):
        expected = case["expected"]
        include = set(expected.get("applicable_controls_should_include", []))
        exclude = set(expected.get("applicable_controls_should_not_include", []))
        overlap = include & exclude
        assert not overlap, f"{case['id']}: Overlap in include/exclude: {overlap}"

    @pytest.mark.parametrize("case", DEMO_CASES, ids=CASE_IDS)
    def test_confidence_range(self, case):
        conf = case["expected"].get("confidence")
        if conf is not None:
            assert 0.0 <= conf <= 1.0, f"{case['id']}: confidence {conf} out of range"

    def test_negative_test_ratio(self):
        """At least 40% of cases should test non-assignment (R5)."""
        cases_with_not_include = sum(
            1 for c in DEMO_CASES
            if c["expected"].get("applicable_controls_should_not_include")
        )
        ratio = cases_with_not_include / len(DEMO_CASES)
        assert ratio >= 0.4, f"Only {ratio:.0%} cases have must_not_include (need >= 40%)"

    def test_unique_ids(self):
        assert len(CASE_IDS) == len(set(CASE_IDS)), "Duplicate case IDs found"


# ---------------------------------------------------------------------------
# Acceptance Rule Tests (always run)
# ---------------------------------------------------------------------------

class TestAcceptanceRules:
    """Verify acceptance rules are encoded in the test data."""

    def test_r1_no_false_certainty(self):
        """R1: Escalation cases must not have high confidence."""
        for case in DEMO_CASES:
            if case["expected"].get("escalation_expected"):
                conf = case["expected"].get("confidence", 1.0)
                assert conf < 0.80, \
                    f"{case['id']}: escalation_expected but confidence={conf} (should be < 0.80)"

    def test_r2_scope_beats_sector(self):
        """R2: scope_beats_sector cases must have scope_triggers."""
        for case in DEMO_CASES:
            if case.get("category") == "scope_beats_sector":
                triggers = case["expected"].get("scope_triggers", [])
                assert triggers, f"{case['id']}: scope_beats_sector but no scope_triggers"

    def test_r3_repo_signals_not_sufficient(self):
        """R3: Cases with repo_signals should not auto-assign heavy regulation."""
        for case in DEMO_CASES:
            if case.get("repo_signals") and not case["expected"].get("escalation_expected"):
                # If repo signals exist but no escalation, PSD2 etc. must NOT be included
                include = case["expected"].get("applicable_controls_should_include", [])
                for ctrl in include:
                    assert "PSD2" not in ctrl, \
                        f"{case['id']}: repo_signals + PSD2 included without escalation (R3)"

    def test_r4_standard_first(self):
        """R4: Standard cases should have applicable_industries set."""
        for case in DEMO_CASES:
            if case.get("category") == "standard":
                industries = case["expected"].get("applicable_industries")
                assert industries, f"{case['id']}: standard case without applicable_industries"


# ---------------------------------------------------------------------------
# Integration Tests (require DB + Applicability Engine)
# ---------------------------------------------------------------------------

@pytest.mark.skipif(
    not os.getenv("DATABASE_URL"),
    reason="Requires DATABASE_URL env var pointing to a running PostgreSQL with compliance schema",
)
class TestApplicabilityIntegration:
    """Run use cases against the real Applicability Engine.

    Enable by setting DATABASE_URL env var.
    Calls the real get_applicable_controls() from services/applicability_engine.py
    and checks results against must_match / must_not_match from demo_cases.yaml.

    The checks use source_citation->>'source' to identify which regulation
    a control comes from (e.g. "TKG", "DSGVO", "NIS2"), since our real
    control_ids use domain prefixes (SEC-001, DATA-002) not regulation names.
    """

    @pytest.fixture(autouse=True)
    def setup_db(self):
        from db.session import SessionLocal
        self.db = SessionLocal()
        yield
        self.db.close()

    @pytest.mark.parametrize("case", DEMO_CASES, ids=CASE_IDS)
    def test_applicability(self, case):
        from services.applicability_engine import get_applicable_controls

        # Map scope_answers booleans to list of active signals
        scope_answers = case.get("scope_answers", {})
        active_signals = [k for k, v in scope_answers.items() if v is True]

        result = get_applicable_controls(
            db=self.db,
            industry=case["company_profile"].get("sector"),
            company_size=case["company_profile"].get("size", "medium"),
            scope_signals=active_signals if active_signals else None,
            limit=10000,
        )

        # Collect all source names and control domains from results
        sources = set()
        domains = set()
        for ctrl in result["controls"]:
            # source from source_citation
            sc = ctrl.get("source_citation")
            if isinstance(sc, str):
                try:
                    import json as _json
                    sc = _json.loads(sc)
                except Exception:
                    sc = {}
            if isinstance(sc, dict):
                src = sc.get("source", "")
                if src:
                    sources.add(src)
            # domain from control_id prefix
            cid = ctrl.get("control_id", "")
            if cid:
                domains.add(cid.split("-")[0].upper())

        all_labels = sources | domains

        # Check must_match: at least one control from this regulation/domain exists
        for required in case["expected"].get("applicable_controls_should_include", []):
            matched = any(required.lower() in label.lower() for label in all_labels)
            # Also check in control titles as fallback
            if not matched:
                matched = any(
                    required.lower() in (ctrl.get("title", "") or "").lower()
                    for ctrl in result["controls"]
                )
            assert matched, (
                f"{case['id']}: Expected controls related to '{required}' "
                f"but not found. Sources: {sorted(sources)[:20]}, "
                f"Domains: {sorted(domains)[:20]}, "
                f"Total: {result['total_applicable']}"
            )

        # Check must_not_match: no control from this regulation/domain
        for forbidden in case["expected"].get("applicable_controls_should_not_include", []):
            found = any(forbidden.lower() in label.lower() for label in all_labels)
            if found:
                # Double check in titles too
                matching_titles = [
                    ctrl.get("control_id", "") + ": " + (ctrl.get("title", "") or "")
                    for ctrl in result["controls"]
                    if forbidden.lower() in (ctrl.get("title", "") or "").lower()
                ]
                assert False, (
                    f"{case['id']}: Controls related to '{forbidden}' should NOT "
                    f"be assigned but found in sources/domains. "
                    f"Examples: {matching_titles[:5]}"
                )
