"""Tests for Executive Security Report Generator."""

from __future__ import annotations

import json
from typing import Any

import pytest

from mcpaudit.report_generator import (
    DEFAULT_TITLE,
    ExecutiveReport,
    ReportOptions,
    ReportSection,
    build_executive_report,
    format_report_json,
    format_report_text,
    generate_executive_report,
    _build_findings_table,
    _build_header,
    _build_owasp_table,
    _build_recommendations,
    _build_severity_bars,
    _build_toc,
    _build_trust_gauge,
    _compute_owasp_coverage,
    _compute_trust_score,
    _esc,
    _extract_findings,
    _extract_severity_counts,
    _generate_recommendations,
    _severity_badge,
    _OWASP_AGENTIC_TOP10,
    _RULE_PREFIX_TO_OWASP,
    _SEVERITY_COLORS,
    _SEVERITY_ORDER,
)


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

def _sample_scan_data() -> dict[str, Any]:
    return {
        "summary": {
            "files_scanned": 3,
            "lines_scanned": 500,
            "total_findings": 5,
            "overall_grade": "C",
            "severity_counts": {
                "CRITICAL": 1,
                "HIGH": 2,
                "MEDIUM": 1,
                "LOW": 1,
                "INFO": 0,
            },
            "scan_time_ms": 42.5,
            "exit_code": 2,
        },
        "results": [
            {
                "file": "server.py",
                "findings": [
                    {
                        "rule_id": "CMD-001",
                        "severity": "CRITICAL",
                        "title": "Shell injection",
                        "file": "server.py",
                        "line": 10,
                    },
                    {
                        "rule_id": "AUTH-001",
                        "severity": "HIGH",
                        "title": "Missing auth",
                        "file": "server.py",
                        "line": 20,
                    },
                ],
            },
            {
                "file": "api.py",
                "findings": [
                    {
                        "rule_id": "SQL-001",
                        "severity": "HIGH",
                        "title": "SQL injection",
                        "file": "api.py",
                        "line": 5,
                    },
                    {
                        "rule_id": "RATE-001",
                        "severity": "MEDIUM",
                        "title": "No rate limit",
                        "file": "api.py",
                        "line": 30,
                    },
                    {
                        "rule_id": "LOG-001",
                        "severity": "LOW",
                        "title": "Verbose logging",
                        "file": "api.py",
                        "line": 45,
                    },
                ],
            },
        ],
    }


def _empty_scan_data() -> dict[str, Any]:
    return {
        "summary": {
            "files_scanned": 1,
            "lines_scanned": 100,
            "total_findings": 0,
            "overall_grade": "A+",
            "severity_counts": {
                "CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0,
            },
            "scan_time_ms": 10.0,
            "exit_code": 0,
        },
        "results": [],
    }


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

class TestConstants:
    def test_severity_colors_complete(self):
        for sev in _SEVERITY_ORDER:
            assert sev in _SEVERITY_COLORS

    def test_severity_order(self):
        assert _SEVERITY_ORDER == ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]

    def test_owasp_top10_has_10_entries(self):
        assert len(_OWASP_AGENTIC_TOP10) == 10

    def test_owasp_ids_sequential(self):
        for i in range(1, 11):
            assert f"A{i:02d}" in _OWASP_AGENTIC_TOP10

    def test_rule_prefix_mapping(self):
        assert "CMD" in _RULE_PREFIX_TO_OWASP
        assert _RULE_PREFIX_TO_OWASP["CMD"] == "A02"
        assert _RULE_PREFIX_TO_OWASP["AUTH"] == "A05"
        assert _RULE_PREFIX_TO_OWASP["SSRF"] == "A07"

    def test_default_title(self):
        assert "Executive" in DEFAULT_TITLE


# ---------------------------------------------------------------------------
# ReportOptions
# ---------------------------------------------------------------------------

class TestReportOptions:
    def test_defaults(self):
        opts = ReportOptions()
        assert opts.title == DEFAULT_TITLE
        assert opts.include_toc is True
        assert opts.include_owasp is True
        assert opts.include_trust_score is True
        assert opts.include_appendix is True
        assert opts.include_recommendations is True
        assert opts.generated_at  # auto-set

    def test_custom_title(self):
        opts = ReportOptions(title="My Report")
        assert opts.title == "My Report"

    def test_with_author(self):
        opts = ReportOptions(author="Carlos")
        assert opts.author == "Carlos"

    def test_to_dict(self):
        opts = ReportOptions(title="Test", author="Test Author")
        d = opts.to_dict()
        assert d["title"] == "Test"
        assert d["author"] == "Test Author"
        assert "generated_at" in d

    def test_from_dict(self):
        data = {"title": "Custom", "author": "Me", "include_toc": False}
        opts = ReportOptions.from_dict(data)
        assert opts.title == "Custom"
        assert opts.author == "Me"
        assert opts.include_toc is False

    def test_from_dict_defaults(self):
        opts = ReportOptions.from_dict({})
        assert opts.title == DEFAULT_TITLE

    def test_roundtrip(self):
        opts = ReportOptions(title="Test", subtitle="Sub", author="A")
        d = opts.to_dict()
        opts2 = ReportOptions.from_dict(d)
        assert opts2.title == opts.title
        assert opts2.subtitle == opts.subtitle
        assert opts2.author == opts.author


# ---------------------------------------------------------------------------
# ReportSection
# ---------------------------------------------------------------------------

class TestReportSection:
    def test_basic(self):
        sec = ReportSection(id="test", title="Test", content="<p>Hi</p>")
        assert sec.id == "test"
        assert sec.title == "Test"

    def test_to_dict(self):
        sec = ReportSection(id="s1", title="Section 1", content="C", order=1)
        d = sec.to_dict()
        assert d["id"] == "s1"
        assert d["order"] == 1


# ---------------------------------------------------------------------------
# ExecutiveReport
# ---------------------------------------------------------------------------

class TestExecutiveReport:
    def test_defaults(self):
        r = ExecutiveReport()
        assert r.title == DEFAULT_TITLE
        assert r.trust_score == 100
        assert r.grade == "A+"
        assert r.total_findings == 0
        assert r.generated_at  # auto-set

    def test_custom(self):
        r = ExecutiveReport(
            title="Custom",
            total_findings=5,
            trust_score=60,
            grade="C",
        )
        assert r.total_findings == 5
        assert r.trust_score == 60
        assert r.grade == "C"

    def test_to_dict(self):
        r = ExecutiveReport(
            total_findings=3,
            severity_counts={"CRITICAL": 1, "HIGH": 2},
        )
        d = r.to_dict()
        assert d["total_findings"] == 3
        assert d["severity_counts"]["CRITICAL"] == 1
        assert "sections" in d


# ---------------------------------------------------------------------------
# Extraction helpers
# ---------------------------------------------------------------------------

class TestExtractFindings:
    def test_basic(self):
        data = _sample_scan_data()
        findings = _extract_findings(data)
        assert len(findings) == 5

    def test_empty(self):
        findings = _extract_findings(_empty_scan_data())
        assert len(findings) == 0

    def test_no_results(self):
        findings = _extract_findings({})
        assert findings == []


class TestExtractSeverityCounts:
    def test_basic(self):
        data = _sample_scan_data()
        counts = _extract_severity_counts(data)
        assert counts["CRITICAL"] == 1
        assert counts["HIGH"] == 2
        assert counts["MEDIUM"] == 1

    def test_empty(self):
        counts = _extract_severity_counts({})
        for sev in _SEVERITY_ORDER:
            assert counts[sev] == 0


# ---------------------------------------------------------------------------
# OWASP coverage
# ---------------------------------------------------------------------------

class TestOWASPCoverage:
    def test_basic(self):
        findings = [
            {"rule_id": "CMD-001"},
            {"rule_id": "AUTH-001"},
            {"rule_id": "RATE-001"},
        ]
        cov = _compute_owasp_coverage(findings)
        assert cov["A02"] == 1  # CMD
        assert cov["A05"] == 1  # AUTH
        assert cov["A09"] == 1  # RATE

    def test_empty(self):
        cov = _compute_owasp_coverage([])
        assert all(v == 0 for v in cov.values())

    def test_multiple_same_category(self):
        findings = [
            {"rule_id": "CMD-001"},
            {"rule_id": "SQL-001"},
        ]
        cov = _compute_owasp_coverage(findings)
        assert cov["A02"] == 2  # Both map to A02

    def test_unknown_prefix(self):
        findings = [{"rule_id": "UNKNOWN-001"}]
        cov = _compute_owasp_coverage(findings)
        assert all(v == 0 for v in cov.values())


# ---------------------------------------------------------------------------
# Trust score computation
# ---------------------------------------------------------------------------

class TestComputeTrustScore:
    def test_clean(self):
        counts = {s: 0 for s in _SEVERITY_ORDER}
        assert _compute_trust_score(counts) == 100

    def test_one_critical(self):
        counts = {"CRITICAL": 1, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0}
        score = _compute_trust_score(counts)
        assert score == 75  # 100 - 25

    def test_max_deductions(self):
        counts = {"CRITICAL": 10, "HIGH": 10, "MEDIUM": 10, "LOW": 10, "INFO": 10}
        score = _compute_trust_score(counts)
        assert score == 0  # Capped at 0

    def test_empty(self):
        assert _compute_trust_score({}) == 100


# ---------------------------------------------------------------------------
# Recommendations
# ---------------------------------------------------------------------------

class TestGenerateRecommendations:
    def test_critical_findings(self):
        counts = {"CRITICAL": 2, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0}
        recs = _generate_recommendations(counts, [], 50)
        assert any("URGENT" in r for r in recs)

    def test_high_findings(self):
        counts = {"CRITICAL": 0, "HIGH": 3, "MEDIUM": 0, "LOW": 0, "INFO": 0}
        recs = _generate_recommendations(counts, [], 70)
        assert any("HIGH PRIORITY" in r for r in recs)

    def test_auth_finding(self):
        counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0}
        findings = [{"rule_id": "AUTH-001"}]
        recs = _generate_recommendations(counts, findings, 80)
        assert any("authentication" in r for r in recs)

    def test_cmd_finding(self):
        counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0}
        findings = [{"rule_id": "CMD-001"}]
        recs = _generate_recommendations(counts, findings, 80)
        assert any("command" in r.lower() or "Sanitize" in r for r in recs)

    def test_low_trust(self):
        counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0}
        recs = _generate_recommendations(counts, [], 40)
        assert any("critically low" in r for r in recs)

    def test_clean_scan(self):
        counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0}
        recs = _generate_recommendations(counts, [], 95)
        assert any("No critical" in r for r in recs)


# ---------------------------------------------------------------------------
# HTML helpers
# ---------------------------------------------------------------------------

class TestEsc:
    def test_escapes_html(self):
        assert _esc("<script>") == "&lt;script&gt;"
        assert _esc('a"b') == "a&quot;b"

    def test_plain(self):
        assert _esc("hello") == "hello"


class TestSeverityBadge:
    def test_critical(self):
        badge = _severity_badge("CRITICAL")
        assert "badge-critical" in badge
        assert "CRITICAL" in badge

    def test_low(self):
        badge = _severity_badge("low")
        assert "badge-low" in badge

    def test_all_severities(self):
        for sev in _SEVERITY_ORDER:
            badge = _severity_badge(sev)
            assert "badge" in badge


class TestBuildHeader:
    def test_default(self):
        opts = ReportOptions()
        html = _build_header(opts)
        assert "<header>" in html
        assert "mcpaudit" in html

    def test_custom_title(self):
        opts = ReportOptions(title="Custom Report")
        html = _build_header(opts)
        assert "Custom Report" in html

    def test_with_author(self):
        opts = ReportOptions(author="Carlos")
        html = _build_header(opts)
        assert "Carlos" in html

    def test_with_logo_url(self):
        opts = ReportOptions(logo_url="https://example.com/logo.png")
        html = _build_header(opts)
        assert "logo-img" in html
        assert "https://example.com/logo.png" in html

    def test_with_subtitle(self):
        opts = ReportOptions(subtitle="Q1 2026")
        html = _build_header(opts)
        assert "Q1 2026" in html


class TestBuildToc:
    def test_basic(self):
        sections = [
            ReportSection(id="s1", title="Section 1", content="", order=1),
            ReportSection(id="s2", title="Section 2", content="", order=2),
        ]
        html = _build_toc(sections)
        assert 'href="#s1"' in html
        assert "Section 1" in html
        assert "Table of Contents" in html

    def test_empty(self):
        html = _build_toc([])
        assert "Table of Contents" in html


class TestBuildSeverityBars:
    def test_basic(self):
        counts = {"CRITICAL": 1, "HIGH": 2, "MEDIUM": 3, "LOW": 0, "INFO": 0}
        html = _build_severity_bars(counts)
        assert "severity-bar" in html
        assert "CRITICAL" in html

    def test_all_zero(self):
        counts = {s: 0 for s in _SEVERITY_ORDER}
        html = _build_severity_bars(counts)
        assert "severity-bar" in html


class TestBuildTrustGauge:
    def test_high_score(self):
        html = _build_trust_gauge(95)
        assert "95" in html
        assert "#16a34a" in html  # green

    def test_low_score(self):
        html = _build_trust_gauge(30)
        assert "30" in html
        assert "#dc2626" in html  # red

    def test_medium_score(self):
        html = _build_trust_gauge(60)
        assert "60" in html
        assert "#f97316" in html  # orange


class TestBuildOWASPTable:
    def test_basic(self):
        cov = {f"A{i:02d}": i for i in range(1, 11)}
        html = _build_owasp_table(cov)
        assert "owasp-table" in html
        assert "A01" in html
        assert "Findings detected" in html

    def test_empty(self):
        cov = {f"A{i:02d}": 0 for i in range(1, 11)}
        html = _build_owasp_table(cov)
        assert "No findings" in html


class TestBuildFindingsTable:
    def test_basic(self):
        findings = [
            {"rule_id": "CMD-001", "severity": "CRITICAL",
             "title": "Shell", "file": "server.py", "line": 10},
        ]
        html = _build_findings_table(findings)
        assert "findings-table" in html
        assert "CMD-001" in html

    def test_empty(self):
        html = _build_findings_table([])
        assert "No findings" in html

    def test_sorted_by_severity(self):
        findings = [
            {"rule_id": "LOG-001", "severity": "LOW", "title": "L",
             "file": "a.py", "line": 1},
            {"rule_id": "CMD-001", "severity": "CRITICAL", "title": "C",
             "file": "a.py", "line": 2},
        ]
        html = _build_findings_table(findings)
        # CRITICAL should appear before LOW
        crit_pos = html.find("CRITICAL")
        low_pos = html.find("LOW")
        assert crit_pos < low_pos


class TestBuildRecommendations:
    def test_basic(self):
        recs = ["Fix this", "Fix that"]
        html = _build_recommendations(recs)
        assert "recommendation" in html
        assert "#1" in html
        assert "#2" in html

    def test_empty(self):
        html = _build_recommendations([])
        assert "No recommendations" in html


# ---------------------------------------------------------------------------
# build_executive_report
# ---------------------------------------------------------------------------

class TestBuildExecutiveReport:
    def test_basic(self):
        data = _sample_scan_data()
        report = build_executive_report(data)
        assert report.total_findings == 5
        assert report.grade == "C"
        assert report.files_scanned == 3
        assert report.severity_counts["CRITICAL"] == 1

    def test_empty_scan(self):
        report = build_executive_report(_empty_scan_data())
        assert report.total_findings == 0
        assert report.grade == "A+"

    def test_with_options(self):
        opts = ReportOptions(title="Custom Title")
        report = build_executive_report(_sample_scan_data(), opts)
        assert report.title == "Custom Title"

    def test_trust_score_from_data(self):
        data = _sample_scan_data()
        data["summary"]["trust_score"] = {"score": 42}
        report = build_executive_report(data)
        assert report.trust_score == 42

    def test_recommendations_generated(self):
        report = build_executive_report(_sample_scan_data())
        assert len(report.recommendations) > 0

    def test_owasp_coverage_computed(self):
        report = build_executive_report(_sample_scan_data())
        assert isinstance(report.owasp_coverage, dict)
        assert len(report.owasp_coverage) == 10


# ---------------------------------------------------------------------------
# generate_executive_report (HTML)
# ---------------------------------------------------------------------------

class TestGenerateExecutiveReport:
    def test_returns_html(self):
        html = generate_executive_report(_sample_scan_data())
        assert html.startswith("<!DOCTYPE html>")
        assert "</html>" in html

    def test_contains_title(self):
        html = generate_executive_report(_sample_scan_data())
        assert DEFAULT_TITLE in html

    def test_custom_title(self):
        opts = ReportOptions(title="My Audit")
        html = generate_executive_report(_sample_scan_data(), opts)
        assert "My Audit" in html

    def test_contains_sections(self):
        html = generate_executive_report(_sample_scan_data())
        assert "executive-summary" in html
        assert "findings-severity" in html
        assert "owasp-coverage" in html
        assert "trust-score" in html
        assert "recommendations" in html
        assert "appendix" in html

    def test_contains_toc(self):
        html = generate_executive_report(_sample_scan_data())
        assert "Table of Contents" in html

    def test_no_toc(self):
        opts = ReportOptions(include_toc=False)
        html = generate_executive_report(_sample_scan_data(), opts)
        assert "Table of Contents" not in html

    def test_no_owasp(self):
        opts = ReportOptions(include_owasp=False)
        html = generate_executive_report(_sample_scan_data(), opts)
        assert "owasp-coverage" not in html

    def test_no_trust_score(self):
        opts = ReportOptions(include_trust_score=False)
        html = generate_executive_report(_sample_scan_data(), opts)
        assert 'id="trust-score"' not in html

    def test_no_appendix(self):
        opts = ReportOptions(include_appendix=False)
        html = generate_executive_report(_sample_scan_data(), opts)
        assert 'id="appendix"' not in html

    def test_no_recommendations(self):
        opts = ReportOptions(include_recommendations=False)
        html = generate_executive_report(_sample_scan_data(), opts)
        assert 'id="recommendations"' not in html

    def test_options_as_dict(self):
        html = generate_executive_report(
            _sample_scan_data(),
            {"title": "Dict Title"},
        )
        assert "Dict Title" in html

    def test_print_media_css(self):
        html = generate_executive_report(_sample_scan_data())
        assert "@media print" in html
        assert "A4" in html

    def test_severity_bars_present(self):
        html = generate_executive_report(_sample_scan_data())
        assert "severity-bar" in html

    def test_findings_table_present(self):
        html = generate_executive_report(_sample_scan_data())
        assert "findings-table" in html
        assert "CMD-001" in html

    def test_custom_css(self):
        opts = ReportOptions(custom_css="body { color: red; }")
        html = generate_executive_report(_sample_scan_data(), opts)
        assert "body { color: red; }" in html

    def test_empty_scan(self):
        html = generate_executive_report(_empty_scan_data())
        assert "<!DOCTYPE html>" in html
        assert "No findings" in html

    def test_page_break_class(self):
        html = generate_executive_report(_sample_scan_data())
        assert "page-break" in html

    def test_footer_present(self):
        html = generate_executive_report(_sample_scan_data())
        assert "<footer>" in html


# ---------------------------------------------------------------------------
# format_report_json
# ---------------------------------------------------------------------------

class TestFormatReportJson:
    def test_valid_json(self):
        report = build_executive_report(_sample_scan_data())
        text = format_report_json(report)
        data = json.loads(text)
        assert data["total_findings"] == 5

    def test_contains_sections(self):
        report = build_executive_report(_sample_scan_data())
        report.sections = [
            ReportSection(id="s1", title="S1", content="C"),
        ]
        text = format_report_json(report)
        data = json.loads(text)
        assert len(data["sections"]) == 1


# ---------------------------------------------------------------------------
# format_report_text
# ---------------------------------------------------------------------------

class TestFormatReportText:
    def test_basic(self):
        report = build_executive_report(_sample_scan_data())
        text = format_report_text(report)
        assert "EXECUTIVE SUMMARY" in text
        assert "Grade:" in text
        assert "Trust Score:" in text

    def test_severity_distribution(self):
        report = build_executive_report(_sample_scan_data())
        text = format_report_text(report)
        assert "SEVERITY DISTRIBUTION" in text
        assert "CRITICAL" in text

    def test_recommendations(self):
        report = build_executive_report(_sample_scan_data())
        text = format_report_text(report)
        assert "RECOMMENDATIONS" in text
