"""Tests for Scorecard Generator."""

from __future__ import annotations

import json

import pytest

from mcpaudit.scorecard import (
    OWASP_AGENTIC_CATEGORIES,
    ScorecardData,
    _build_compliance_badges,
    _build_compliance_status,
    _build_findings_table,
    _build_pie_gradient,
    _build_recommendations,
    _build_severity_legend,
    _compute_trust_score,
    _grade_color,
    _grade_from_score,
    _severity_color,
    _trust_label,
    build_scorecard_data,
    build_scorecard_from_summary,
    format_scorecard_json,
    generate_scorecard,
    generate_scorecard_from_summary,
    generate_scorecard_html,
)
from mcpaudit.models import Finding, ScanResult, ScanSummary, Severity


# --- Trust score calculation ---


class TestTrustScore:
    def test_clean_score(self):
        score = _compute_trust_score({})
        assert score == 100

    def test_critical_deduction(self):
        score = _compute_trust_score({"CRITICAL": 1})
        assert score == 75

    def test_max_critical_cap(self):
        # Max 2 CRITICAL deductions: 2 * 25 = 50
        score = _compute_trust_score({"CRITICAL": 10})
        assert score == 50

    def test_high_deduction(self):
        score = _compute_trust_score({"HIGH": 1})
        assert score == 85

    def test_medium_deduction(self):
        score = _compute_trust_score({"MEDIUM": 1})
        assert score == 92

    def test_low_deduction(self):
        score = _compute_trust_score({"LOW": 1})
        assert score == 97

    def test_info_deduction(self):
        score = _compute_trust_score({"INFO": 1})
        assert score == 99

    def test_combined_deductions(self):
        score = _compute_trust_score({"CRITICAL": 1, "HIGH": 1})
        assert score == 60  # 100 - 25 - 15

    def test_score_never_below_zero(self):
        score = _compute_trust_score({
            "CRITICAL": 100, "HIGH": 100,
            "MEDIUM": 100, "LOW": 100, "INFO": 100,
        })
        assert score >= 0
        assert score <= 10  # very low but may not be exactly 0

    def test_score_never_above_100(self):
        score = _compute_trust_score({})
        assert score <= 100


# --- Trust label ---


class TestTrustLabel:
    def test_excellent(self):
        assert _trust_label(95) == "EXCELLENT"

    def test_good(self):
        assert _trust_label(80) == "GOOD"

    def test_moderate(self):
        assert _trust_label(60) == "MODERATE"

    def test_low(self):
        assert _trust_label(30) == "LOW"

    def test_critical(self):
        assert _trust_label(10) == "CRITICAL"


# --- Grade from score ---


class TestGradeFromScore:
    def test_a_plus(self):
        assert _grade_from_score(100) == "A+"

    def test_a(self):
        assert _grade_from_score(90) == "A"

    def test_b(self):
        assert _grade_from_score(75) == "B"

    def test_c(self):
        assert _grade_from_score(55) == "C"

    def test_d(self):
        assert _grade_from_score(30) == "D"

    def test_f(self):
        assert _grade_from_score(10) == "F"


# --- Colors ---


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

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

    def test_severity_colors(self):
        assert _severity_color("CRITICAL") == "#dc2626"
        assert _severity_color("HIGH") == "#f97316"
        assert _severity_color("INFO") == "#6b7280"

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


# --- ScorecardData model ---


class TestScorecardData:
    def test_defaults(self):
        data = ScorecardData()
        assert data.overall_grade == "A+"
        assert data.trust_score == 100
        assert data.total_findings == 0

    def test_to_dict(self):
        data = ScorecardData(
            overall_grade="B", trust_score=75,
            total_findings=5,
        )
        d = data.to_dict()
        assert d["overall_grade"] == "B"
        assert d["trust_score"] == 75
        assert d["total_findings"] == 5

    def test_to_dict_has_all_keys(self):
        d = ScorecardData().to_dict()
        expected_keys = {
            "overall_grade", "trust_score", "trust_label",
            "total_findings", "severity_counts", "top_findings",
            "compliance_status", "recommendations",
            "files_scanned", "lines_scanned", "scan_time_ms", "target",
        }
        assert set(d.keys()) == expected_keys


# --- Build scorecard data ---


class TestBuildScorecardData:
    def _make_scan_data(self, findings=None) -> dict:
        if findings is None:
            findings = []
        return {
            "summary": {
                "files_scanned": 3,
                "lines_scanned": 500,
                "total_findings": len(findings),
                "severity_counts": {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0},
                "scan_time_ms": 42.5,
            },
            "results": [{
                "file": "test.py",
                "findings": findings,
            }],
        }

    def test_clean_scan(self):
        data = build_scorecard_data(self._make_scan_data())
        assert data.overall_grade == "A+"
        assert data.trust_score == 100
        assert data.total_findings == 0

    def test_with_findings(self):
        findings = [
            {
                "rule_id": "CMD-001",
                "severity": "HIGH",
                "title": "Command injection",
                "file": "test.py",
                "line": 10,
            },
        ]
        scan_data = self._make_scan_data(findings)
        scan_data["summary"]["severity_counts"]["HIGH"] = 1
        scan_data["summary"]["total_findings"] = 1
        data = build_scorecard_data(scan_data)
        assert data.total_findings == 1
        assert len(data.top_findings) == 1

    def test_top_findings_limited_to_5(self):
        findings = [
            {
                "rule_id": f"RULE-{i:03d}",
                "severity": "MEDIUM",
                "title": f"Finding {i}",
                "file": "test.py",
                "line": i,
            }
            for i in range(10)
        ]
        scan_data = self._make_scan_data(findings)
        scan_data["summary"]["severity_counts"]["MEDIUM"] = 10
        data = build_scorecard_data(scan_data)
        assert len(data.top_findings) <= 5

    def test_top_findings_deduplicated(self):
        findings = [
            {
                "rule_id": "CMD-001",
                "severity": "HIGH",
                "title": "Command injection",
                "file": f"test{i}.py",
                "line": i,
            }
            for i in range(5)
        ]
        scan_data = self._make_scan_data(findings)
        data = build_scorecard_data(scan_data)
        assert len(data.top_findings) == 1  # Deduplicated by rule_id

    def test_compliance_status_built(self):
        data = build_scorecard_data(self._make_scan_data())
        assert len(data.compliance_status) == len(OWASP_AGENTIC_CATEGORIES)

    def test_recommendations_built(self):
        data = build_scorecard_data(self._make_scan_data())
        assert len(data.recommendations) >= 1

    def test_from_summary_object(self):
        summary = ScanSummary(results=[
            ScanResult(file="test.py", lines_scanned=100),
        ])
        data = build_scorecard_from_summary(summary)
        assert data.files_scanned == 1


# --- Compliance status ---


class TestComplianceStatus:
    def test_all_pass_when_clean(self):
        status = _build_compliance_status([])
        assert all(s["pass"] for s in status)

    def test_fail_on_matching_prefix(self):
        findings = [{"rule_id": "CMD-001"}]
        status = _build_compliance_status(findings)
        # A05 (Remote Access Trojan) has CMD- prefix
        a05 = [s for s in status if s["id"] == "A05"][0]
        assert a05["pass"] is False
        assert a05["findings"] == 1


# --- Recommendations ---


class TestRecommendations:
    def test_critical_recommendation(self):
        recs = _build_recommendations({"CRITICAL": 1}, 1, 50)
        assert any("CRITICAL" in r for r in recs)

    def test_high_recommendation(self):
        recs = _build_recommendations({"HIGH": 1}, 1, 70)
        assert any("HIGH" in r for r in recs)

    def test_clean_recommendation(self):
        recs = _build_recommendations({}, 0, 100)
        assert any("No findings" in r for r in recs)

    def test_low_trust_recommendation(self):
        recs = _build_recommendations({"CRITICAL": 2}, 2, 30)
        assert any("below 50" in r for r in recs)

    def test_many_medium_recommendation(self):
        recs = _build_recommendations({"MEDIUM": 5}, 5, 60)
        assert any("MEDIUM" in r for r in recs)


# --- HTML generation ---


class TestGenerateScorecardHtml:
    def _make_data(self) -> ScorecardData:
        return ScorecardData(
            overall_grade="B",
            trust_score=75,
            trust_label="GOOD",
            total_findings=3,
            severity_counts={"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 0, "INFO": 0},
            top_findings=[{
                "rule_id": "CMD-001",
                "severity": "HIGH",
                "title": "Command injection",
                "file": "test.py",
                "line": "10",
            }],
            compliance_status=[{
                "id": "A01", "name": "Tool Poisoning",
                "pass": True, "findings": 0,
            }],
            recommendations=["Fix HIGH findings."],
            files_scanned=5,
            lines_scanned=1000,
            scan_time_ms=50.0,
            target="/path/to/server",
        )

    def test_html_contains_doctype(self):
        html = generate_scorecard_html(self._make_data())
        assert "<!DOCTYPE html>" in html

    def test_html_contains_grade(self):
        html = generate_scorecard_html(self._make_data())
        assert ">B<" in html

    def test_html_contains_trust_score(self):
        html = generate_scorecard_html(self._make_data())
        assert "75/100" in html

    def test_html_contains_findings(self):
        html = generate_scorecard_html(self._make_data())
        assert "CMD-001" in html

    def test_html_contains_recommendations(self):
        html = generate_scorecard_html(self._make_data())
        assert "Fix HIGH findings" in html

    def test_html_contains_compliance(self):
        html = generate_scorecard_html(self._make_data())
        assert "Tool Poisoning" in html

    def test_html_has_css(self):
        html = generate_scorecard_html(self._make_data())
        assert "<style>" in html

    def test_html_target_display(self):
        html = generate_scorecard_html(self._make_data())
        assert "/path/to/server" in html

    def test_html_no_target(self):
        data = self._make_data()
        data.target = ""
        html = generate_scorecard_html(data)
        assert "N/A" in html


# --- Pie chart ---


class TestPieGradient:
    def test_empty_returns_green(self):
        css = _build_pie_gradient({}, 0)
        assert css == "#22c55e"

    def test_single_severity(self):
        css = _build_pie_gradient({"HIGH": 5}, 5)
        assert "conic-gradient" in css
        assert "#f97316" in css

    def test_multiple_severities(self):
        css = _build_pie_gradient({"HIGH": 3, "MEDIUM": 2}, 5)
        assert "#f97316" in css
        assert "#eab308" in css


# --- Severity legend ---


class TestSeverityLegend:
    def test_all_severities_present(self):
        legend = _build_severity_legend({"CRITICAL": 1, "HIGH": 2})
        assert "CRITICAL" in legend
        assert "HIGH" in legend
        assert "MEDIUM" in legend  # Shows 0 count too


# --- Findings table ---


class TestFindingsTable:
    def test_empty_findings(self):
        html = _build_findings_table([])
        assert "No findings" in html

    def test_with_findings(self):
        findings = [{
            "rule_id": "CMD-001",
            "severity": "HIGH",
            "title": "test",
            "file": "test.py",
            "line": "5",
        }]
        html = _build_findings_table(findings)
        assert "CMD-001" in html
        assert "<table" in html


# --- Compliance badges ---


class TestComplianceBadges:
    def test_pass_badge(self):
        items = [{"id": "A01", "name": "Test", "pass": True, "findings": 0}]
        html = _build_compliance_badges(items)
        assert "badge-pass" in html
        assert "PASS" in html

    def test_fail_badge(self):
        items = [{"id": "A01", "name": "Test", "pass": False, "findings": 3}]
        html = _build_compliance_badges(items)
        assert "badge-fail" in html
        assert "FAIL" in html


# --- Convenience functions ---


class TestConvenienceFunctions:
    def test_generate_scorecard(self):
        scan_data = {
            "summary": {
                "files_scanned": 1,
                "lines_scanned": 100,
                "total_findings": 0,
                "severity_counts": {},
                "scan_time_ms": 10.0,
            },
            "results": [],
        }
        html = generate_scorecard(scan_data)
        assert "<!DOCTYPE html>" in html

    def test_generate_from_summary(self):
        summary = ScanSummary(results=[
            ScanResult(file="test.py", lines_scanned=50),
        ])
        html = generate_scorecard_from_summary(summary, target="test.py")
        assert "<!DOCTYPE html>" in html
        assert "test.py" in html

    def test_format_json(self):
        data = ScorecardData(
            overall_grade="A", trust_score=90,
        )
        j = format_scorecard_json(data)
        parsed = json.loads(j)
        assert parsed["overall_grade"] == "A"


# --- OWASP categories ---


class TestOwaspCategories:
    def test_has_10_categories(self):
        assert len(OWASP_AGENTIC_CATEGORIES) == 10

    def test_categories_have_required_keys(self):
        for cat in OWASP_AGENTIC_CATEGORIES:
            assert "id" in cat
            assert "name" in cat
            assert "rule_prefix" in cat

    def test_ids_are_sequential(self):
        ids = [cat["id"] for cat in OWASP_AGENTIC_CATEGORIES]
        expected = [f"A{i:02d}" for i in range(1, 11)]
        assert ids == expected
