"""Tests for MCP Dashboard Generator (mcpaudit.dashboard_gen).

Tests HTML dashboard generation, data aggregation, section rendering,
configuration, text/JSON formatting, and edge cases.
"""

from __future__ import annotations

import json

import pytest

from mcpaudit.dashboard_gen import (
    DashboardConfig,
    DashboardData,
    build_dashboard_data,
    format_dashboard_json,
    format_dashboard_text,
    generate_dashboard_from_scan_data,
    generate_security_dashboard,
    _compute_risk_score,
    _grade_color,
    _risk_label,
    _severity_color,
    _section_executive_summary,
    _section_trust_score,
    _section_severity_chart,
    _section_owasp_coverage,
    _section_top_findings,
    _section_compliance,
    _wrap_html,
    _SEVERITY_COLORS,
    _GRADE_COLORS,
    _OWASP_TOP10,
)
from mcpaudit.models import Finding, ScanResult, ScanSummary, Severity


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

def _make_finding(
    rule_id: str = "CMD-001",
    severity: Severity = Severity.HIGH,
    title: str = "Command injection",
    file: str = "server.py",
    line: int = 10,
    **kwargs,
) -> Finding:
    return Finding(
        rule_id=rule_id,
        severity=severity,
        title=title,
        description=kwargs.get("description", "desc"),
        file=file,
        line=line,
        recommendation=kwargs.get("recommendation", "fix it"),
        cwe=kwargs.get("cwe", "CWE-78"),
    )


def _make_summary(findings_spec: list[tuple[str, str]] | None = None) -> ScanSummary:
    """Build ScanSummary from (rule_id, severity_str) tuples."""
    if findings_spec is None:
        findings_spec = [("CMD-001", "HIGH"), ("SQL-001", "CRITICAL")]
    findings = [
        _make_finding(rule_id=rid, severity=Severity.from_string(sev))
        for rid, sev in findings_spec
    ]
    result = ScanResult(file="server.py", findings=findings, lines_scanned=100)
    return ScanSummary(results=[result], total_scan_time_ms=42.5)


# ---------------------------------------------------------------------------
# DashboardConfig tests
# ---------------------------------------------------------------------------

class TestDashboardConfig:
    def test_defaults(self):
        cfg = DashboardConfig()
        assert cfg.title == "MCP Security Dashboard"
        assert cfg.show_trust_score is True
        assert cfg.show_owasp_coverage is True
        assert cfg.show_compliance is True
        assert cfg.show_top_findings is True
        assert cfg.top_findings_limit == 10
        assert cfg.show_severity_chart is True
        assert cfg.show_file_breakdown is True
        assert cfg.timestamp == ""

    def test_custom_values(self):
        cfg = DashboardConfig(title="Custom", top_findings_limit=5)
        assert cfg.title == "Custom"
        assert cfg.top_findings_limit == 5

    def test_to_dict(self):
        cfg = DashboardConfig(title="Test")
        d = cfg.to_dict()
        assert d["title"] == "Test"
        assert "show_trust_score" in d
        assert isinstance(d["top_findings_limit"], int)

    def test_to_dict_keys(self):
        cfg = DashboardConfig()
        d = cfg.to_dict()
        expected_keys = {
            "title", "show_trust_score", "show_owasp_coverage",
            "show_compliance", "show_top_findings", "top_findings_limit",
            "show_severity_chart", "show_file_breakdown",
        }
        assert set(d.keys()) == expected_keys


# ---------------------------------------------------------------------------
# DashboardData tests
# ---------------------------------------------------------------------------

class TestDashboardData:
    def test_defaults(self):
        data = DashboardData()
        assert data.grade == "A+"
        assert data.risk_score == 0
        assert data.trust_score == 100
        assert data.total_findings == 0

    def test_to_dict(self):
        data = DashboardData(grade="B", risk_score=30, total_findings=5)
        d = data.to_dict()
        assert d["grade"] == "B"
        assert d["risk_score"] == 30
        assert d["total_findings"] == 5

    def test_to_dict_has_all_keys(self):
        data = DashboardData()
        d = data.to_dict()
        expected = {
            "grade", "risk_score", "trust_score", "trust_label",
            "total_files", "total_findings", "total_lines",
            "scan_time_ms", "severity_counts", "owasp_coverage",
            "compliance_status", "timestamp",
        }
        assert expected.issubset(set(d.keys()))

    def test_scan_time_rounded(self):
        data = DashboardData(scan_time_ms=42.567)
        d = data.to_dict()
        assert d["scan_time_ms"] == 42.57


# ---------------------------------------------------------------------------
# Helper functions
# ---------------------------------------------------------------------------

class TestHelpers:
    def test_severity_color_known(self):
        assert _severity_color("CRITICAL") == "#dc2626"
        assert _severity_color("HIGH") == "#f97316"
        assert _severity_color("MEDIUM") == "#eab308"
        assert _severity_color("LOW") == "#22c55e"
        assert _severity_color("INFO") == "#6b7280"

    def test_severity_color_unknown(self):
        assert _severity_color("UNKNOWN") == "#6b7280"

    def test_grade_color_known(self):
        assert _grade_color("A+") == "#22c55e"
        assert _grade_color("F") == "#dc2626"

    def test_grade_color_unknown(self):
        assert _grade_color("Z") == "#6b7280"

    def test_compute_risk_score_empty(self):
        assert _compute_risk_score({}) == 0

    def test_compute_risk_score_critical(self):
        score = _compute_risk_score({"CRITICAL": 4})
        assert score == 100  # 4*25 = 100

    def test_compute_risk_score_mixed(self):
        score = _compute_risk_score({"HIGH": 1, "MEDIUM": 2})
        assert score == 25  # 15 + 10

    def test_compute_risk_score_capped_at_100(self):
        score = _compute_risk_score({"CRITICAL": 10})
        assert score == 100

    def test_risk_label_clean(self):
        assert _risk_label(0) == "CLEAN"

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

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

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

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


# ---------------------------------------------------------------------------
# Data aggregation
# ---------------------------------------------------------------------------

class TestBuildDashboardData:
    def test_basic(self):
        summary = _make_summary()
        data = build_dashboard_data(summary)
        assert data.total_files == 1
        assert data.total_findings == 2
        assert data.total_lines == 100

    def test_severity_counts(self):
        summary = _make_summary([("A", "HIGH"), ("B", "HIGH"), ("C", "LOW")])
        data = build_dashboard_data(summary)
        assert data.severity_counts["HIGH"] == 2
        assert data.severity_counts["LOW"] == 1

    def test_trust_score_present(self):
        summary = _make_summary()
        data = build_dashboard_data(summary)
        assert isinstance(data.trust_score, int)
        assert 0 <= data.trust_score <= 100

    def test_top_findings_sorted(self):
        specs = [("CMD-001", "HIGH")] * 3 + [("SQL-001", "CRITICAL")]
        summary = _make_summary(specs)
        data = build_dashboard_data(summary)
        assert len(data.top_findings) > 0
        # CMD-001 has 3 occurrences, should be first
        assert data.top_findings[0]["rule_id"] == "CMD-001"
        assert data.top_findings[0]["count"] == 3

    def test_owasp_coverage(self):
        summary = _make_summary()
        data = build_dashboard_data(summary)
        assert len(data.owasp_coverage) == 10

    def test_compliance_pass(self):
        summary = _make_summary([("X", "LOW")])
        data = build_dashboard_data(summary)
        assert data.compliance_status["overall"] == "PASS"

    def test_compliance_fail_critical(self):
        summary = _make_summary([("X", "CRITICAL")])
        data = build_dashboard_data(summary)
        assert data.compliance_status["overall"] == "FAIL"

    def test_compliance_warning_high(self):
        summary = _make_summary([("X", "HIGH")])
        data = build_dashboard_data(summary)
        assert data.compliance_status["overall"] == "WARNING"

    def test_custom_config(self):
        cfg = DashboardConfig(
            title="Custom",
            top_findings_limit=2,
            timestamp="2025-01-01 00:00 UTC",
        )
        summary = _make_summary()
        data = build_dashboard_data(summary, config=cfg)
        assert data.timestamp == "2025-01-01 00:00 UTC"

    def test_empty_summary(self):
        summary = ScanSummary(results=[])
        data = build_dashboard_data(summary)
        assert data.total_findings == 0
        assert data.grade == "A+"

    def test_timestamp_auto_generated(self):
        summary = _make_summary()
        data = build_dashboard_data(summary)
        assert "UTC" in data.timestamp

    def test_findings_by_file(self):
        f1 = _make_finding(file="a.py")
        f2 = _make_finding(file="b.py")
        r1 = ScanResult(file="a.py", findings=[f1], lines_scanned=50)
        r2 = ScanResult(file="b.py", findings=[f2], lines_scanned=50)
        summary = ScanSummary(results=[r1, r2])
        data = build_dashboard_data(summary)
        assert "a.py" in data.findings_by_file
        assert "b.py" in data.findings_by_file


# ---------------------------------------------------------------------------
# HTML section rendering
# ---------------------------------------------------------------------------

class TestSectionExecutiveSummary:
    def test_contains_grade(self):
        data = DashboardData(grade="A+")
        html = _section_executive_summary(data)
        assert "A+" in html

    def test_contains_risk_score(self):
        data = DashboardData(risk_score=42)
        html = _section_executive_summary(data)
        assert "42/100" in html

    def test_contains_files_scanned(self):
        data = DashboardData(total_files=10)
        html = _section_executive_summary(data)
        assert "10" in html

    def test_contains_timestamp(self):
        data = DashboardData(timestamp="2025-01-01")
        html = _section_executive_summary(data)
        assert "2025-01-01" in html


class TestSectionTrustScore:
    def test_contains_score(self):
        data = DashboardData(trust_score=85, trust_label="GOOD")
        html = _section_trust_score(data)
        assert "85/100" in html
        assert "GOOD" in html

    def test_deductions_shown(self):
        data = DashboardData(trust_deductions={"CMD-001": 10})
        html = _section_trust_score(data)
        assert "CMD-001" in html
        assert "-10" in html

    def test_bonuses_shown(self):
        data = DashboardData(trust_bonuses={"auth_present": 5})
        html = _section_trust_score(data)
        assert "auth_present" in html
        assert "+5" in html


class TestSectionSeverityChart:
    def test_empty_findings(self):
        data = DashboardData(total_findings=0)
        html = _section_severity_chart(data)
        assert "No findings" in html

    def test_with_findings(self):
        data = DashboardData(
            total_findings=3,
            severity_counts={"CRITICAL": 1, "HIGH": 2},
        )
        html = _section_severity_chart(data)
        assert "CRITICAL" in html
        assert "HIGH" in html
        assert "conic-gradient" in html


class TestSectionOwaspCoverage:
    def test_all_categories_present(self):
        cov = {item["id"]: True for item in _OWASP_TOP10}
        data = DashboardData(owasp_coverage=cov)
        html = _section_owasp_coverage(data)
        for item in _OWASP_TOP10:
            assert item["id"] in html

    def test_fail_status(self):
        cov = {"A01": False}
        data = DashboardData(owasp_coverage=cov)
        html = _section_owasp_coverage(data)
        assert "FAIL" in html


class TestSectionTopFindings:
    def test_empty(self):
        data = DashboardData(top_findings=[])
        html = _section_top_findings(data)
        assert html == ""

    def test_with_findings(self):
        data = DashboardData(top_findings=[
            {"rule_id": "CMD-001", "severity": "HIGH", "title": "Injection", "count": 3},
        ])
        html = _section_top_findings(data)
        assert "CMD-001" in html
        assert "Injection" in html


class TestSectionCompliance:
    def test_empty(self):
        data = DashboardData(compliance_status={})
        html = _section_compliance(data)
        assert html == ""

    def test_pass(self):
        data = DashboardData(compliance_status={"overall": "PASS", "owasp_coverage": "10/10", "grade": "A+"})
        html = _section_compliance(data)
        assert "PASS" in html

    def test_fail(self):
        data = DashboardData(compliance_status={"overall": "FAIL", "owasp_coverage": "5/10", "grade": "D"})
        html = _section_compliance(data)
        assert "FAIL" in html


# ---------------------------------------------------------------------------
# HTML generation
# ---------------------------------------------------------------------------

class TestGenerateSecurityDashboard:
    def test_basic_html(self):
        summary = _make_summary()
        html = generate_security_dashboard(summary)
        assert "<!DOCTYPE html>" in html
        assert "MCP Security Dashboard" in html

    def test_custom_title(self):
        summary = _make_summary()
        cfg = DashboardConfig(title="My Dashboard")
        html = generate_security_dashboard(summary, config=cfg)
        assert "My Dashboard" in html

    def test_no_trust_score(self):
        summary = _make_summary()
        cfg = DashboardConfig(show_trust_score=False)
        html = generate_security_dashboard(summary, config=cfg)
        assert "Trust Score" not in html

    def test_no_owasp_coverage(self):
        summary = _make_summary()
        cfg = DashboardConfig(show_owasp_coverage=False)
        html = generate_security_dashboard(summary, config=cfg)
        assert "OWASP Agentic Top 10" not in html

    def test_no_compliance(self):
        summary = _make_summary()
        cfg = DashboardConfig(show_compliance=False)
        html = generate_security_dashboard(summary, config=cfg)
        assert "Compliance Status" not in html

    def test_empty_scan(self):
        summary = ScanSummary(results=[])
        html = generate_security_dashboard(summary)
        assert "<!DOCTYPE html>" in html
        assert "A+" in html

    def test_contains_css(self):
        summary = _make_summary()
        html = generate_security_dashboard(summary)
        assert "<style>" in html
        assert "--bg:" in html

    def test_contains_footer(self):
        summary = _make_summary()
        html = generate_security_dashboard(summary)
        assert "Generated by mcpaudit" in html


class TestGenerateDashboardFromScanData:
    def test_from_dict(self):
        summary = _make_summary()
        data = summary.to_dict()
        html = generate_dashboard_from_scan_data(data)
        assert "<!DOCTYPE html>" in html

    def test_empty_data(self):
        html = generate_dashboard_from_scan_data({})
        assert "<!DOCTYPE html>" in html

    def test_custom_config(self):
        summary = _make_summary()
        data = summary.to_dict()
        cfg = DashboardConfig(title="From Dict")
        html = generate_dashboard_from_scan_data(data, config=cfg)
        assert "From Dict" in html


class TestWrapHtml:
    def test_basic(self):
        html = _wrap_html("Test", "<p>body</p>")
        assert "<!DOCTYPE html>" in html
        assert "<p>body</p>" in html
        assert "Test" in html

    def test_escapes_title(self):
        html = _wrap_html("<script>alert(1)</script>", "")
        assert "<script>" not in html
        assert "&lt;script&gt;" in html


# ---------------------------------------------------------------------------
# Text/JSON formatters
# ---------------------------------------------------------------------------

class TestFormatDashboardText:
    def test_basic(self):
        data = DashboardData(grade="B", risk_score=30, trust_score=70, trust_label="FAIR")
        text = format_dashboard_text(data)
        assert "Grade: B" in text
        assert "Risk Score: 30/100" in text
        assert "Trust Score: 70/100" in text

    def test_severity_breakdown(self):
        data = DashboardData(severity_counts={"HIGH": 3, "LOW": 1})
        text = format_dashboard_text(data)
        assert "HIGH: 3" in text

    def test_compliance_shown(self):
        data = DashboardData(compliance_status={"overall": "PASS", "owasp_coverage": "10/10"})
        text = format_dashboard_text(data)
        assert "Compliance: PASS" in text


class TestFormatDashboardJson:
    def test_valid_json(self):
        data = DashboardData(grade="A", risk_score=5)
        result = format_dashboard_json(data)
        parsed = json.loads(result)
        assert parsed["grade"] == "A"
        assert parsed["risk_score"] == 5

    def test_all_keys_present(self):
        data = DashboardData()
        result = format_dashboard_json(data)
        parsed = json.loads(result)
        assert "grade" in parsed
        assert "trust_score" in parsed


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

class TestConstants:
    def test_severity_colors_complete(self):
        for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]:
            assert sev in _SEVERITY_COLORS

    def test_grade_colors_complete(self):
        for g in ["A+", "A", "B", "C", "D", "F"]:
            assert g in _GRADE_COLORS

    def test_owasp_top10_count(self):
        assert len(_OWASP_TOP10) == 10

    def test_owasp_top10_has_ids(self):
        ids = [item["id"] for item in _OWASP_TOP10]
        assert "A01" in ids
        assert "A10" in ids

    def test_owasp_top10_has_rules(self):
        for item in _OWASP_TOP10:
            assert "rules" in item
            assert len(item["rules"]) > 0
