"""Tests for MCP Compliance Validator."""

from __future__ import annotations

import json
import tempfile
from pathlib import Path

import pytest

from mcpaudit.compliance_validator import (
    ComplianceCheck,
    ComplianceFramework,
    ComplianceResult,
    ControlResult,
    ControlStatus,
    _evaluate_control,
    _extract_findings,
    _nist_ai_checks,
    _owasp_agentic_checks,
    _soc2_ai_checks,
    format_compliance_html,
    format_compliance_json,
    format_compliance_text,
    get_framework_checks,
    list_frameworks,
    validate_compliance,
    validate_compliance_from_file,
)


# --- ComplianceFramework ---


class TestComplianceFramework:
    def test_all_values(self):
        assert ComplianceFramework.OWASP_AGENTIC.value == "owasp"
        assert ComplianceFramework.NIST_AI.value == "nist"
        assert ComplianceFramework.SOC2_AI.value == "soc2"

    def test_from_string_owasp(self):
        assert ComplianceFramework.from_string("owasp") == ComplianceFramework.OWASP_AGENTIC
        assert ComplianceFramework.from_string("OWASP") == ComplianceFramework.OWASP_AGENTIC
        assert ComplianceFramework.from_string("owasp-agentic") == ComplianceFramework.OWASP_AGENTIC
        assert ComplianceFramework.from_string("owasp_agentic") == ComplianceFramework.OWASP_AGENTIC

    def test_from_string_nist(self):
        assert ComplianceFramework.from_string("nist") == ComplianceFramework.NIST_AI
        assert ComplianceFramework.from_string("nist-ai") == ComplianceFramework.NIST_AI

    def test_from_string_soc2(self):
        assert ComplianceFramework.from_string("soc2") == ComplianceFramework.SOC2_AI
        assert ComplianceFramework.from_string("soc2-ai") == ComplianceFramework.SOC2_AI

    def test_from_string_invalid(self):
        with pytest.raises(ValueError, match="Unknown framework"):
            ComplianceFramework.from_string("invalid")

    def test_enum_iteration(self):
        frameworks = list(ComplianceFramework)
        assert len(frameworks) == 3


# --- ControlStatus ---


class TestControlStatus:
    def test_values(self):
        assert ControlStatus.PASS.value == "PASS"
        assert ControlStatus.FAIL.value == "FAIL"
        assert ControlStatus.NOT_APPLICABLE.value == "N/A"
        assert ControlStatus.PARTIAL.value == "PARTIAL"


# --- ComplianceCheck ---


class TestComplianceCheck:
    def test_creation(self):
        check = ComplianceCheck(
            control_id="TEST-01",
            name="Test Check",
            description="A test compliance check",
            how_to_validate="Run the test",
            scanner_rules=["CMD-001"],
        )
        assert check.control_id == "TEST-01"
        assert check.scanner_rules == ["CMD-001"]

    def test_to_dict(self):
        check = ComplianceCheck(
            control_id="TC-01",
            name="Test",
            description="Desc",
            how_to_validate="Validate",
            scanner_rules=["A", "B"],
            severity="CRITICAL",
        )
        d = check.to_dict()
        assert d["control_id"] == "TC-01"
        assert d["scanner_rules"] == ["A", "B"]
        assert d["severity"] == "CRITICAL"

    def test_default_severity(self):
        check = ComplianceCheck(
            control_id="X",
            name="X",
            description="X",
            how_to_validate="X",
        )
        assert check.severity == "HIGH"


# --- ControlResult ---


class TestControlResult:
    def test_pass_result(self):
        check = ComplianceCheck("C1", "Check", "Desc", "Validate")
        result = ControlResult(check=check, status=ControlStatus.PASS)
        d = result.to_dict()
        assert d["status"] == "PASS"
        assert d["findings_count"] == 0

    def test_fail_result(self):
        check = ComplianceCheck("C2", "Check", "Desc", "Validate")
        result = ControlResult(
            check=check,
            status=ControlStatus.FAIL,
            findings_count=3,
            notes="3 violations",
        )
        d = result.to_dict()
        assert d["status"] == "FAIL"
        assert d["findings_count"] == 3


# --- ComplianceResult ---


class TestComplianceResult:
    def test_empty_result(self):
        result = ComplianceResult(framework=ComplianceFramework.OWASP_AGENTIC)
        assert result.total_controls == 0
        assert result.passed == 0
        assert result.failed == 0
        assert result.compliance_score == 100.0
        assert result.overall_status == "COMPLIANT"

    def test_all_pass(self):
        checks = [
            ComplianceCheck("C1", "A", "D", "V"),
            ComplianceCheck("C2", "B", "D", "V"),
        ]
        controls = [
            ControlResult(check=c, status=ControlStatus.PASS) for c in checks
        ]
        result = ComplianceResult(
            framework=ComplianceFramework.NIST_AI,
            controls=controls,
        )
        assert result.passed == 2
        assert result.failed == 0
        assert result.compliance_score == 100.0
        assert result.overall_status == "COMPLIANT"

    def test_all_fail(self):
        checks = [
            ComplianceCheck("C1", "A", "D", "V"),
            ComplianceCheck("C2", "B", "D", "V"),
        ]
        controls = [
            ControlResult(check=c, status=ControlStatus.FAIL) for c in checks
        ]
        result = ComplianceResult(
            framework=ComplianceFramework.SOC2_AI,
            controls=controls,
        )
        assert result.failed == 2
        assert result.compliance_score == 0.0
        assert result.overall_status == "NON_COMPLIANT"

    def test_partial_compliance(self):
        checks = [
            ComplianceCheck("C1", "A", "D", "V"),
            ComplianceCheck("C2", "B", "D", "V"),
            ComplianceCheck("C3", "C", "D", "V"),
            ComplianceCheck("C4", "D", "D", "V"),
            ComplianceCheck("C5", "E", "D", "V"),
        ]
        controls = [
            ControlResult(check=checks[0], status=ControlStatus.PASS),
            ControlResult(check=checks[1], status=ControlStatus.PASS),
            ControlResult(check=checks[2], status=ControlStatus.PASS),
            ControlResult(check=checks[3], status=ControlStatus.PASS),
            ControlResult(check=checks[4], status=ControlStatus.FAIL),
        ]
        result = ComplianceResult(
            framework=ComplianceFramework.OWASP_AGENTIC,
            controls=controls,
        )
        assert result.compliance_score == 80.0
        assert result.overall_status == "PARTIALLY_COMPLIANT"

    def test_na_excluded_from_score(self):
        checks = [
            ComplianceCheck("C1", "A", "D", "V"),
            ComplianceCheck("C2", "B", "D", "V"),
        ]
        controls = [
            ControlResult(check=checks[0], status=ControlStatus.PASS),
            ControlResult(check=checks[1], status=ControlStatus.NOT_APPLICABLE),
        ]
        result = ComplianceResult(
            framework=ComplianceFramework.NIST_AI,
            controls=controls,
        )
        assert result.compliance_score == 100.0

    def test_partial_counts_half(self):
        checks = [
            ComplianceCheck("C1", "A", "D", "V"),
            ComplianceCheck("C2", "B", "D", "V"),
        ]
        controls = [
            ControlResult(check=checks[0], status=ControlStatus.PARTIAL),
            ControlResult(check=checks[1], status=ControlStatus.FAIL),
        ]
        result = ComplianceResult(
            framework=ComplianceFramework.SOC2_AI,
            controls=controls,
        )
        # 0.5 / 2 = 25%
        assert result.compliance_score == 25.0

    def test_to_dict(self):
        result = ComplianceResult(
            framework=ComplianceFramework.OWASP_AGENTIC,
        )
        d = result.to_dict()
        assert d["framework"] == "owasp"
        assert "summary" in d
        assert "controls" in d


# --- Framework Definitions ---


class TestFrameworkDefinitions:
    def test_owasp_has_10_controls(self):
        checks = _owasp_agentic_checks()
        assert len(checks) == 10

    def test_nist_has_6_controls(self):
        checks = _nist_ai_checks()
        assert len(checks) == 6

    def test_soc2_has_6_controls(self):
        checks = _soc2_ai_checks()
        assert len(checks) == 6

    def test_owasp_control_ids(self):
        checks = _owasp_agentic_checks()
        for c in checks:
            assert c.control_id.startswith("OWASP-")

    def test_nist_control_ids(self):
        checks = _nist_ai_checks()
        for c in checks:
            assert c.control_id.startswith("NIST-")

    def test_soc2_control_ids(self):
        checks = _soc2_ai_checks()
        for c in checks:
            assert c.control_id.startswith("SOC2-")

    def test_all_controls_have_rules(self):
        for fw in ComplianceFramework:
            checks = get_framework_checks(fw)
            for c in checks:
                assert len(c.scanner_rules) > 0, (
                    f"{c.control_id} has no scanner rules"
                )

    def test_all_controls_have_descriptions(self):
        for fw in ComplianceFramework:
            checks = get_framework_checks(fw)
            for c in checks:
                assert len(c.description) > 10

    def test_get_framework_checks(self):
        checks = get_framework_checks(ComplianceFramework.OWASP_AGENTIC)
        assert len(checks) == 10

    def test_unique_control_ids_per_framework(self):
        for fw in ComplianceFramework:
            checks = get_framework_checks(fw)
            ids = [c.control_id for c in checks]
            assert len(ids) == len(set(ids))


# --- _extract_findings ---


class TestExtractFindings:
    def test_extracts_from_results(self):
        scan_data = {
            "results": [
                {"file": "a.py", "findings": [{"rule_id": "CMD-001"}]},
                {"file": "b.py", "findings": [{"rule_id": "INJ-001"}]},
            ],
        }
        findings = _extract_findings(scan_data)
        assert len(findings) == 2

    def test_empty_results(self):
        findings = _extract_findings({"results": []})
        assert len(findings) == 0

    def test_no_results_key(self):
        findings = _extract_findings({})
        assert len(findings) == 0


# --- _evaluate_control ---


class TestEvaluateControl:
    def test_pass_no_findings(self):
        check = ComplianceCheck("C1", "Check", "D", "V", scanner_rules=["CMD-001"])
        result = _evaluate_control(check, [])
        assert result.status == ControlStatus.PASS

    def test_fail_matching_finding(self):
        check = ComplianceCheck("C1", "Check", "D", "V", scanner_rules=["CMD-001"])
        findings = [{"rule_id": "CMD-001", "title": "Inj", "file": "f.py", "line": 1}]
        result = _evaluate_control(check, findings)
        assert result.status == ControlStatus.FAIL

    def test_pass_no_matching_rules(self):
        check = ComplianceCheck("C1", "Check", "D", "V", scanner_rules=["AUTH-001"])
        findings = [{"rule_id": "CMD-001", "title": "Inj", "file": "f.py", "line": 1}]
        result = _evaluate_control(check, findings)
        assert result.status == ControlStatus.PASS

    def test_partial_some_rules_match(self):
        check = ComplianceCheck(
            "C1", "Check", "D", "V",
            scanner_rules=["CMD-001", "CMD-002", "CMD-003"],
        )
        findings = [
            {"rule_id": "CMD-001", "title": "Inj", "file": "f.py", "line": 1},
        ]
        result = _evaluate_control(check, findings)
        assert result.status == ControlStatus.PARTIAL

    def test_finding_details_populated(self):
        check = ComplianceCheck("C1", "Check", "D", "V", scanner_rules=["CMD-001"])
        findings = [
            {
                "rule_id": "CMD-001",
                "title": "Command Injection",
                "file": "server.py",
                "line": 42,
                "severity": "HIGH",
            },
        ]
        result = _evaluate_control(check, findings)
        assert result.findings_count == 1
        assert result.finding_details[0]["rule_id"] == "CMD-001"
        assert result.finding_details[0]["line"] == 42


# --- validate_compliance ---


class TestValidateCompliance:
    def _clean_scan(self) -> dict:
        return {"results": [{"file": "clean.py", "findings": []}]}

    def _dirty_scan(self) -> dict:
        return {
            "results": [
                {
                    "file": "server.py",
                    "findings": [
                        {"rule_id": "CMD-001", "title": "CI", "file": "server.py",
                         "line": 10, "severity": "HIGH"},
                        {"rule_id": "AUTH-001", "title": "No auth", "file": "server.py",
                         "line": 20, "severity": "CRITICAL"},
                        {"rule_id": "INJ-001", "title": "Inj", "file": "server.py",
                         "line": 30, "severity": "CRITICAL"},
                    ],
                },
            ],
        }

    def test_clean_scan_all_pass(self):
        result = validate_compliance(self._clean_scan(), ComplianceFramework.OWASP_AGENTIC)
        assert result.compliance_score == 100.0
        assert result.overall_status == "COMPLIANT"

    def test_dirty_scan_has_failures(self):
        result = validate_compliance(self._dirty_scan(), ComplianceFramework.OWASP_AGENTIC)
        assert result.failed > 0 or result.partial > 0

    def test_all_frameworks(self):
        scan = self._clean_scan()
        for fw in ComplianceFramework:
            result = validate_compliance(scan, fw)
            assert result.framework == fw
            assert result.total_controls > 0

    def test_dirty_nist(self):
        result = validate_compliance(self._dirty_scan(), ComplianceFramework.NIST_AI)
        assert result.total_controls == 6

    def test_dirty_soc2(self):
        result = validate_compliance(self._dirty_scan(), ComplianceFramework.SOC2_AI)
        assert result.total_controls == 6


# --- validate_compliance_from_file ---


class TestValidateComplianceFromFile:
    def test_from_file(self, tmp_path):
        scan_data = {"results": [{"file": "a.py", "findings": []}]}
        scan_file = tmp_path / "scan.json"
        scan_file.write_text(json.dumps(scan_data))

        result = validate_compliance_from_file(scan_file, ComplianceFramework.OWASP_AGENTIC)
        assert result.compliance_score == 100.0
        assert result.scan_file == str(scan_file)

    def test_from_file_with_findings(self, tmp_path):
        scan_data = {
            "results": [
                {
                    "file": "x.py",
                    "findings": [
                        {"rule_id": "CMD-001", "title": "CI", "file": "x.py",
                         "line": 1, "severity": "HIGH"},
                    ],
                },
            ],
        }
        scan_file = tmp_path / "scan2.json"
        scan_file.write_text(json.dumps(scan_data))

        result = validate_compliance_from_file(scan_file, ComplianceFramework.OWASP_AGENTIC)
        assert result.failed > 0 or result.partial > 0


# --- Formatting ---


class TestFormatting:
    def _make_result(self) -> ComplianceResult:
        checks = [
            ComplianceCheck("C1", "Auth", "D", "V", ["AUTH-001"]),
            ComplianceCheck("C2", "Inj", "D", "V", ["INJ-001"]),
        ]
        return ComplianceResult(
            framework=ComplianceFramework.OWASP_AGENTIC,
            controls=[
                ControlResult(checks[0], ControlStatus.PASS),
                ControlResult(
                    checks[1], ControlStatus.FAIL, findings_count=2,
                    finding_details=[
                        {"rule_id": "INJ-001", "title": "Injection", "file": "s.py", "line": 5},
                        {"rule_id": "INJ-001", "title": "Injection2", "file": "s.py", "line": 10},
                    ],
                    notes="2 violations",
                ),
            ],
        )

    def test_format_text(self):
        result = self._make_result()
        text = format_compliance_text(result)
        assert "Compliance Report" in text
        assert "[PASS]" in text
        assert "[FAIL]" in text
        assert "OWASP" in text

    def test_format_json(self):
        result = self._make_result()
        j = format_compliance_json(result)
        data = json.loads(j)
        assert data["framework"] == "owasp"
        assert len(data["controls"]) == 2

    def test_format_html(self):
        result = self._make_result()
        html = format_compliance_html(result)
        assert "<html>" in html
        assert "Compliance Report" in html
        assert "OWASP" in html

    def test_format_text_empty(self):
        result = ComplianceResult(framework=ComplianceFramework.NIST_AI)
        text = format_compliance_text(result)
        assert "NIST" in text
        assert "COMPLIANT" in text

    def test_format_html_colors(self):
        result = self._make_result()
        html = format_compliance_html(result)
        assert "#28a745" in html or "#dc3545" in html

    def test_format_text_many_findings(self):
        check = ComplianceCheck("C1", "Check", "D", "V", ["R1"])
        details = [
            {"rule_id": "R1", "title": f"F{i}", "file": "f.py", "line": i}
            for i in range(10)
        ]
        ctrl = ControlResult(
            check=check, status=ControlStatus.FAIL,
            findings_count=10, finding_details=details,
            notes="10 violations",
        )
        result = ComplianceResult(
            framework=ComplianceFramework.SOC2_AI,
            controls=[ctrl],
        )
        text = format_compliance_text(result)
        assert "... and 7 more" in text


# --- list_frameworks ---


class TestListFrameworks:
    def test_lists_all(self):
        fws = list_frameworks()
        assert len(fws) == 3

    def test_each_has_controls(self):
        for fw in list_frameworks():
            assert fw["controls"] > 0
            assert "id" in fw
            assert "name" in fw
