"""Tests for mcpaudit scan_compare — Scan Comparison Report."""

import json
import tempfile
from pathlib import Path

from mcpaudit.scan_compare import (
    ComparedFinding,
    ComparisonReport,
    SeverityDelta,
    TimelinePoint,
    TimelineReport,
    build_timeline,
    compare_scan_files,
    compare_scans,
    detect_regressions_from_history,
    format_comparison_json,
    format_comparison_text,
    generate_comparison_html,
    _finding_key,
    _extract_findings,
    _count_severities,
    _estimate_score,
    _extract_score,
    _to_compared,
    _esc,
    _render_score_panel,
    _render_stats,
    _render_finding_row,
    _render_findings_section,
    _render_timeline,
    _comparison_css,
)


# --- Helpers ---

def _finding(rule_id="CMD-001", severity="HIGH", title="Test",
             file="server.py", line=10, code="subprocess.run(cmd)"):
    return {
        "rule_id": rule_id,
        "severity": severity,
        "title": title,
        "description": f"Desc: {title}",
        "file": file,
        "line": line,
        "code_snippet": code,
    }


def _scan_data(findings, trust_score=80, grade="B"):
    return {
        "summary": {
            "files_scanned": 1,
            "total_findings": len(findings),
            "trust_score": {"score": trust_score, "grade": grade},
            "overall_grade": grade,
        },
        "results": [{"file": "server.py", "findings": findings}],
    }


def _write_json(data):
    f = tempfile.NamedTemporaryFile(suffix=".json", mode="w", delete=False)
    json.dump(data, f)
    f.flush()
    f.close()
    return Path(f.name)


# --- ComparedFinding ---

class TestComparedFinding:
    def test_to_dict_basic(self):
        cf = ComparedFinding(
            status="new", rule_id="CMD-001", severity="HIGH",
            title="T", file="a.py", line=5,
        )
        d = cf.to_dict()
        assert d["status"] == "new"
        assert d["rule_id"] == "CMD-001"

    def test_to_dict_with_code(self):
        cf = ComparedFinding(
            status="resolved", rule_id="X", severity="LOW",
            title="T", file="", line=0,
            code_snippet="code_here",
        )
        d = cf.to_dict()
        assert d["code_snippet"] == "code_here"

    def test_to_dict_no_optional(self):
        cf = ComparedFinding(status="persistent")
        d = cf.to_dict()
        assert "code_snippet" not in d
        assert "previous_severity" not in d

    def test_to_dict_with_previous_severity(self):
        cf = ComparedFinding(
            status="regression", rule_id="A", severity="HIGH",
            title="T", file="", line=0, previous_severity="LOW",
        )
        d = cf.to_dict()
        assert d["previous_severity"] == "LOW"


# --- SeverityDelta ---

class TestSeverityDelta:
    def test_delta_calculation(self):
        sd = SeverityDelta(
            before={"HIGH": 3, "LOW": 1},
            after={"HIGH": 1, "LOW": 2, "CRITICAL": 1},
        )
        d = sd.delta
        assert d["HIGH"] == -2
        assert d["LOW"] == 1
        assert d["CRITICAL"] == 1

    def test_delta_empty(self):
        sd = SeverityDelta()
        assert sd.delta == {}

    def test_to_dict(self):
        sd = SeverityDelta(before={"HIGH": 1}, after={"HIGH": 2})
        d = sd.to_dict()
        assert d["before"]["HIGH"] == 1
        assert d["after"]["HIGH"] == 2
        assert d["delta"]["HIGH"] == 1


# --- ComparisonReport ---

class TestComparisonReport:
    def test_improvement_score_positive(self):
        r = ComparisonReport(before_score=60, after_score=80)
        assert r.improvement_score == 20
        assert r.improved is True
        assert r.degraded is False

    def test_improvement_score_negative(self):
        r = ComparisonReport(before_score=80, after_score=60)
        assert r.improvement_score == -20
        assert r.improved is False
        assert r.degraded is True

    def test_improvement_score_zero(self):
        r = ComparisonReport(before_score=70, after_score=70)
        assert r.improvement_score == 0
        assert r.improved is False
        assert r.degraded is False

    def test_totals(self):
        r = ComparisonReport(
            new_findings=[ComparedFinding(status="new")] * 3,
            resolved_findings=[ComparedFinding(status="resolved")] * 2,
            persistent_findings=[ComparedFinding(status="persistent")],
            regression_findings=[ComparedFinding(status="regression")],
        )
        assert r.total_new == 3
        assert r.total_resolved == 2
        assert r.total_persistent == 1
        assert r.total_regressions == 1

    def test_has_regressions(self):
        r = ComparisonReport(
            regression_findings=[ComparedFinding(status="regression")]
        )
        assert r.has_regressions is True

    def test_no_regressions(self):
        r = ComparisonReport()
        assert r.has_regressions is False

    def test_exit_code_clean(self):
        r = ComparisonReport()
        assert r.exit_code == 0

    def test_exit_code_critical(self):
        r = ComparisonReport(
            new_findings=[ComparedFinding(
                status="new", severity="CRITICAL"
            )]
        )
        assert r.exit_code == 2

    def test_exit_code_high(self):
        r = ComparisonReport(
            new_findings=[ComparedFinding(
                status="new", severity="HIGH"
            )]
        )
        assert r.exit_code == 1

    def test_exit_code_regression_critical(self):
        r = ComparisonReport(
            regression_findings=[ComparedFinding(
                status="regression", severity="CRITICAL"
            )]
        )
        assert r.exit_code == 2

    def test_to_dict(self):
        r = ComparisonReport(
            before_label="v1", after_label="v2",
            before_score=70, after_score=85,
        )
        d = r.to_dict()
        assert d["before_label"] == "v1"
        assert d["improvement_score"] == 15
        assert d["improved"] is True
        assert "summary" in d


# --- TimelinePoint ---

class TestTimelinePoint:
    def test_to_dict(self):
        tp = TimelinePoint(
            label="scan-1", score=85, grade="A",
            total_findings=5,
            severity_counts={"HIGH": 3, "LOW": 2},
        )
        d = tp.to_dict()
        assert d["score"] == 85
        assert d["total_findings"] == 5


# --- TimelineReport ---

class TestTimelineReport:
    def test_trend_improving(self):
        tl = TimelineReport(points=[
            TimelinePoint(score=60),
            TimelinePoint(score=70),
            TimelinePoint(score=90),
        ])
        assert tl.trend == "improving"

    def test_trend_degrading(self):
        tl = TimelineReport(points=[
            TimelinePoint(score=90),
            TimelinePoint(score=70),
            TimelinePoint(score=60),
        ])
        assert tl.trend == "degrading"

    def test_trend_stable(self):
        tl = TimelineReport(points=[
            TimelinePoint(score=80),
            TimelinePoint(score=82),
        ])
        assert tl.trend == "stable"

    def test_trend_single_point(self):
        tl = TimelineReport(points=[TimelinePoint(score=80)])
        assert tl.trend == "stable"

    def test_trend_empty(self):
        tl = TimelineReport()
        assert tl.trend == "stable"

    def test_to_dict(self):
        tl = TimelineReport(points=[TimelinePoint(score=80)])
        d = tl.to_dict()
        assert d["trend"] == "stable"
        assert len(d["points"]) == 1


# --- Internal helpers ---

class TestInternalHelpers:
    def test_finding_key_same_finding(self):
        f = _finding()
        assert _finding_key(f) == _finding_key(f)

    def test_finding_key_different_rule(self):
        f1 = _finding(rule_id="A")
        f2 = _finding(rule_id="B")
        assert _finding_key(f1) != _finding_key(f2)

    def test_finding_key_different_title(self):
        f1 = _finding(title="X")
        f2 = _finding(title="Y")
        assert _finding_key(f1) != _finding_key(f2)

    def test_extract_findings_standard(self):
        data = _scan_data([_finding()])
        assert len(_extract_findings(data)) == 1

    def test_extract_findings_flat(self):
        data = {"findings": [_finding()]}
        assert len(_extract_findings(data)) == 1

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

    def test_count_severities(self):
        findings = [_finding(severity="HIGH"), _finding(severity="HIGH"),
                     _finding(severity="LOW")]
        counts = _count_severities(findings)
        assert counts["HIGH"] == 2
        assert counts["LOW"] == 1

    def test_estimate_score_clean(self):
        assert _estimate_score([]) == 100

    def test_estimate_score_penalized(self):
        findings = [_finding(severity="CRITICAL")]
        score = _estimate_score(findings)
        assert score < 100

    def test_estimate_score_floor(self):
        findings = [_finding(severity="CRITICAL")] * 20
        score = _estimate_score(findings)
        assert score == 0

    def test_extract_score_from_summary(self):
        data = _scan_data([], trust_score=88, grade="A")
        score, grade = _extract_score(data)
        assert score == 88
        assert grade == "A"

    def test_extract_score_numeric(self):
        data = {"summary": {"trust_score": 75}}
        score, _ = _extract_score(data)
        assert score == 75

    def test_extract_score_fallback(self):
        data = {"summary": {}, "results": [{"file": "a.py", "findings": [
            _finding(severity="HIGH")
        ]}]}
        score, _ = _extract_score(data)
        assert score == 95  # 100 - 5 (HIGH weight)

    def test_to_compared(self):
        f = _finding(rule_id="CMD-001", severity="HIGH")
        cf = _to_compared(f, "new")
        assert cf.status == "new"
        assert cf.rule_id == "CMD-001"

    def test_esc(self):
        assert _esc("<b>") == "&lt;b&gt;"
        assert _esc('"x"') == "&quot;x&quot;"


# --- compare_scans ---

class TestCompareScans:
    def test_identical_scans(self):
        data = _scan_data([_finding()])
        report = compare_scans(data, data)
        assert report.total_new == 0
        assert report.total_resolved == 0
        assert report.total_persistent == 1

    def test_new_finding(self):
        before = _scan_data([])
        after = _scan_data([_finding()])
        report = compare_scans(before, after)
        assert report.total_new == 1
        assert report.total_resolved == 0

    def test_resolved_finding(self):
        before = _scan_data([_finding()])
        after = _scan_data([])
        report = compare_scans(before, after)
        assert report.total_new == 0
        assert report.total_resolved == 1

    def test_score_delta_positive(self):
        before = _scan_data([_finding()], trust_score=60)
        after = _scan_data([], trust_score=90)
        report = compare_scans(before, after)
        assert report.improvement_score == 30
        assert report.improved is True

    def test_score_delta_negative(self):
        before = _scan_data([], trust_score=90)
        after = _scan_data([_finding()], trust_score=60)
        report = compare_scans(before, after)
        assert report.improvement_score == -30
        assert report.degraded is True

    def test_labels(self):
        data = _scan_data([])
        report = compare_scans(data, data, "v1", "v2")
        assert report.before_label == "v1"
        assert report.after_label == "v2"

    def test_severity_delta(self):
        before = _scan_data([_finding(severity="HIGH")])
        after = _scan_data([
            _finding(severity="HIGH"),
            _finding(severity="CRITICAL", rule_id="X", title="New"),
        ])
        report = compare_scans(before, after)
        delta = report.severity_delta.delta
        assert delta.get("CRITICAL", 0) == 1

    def test_multiple_new_and_resolved(self):
        f1 = _finding(rule_id="A", title="A")
        f2 = _finding(rule_id="B", title="B")
        f3 = _finding(rule_id="C", title="C")
        before = _scan_data([f1, f2])
        after = _scan_data([f2, f3])
        report = compare_scans(before, after)
        assert report.total_new == 1
        assert report.total_resolved == 1
        assert report.total_persistent == 1

    def test_empty_scans(self):
        report = compare_scans(
            _scan_data([]), _scan_data([])
        )
        assert report.total_new == 0
        assert report.total_resolved == 0
        assert report.total_persistent == 0

    def test_timestamp_set(self):
        report = compare_scans(_scan_data([]), _scan_data([]))
        assert report.timestamp != ""


# --- compare_scan_files ---

class TestCompareScanFiles:
    def test_from_files(self):
        before = _scan_data([_finding()])
        after = _scan_data([])
        bp = _write_json(before)
        ap = _write_json(after)
        report = compare_scan_files(bp, ap)
        assert report.total_resolved == 1
        assert report.before_label == str(bp)

    def test_identical_files(self):
        data = _scan_data([_finding()])
        p = _write_json(data)
        report = compare_scan_files(p, p)
        assert report.total_persistent == 1


# --- detect_regressions_from_history ---

class TestDetectRegressions:
    def test_no_regressions(self):
        scan1 = _scan_data([_finding()])
        scan2 = _scan_data([_finding()])
        result = detect_regressions_from_history([scan1, scan2])
        assert len(result) == 0

    def test_regression_detected(self):
        f = _finding(rule_id="CMD-001", title="cmd inj")
        scan1 = _scan_data([f])   # present
        scan2 = _scan_data([])    # resolved
        scan3 = _scan_data([f])   # reappears
        result = detect_regressions_from_history([scan1, scan2, scan3])
        assert len(result) >= 1
        assert result[0].status == "regression"

    def test_single_scan(self):
        result = detect_regressions_from_history([_scan_data([])])
        assert result == []

    def test_empty_list(self):
        result = detect_regressions_from_history([])
        assert result == []


# --- build_timeline ---

class TestBuildTimeline:
    def test_basic_timeline(self):
        scans = [
            _scan_data([_finding()], trust_score=70),
            _scan_data([], trust_score=95),
        ]
        tl = build_timeline(scans, labels=["v1", "v2"])
        assert len(tl.points) == 2
        assert tl.points[0].label == "v1"
        assert tl.points[1].score == 95

    def test_auto_labels(self):
        scans = [_scan_data([]), _scan_data([])]
        tl = build_timeline(scans)
        assert tl.points[0].label == "scan-0"
        assert tl.points[1].label == "scan-1"

    def test_empty_scans(self):
        tl = build_timeline([])
        assert len(tl.points) == 0

    def test_finding_counts(self):
        scans = [_scan_data([_finding(), _finding(rule_id="X", title="X")])]
        tl = build_timeline(scans)
        assert tl.points[0].total_findings == 2

    def test_trend_computation(self):
        scans = [
            _scan_data([_finding()] * 5, trust_score=50),
            _scan_data([_finding()], trust_score=80),
            _scan_data([], trust_score=100),
        ]
        tl = build_timeline(scans)
        assert tl.trend == "improving"


# --- HTML generation ---

class TestComparisonCSS:
    def test_contains_dark_theme(self):
        css = _comparison_css()
        assert "--bg" in css
        assert "#0f172a" in css

    def test_contains_badge_styles(self):
        css = _comparison_css()
        assert "badge-new" in css
        assert "badge-resolved" in css


class TestRenderScorePanel:
    def test_contains_scores(self):
        r = ComparisonReport(before_score=70, after_score=85)
        html = _render_score_panel(r)
        assert "70" in html
        assert "85" in html

    def test_positive_delta(self):
        r = ComparisonReport(before_score=60, after_score=80)
        html = _render_score_panel(r)
        assert "+20" in html
        assert "delta-positive" in html

    def test_negative_delta(self):
        r = ComparisonReport(before_score=80, after_score=60)
        html = _render_score_panel(r)
        assert "-20" in html
        assert "delta-negative" in html

    def test_zero_delta(self):
        r = ComparisonReport(before_score=70, after_score=70)
        html = _render_score_panel(r)
        assert "delta-neutral" in html


class TestRenderStats:
    def test_contains_counts(self):
        r = ComparisonReport(
            new_findings=[ComparedFinding(status="new")] * 3,
            resolved_findings=[ComparedFinding(status="resolved")] * 2,
        )
        html = _render_stats(r)
        assert "3" in html
        assert "2" in html


class TestRenderFindingRow:
    def test_basic_row(self):
        cf = ComparedFinding(
            status="new", rule_id="CMD-001",
            severity="HIGH", title="Injection",
            file="s.py", line=5,
        )
        html = _render_finding_row(cf, "badge-new", "NEW")
        assert "CMD-001" in html
        assert "Injection" in html
        assert "badge-new" in html

    def test_escapes_content(self):
        cf = ComparedFinding(
            status="new", title="<script>", severity="LOW",
        )
        html = _render_finding_row(cf, "badge-new", "NEW")
        assert "<script>" not in html
        assert "&lt;script&gt;" in html


class TestRenderFindingsSection:
    def test_empty_list(self):
        html = _render_findings_section("Title", [], "badge-new", "NEW")
        assert html == ""

    def test_non_empty(self):
        findings = [ComparedFinding(
            status="new", severity="HIGH",
            rule_id="X", title="Test",
        )]
        html = _render_findings_section("New", findings, "badge-new", "NEW")
        assert "New" in html
        assert "Test" in html


class TestRenderTimeline:
    def test_empty_timeline(self):
        tl = TimelineReport()
        assert _render_timeline(tl) == ""

    def test_renders_bars(self):
        tl = TimelineReport(points=[
            TimelinePoint(label="v1", score=70),
            TimelinePoint(label="v2", score=90),
        ])
        html = _render_timeline(tl)
        assert "v1" in html
        assert "v2" in html
        assert "tl-bar" in html


# --- generate_comparison_html ---

class TestGenerateComparisonHTML:
    def test_valid_html(self):
        report = ComparisonReport(before_score=70, after_score=85)
        html = generate_comparison_html(report)
        assert "<!DOCTYPE html>" in html
        assert "</html>" in html

    def test_contains_css(self):
        html = generate_comparison_html(ComparisonReport())
        assert "<style>" in html
        assert "--bg" in html

    def test_contains_findings(self):
        report = ComparisonReport(
            new_findings=[ComparedFinding(
                status="new", severity="HIGH",
                rule_id="CMD-001", title="Injection",
            )]
        )
        html = generate_comparison_html(report)
        assert "CMD-001" in html
        assert "Injection" in html

    def test_with_timeline(self):
        report = ComparisonReport()
        tl = TimelineReport(points=[
            TimelinePoint(label="v1", score=70),
        ])
        html = generate_comparison_html(report, timeline=tl)
        assert "Timeline" in html
        assert "v1" in html

    def test_no_timeline(self):
        html = generate_comparison_html(ComparisonReport())
        assert "Timeline" not in html

    def test_dark_theme(self):
        html = generate_comparison_html(ComparisonReport())
        assert "#0f172a" in html


# --- format_comparison_text ---

class TestFormatComparisonText:
    def test_basic_output(self):
        report = ComparisonReport(
            before_label="scan1", after_label="scan2",
            before_score=60, after_score=80,
            new_findings=[ComparedFinding(
                status="new", severity="HIGH",
                rule_id="CMD-001", title="Injection",
            )],
        )
        text = format_comparison_text(report)
        assert "scan1" in text
        assert "scan2" in text
        assert "+20" in text
        assert "CMD-001" in text

    def test_resolved_section(self):
        report = ComparisonReport(
            resolved_findings=[ComparedFinding(
                status="resolved", severity="LOW",
                rule_id="X", title="Fixed",
            )],
        )
        text = format_comparison_text(report)
        assert "RESOLVED" in text
        assert "Fixed" in text

    def test_regression_section(self):
        report = ComparisonReport(
            regression_findings=[ComparedFinding(
                status="regression", severity="HIGH",
                rule_id="Y", title="Back again",
            )],
        )
        text = format_comparison_text(report)
        assert "REGRESSIONS" in text
        assert "Back again" in text

    def test_negative_delta(self):
        report = ComparisonReport(before_score=90, after_score=60)
        text = format_comparison_text(report)
        assert "-30" in text

    def test_empty_report(self):
        text = format_comparison_text(ComparisonReport())
        assert "Scan Comparison" in text


# --- format_comparison_json ---

class TestFormatComparisonJson:
    def test_valid_json(self):
        report = ComparisonReport(before_score=70, after_score=85)
        data = json.loads(format_comparison_json(report))
        assert data["improvement_score"] == 15
        assert data["improved"] is True

    def test_findings_included(self):
        report = ComparisonReport(
            new_findings=[ComparedFinding(
                status="new", rule_id="X", severity="HIGH", title="T",
            )]
        )
        data = json.loads(format_comparison_json(report))
        assert len(data["new_findings"]) == 1
        assert data["new_findings"][0]["rule_id"] == "X"

    def test_empty_report(self):
        data = json.loads(format_comparison_json(ComparisonReport()))
        assert data["summary"]["new"] == 0
