"""Tests for the Summary Dashboard HTML generation.

Tests cover the dashboard generation, all HTML sections, risk scoring,
remediation queue, timeline, and CLI integration.
"""

import json
import tempfile
from pathlib import Path

import pytest

from mcpaudit.models import Finding, ScanResult, ScanSummary, Severity
from mcpaudit.summary_dashboard import (
    RemediationItem,
    _build_category_bars,
    _build_executive_summary,
    _build_remediation_section,
    _build_severity_pie,
    _build_timeline,
    _build_top_findings,
    _compute_risk_score,
    _extract_category,
    _build_remediation_queue,
    _risk_color,
    _risk_label,
    generate_dashboard,
    generate_dashboard_from_dict,
)


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

def _make_finding(
    rule_id: str = "TEST-001",
    severity: Severity = Severity.MEDIUM,
    title: str = "Test finding",
    recommendation: str = "Fix it.",
) -> Finding:
    return Finding(
        rule_id=rule_id,
        severity=severity,
        title=title,
        description="Test description",
        file="test.py",
        line=1,
        recommendation=recommendation,
    )


def _make_summary(findings: list[Finding] | None = None) -> ScanSummary:
    if findings is None:
        findings = []
    result = ScanResult(file="test.py", findings=findings, lines_scanned=100)
    summary = ScanSummary(results=[result])
    summary.total_scan_time_ms = 42.0
    return summary


# ---------------------------------------------------------------------------
# Risk Score
# ---------------------------------------------------------------------------

class TestRiskScore:
    def test_clean_score(self):
        counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0}
        assert _compute_risk_score(counts) == 0

    def test_one_critical(self):
        counts = {"CRITICAL": 1, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0}
        assert _compute_risk_score(counts) == 25

    def test_one_high(self):
        counts = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 0, "LOW": 0, "INFO": 0}
        assert _compute_risk_score(counts) == 15

    def test_mixed_severities(self):
        counts = {"CRITICAL": 1, "HIGH": 2, "MEDIUM": 3, "LOW": 4, "INFO": 5}
        expected = 25 + 30 + 15 + 8 + 0
        assert _compute_risk_score(counts) == min(100, expected)

    def test_capped_at_100(self):
        counts = {"CRITICAL": 10, "HIGH": 10, "MEDIUM": 10, "LOW": 10, "INFO": 10}
        assert _compute_risk_score(counts) == 100

    def test_info_only(self):
        counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 10}
        assert _compute_risk_score(counts) == 0


class TestRiskLabel:
    def test_clean(self):
        assert _risk_label(0) == "CLEAN"

    def test_low(self):
        assert _risk_label(10) == "LOW"

    def test_moderate(self):
        assert _risk_label(30) == "MODERATE"

    def test_high(self):
        assert _risk_label(50) == "HIGH"

    def test_critical(self):
        assert _risk_label(80) == "CRITICAL"


class TestRiskColor:
    def test_clean_is_green(self):
        assert _risk_color(0) == "#22c55e"

    def test_critical_is_red(self):
        assert _risk_color(90) == "#dc2626"


# ---------------------------------------------------------------------------
# Category extraction
# ---------------------------------------------------------------------------

class TestExtractCategory:
    def test_standard_rule_id(self):
        assert _extract_category("MCP-003") == "MCP"

    def test_multi_part_prefix(self):
        assert _extract_category("TS-CMD-001") == "TS-CMD"

    def test_no_dash(self):
        assert _extract_category("CUSTOM") == "CUSTOM"


# ---------------------------------------------------------------------------
# Remediation Queue
# ---------------------------------------------------------------------------

class TestRemediationQueue:
    def test_empty_findings(self):
        assert _build_remediation_queue([]) == []

    def test_groups_by_rule_id(self):
        findings = [
            _make_finding("A-001", Severity.HIGH),
            _make_finding("A-001", Severity.HIGH),
            _make_finding("B-001", Severity.LOW),
        ]
        items = _build_remediation_queue(findings)
        assert len(items) == 2
        assert items[0].rule_id == "A-001"
        assert items[0].count == 2

    def test_sorted_by_severity(self):
        findings = [
            _make_finding("LOW-001", Severity.LOW),
            _make_finding("CRIT-001", Severity.CRITICAL),
            _make_finding("MED-001", Severity.MEDIUM),
        ]
        items = _build_remediation_queue(findings)
        assert items[0].severity == "CRITICAL"
        assert items[1].severity == "MEDIUM"
        assert items[2].severity == "LOW"


# ---------------------------------------------------------------------------
# HTML Section Builders
# ---------------------------------------------------------------------------

class TestBuildExecutiveSummary:
    def test_contains_grade(self):
        html_output = _build_executive_summary(
            grade="B", risk_score=30, total_files=5,
            total_findings=10, total_lines=1000,
            scan_time_ms=42.0, severity_counts={},
            timestamp="2026-03-18",
        )
        assert "B" in html_output
        assert "30/100" in html_output

    def test_contains_file_count(self):
        html_output = _build_executive_summary(
            grade="A+", risk_score=0, total_files=42,
            total_findings=0, total_lines=5000,
            scan_time_ms=10.0, severity_counts={},
            timestamp="2026-03-18",
        )
        assert "42" in html_output

    def test_html_escaping(self):
        html_output = _build_executive_summary(
            grade="A+", risk_score=0, total_files=1,
            total_findings=0, total_lines=10,
            scan_time_ms=1.0, severity_counts={},
            timestamp='<script>alert("xss")</script>',
        )
        assert "<script>" not in html_output


class TestBuildSeverityPie:
    def test_empty_findings(self):
        html_output = _build_severity_pie({}, 0)
        assert "No findings detected" in html_output

    def test_contains_conic_gradient(self):
        counts = {"CRITICAL": 2, "HIGH": 3, "MEDIUM": 5}
        html_output = _build_severity_pie(counts, 10)
        assert "conic-gradient" in html_output

    def test_contains_legend(self):
        counts = {"HIGH": 5, "MEDIUM": 3}
        html_output = _build_severity_pie(counts, 8)
        assert "HIGH" in html_output
        assert "MEDIUM" in html_output

    def test_single_severity(self):
        counts = {"CRITICAL": 10}
        html_output = _build_severity_pie(counts, 10)
        assert "CRITICAL" in html_output
        assert "100%" in html_output


class TestBuildCategoryBars:
    def test_empty_findings(self):
        assert _build_category_bars([]) == ""

    def test_contains_categories(self):
        findings = [
            _make_finding("MCP-003"),
            _make_finding("MCP-003"),
            _make_finding("AUTH-001"),
        ]
        html_output = _build_category_bars(findings)
        assert "MCP" in html_output
        assert "AUTH" in html_output

    def test_limits_to_15_categories(self):
        findings = []
        for i in range(20):
            findings.append(_make_finding(f"CAT{i:02d}-001"))
        html_output = _build_category_bars(findings)
        # Should still generate valid HTML
        assert "bar-row" in html_output


class TestBuildTopFindings:
    def test_empty_findings(self):
        assert _build_top_findings([]) == ""

    def test_top_5_displayed(self):
        findings = []
        for i in range(10):
            findings.append(_make_finding(f"RULE-{i:03d}"))
        # Add duplicates for first 3
        for i in range(3):
            findings.extend([_make_finding(f"RULE-{i:03d}")] * (5 - i))
        html_output = _build_top_findings(findings, limit=5)
        assert "RULE-000" in html_output

    def test_custom_limit(self):
        findings = [_make_finding("A-001"), _make_finding("B-001")]
        html_output = _build_top_findings(findings, limit=1)
        assert "Top 1" in html_output

    def test_shows_severity_badge(self):
        findings = [_make_finding("X-001", Severity.CRITICAL)]
        html_output = _build_top_findings(findings)
        assert "CRITICAL" in html_output


class TestBuildTimeline:
    def test_empty_history(self):
        assert _build_timeline([]) == ""

    def test_with_history(self):
        history = [
            {
                "timestamp": "2026-03-17T10:00:00Z",
                "scan": {
                    "summary": {
                        "total_findings": 5,
                        "overall_grade": "B",
                    }
                },
            },
            {
                "timestamp": "2026-03-18T10:00:00Z",
                "scan": {
                    "summary": {
                        "total_findings": 3,
                        "overall_grade": "A",
                    }
                },
            },
        ]
        html_output = _build_timeline(history)
        assert "Timeline" in html_output
        assert "2026-03-17" in html_output

    def test_limits_to_10_entries(self):
        history = []
        for i in range(15):
            history.append({
                "timestamp": f"2026-03-{i+1:02d}T10:00:00Z",
                "scan": {
                    "summary": {
                        "total_findings": i,
                        "overall_grade": "B",
                    }
                },
            })
        html_output = _build_timeline(history)
        # Should include last 10 entries
        assert "timeline-bar" in html_output

    def test_handles_missing_fields(self):
        history = [{"scan": {}}]
        html_output = _build_timeline(history)
        # Should not crash
        assert isinstance(html_output, str)


class TestBuildRemediationSection:
    def test_empty_items(self):
        assert _build_remediation_section([]) == ""

    def test_shows_items(self):
        items = [
            RemediationItem(
                rule_id="MCP-003",
                severity="HIGH",
                title="Missing validation",
                count=5,
                recommendation="Add validation.",
                priority=1,
            ),
        ]
        html_output = _build_remediation_section(items)
        assert "MCP-003" in html_output
        assert "5x" in html_output
        assert "Add validation" in html_output


# ---------------------------------------------------------------------------
# Full Dashboard Generation
# ---------------------------------------------------------------------------

class TestGenerateDashboard:
    def test_clean_scan(self):
        summary = _make_summary([])
        html_output = generate_dashboard(summary)
        assert "<!DOCTYPE html>" in html_output
        assert "A+" in html_output
        assert "0/100" in html_output
        assert "No findings detected" in html_output

    def test_with_findings(self):
        findings = [
            _make_finding("MCP-003", Severity.HIGH),
            _make_finding("MCP-003", Severity.HIGH),
            _make_finding("AUTH-001", Severity.CRITICAL),
            _make_finding("PATH-001", Severity.MEDIUM),
        ]
        summary = _make_summary(findings)
        html_output = generate_dashboard(summary)
        assert "<!DOCTYPE html>" in html_output
        assert "MCP-003" in html_output
        assert "conic-gradient" in html_output
        assert "Remediation" in html_output

    def test_custom_title(self):
        summary = _make_summary([])
        html_output = generate_dashboard(summary, title="My Report")
        assert "My Report" in html_output

    def test_with_history(self):
        summary = _make_summary([])
        history = [
            {
                "timestamp": "2026-03-17",
                "scan": {
                    "summary": {
                        "total_findings": 5,
                        "overall_grade": "C",
                    }
                },
            },
        ]
        html_output = generate_dashboard(summary, history=history)
        assert "Timeline" in html_output

    def test_xss_protection(self):
        findings = [
            Finding(
                rule_id="XSS-001",
                severity=Severity.HIGH,
                title='<script>alert("xss")</script>',
                description="test",
                file="test.py",
                line=1,
                recommendation='<img onerror="alert(1)">',
            ),
        ]
        summary = _make_summary(findings)
        html_output = generate_dashboard(summary)
        assert "<script>alert" not in html_output
        assert "&lt;script&gt;" in html_output

    def test_returns_valid_html(self):
        findings = [_make_finding("A-001", Severity.CRITICAL)]
        summary = _make_summary(findings)
        html_output = generate_dashboard(summary)
        assert html_output.startswith("<!DOCTYPE html>")
        assert "</html>" in html_output

    def test_self_contained_no_external_deps(self):
        summary = _make_summary([_make_finding()])
        html_output = generate_dashboard(summary)
        # Should not reference external CSS/JS
        assert "http://" not in html_output
        assert "https://" not in html_output
        assert "<link" not in html_output.lower()
        assert "<script src" not in html_output.lower()

    def test_multiple_files(self):
        r1 = ScanResult(
            file="a.py",
            findings=[_make_finding("A-001", Severity.HIGH)],
            lines_scanned=50,
        )
        r2 = ScanResult(
            file="b.py",
            findings=[_make_finding("B-001", Severity.LOW)],
            lines_scanned=30,
        )
        summary = ScanSummary(results=[r1, r2])
        html_output = generate_dashboard(summary)
        assert "2" in html_output  # 2 files

    def test_all_severities_represented(self):
        findings = [
            _make_finding("A-001", Severity.CRITICAL),
            _make_finding("B-001", Severity.HIGH),
            _make_finding("C-001", Severity.MEDIUM),
            _make_finding("D-001", Severity.LOW),
            _make_finding("E-001", Severity.INFO),
        ]
        summary = _make_summary(findings)
        html_output = generate_dashboard(summary)
        for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]:
            assert sev in html_output


class TestGenerateDashboardFromDict:
    def test_basic_conversion(self):
        data = {
            "summary": {
                "files_scanned": 1,
                "total_findings": 2,
                "overall_grade": "C",
                "scan_time_ms": 10.0,
            },
            "results": [
                {
                    "file": "test.py",
                    "lines_scanned": 50,
                    "findings": [
                        {
                            "rule_id": "TEST-001",
                            "severity": "HIGH",
                            "title": "Test",
                            "description": "Test desc",
                            "file": "test.py",
                            "line": 1,
                            "recommendation": "Fix it",
                            "cwe": "CWE-123",
                        },
                    ],
                },
            ],
        }
        html_output = generate_dashboard_from_dict(data)
        assert "<!DOCTYPE html>" in html_output
        assert "TEST-001" in html_output

    def test_empty_results(self):
        data = {"summary": {}, "results": []}
        html_output = generate_dashboard_from_dict(data)
        assert "<!DOCTYPE html>" in html_output

    def test_with_history(self):
        data = {"summary": {}, "results": []}
        history = [
            {
                "timestamp": "2026-03-17",
                "scan": {
                    "summary": {
                        "total_findings": 10,
                        "overall_grade": "D",
                    }
                },
            },
        ]
        html_output = generate_dashboard_from_dict(data, history=history)
        assert "Timeline" in html_output


# ---------------------------------------------------------------------------
# CLI Integration
# ---------------------------------------------------------------------------

class TestDashboardCLI:
    def test_dashboard_flag_generates_file(self):
        """Test that --dashboard writes HTML to file."""
        from mcpaudit.__main__ import main

        with tempfile.TemporaryDirectory() as tmpdir:
            # Create a minimal Python file to scan
            target = Path(tmpdir) / "server.py"
            target.write_text("# clean file\nx = 1\n", encoding="utf-8")

            output_file = Path(tmpdir) / "dashboard.html"
            exit_code = main([
                str(target),
                "--dashboard", str(output_file),
            ])

            assert output_file.exists()
            content = output_file.read_text(encoding="utf-8")
            assert "<!DOCTYPE html>" in content
            assert "mcpaudit" in content

    def test_dashboard_with_format_flag(self):
        """Test --dashboard combined with --format json outputs both."""
        from mcpaudit.__main__ import main

        with tempfile.TemporaryDirectory() as tmpdir:
            target = Path(tmpdir) / "server.py"
            target.write_text("# clean file\nx = 1\n", encoding="utf-8")

            dashboard_file = Path(tmpdir) / "dash.html"
            json_file = Path(tmpdir) / "out.json"
            exit_code = main([
                str(target),
                "--dashboard", str(dashboard_file),
                "--format", "json",
                "--output", str(json_file),
            ])

            assert dashboard_file.exists()
            assert json_file.exists()
            # Dashboard is HTML
            assert "<!DOCTYPE html>" in dashboard_file.read_text(encoding="utf-8")
            # JSON output is valid
            json_data = json.loads(json_file.read_text(encoding="utf-8"))
            assert "summary" in json_data


# ---------------------------------------------------------------------------
# Edge Cases
# ---------------------------------------------------------------------------

class TestDashboardEdgeCases:
    def test_unicode_in_findings(self):
        findings = [
            Finding(
                rule_id="UNI-001",
                severity=Severity.MEDIUM,
                title="Error: archivo no encontrado",
                description="Descripcion con acentos: arbol",
                file="test.py",
                line=1,
            ),
        ]
        summary = _make_summary(findings)
        html_output = generate_dashboard(summary)
        assert "archivo" in html_output

    def test_very_long_title(self):
        findings = [
            _make_finding("LONG-001", title="A" * 500),
        ]
        summary = _make_summary(findings)
        html_output = generate_dashboard(summary)
        assert "<!DOCTYPE html>" in html_output

    def test_many_findings(self):
        findings = [_make_finding(f"R-{i:03d}") for i in range(100)]
        summary = _make_summary(findings)
        html_output = generate_dashboard(summary)
        assert "<!DOCTYPE html>" in html_output
        assert "Top 5" in html_output

    def test_zero_lines_scanned(self):
        result = ScanResult(file="empty.py", findings=[], lines_scanned=0)
        summary = ScanSummary(results=[result])
        html_output = generate_dashboard(summary)
        assert "<!DOCTYPE html>" in html_output

    def test_empty_summary(self):
        summary = ScanSummary(results=[])
        html_output = generate_dashboard(summary)
        assert "<!DOCTYPE html>" in html_output
        assert "0/100" in html_output
