"""Tests for the Report Aggregator (mcpaudit.report_aggregator).

Tests merging, deduplication, timeline, trend analysis,
executive summary, and output formatting.
"""

from __future__ import annotations

import json
import os
import tempfile
from pathlib import Path

import pytest

from mcpaudit.report_aggregator import (
    AggregatedFinding,
    AggregatedReport,
    TimelineEntry,
    TrendAnalysis,
    compute_trend_from_reports,
    format_aggregate_html,
    format_aggregate_json,
    format_aggregate_text,
    merge_report_files,
    merge_reports,
    _build_timeline,
    _compute_trend,
    _dedup_findings,
    _extract_findings,
    _generate_executive_summary,
)


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

def _make_finding_dict(
    rule_id: str = "CMD-001",
    severity: str = "MEDIUM",
    file: str = "server.py",
    line: int = 10,
    title: str = "Test finding",
) -> dict:
    return {
        "rule_id": rule_id,
        "severity": severity,
        "title": title,
        "description": "test",
        "file": file,
        "line": line,
        "code_snippet": "code",
        "recommendation": "fix it",
    }


def _make_report(
    findings: list[dict] | None = None,
    total_findings: int | None = None,
) -> dict:
    if findings is None:
        findings = []
    return {
        "summary": {
            "total_findings": total_findings or len(findings),
            "severity_counts": _count_severities(findings),
        },
        "results": [{"findings": findings, "file": "server.py", "lines_scanned": 100}],
    }


def _count_severities(findings: list[dict]) -> dict:
    counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0}
    for f in findings:
        sev = f.get("severity", "INFO")
        counts[sev] = counts.get(sev, 0) + 1
    return counts


# ---------------------------------------------------------------------------
# AggregatedFinding
# ---------------------------------------------------------------------------

class TestAggregatedFinding:
    def test_dedup_key(self):
        f = AggregatedFinding(
            rule_id="CMD-001", severity="HIGH", title="t",
            description="d", file="a.py", line=10,
        )
        assert f.dedup_key == ("CMD-001", "a.py", 10)

    def test_to_dict(self):
        f = AggregatedFinding(
            rule_id="CMD-001", severity="HIGH", title="t",
            description="d", file="a.py", line=10,
            first_seen="2024-01-01", last_seen="2024-01-02",
            occurrence_count=3, reports=["r1", "r2"],
        )
        d = f.to_dict()
        assert d["rule_id"] == "CMD-001"
        assert d["occurrence_count"] == 3
        assert d["reports"] == ["r1", "r2"]

    def test_default_values(self):
        f = AggregatedFinding(
            rule_id="X", severity="INFO", title="",
            description="", file="", line=0,
        )
        assert f.occurrence_count == 1
        assert f.reports == []


# ---------------------------------------------------------------------------
# TimelineEntry
# ---------------------------------------------------------------------------

class TestTimelineEntry:
    def test_to_dict(self):
        entry = TimelineEntry(
            date="2024-01-15", total_findings=5,
            severity_counts={"CRITICAL": 1, "HIGH": 2},
            report_name="scan1",
        )
        d = entry.to_dict()
        assert d["date"] == "2024-01-15"
        assert d["total_findings"] == 5


# ---------------------------------------------------------------------------
# TrendAnalysis
# ---------------------------------------------------------------------------

class TestTrendAnalysis:
    def test_to_dict(self):
        t = TrendAnalysis(
            direction="improving",
            finding_delta=-5,
            critical_delta=-1,
        )
        d = t.to_dict()
        assert d["direction"] == "improving"
        assert d["finding_delta"] == -5

    def test_default_values(self):
        t = TrendAnalysis(direction="stable")
        assert t.new_findings == 0
        assert t.resolved_findings == 0


# ---------------------------------------------------------------------------
# _extract_findings
# ---------------------------------------------------------------------------

class TestExtractFindings:
    def test_from_results(self):
        report = _make_report([
            _make_finding_dict("CMD-001"),
            _make_finding_dict("AUTH-001"),
        ])
        findings = _extract_findings(report, "scan1", "2024-01-15")
        assert len(findings) == 2
        assert findings[0].reports == ["scan1"]
        assert findings[0].first_seen == "2024-01-15"

    def test_from_flat_findings(self):
        report = {"findings": [_make_finding_dict()]}
        findings = _extract_findings(report, "flat", "2024-02-01")
        assert len(findings) == 1

    def test_empty_report(self):
        findings = _extract_findings({}, "empty", "2024-01-01")
        assert findings == []

    def test_mixed_results_and_flat(self):
        report = {
            "results": [{"findings": [_make_finding_dict("A-001")]}],
            "findings": [_make_finding_dict("B-001")],
        }
        findings = _extract_findings(report, "mixed", "2024-01-01")
        assert len(findings) == 2


# ---------------------------------------------------------------------------
# _dedup_findings
# ---------------------------------------------------------------------------

class TestDedupFindings:
    def test_no_duplicates(self):
        findings = [
            AggregatedFinding("A-001", "HIGH", "t1", "d", "a.py", 1, reports=["r1"]),
            AggregatedFinding("B-001", "LOW", "t2", "d", "b.py", 2, reports=["r1"]),
        ]
        result = _dedup_findings(findings)
        assert len(result) == 2

    def test_duplicates_merged(self):
        findings = [
            AggregatedFinding("A-001", "HIGH", "t", "d", "a.py", 10,
                              first_seen="2024-01-01", last_seen="2024-01-01",
                              reports=["r1"]),
            AggregatedFinding("A-001", "HIGH", "t", "d", "a.py", 10,
                              first_seen="2024-01-15", last_seen="2024-01-15",
                              reports=["r2"]),
        ]
        result = _dedup_findings(findings)
        assert len(result) == 1
        assert result[0].occurrence_count == 2
        assert result[0].first_seen == "2024-01-01"
        assert result[0].last_seen == "2024-01-15"
        assert "r1" in result[0].reports
        assert "r2" in result[0].reports

    def test_different_lines_not_merged(self):
        findings = [
            AggregatedFinding("A-001", "HIGH", "t", "d", "a.py", 10, reports=["r1"]),
            AggregatedFinding("A-001", "HIGH", "t", "d", "a.py", 20, reports=["r1"]),
        ]
        result = _dedup_findings(findings)
        assert len(result) == 2

    def test_sorted_by_severity(self):
        findings = [
            AggregatedFinding("L-001", "LOW", "t", "d", "a.py", 1),
            AggregatedFinding("C-001", "CRITICAL", "t", "d", "a.py", 2),
            AggregatedFinding("H-001", "HIGH", "t", "d", "a.py", 3),
        ]
        result = _dedup_findings(findings)
        assert result[0].severity == "CRITICAL"
        assert result[1].severity == "HIGH"
        assert result[2].severity == "LOW"

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

    def test_triple_duplicate(self):
        findings = [
            AggregatedFinding("X-001", "MEDIUM", "t", "d", "f.py", 5,
                              reports=[f"r{i}"]) for i in range(3)
        ]
        result = _dedup_findings(findings)
        assert len(result) == 1
        assert result[0].occurrence_count == 3


# ---------------------------------------------------------------------------
# _build_timeline
# ---------------------------------------------------------------------------

class TestBuildTimeline:
    def test_single_report(self):
        report = _make_report([_make_finding_dict(severity="HIGH")])
        timeline = _build_timeline([("scan1", "2024-01-15", report)])
        assert len(timeline) == 1
        assert timeline[0].total_findings == 1

    def test_chronological_order(self):
        r1 = _make_report([_make_finding_dict()])
        r2 = _make_report([_make_finding_dict(), _make_finding_dict(rule_id="X-001")])
        timeline = _build_timeline([
            ("scan2", "2024-02-01", r2),
            ("scan1", "2024-01-01", r1),
        ])
        assert timeline[0].date == "2024-01-01"
        assert timeline[1].date == "2024-02-01"

    def test_severity_counts(self):
        report = _make_report([
            _make_finding_dict(severity="HIGH"),
            _make_finding_dict(severity="HIGH", rule_id="Y-001"),
            _make_finding_dict(severity="LOW", rule_id="Z-001"),
        ])
        timeline = _build_timeline([("s1", "2024-01-01", report)])
        assert timeline[0].severity_counts["HIGH"] == 2
        assert timeline[0].severity_counts["LOW"] == 1

    def test_empty_reports(self):
        assert _build_timeline([]) == []


# ---------------------------------------------------------------------------
# _compute_trend
# ---------------------------------------------------------------------------

class TestComputeTrend:
    def test_single_entry_stable(self):
        timeline = [TimelineEntry("2024-01-01", 5)]
        trend = _compute_trend(timeline)
        assert trend.direction == "stable"

    def test_improving(self):
        timeline = [
            TimelineEntry("2024-01-01", 10,
                          severity_counts={"CRITICAL": 2, "HIGH": 3, "MEDIUM": 5}),
            TimelineEntry("2024-02-01", 3,
                          severity_counts={"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2}),
        ]
        trend = _compute_trend(timeline)
        assert trend.direction == "improving"
        assert trend.finding_delta == -7
        assert trend.critical_delta == -2

    def test_degrading(self):
        timeline = [
            TimelineEntry("2024-01-01", 2, severity_counts={"CRITICAL": 0}),
            TimelineEntry("2024-02-01", 10, severity_counts={"CRITICAL": 3}),
        ]
        trend = _compute_trend(timeline)
        assert trend.direction == "degrading"
        assert trend.finding_delta == 8

    def test_stable_small_change(self):
        timeline = [
            TimelineEntry("2024-01-01", 5),
            TimelineEntry("2024-02-01", 6),
        ]
        trend = _compute_trend(timeline)
        assert trend.direction == "stable"


# ---------------------------------------------------------------------------
# merge_reports
# ---------------------------------------------------------------------------

class TestMergeReports:
    def test_single_report(self):
        report = _make_report([_make_finding_dict()])
        agg = merge_reports([("scan1", "2024-01-15", report)])
        assert agg.total_reports == 1
        assert agg.total_unique_findings == 1

    def test_multiple_reports_dedup(self):
        f = _make_finding_dict("CMD-001", "HIGH", "server.py", 10)
        r1 = _make_report([f])
        r2 = _make_report([f])
        agg = merge_reports([
            ("scan1", "2024-01-01", r1),
            ("scan2", "2024-02-01", r2),
        ])
        assert agg.total_unique_findings == 1
        assert agg.findings[0].occurrence_count == 2

    def test_multiple_reports_different_findings(self):
        r1 = _make_report([_make_finding_dict("A-001")])
        r2 = _make_report([_make_finding_dict("B-001")])
        agg = merge_reports([
            ("scan1", "2024-01-01", r1),
            ("scan2", "2024-02-01", r2),
        ])
        assert agg.total_unique_findings == 2
        assert agg.total_reports == 2

    def test_severity_counts(self):
        r = _make_report([
            _make_finding_dict("A-001", "CRITICAL"),
            _make_finding_dict("B-001", "HIGH"),
            _make_finding_dict("C-001", "HIGH"),
        ])
        agg = merge_reports([("s1", "2024-01-01", r)])
        assert agg.severity_counts["CRITICAL"] == 1
        assert agg.severity_counts["HIGH"] == 2

    def test_report_names_tracked(self):
        r1 = _make_report([])
        r2 = _make_report([])
        agg = merge_reports([
            ("alpha", "2024-01-01", r1),
            ("beta", "2024-02-01", r2),
        ])
        assert agg.report_names == ["alpha", "beta"]

    def test_executive_summary_generated(self):
        r = _make_report([_make_finding_dict("X-001", "CRITICAL")])
        agg = merge_reports([("s1", "2024-01-01", r)])
        assert agg.executive_summary
        assert "1" in agg.executive_summary
        assert "CRITICAL" in agg.executive_summary

    def test_timeline_populated(self):
        r1 = _make_report([_make_finding_dict()])
        r2 = _make_report([])
        agg = merge_reports([
            ("s1", "2024-01-01", r1),
            ("s2", "2024-02-01", r2),
        ])
        assert len(agg.timeline) == 2

    def test_trend_populated(self):
        r1 = _make_report([_make_finding_dict()] * 5)
        r2 = _make_report([_make_finding_dict()])
        agg = merge_reports([
            ("s1", "2024-01-01", r1),
            ("s2", "2024-02-01", r2),
        ])
        assert agg.trend is not None

    def test_empty_reports(self):
        agg = merge_reports([])
        assert agg.total_reports == 0
        assert agg.total_unique_findings == 0

    def test_to_dict(self):
        r = _make_report([_make_finding_dict()])
        agg = merge_reports([("s1", "2024-01-01", r)])
        d = agg.to_dict()
        assert "findings" in d
        assert "timeline" in d
        assert "executive_summary" in d


# ---------------------------------------------------------------------------
# merge_report_files
# ---------------------------------------------------------------------------

class TestMergeReportFiles:
    def test_merge_files(self):
        with tempfile.TemporaryDirectory() as td:
            r1 = _make_report([_make_finding_dict("A-001", "HIGH")])
            r2 = _make_report([_make_finding_dict("B-001", "LOW")])
            p1 = Path(td) / "scan1.json"
            p2 = Path(td) / "scan2.json"
            p1.write_text(json.dumps(r1))
            p2.write_text(json.dumps(r2))
            agg = merge_report_files([p1, p2])
            assert agg.total_reports == 2
            assert agg.total_unique_findings == 2

    def test_nonexistent_file_raises(self):
        with pytest.raises(FileNotFoundError):
            merge_report_files(["/tmp/nonexistent_report_7878.json"])

    def test_single_file(self):
        with tempfile.TemporaryDirectory() as td:
            r = _make_report([_make_finding_dict()])
            p = Path(td) / "scan.json"
            p.write_text(json.dumps(r))
            agg = merge_report_files([p])
            assert agg.total_unique_findings == 1


# ---------------------------------------------------------------------------
# compute_trend_from_reports
# ---------------------------------------------------------------------------

class TestComputeTrendFromReports:
    def test_trend_improving(self):
        r1 = _make_report([_make_finding_dict()] * 5)
        r2 = _make_report([_make_finding_dict()])
        trend = compute_trend_from_reports([
            ("s1", "2024-01-01", r1),
            ("s2", "2024-02-01", r2),
        ])
        assert trend.direction == "improving"

    def test_trend_single_report(self):
        r1 = _make_report([_make_finding_dict()])
        trend = compute_trend_from_reports([("s1", "2024-01-01", r1)])
        assert trend.direction == "stable"


# ---------------------------------------------------------------------------
# _generate_executive_summary
# ---------------------------------------------------------------------------

class TestExecutiveSummary:
    def test_with_critical(self):
        r = AggregatedReport(
            total_reports=2, total_unique_findings=5,
            severity_counts={"CRITICAL": 2, "HIGH": 3},
        )
        summary = _generate_executive_summary(r)
        assert "CRITICAL" in summary
        assert "2" in summary

    def test_no_critical(self):
        r = AggregatedReport(
            total_reports=1, total_unique_findings=1,
            severity_counts={"CRITICAL": 0, "HIGH": 0, "MEDIUM": 1},
        )
        summary = _generate_executive_summary(r)
        assert "No critical" in summary

    def test_with_trend(self):
        r = AggregatedReport(
            total_reports=2, total_unique_findings=3,
            severity_counts={"CRITICAL": 0, "HIGH": 0},
            trend=TrendAnalysis(direction="improving"),
        )
        summary = _generate_executive_summary(r)
        assert "IMPROVING" in summary


# ---------------------------------------------------------------------------
# Formatting
# ---------------------------------------------------------------------------

class TestFormatText:
    def test_basic(self):
        r = _make_report([_make_finding_dict("CMD-001", "HIGH")])
        agg = merge_reports([("s1", "2024-01-01", r)])
        text = format_aggregate_text(agg)
        assert "Aggregated Report" in text
        assert "Reports merged: 1" in text
        assert "Unique findings: 1" in text

    def test_with_findings(self):
        r = _make_report([
            _make_finding_dict("A-001", "CRITICAL"),
            _make_finding_dict("B-001", "HIGH"),
        ])
        agg = merge_reports([("s1", "2024-01-01", r)])
        text = format_aggregate_text(agg)
        assert "CRITICAL" in text
        assert "A-001" in text

    def test_with_timeline(self):
        r1 = _make_report([_make_finding_dict()])
        r2 = _make_report([])
        agg = merge_reports([
            ("s1", "2024-01-01", r1),
            ("s2", "2024-02-01", r2),
        ])
        text = format_aggregate_text(agg)
        assert "Timeline:" in text
        assert "2024-01-01" in text

    def test_empty_report(self):
        agg = merge_reports([])
        text = format_aggregate_text(agg)
        assert "Reports merged: 0" in text


class TestFormatJson:
    def test_valid_json(self):
        r = _make_report([_make_finding_dict()])
        agg = merge_reports([("s1", "2024-01-01", r)])
        j = format_aggregate_json(agg)
        parsed = json.loads(j)
        assert parsed["total_unique_findings"] == 1

    def test_roundtrip(self):
        r = _make_report([
            _make_finding_dict("A-001", "CRITICAL"),
            _make_finding_dict("B-001", "LOW"),
        ])
        agg = merge_reports([("s1", "2024-01-01", r)])
        j = format_aggregate_json(agg)
        parsed = json.loads(j)
        assert parsed["severity_counts"]["CRITICAL"] == 1
        assert len(parsed["findings"]) == 2


class TestFormatHtml:
    def test_valid_html(self):
        r = _make_report([_make_finding_dict("CMD-001", "HIGH")])
        agg = merge_reports([("s1", "2024-01-01", r)])
        html = format_aggregate_html(agg)
        assert "<html>" in html
        assert "Aggregated Report" in html
        assert "CMD-001" in html

    def test_html_escaping(self):
        f = _make_finding_dict()
        f["title"] = "<script>alert(1)</script>"
        r = _make_report([f])
        agg = merge_reports([("s1", "2024-01-01", r)])
        html = format_aggregate_html(agg)
        assert "<script>" not in html
        assert "&lt;script&gt;" in html

    def test_empty_report_html(self):
        agg = merge_reports([])
        html = format_aggregate_html(agg)
        assert "<html>" in html
