"""Tests for OWASP Agentic Top 10 2026 Compliance Report.

Validates the compliance module: category mapping, status computation,
report generation, HTML output, and CLI integration.
"""

from __future__ import annotations

import tempfile
from pathlib import Path

import pytest

from mcpaudit.compliance import (
    OWASP_AGENTIC_TOP10,
    OWASP_BY_ID,
    CategoryStatus,
    ComplianceReport,
    OWASPCategory,
    build_compliance_report,
    generate_compliance_html,
)
from mcpaudit.models import Finding, ScanResult, ScanSummary, Severity


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_finding(
    rule_id: str = "CMD-001",
    severity: Severity = Severity.CRITICAL,
    title: str = "Test Finding",
    description: str = "Test description",
    file: str = "test.py",
    line: int = 1,
    code_snippet: str = "",
    recommendation: str = "",
) -> Finding:
    """Create a Finding with defaults for testing."""
    return Finding(
        rule_id=rule_id,
        severity=severity,
        title=title,
        description=description,
        file=file,
        line=line,
        code_snippet=code_snippet,
        recommendation=recommendation,
    )


def _make_summary(findings: list[Finding] | None = None) -> ScanSummary:
    """Create a ScanSummary from a list of findings."""
    if findings is None:
        findings = []
    result = ScanResult(
        file="test_project/server.py",
        findings=findings,
        lines_scanned=200,
    )
    return ScanSummary(results=[result])


# ===========================================================================
# 1. OWASP Category Definitions
# ===========================================================================

class TestOWASPCategories:
    """Tests for OWASP Agentic Top 10 category definitions."""

    def test_exactly_10_categories(self):
        assert len(OWASP_AGENTIC_TOP10) == 10

    def test_categories_ranked_1_to_10(self):
        ranks = [c.rank for c in OWASP_AGENTIC_TOP10]
        assert ranks == list(range(1, 11))

    def test_all_categories_have_rule_ids(self):
        for cat in OWASP_AGENTIC_TOP10:
            assert len(cat.rule_ids) > 0, f"{cat.id} has no rule_ids"

    def test_category_ids_unique(self):
        ids = [c.id for c in OWASP_AGENTIC_TOP10]
        assert len(ids) == len(set(ids))

    def test_owasp_by_id_index(self):
        assert len(OWASP_BY_ID) == 10
        assert "OWASP-A01" in OWASP_BY_ID
        assert OWASP_BY_ID["OWASP-A01"].name == "Prompt Injection"

    def test_category_to_dict(self):
        cat = OWASP_AGENTIC_TOP10[0]
        d = cat.to_dict()
        assert d["rank"] == 1
        assert d["id"] == "OWASP-A01"
        assert "rule_ids" in d
        assert isinstance(d["rule_ids"], list)


# ===========================================================================
# 2. CategoryStatus Logic
# ===========================================================================

class TestCategoryStatus:
    """Tests for per-category compliance status."""

    def test_pass_when_no_findings(self):
        cat = OWASP_AGENTIC_TOP10[0]
        status = CategoryStatus(category=cat)
        assert status.status == "PASS"

    def test_fail_when_many_rules_triggered(self):
        cat = OWASP_AGENTIC_TOP10[1]  # Insecure Tool Impl (9 rule_ids)
        status = CategoryStatus(
            category=cat,
            findings=[
                _make_finding(rule_id="CMD-001"),
                _make_finding(rule_id="CMD-002"),
                _make_finding(rule_id="SQL-001"),
                _make_finding(rule_id="PATH-001"),
                _make_finding(rule_id="DESER-001"),
            ],
            rules_checked=9,
            rules_triggered=5,
        )
        assert status.status == "FAIL"

    def test_partial_when_few_rules_triggered(self):
        cat = OWASP_AGENTIC_TOP10[1]  # 9 rule_ids
        status = CategoryStatus(
            category=cat,
            findings=[_make_finding(rule_id="CMD-001")],
            rules_checked=9,
            rules_triggered=1,
        )
        assert status.status == "PARTIAL"

    def test_max_severity_returns_highest(self):
        cat = OWASP_AGENTIC_TOP10[0]
        status = CategoryStatus(
            category=cat,
            findings=[
                _make_finding(severity=Severity.MEDIUM),
                _make_finding(severity=Severity.CRITICAL),
                _make_finding(severity=Severity.LOW),
            ],
        )
        assert status.max_severity == Severity.CRITICAL

    def test_max_severity_none_when_no_findings(self):
        cat = OWASP_AGENTIC_TOP10[0]
        status = CategoryStatus(category=cat)
        assert status.max_severity is None

    def test_to_dict(self):
        cat = OWASP_AGENTIC_TOP10[0]
        status = CategoryStatus(
            category=cat,
            findings=[_make_finding(severity=Severity.HIGH)],
            rules_checked=2,
            rules_triggered=1,
        )
        d = status.to_dict()
        assert d["status"] == "PARTIAL"
        assert d["findings_count"] == 1
        assert d["max_severity"] == "HIGH"


# ===========================================================================
# 3. ComplianceReport Properties
# ===========================================================================

class TestComplianceReport:
    """Tests for the ComplianceReport data model."""

    def test_pass_count(self):
        summary = _make_summary()
        report = build_compliance_report(summary, project_name="test")
        # No findings => all 10 PASS
        assert report.pass_count == 10

    def test_fail_count_with_findings(self):
        findings = [
            _make_finding(rule_id="CMD-001"),
            _make_finding(rule_id="CMD-002"),
            _make_finding(rule_id="SQL-001"),
            _make_finding(rule_id="PATH-001"),
            _make_finding(rule_id="DESER-001"),
        ]
        summary = _make_summary(findings)
        report = build_compliance_report(summary, project_name="test")
        assert report.fail_count >= 1

    def test_compliance_score_100_when_clean(self):
        summary = _make_summary()
        report = build_compliance_report(summary, project_name="test")
        assert report.compliance_score == 100

    def test_compliance_score_less_than_100_with_findings(self):
        findings = [_make_finding(rule_id="CMD-001")]
        summary = _make_summary(findings)
        report = build_compliance_report(summary, project_name="test")
        assert report.compliance_score < 100

    def test_partial_count(self):
        findings = [_make_finding(rule_id="MCP-004")]
        summary = _make_summary(findings)
        report = build_compliance_report(summary, project_name="test")
        # MCP-004 maps to A01 (Prompt Injection) which has 2 rule_ids
        # Only 1 triggered => PARTIAL
        a01_status = report.categories[0]
        assert a01_status.status == "PARTIAL"
        assert report.partial_count >= 1

    def test_to_dict(self):
        summary = _make_summary()
        report = build_compliance_report(summary, project_name="test")
        d = report.to_dict()
        assert d["project_name"] == "test"
        assert d["compliance_score"] == 100
        assert len(d["categories"]) == 10

    def test_timestamp_auto_generated(self):
        summary = _make_summary()
        report = build_compliance_report(summary, project_name="test")
        assert report.timestamp
        assert "UTC" in report.timestamp


# ===========================================================================
# 4. build_compliance_report
# ===========================================================================

class TestBuildComplianceReport:
    """Tests for the build_compliance_report function."""

    def test_maps_findings_to_correct_categories(self):
        findings = [
            _make_finding(rule_id="SSRF-001"),
            _make_finding(rule_id="RATE-001"),
        ]
        summary = _make_summary(findings)
        report = build_compliance_report(summary, project_name="test")
        # SSRF-001 => A07 (SSRF)
        a07 = report.categories[6]
        assert a07.category.id == "OWASP-A07"
        assert len(a07.findings) == 1
        # RATE-001 => A09 (DoS)
        a09 = report.categories[8]
        assert a09.category.id == "OWASP-A09"
        assert len(a09.findings) == 1

    def test_finding_in_multiple_rule_ids(self):
        # CMD-001 and CMD-002 both in A02
        findings = [
            _make_finding(rule_id="CMD-001"),
            _make_finding(rule_id="CMD-002"),
        ]
        summary = _make_summary(findings)
        report = build_compliance_report(summary, project_name="test")
        a02 = report.categories[1]
        assert a02.category.id == "OWASP-A02"
        assert len(a02.findings) == 2

    def test_auto_project_name_from_file(self):
        summary = _make_summary()
        report = build_compliance_report(summary)
        # File is "test_project/server.py" => project_name = "test_project"
        assert report.project_name == "test_project"

    def test_empty_scan_summary(self):
        summary = ScanSummary(results=[])
        report = build_compliance_report(summary, project_name="empty")
        assert report.pass_count == 10
        assert report.compliance_score == 100


# ===========================================================================
# 5. HTML Generation
# ===========================================================================

class TestComplianceHTML:
    """Tests for HTML compliance report generation."""

    def test_html_valid_structure(self):
        summary = _make_summary()
        report = build_compliance_report(summary, project_name="test")
        html = generate_compliance_html(report)
        assert "<!DOCTYPE html>" in html
        assert "</html>" in html

    def test_html_contains_owasp_title(self):
        summary = _make_summary()
        report = build_compliance_report(summary, project_name="test")
        html = generate_compliance_html(report)
        assert "OWASP Agentic Top 10" in html

    def test_html_contains_project_name(self):
        summary = _make_summary()
        report = build_compliance_report(summary, project_name="MyProject")
        html = generate_compliance_html(report)
        assert "MyProject" in html

    def test_html_contains_all_10_categories(self):
        summary = _make_summary()
        report = build_compliance_report(summary, project_name="test")
        html = generate_compliance_html(report)
        for cat in OWASP_AGENTIC_TOP10:
            assert cat.name in html, f"{cat.name} not found in HTML"

    def test_html_shows_pass_badges(self):
        summary = _make_summary()
        report = build_compliance_report(summary, project_name="test")
        html = generate_compliance_html(report)
        assert "PASS" in html

    def test_html_shows_fail_badges(self):
        findings = [
            _make_finding(rule_id="CMD-001"),
            _make_finding(rule_id="CMD-002"),
            _make_finding(rule_id="SQL-001"),
            _make_finding(rule_id="PATH-001"),
            _make_finding(rule_id="DESER-001"),
        ]
        summary = _make_summary(findings)
        report = build_compliance_report(summary, project_name="test")
        html = generate_compliance_html(report)
        assert "FAIL" in html

    def test_html_escapes_xss(self):
        findings = [
            _make_finding(
                title="<script>alert(1)</script>",
                rule_id="CMD-001",
            ),
        ]
        summary = _make_summary(findings)
        report = build_compliance_report(summary, project_name="test")
        html = generate_compliance_html(report)
        assert "<script>" not in html
        assert "&lt;script&gt;" in html

    def test_html_severity_chart_section(self):
        findings = [_make_finding(severity=Severity.CRITICAL)]
        summary = _make_summary(findings)
        report = build_compliance_report(summary, project_name="test")
        html = generate_compliance_html(report)
        assert "Findings by Severity" in html
        assert "CRITICAL" in html

    def test_html_recommendations_section(self):
        findings = [_make_finding(rule_id="CMD-001")]
        summary = _make_summary(findings)
        report = build_compliance_report(summary, project_name="test")
        html = generate_compliance_html(report)
        assert "Recommendations" in html

    def test_html_no_findings_message(self):
        summary = _make_summary()
        report = build_compliance_report(summary, project_name="test")
        html = generate_compliance_html(report)
        assert "No security findings" in html

    def test_html_contains_grade(self):
        summary = _make_summary()
        report = build_compliance_report(summary, project_name="test")
        html = generate_compliance_html(report)
        assert "Grade" in html
        assert "A+" in html

    def test_html_contains_score(self):
        summary = _make_summary()
        report = build_compliance_report(summary, project_name="test")
        html = generate_compliance_html(report)
        assert "100/100" in html

    def test_html_contains_footer(self):
        summary = _make_summary()
        report = build_compliance_report(summary, project_name="test")
        html = generate_compliance_html(report)
        assert "mcpaudit" in html
        assert "footer" in html.lower()

    def test_html_code_snippet_in_top_findings(self):
        findings = [
            _make_finding(
                rule_id="CMD-001",
                code_snippet='subprocess.run(cmd, shell=True)',
                recommendation="Use subprocess.run with a list of args",
            ),
        ]
        summary = _make_summary(findings)
        report = build_compliance_report(summary, project_name="test")
        html = generate_compliance_html(report)
        assert "subprocess.run" in html
        assert "Remediation" in html


# ===========================================================================
# 6. CLI Integration (uses build_parser + direct report generation
#    to avoid importing the broken hardcoded_secrets module)
# ===========================================================================

class TestComplianceCLI:
    """Tests for --compliance-report CLI integration."""

    def test_cli_flag_registered(self):
        """Verify --compliance-report is recognized by argparse."""
        from mcpaudit.__main__ import build_parser
        parser = build_parser()
        args = parser.parse_args(["server.py", "--compliance-report", "out.html"])
        assert args.compliance_report == "out.html"

    def test_cli_flag_default_value(self):
        """Verify --compliance-report without argument uses default filename."""
        from mcpaudit.__main__ import build_parser
        parser = build_parser()
        args = parser.parse_args(["server.py", "--compliance-report"])
        assert args.compliance_report == "compliance-report.html"

    def test_cli_flag_none_when_not_set(self):
        """Verify --compliance-report is None when not provided."""
        from mcpaudit.__main__ import build_parser
        parser = build_parser()
        args = parser.parse_args(["server.py"])
        assert args.compliance_report is None

    def test_end_to_end_report_generation(self, tmp_path: Path):
        """Full end-to-end: build summary, generate report, write file."""
        findings = [
            _make_finding(rule_id="CMD-001", severity=Severity.CRITICAL),
            _make_finding(rule_id="SSRF-001", severity=Severity.HIGH),
        ]
        summary = _make_summary(findings)
        report = build_compliance_report(summary, project_name="e2e-test")
        html = generate_compliance_html(report)
        out = tmp_path / "compliance-report.html"
        out.write_text(html, encoding="utf-8")

        assert out.exists()
        content = out.read_text()
        assert "<!DOCTYPE html>" in content
        assert "OWASP Agentic Top 10" in content
        assert "e2e-test" in content
        assert "CMD-001" in content

    def test_report_file_is_self_contained_html(self, tmp_path: Path):
        """Verify the output file is a complete, self-contained HTML doc."""
        summary = _make_summary()
        report = build_compliance_report(summary, project_name="test")
        html = generate_compliance_html(report)
        out = tmp_path / "report.html"
        out.write_text(html, encoding="utf-8")

        content = out.read_text()
        assert content.startswith("<!DOCTYPE html>")
        assert "<style>" in content  # CSS is inline
        assert "</html>" in content
