"""Tests for mcpaudit interactive_report — Interactive HTML Report Generator."""

import json

from mcpaudit.interactive_report import (
    InteractiveReportConfig,
    ReportFinding,
    ReportMetadata,
    extract_findings,
    extract_metadata,
    format_interactive_json,
    generate_interactive_report,
    generate_interactive_report_from_scan,
    _convert_findings,
    _build_metadata,
    _get_owasp_category,
    _severity_badge_color,
    _severity_sort_key,
    _escape,
    _render_summary_section,
    _render_severity_bar,
    _render_controls,
    _render_finding_card,
    _generate_css,
    _generate_js,
)


# --- Helpers ---

def _finding(
    rule_id="CMD-001", severity="HIGH", title="Command injection",
    file="server.py", line=10, code="subprocess.run(cmd)",
    recommendation="Use allowlist", cwe="CWE-78",
):
    return {
        "rule_id": rule_id,
        "severity": severity,
        "title": title,
        "description": f"Desc: {title}",
        "file": file,
        "line": line,
        "code_snippet": code,
        "recommendation": recommendation,
        "cwe": cwe,
    }


def _scan_data(findings, trust_score=80, grade="B"):
    return {
        "summary": {
            "files_scanned": 2,
            "lines_scanned": 150,
            "total_findings": len(findings),
            "trust_score": {"score": trust_score, "grade": grade},
            "overall_grade": grade,
            "scan_time_ms": 42.5,
            "severity_counts": _sev_counts(findings),
        },
        "results": [{"file": "server.py", "findings": findings}],
    }


def _sev_counts(findings):
    counts = {}
    for f in findings:
        s = f.get("severity", "INFO")
        counts[s] = counts.get(s, 0) + 1
    return counts


# --- ReportFinding dataclass ---

class TestReportFinding:
    def test_to_dict_full(self):
        rf = ReportFinding(
            rule_id="CMD-001", severity="HIGH",
            title="Test", description="Desc", file="a.py", line=5,
            code_snippet="code", recommendation="Fix it",
            cwe="CWE-78", owasp_category="A02",
        )
        d = rf.to_dict()
        assert d["rule_id"] == "CMD-001"
        assert d["severity"] == "HIGH"
        assert d["owasp_category"] == "A02"
        assert d["cwe"] == "CWE-78"

    def test_to_dict_minimal(self):
        rf = ReportFinding(rule_id="X", severity="LOW", title="T",
                           description="", file="", line=0)
        d = rf.to_dict()
        assert d["rule_id"] == "X"
        assert d["code_snippet"] == ""


# --- ReportMetadata dataclass ---

class TestReportMetadata:
    def test_to_dict(self):
        rm = ReportMetadata(total_files=3, total_findings=5,
                            trust_score=90, grade="A")
        d = rm.to_dict()
        assert d["total_files"] == 3
        assert d["trust_score"] == 90

    def test_scan_time_rounded(self):
        rm = ReportMetadata(scan_time_ms=3.14159)
        assert rm.to_dict()["scan_time_ms"] == 3.14

    def test_defaults(self):
        rm = ReportMetadata()
        assert rm.total_files == 0
        assert rm.grade == "A+"
        assert rm.trust_score == 100


# --- InteractiveReportConfig ---

class TestInteractiveReportConfig:
    def test_defaults(self):
        cfg = InteractiveReportConfig()
        assert cfg.show_summary is True
        assert cfg.show_owasp_filter is True
        assert cfg.show_search is True

    def test_to_dict(self):
        cfg = InteractiveReportConfig(title="Custom")
        d = cfg.to_dict()
        assert d["title"] == "Custom"

    def test_custom_title(self):
        cfg = InteractiveReportConfig(title="My Report")
        assert cfg.title == "My Report"


# --- OWASP category extraction ---

class TestOwaspCategory:
    def test_cmd_rule(self):
        assert "A02" in _get_owasp_category("CMD-001")

    def test_auth_rule(self):
        assert "A05" in _get_owasp_category("AUTH-001")

    def test_ssrf_rule(self):
        assert "A07" in _get_owasp_category("SSRF-001")

    def test_unknown_prefix(self):
        assert _get_owasp_category("ZZZZZ-001") == "Uncategorized"

    def test_no_dash(self):
        assert _get_owasp_category("NODASH") == "Uncategorized"

    def test_rate_rule(self):
        assert "A09" in _get_owasp_category("RATE-001")

    def test_perm_rule(self):
        assert "A03" in _get_owasp_category("PERM-001")

    def test_val_rule(self):
        assert "A06" in _get_owasp_category("VAL-001")


# --- Helper functions ---

class TestHelpers:
    def test_escape_html(self):
        assert _escape("<script>") == "&lt;script&gt;"

    def test_escape_quotes(self):
        assert "&quot;" in _escape('"hello"')

    def test_severity_badge_color_critical(self):
        assert _severity_badge_color("CRITICAL") == "#dc2626"

    def test_severity_badge_color_unknown(self):
        assert _severity_badge_color("UNKNOWN") == "#6b7280"

    def test_severity_sort_key_order(self):
        assert _severity_sort_key("CRITICAL") < _severity_sort_key("HIGH")
        assert _severity_sort_key("HIGH") < _severity_sort_key("MEDIUM")
        assert _severity_sort_key("MEDIUM") < _severity_sort_key("LOW")
        assert _severity_sort_key("LOW") < _severity_sort_key("INFO")

    def test_severity_sort_key_unknown(self):
        assert _severity_sort_key("BOGUS") == 99


# --- extract_findings ---

class TestExtractFindings:
    def test_standard_format(self):
        data = _scan_data([_finding()])
        result = extract_findings(data)
        assert len(result) == 1
        assert isinstance(result[0], ReportFinding)

    def test_flat_format(self):
        data = {"findings": [_finding()]}
        result = extract_findings(data)
        assert len(result) == 1

    def test_empty_results(self):
        data = {"results": []}
        assert extract_findings(data) == []

    def test_no_findings_key(self):
        assert extract_findings({}) == []

    def test_owasp_assigned(self):
        data = {"findings": [_finding(rule_id="SQL-001")]}
        result = extract_findings(data)
        assert "A02" in result[0].owasp_category

    def test_multiple_files(self):
        data = {
            "results": [
                {"file": "a.py", "findings": [_finding(file="a.py")]},
                {"file": "b.py", "findings": [_finding(file="b.py")]},
            ]
        }
        result = extract_findings(data)
        assert len(result) == 2

    def test_preserves_code_snippet(self):
        data = {"findings": [_finding(code="dangerous_call()")]}
        result = extract_findings(data)
        assert result[0].code_snippet == "dangerous_call()"


# --- extract_metadata ---

class TestExtractMetadata:
    def test_from_scan_data(self):
        data = _scan_data([_finding()], trust_score=85, grade="A")
        meta = extract_metadata(data)
        assert meta.trust_score == 85
        assert meta.grade == "A"
        assert meta.total_findings == 1

    def test_missing_trust_score(self):
        data = {"summary": {}, "results": []}
        meta = extract_metadata(data)
        assert meta.trust_score == 100

    def test_numeric_trust_score(self):
        data = {"summary": {"trust_score": 72}, "results": []}
        meta = extract_metadata(data)
        assert meta.trust_score == 72

    def test_sev_counts_from_findings(self):
        data = {"findings": [
            _finding(severity="HIGH"),
            _finding(severity="HIGH", rule_id="X-001"),
            _finding(severity="LOW", rule_id="Y-001"),
        ]}
        meta = extract_metadata(data)
        assert meta.severity_counts.get("HIGH", 0) == 2

    def test_empty_data(self):
        meta = extract_metadata({})
        assert meta.total_files == 0


# --- _convert_findings ---

class TestConvertFindings:
    def test_basic_conversion(self):
        raw = [_finding()]
        result = _convert_findings(raw)
        assert len(result) == 1
        assert result[0].rule_id == "CMD-001"

    def test_empty_list(self):
        assert _convert_findings([]) == []

    def test_missing_fields(self):
        result = _convert_findings([{"rule_id": "X"}])
        assert result[0].severity == "INFO"
        assert result[0].title == ""


# --- _build_metadata ---

class TestBuildMetadata:
    def test_from_findings(self):
        findings = [_finding(severity="CRITICAL")]
        meta = _build_metadata(findings, None)
        assert meta.severity_counts["CRITICAL"] == 1
        assert meta.total_findings == 1

    def test_with_metadata_dict(self):
        findings = [_finding()]
        md = {"total_files": 5, "trust_score": 88, "grade": "A"}
        meta = _build_metadata(findings, md)
        assert meta.total_files == 5
        assert meta.trust_score == 88


# --- CSS/JS generation ---

class TestCSSJS:
    def test_css_contains_dark_theme(self):
        css = _generate_css()
        assert "--bg-primary" in css
        assert "#0f172a" in css

    def test_css_contains_severity_colors(self):
        css = _generate_css()
        assert "sev-CRITICAL" in css
        assert "sev-HIGH" in css

    def test_js_contains_filter_logic(self):
        js = _generate_js()
        assert "activeSeverities" in js
        assert "applyFilters" in js

    def test_js_contains_sort_logic(self):
        js = _generate_js()
        assert "sortFindings" in js
        assert "sortField" in js

    def test_js_contains_search(self):
        js = _generate_js()
        assert "searchText" in js
        assert "search-input" in js


# --- Render functions ---

class TestRenderSummary:
    def test_contains_grade(self):
        meta = ReportMetadata(grade="B", trust_score=75)
        result = _render_summary_section(meta)
        assert "B" in result
        assert "75" in result

    def test_contains_findings_count(self):
        meta = ReportMetadata(total_findings=12)
        result = _render_summary_section(meta)
        assert "12" in result


class TestRenderSeverityBar:
    def test_renders_bar_segments(self):
        meta = ReportMetadata(
            total_findings=3,
            severity_counts={"HIGH": 2, "LOW": 1},
        )
        result = _render_severity_bar(meta)
        assert _severity_badge_color("HIGH") in result

    def test_empty_findings(self):
        meta = ReportMetadata(total_findings=0, severity_counts={})
        assert _render_severity_bar(meta) == ""


class TestRenderControls:
    def test_contains_severity_buttons(self):
        findings = [ReportFinding(
            rule_id="X", severity="HIGH", title="T",
            description="", file="", line=0,
        )]
        result = _render_controls(findings)
        assert "CRITICAL" in result
        assert "HIGH" in result
        assert "sev-filter" in result

    def test_contains_search_input(self):
        result = _render_controls([])
        assert "search-input" in result

    def test_contains_sort_buttons(self):
        result = _render_controls([])
        assert "sort-btn" in result
        assert "data-sort" in result

    def test_owasp_filter_populated(self):
        findings = [ReportFinding(
            rule_id="CMD-001", severity="HIGH", title="T",
            description="", file="", line=0,
            owasp_category="A02 — Insecure Tool Implementation",
        )]
        result = _render_controls(findings)
        assert "owasp-filter" in result
        assert "A02" in result


class TestRenderFindingCard:
    def test_card_has_severity_badge(self):
        f = ReportFinding(
            rule_id="CMD-001", severity="CRITICAL", title="Injection",
            description="Bad", file="s.py", line=5,
        )
        result = _render_finding_card(f)
        assert "sev-CRITICAL" in result
        assert "CRITICAL" in result

    def test_card_has_code_block(self):
        f = ReportFinding(
            rule_id="X", severity="LOW", title="T",
            description="D", file="a.py", line=1,
            code_snippet="dangerous_func(user_input)",
        )
        result = _render_finding_card(f)
        assert "code-block" in result
        assert "dangerous_func(user_input)" in result

    def test_card_has_data_attributes(self):
        f = ReportFinding(
            rule_id="AUTH-001", severity="HIGH", title="No auth",
            description="", file="app.py", line=3,
        )
        result = _render_finding_card(f)
        assert 'data-severity="HIGH"' in result
        assert 'data-rule="AUTH-001"' in result
        assert 'data-file="app.py"' in result

    def test_card_collapsible(self):
        f = ReportFinding(
            rule_id="X", severity="INFO", title="T",
            description="D", file="", line=0,
        )
        result = _render_finding_card(f)
        assert "finding-details" in result
        assert "finding-header" in result

    def test_card_escapes_html(self):
        f = ReportFinding(
            rule_id="X", severity="LOW", title="<b>bold</b>",
            description="", file="", line=0,
        )
        result = _render_finding_card(f)
        assert "<b>" not in result
        assert "&lt;b&gt;" in result


# --- Full report generation ---

class TestGenerateInteractiveReport:
    def test_returns_valid_html(self):
        findings = [_finding()]
        result = generate_interactive_report(findings)
        assert result.startswith("<!DOCTYPE html>")
        assert "</html>" in result

    def test_contains_css_and_js(self):
        result = generate_interactive_report([_finding()])
        assert "<style>" in result
        assert "<script>" in result

    def test_findings_rendered(self):
        result = generate_interactive_report([_finding(title="Test vuln")])
        assert "Test vuln" in result

    def test_empty_findings(self):
        result = generate_interactive_report([])
        assert "<!DOCTYPE html>" in result
        assert "0 of 0 findings" in result

    def test_custom_title(self):
        cfg = InteractiveReportConfig(title="My Audit")
        result = generate_interactive_report([_finding()], config=cfg)
        assert "My Audit" in result

    def test_metadata_rendered(self):
        meta = {"total_files": 10, "trust_score": 95, "grade": "A+"}
        result = generate_interactive_report([_finding()], metadata=meta)
        assert "95" in result

    def test_severity_filters_present(self):
        result = generate_interactive_report([_finding()])
        assert "sev-CRITICAL" in result
        assert "sev-HIGH" in result
        assert "sev-MEDIUM" in result
        assert "sev-LOW" in result
        assert "sev-INFO" in result

    def test_search_present(self):
        result = generate_interactive_report([_finding()])
        assert "search-input" in result

    def test_sort_buttons_present(self):
        result = generate_interactive_report([_finding()])
        assert "data-sort" in result

    def test_custom_timestamp(self):
        cfg = InteractiveReportConfig(timestamp="2025-01-01")
        result = generate_interactive_report([_finding()], config=cfg)
        assert "2025-01-01" in result

    def test_multiple_findings_sorted(self):
        findings = [
            _finding(severity="LOW", title="Low vuln"),
            _finding(severity="CRITICAL", title="Crit vuln",
                     rule_id="SEC-001"),
        ]
        result = generate_interactive_report(findings)
        crit_pos = result.index("Crit vuln")
        low_pos = result.index("Low vuln")
        assert crit_pos < low_pos

    def test_dark_theme_colors(self):
        result = generate_interactive_report([_finding()])
        assert "#0f172a" in result


# --- From scan data ---

class TestGenerateFromScan:
    def test_from_scan_data(self):
        data = _scan_data([_finding()])
        result = generate_interactive_report_from_scan(data)
        assert "<!DOCTYPE html>" in result
        assert "CMD-001" in result

    def test_empty_scan(self):
        data = _scan_data([])
        result = generate_interactive_report_from_scan(data)
        assert "0 of 0 findings" in result

    def test_extracts_trust_score(self):
        data = _scan_data([_finding()], trust_score=65)
        result = generate_interactive_report_from_scan(data)
        assert "65" in result


# --- JSON export ---

class TestFormatInteractiveJson:
    def test_valid_json(self):
        result = format_interactive_json([_finding()])
        data = json.loads(result)
        assert "metadata" in data
        assert "findings" in data

    def test_finding_count(self):
        result = format_interactive_json([_finding(), _finding(rule_id="X")])
        data = json.loads(result)
        assert len(data["findings"]) == 2

    def test_metadata_included(self):
        meta = {"total_files": 7}
        result = format_interactive_json([_finding()], metadata=meta)
        data = json.loads(result)
        assert data["metadata"]["total_files"] == 7

    def test_empty_findings(self):
        result = format_interactive_json([])
        data = json.loads(result)
        assert len(data["findings"]) == 0
        assert data["metadata"]["total_findings"] == 0
