"""Tests for Scan Diff Reporter."""

from __future__ import annotations

import json

import pytest

from mcpaudit.scan_diff_reporter import (
    DiffReporter,
    DiffStatus,
    DiffSummary,
    ScanDiffEntry,
    Severity,
    TimeSeriesPoint,
    TimeSeriesReport,
    _extract_findings,
    _extract_scan_id,
    _extract_timestamp,
    _finding_id,
    format_json,
    format_markdown,
    format_time_series_json,
    format_time_series_markdown,
)


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

class TestDiffStatus:
    def test_new(self):
        assert DiffStatus.NEW == "NEW"

    def test_fixed(self):
        assert DiffStatus.FIXED == "FIXED"

    def test_unchanged(self):
        assert DiffStatus.UNCHANGED == "UNCHANGED"

    def test_modified(self):
        assert DiffStatus.MODIFIED == "MODIFIED"

    def test_all_contains_four(self):
        assert len(DiffStatus.ALL) == 4
        assert "NEW" in DiffStatus.ALL
        assert "FIXED" in DiffStatus.ALL


class TestSeverity:
    def test_critical(self):
        assert Severity.CRITICAL == "CRITICAL"

    def test_all_contains_five(self):
        assert len(Severity.ALL) == 5


# ---------- ScanDiffEntry ----------

class TestScanDiffEntry:
    def test_basic_creation(self):
        entry = ScanDiffEntry(finding_id="abc123", status=DiffStatus.NEW)
        assert entry.finding_id == "abc123"
        assert entry.status == "NEW"
        assert entry.severity == Severity.INFO

    def test_to_dict(self):
        entry = ScanDiffEntry(
            finding_id="abc123",
            status=DiffStatus.NEW,
            severity=Severity.HIGH,
            file="server.py",
            line=42,
            title="Unsafe input",
            rule_id="MCP-001",
        )
        d = entry.to_dict()
        assert d["finding_id"] == "abc123"
        assert d["status"] == "NEW"
        assert d["severity"] == "HIGH"
        assert d["file"] == "server.py"
        assert d["line"] == 42
        assert d["rule_id"] == "MCP-001"
        assert "previous_severity" not in d

    def test_to_dict_with_previous_severity(self):
        entry = ScanDiffEntry(
            finding_id="abc123",
            status=DiffStatus.MODIFIED,
            severity=Severity.CRITICAL,
            previous_severity=Severity.HIGH,
        )
        d = entry.to_dict()
        assert d["previous_severity"] == "HIGH"

    def test_default_values(self):
        entry = ScanDiffEntry(finding_id="x", status="NEW")
        assert entry.file == ""
        assert entry.line == 0
        assert entry.title == ""
        assert entry.rule_id == ""
        assert entry.previous_severity == ""


# ---------- DiffSummary ----------

class TestDiffSummary:
    def test_empty_summary(self):
        s = DiffSummary()
        assert s.new_count == 0
        assert s.fixed_count == 0
        assert s.unchanged_count == 0
        assert s.modified_count == 0
        assert s.net_delta == 0
        assert s.improvement_score == 0.0

    def test_counts(self):
        s = DiffSummary(entries=[
            ScanDiffEntry("a", DiffStatus.NEW, Severity.HIGH),
            ScanDiffEntry("b", DiffStatus.NEW, Severity.MEDIUM),
            ScanDiffEntry("c", DiffStatus.FIXED, Severity.LOW),
            ScanDiffEntry("d", DiffStatus.UNCHANGED, Severity.LOW),
            ScanDiffEntry("e", DiffStatus.MODIFIED, Severity.HIGH),
        ])
        assert s.new_count == 2
        assert s.fixed_count == 1
        assert s.unchanged_count == 1
        assert s.modified_count == 1

    def test_net_delta_positive(self):
        s = DiffSummary(entries=[
            ScanDiffEntry("a", DiffStatus.NEW, Severity.HIGH),
            ScanDiffEntry("b", DiffStatus.NEW, Severity.MEDIUM),
            ScanDiffEntry("c", DiffStatus.FIXED, Severity.LOW),
        ])
        assert s.net_delta == 1

    def test_net_delta_negative(self):
        s = DiffSummary(entries=[
            ScanDiffEntry("a", DiffStatus.FIXED, Severity.HIGH),
            ScanDiffEntry("b", DiffStatus.FIXED, Severity.MEDIUM),
            ScanDiffEntry("c", DiffStatus.NEW, Severity.LOW),
        ])
        assert s.net_delta == -1

    def test_net_delta_zero(self):
        s = DiffSummary(entries=[
            ScanDiffEntry("a", DiffStatus.NEW, Severity.HIGH),
            ScanDiffEntry("b", DiffStatus.FIXED, Severity.LOW),
        ])
        assert s.net_delta == 0

    def test_improvement_score_all_fixed(self):
        s = DiffSummary(entries=[
            ScanDiffEntry("a", DiffStatus.FIXED, Severity.CRITICAL),
        ])
        assert s.improvement_score == 100.0

    def test_improvement_score_all_new(self):
        s = DiffSummary(entries=[
            ScanDiffEntry("a", DiffStatus.NEW, Severity.HIGH),
        ])
        assert s.improvement_score == -100.0

    def test_improvement_score_mixed(self):
        s = DiffSummary(entries=[
            ScanDiffEntry("a", DiffStatus.NEW, Severity.HIGH),
            ScanDiffEntry("b", DiffStatus.FIXED, Severity.HIGH),
        ])
        assert s.improvement_score == 0.0

    def test_improvement_score_weighted(self):
        # Fixed critical (10) vs new low (1) -> (10-1)/(10+1)*100 = 81.82
        s = DiffSummary(entries=[
            ScanDiffEntry("a", DiffStatus.FIXED, Severity.CRITICAL),
            ScanDiffEntry("b", DiffStatus.NEW, Severity.LOW),
        ])
        assert s.improvement_score > 80

    def test_severity_breakdown(self):
        s = DiffSummary(entries=[
            ScanDiffEntry("a", DiffStatus.NEW, Severity.CRITICAL),
            ScanDiffEntry("b", DiffStatus.FIXED, Severity.HIGH),
            ScanDiffEntry("c", DiffStatus.FIXED, Severity.HIGH),
            ScanDiffEntry("d", DiffStatus.UNCHANGED, Severity.LOW),
        ])
        bd = s.severity_breakdown
        assert bd["CRITICAL"]["new"] == 1
        assert bd["CRITICAL"]["fixed"] == 0
        assert bd["HIGH"]["new"] == 0
        assert bd["HIGH"]["fixed"] == 2
        assert bd["LOW"]["new"] == 0
        assert bd["LOW"]["fixed"] == 0

    def test_to_dict(self):
        s = DiffSummary(
            scan_before_id="scan1",
            scan_after_id="scan2",
            timestamp_before="2024-01-01",
            timestamp_after="2024-01-02",
            entries=[ScanDiffEntry("a", DiffStatus.NEW, Severity.HIGH)],
        )
        d = s.to_dict()
        assert d["scan_before_id"] == "scan1"
        assert d["scan_after_id"] == "scan2"
        assert d["counts"]["new"] == 1
        assert d["net_delta"] == 1
        assert len(d["entries"]) == 1


# ---------- TimeSeriesPoint ----------

class TestTimeSeriesPoint:
    def test_creation(self):
        p = TimeSeriesPoint(timestamp="2024-01-01", total_findings=5)
        assert p.timestamp == "2024-01-01"
        assert p.total_findings == 5

    def test_to_dict(self):
        p = TimeSeriesPoint(
            timestamp="t1", total_findings=10,
            new_count=3, fixed_count=2,
            net_delta=1, improvement_score=-20.0,
        )
        d = p.to_dict()
        assert d["timestamp"] == "t1"
        assert d["new_count"] == 3
        assert d["net_delta"] == 1


# ---------- TimeSeriesReport ----------

class TestTimeSeriesReport:
    def test_empty(self):
        r = TimeSeriesReport()
        assert r.points == []
        assert r.overall_trend == ""

    def test_to_dict(self):
        r = TimeSeriesReport(
            points=[TimeSeriesPoint("t1")],
            overall_trend="improving",
            total_new_across_all=5,
            total_fixed_across_all=10,
        )
        d = r.to_dict()
        assert len(d["points"]) == 1
        assert d["overall_trend"] == "improving"


# ---------- Helper Functions ----------

class TestFindingId:
    def test_deterministic(self):
        f = {"rule_id": "R1", "file": "a.py", "title": "bug"}
        assert _finding_id(f) == _finding_id(f)

    def test_different_findings_different_ids(self):
        f1 = {"rule_id": "R1", "file": "a.py", "title": "bug1"}
        f2 = {"rule_id": "R1", "file": "a.py", "title": "bug2"}
        assert _finding_id(f1) != _finding_id(f2)

    def test_empty_finding(self):
        fid = _finding_id({})
        assert isinstance(fid, str)
        assert len(fid) == 16

    def test_uses_description_fallback(self):
        f = {"rule_id": "R1", "file": "a.py", "description": "desc"}
        fid = _finding_id(f)
        assert len(fid) == 16


class TestExtractFindings:
    def test_results_format(self):
        data = {"results": [{"findings": [{"rule_id": "R1"}, {"rule_id": "R2"}]}]}
        assert len(_extract_findings(data)) == 2

    def test_findings_format(self):
        data = {"findings": [{"rule_id": "R1"}]}
        assert len(_extract_findings(data)) == 1

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

    def test_multiple_results(self):
        data = {"results": [
            {"findings": [{"rule_id": "R1"}]},
            {"findings": [{"rule_id": "R2"}, {"rule_id": "R3"}]},
        ]}
        assert len(_extract_findings(data)) == 3


class TestExtractTimestamp:
    def test_top_level(self):
        assert _extract_timestamp({"timestamp": "2024-01-01"}) == "2024-01-01"

    def test_in_summary(self):
        data = {"summary": {"timestamp": "2024-02-01"}}
        assert _extract_timestamp(data) == "2024-02-01"

    def test_missing(self):
        assert _extract_timestamp({}) == ""


class TestExtractScanId:
    def test_explicit_id(self):
        assert _extract_scan_id({"scan_id": "abc"}) == "abc"

    def test_generated_id(self):
        sid = _extract_scan_id({"findings": []})
        assert isinstance(sid, str)
        assert len(sid) == 12


# ---------- DiffReporter ----------

class TestDiffReporter:
    def setup_method(self):
        self.reporter = DiffReporter()

    def test_empty_scans(self):
        s = self.reporter.compare({}, {})
        assert s.new_count == 0
        assert s.fixed_count == 0

    def test_all_new(self):
        before = {"findings": []}
        after = {"findings": [
            {"rule_id": "R1", "file": "a.py", "title": "Bug", "severity": "HIGH"},
        ]}
        s = self.reporter.compare(before, after)
        assert s.new_count == 1
        assert s.fixed_count == 0

    def test_all_fixed(self):
        before = {"findings": [
            {"rule_id": "R1", "file": "a.py", "title": "Bug", "severity": "HIGH"},
        ]}
        after = {"findings": []}
        s = self.reporter.compare(before, after)
        assert s.new_count == 0
        assert s.fixed_count == 1

    def test_unchanged(self):
        finding = {"rule_id": "R1", "file": "a.py", "title": "Bug", "severity": "HIGH"}
        s = self.reporter.compare({"findings": [finding]}, {"findings": [finding]})
        assert s.unchanged_count == 1
        assert s.new_count == 0
        assert s.fixed_count == 0

    def test_modified_severity(self):
        before = {"findings": [
            {"rule_id": "R1", "file": "a.py", "title": "Bug", "severity": "HIGH"},
        ]}
        after = {"findings": [
            {"rule_id": "R1", "file": "a.py", "title": "Bug", "severity": "CRITICAL"},
        ]}
        s = self.reporter.compare(before, after)
        assert s.modified_count == 1
        mod = [e for e in s.entries if e.status == DiffStatus.MODIFIED][0]
        assert mod.previous_severity == "HIGH"
        assert mod.severity == "CRITICAL"

    def test_mixed_changes(self):
        before = {"findings": [
            {"rule_id": "R1", "file": "a.py", "title": "Old bug", "severity": "LOW"},
            {"rule_id": "R2", "file": "b.py", "title": "Stays", "severity": "MEDIUM"},
        ]}
        after = {"findings": [
            {"rule_id": "R2", "file": "b.py", "title": "Stays", "severity": "MEDIUM"},
            {"rule_id": "R3", "file": "c.py", "title": "New bug", "severity": "HIGH"},
        ]}
        s = self.reporter.compare(before, after)
        assert s.new_count == 1
        assert s.fixed_count == 1
        assert s.unchanged_count == 1

    def test_timestamps_extracted(self):
        before = {"timestamp": "2024-01-01", "findings": []}
        after = {"timestamp": "2024-01-02", "findings": []}
        s = self.reporter.compare(before, after)
        assert s.timestamp_before == "2024-01-01"
        assert s.timestamp_after == "2024-01-02"

    def test_scan_ids_generated(self):
        s = self.reporter.compare({"findings": []}, {"findings": []})
        assert len(s.scan_before_id) > 0
        assert len(s.scan_after_id) > 0

    def test_results_format_input(self):
        before = {"results": [{"findings": [
            {"rule_id": "R1", "file": "a.py", "title": "Bug", "severity": "HIGH"},
        ]}]}
        after = {"results": [{"findings": []}]}
        s = self.reporter.compare(before, after)
        assert s.fixed_count == 1

    def test_large_diff(self):
        before_findings = [
            {"rule_id": f"R{i}", "file": f"f{i}.py", "title": f"Bug {i}", "severity": "LOW"}
            for i in range(50)
        ]
        after_findings = [
            {"rule_id": f"R{i}", "file": f"f{i}.py", "title": f"Bug {i}", "severity": "LOW"}
            for i in range(25, 75)
        ]
        s = self.reporter.compare(
            {"findings": before_findings},
            {"findings": after_findings},
        )
        assert s.fixed_count == 25
        assert s.new_count == 25
        assert s.unchanged_count == 25


# ---------- Time Series ----------

class TestTimeSeries:
    def setup_method(self):
        self.reporter = DiffReporter()

    def test_single_scan_no_comparison(self):
        r = self.reporter.compare_time_series([{"findings": []}])
        assert r.points == []

    def test_two_scans(self):
        scan1 = {"timestamp": "t1", "findings": [
            {"rule_id": "R1", "file": "a.py", "title": "Bug", "severity": "HIGH"},
        ]}
        scan2 = {"timestamp": "t2", "findings": []}
        r = self.reporter.compare_time_series([scan1, scan2])
        assert len(r.points) == 1
        assert r.points[0].fixed_count == 1

    def test_three_scans(self):
        scans = [
            {"timestamp": "t1", "findings": [
                {"rule_id": "R1", "file": "a.py", "title": "Bug", "severity": "HIGH"},
            ]},
            {"timestamp": "t2", "findings": [
                {"rule_id": "R1", "file": "a.py", "title": "Bug", "severity": "HIGH"},
                {"rule_id": "R2", "file": "b.py", "title": "New", "severity": "LOW"},
            ]},
            {"timestamp": "t3", "findings": []},
        ]
        r = self.reporter.compare_time_series(scans)
        assert len(r.points) == 2

    def test_improving_trend(self):
        scans = [
            {"timestamp": "t1", "findings": [
                {"rule_id": f"R{i}", "file": f"f{i}.py", "title": f"B{i}", "severity": "HIGH"}
                for i in range(10)
            ]},
            {"timestamp": "t2", "findings": [
                {"rule_id": f"R{i}", "file": f"f{i}.py", "title": f"B{i}", "severity": "HIGH"}
                for i in range(5)
            ]},
        ]
        r = self.reporter.compare_time_series(scans)
        assert r.overall_trend == "improving"

    def test_degrading_trend(self):
        scans = [
            {"timestamp": "t1", "findings": []},
            {"timestamp": "t2", "findings": [
                {"rule_id": f"R{i}", "file": f"f{i}.py", "title": f"B{i}", "severity": "HIGH"}
                for i in range(10)
            ]},
        ]
        r = self.reporter.compare_time_series(scans)
        assert r.overall_trend == "degrading"

    def test_stable_trend(self):
        finding = {"rule_id": "R1", "file": "a.py", "title": "Bug", "severity": "HIGH"}
        scans = [
            {"timestamp": "t1", "findings": [finding]},
            {"timestamp": "t2", "findings": [finding]},
        ]
        r = self.reporter.compare_time_series(scans)
        assert r.overall_trend == "stable"

    def test_total_counts(self):
        scans = [
            {"timestamp": "t1", "findings": [
                {"rule_id": "R1", "file": "a.py", "title": "B1", "severity": "HIGH"},
            ]},
            {"timestamp": "t2", "findings": [
                {"rule_id": "R2", "file": "b.py", "title": "B2", "severity": "LOW"},
            ]},
            {"timestamp": "t3", "findings": []},
        ]
        r = self.reporter.compare_time_series(scans)
        assert r.total_new_across_all >= 1
        assert r.total_fixed_across_all >= 1

    def test_empty_scans_list(self):
        r = self.reporter.compare_time_series([])
        assert r.points == []
        assert r.overall_trend == ""


# ---------- Filter ----------

class TestFilterEntries:
    def setup_method(self):
        self.reporter = DiffReporter()
        self.summary = DiffSummary(entries=[
            ScanDiffEntry("a", DiffStatus.NEW, Severity.HIGH, file="server.py"),
            ScanDiffEntry("b", DiffStatus.FIXED, Severity.LOW, file="client.py"),
            ScanDiffEntry("c", DiffStatus.NEW, Severity.CRITICAL, file="auth.py"),
            ScanDiffEntry("d", DiffStatus.UNCHANGED, Severity.MEDIUM, file="server.py"),
        ])

    def test_filter_by_status(self):
        result = self.reporter.filter_entries(self.summary, status=DiffStatus.NEW)
        assert len(result) == 2

    def test_filter_by_severity(self):
        result = self.reporter.filter_entries(self.summary, severity=Severity.HIGH)
        assert len(result) == 1
        assert result[0].finding_id == "a"

    def test_filter_by_file_pattern(self):
        result = self.reporter.filter_entries(self.summary, file_pattern="server")
        assert len(result) == 2

    def test_filter_combined(self):
        result = self.reporter.filter_entries(
            self.summary, status=DiffStatus.NEW, severity=Severity.HIGH,
        )
        assert len(result) == 1

    def test_filter_no_match(self):
        result = self.reporter.filter_entries(self.summary, severity="UNKNOWN")
        assert len(result) == 0


# ---------- Markdown Formatter ----------

class TestFormatMarkdown:
    def test_basic_output(self):
        s = DiffSummary(entries=[
            ScanDiffEntry("a", DiffStatus.NEW, Severity.HIGH, file="x.py", line=10, title="Bug"),
        ])
        md = format_markdown(s)
        assert "# Scan Diff Report" in md
        assert "New findings" in md
        assert "| NEW |" in md

    def test_timestamps_shown(self):
        s = DiffSummary(
            timestamp_before="2024-01-01",
            timestamp_after="2024-01-02",
        )
        md = format_markdown(s)
        assert "2024-01-01" in md
        assert "2024-01-02" in md

    def test_severity_breakdown_shown(self):
        s = DiffSummary(entries=[
            ScanDiffEntry("a", DiffStatus.NEW, Severity.CRITICAL),
        ])
        md = format_markdown(s)
        assert "CRITICAL" in md

    def test_modified_shows_arrow(self):
        s = DiffSummary(entries=[
            ScanDiffEntry(
                "a", DiffStatus.MODIFIED, Severity.CRITICAL,
                previous_severity=Severity.HIGH, file="x.py", line=1,
            ),
        ])
        md = format_markdown(s)
        assert "HIGH -> CRITICAL" in md

    def test_empty_summary(self):
        s = DiffSummary()
        md = format_markdown(s)
        assert "# Scan Diff Report" in md


# ---------- JSON Formatter ----------

class TestFormatJson:
    def test_valid_json(self):
        s = DiffSummary(entries=[
            ScanDiffEntry("a", DiffStatus.NEW, Severity.HIGH),
        ])
        text = format_json(s)
        data = json.loads(text)
        assert data["counts"]["new"] == 1

    def test_roundtrip(self):
        s = DiffSummary(
            scan_before_id="s1",
            scan_after_id="s2",
            entries=[
                ScanDiffEntry("a", DiffStatus.FIXED, Severity.LOW),
            ],
        )
        data = json.loads(format_json(s))
        assert data["scan_before_id"] == "s1"
        assert data["counts"]["fixed"] == 1


# ---------- Time Series Formatters ----------

class TestTimeSeriesFormatters:
    def test_markdown_basic(self):
        r = TimeSeriesReport(
            points=[TimeSeriesPoint("t1", 5, 2, 1, 1, -10.0)],
            overall_trend="degrading",
        )
        md = format_time_series_markdown(r)
        assert "Time Series" in md
        assert "degrading" in md

    def test_json_valid(self):
        r = TimeSeriesReport(
            points=[TimeSeriesPoint("t1")],
            overall_trend="stable",
        )
        text = format_time_series_json(r)
        data = json.loads(text)
        assert data["overall_trend"] == "stable"

    def test_empty_report_markdown(self):
        r = TimeSeriesReport()
        md = format_time_series_markdown(r)
        assert "Time Series" in md
