"""Tests for the Scan History Analytics module."""

from __future__ import annotations

import json
from datetime import datetime, timezone

import pytest

from mcpaudit.history_analytics import (
    CategoryTrend,
    FindingRecord,
    HistoryAnalytics,
    Prediction,
    analyze_history,
    format_analytics_html,
    format_analytics_json,
    format_analytics_text,
    predict_target_date,
    _best_grade,
    _escape_html,
    _grade_color,
    _grade_to_score,
    _parse_ts,
    _score_color,
    _worst_grade,
)


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

def _scan_entry(
    ts: str = "20260101T000000Z",
    grade: str = "C",
    total: int = 5,
    critical: int = 1,
    high: int = 1,
    medium: int = 2,
    low: int = 1,
    info: int = 0,
    rules: list[str] | None = None,
    label: str = "",
) -> dict:
    """Build a scan history entry."""
    if rules is None:
        rules = ["CMD-001", "AUTH-001"]
    findings = [
        {"rule_id": r, "severity": "HIGH", "title": "t", "description": "d",
         "file": "f.py", "line": i + 1}
        for i, r in enumerate(rules)
    ]
    return {
        "timestamp": ts,
        "label": label,
        "scan": {
            "summary": {
                "files_scanned": 5,
                "total_findings": total,
                "overall_grade": grade,
                "severity_counts": {
                    "CRITICAL": critical,
                    "HIGH": high,
                    "MEDIUM": medium,
                    "LOW": low,
                    "INFO": info,
                },
            },
            "results": [{"file": "f.py", "findings": findings}],
        },
    }


def _history_series(n: int = 5) -> list[dict]:
    """Build a series of scan entries with improving trend."""
    entries = []
    grades = ["F", "D", "C", "B", "A", "A+"]
    for i in range(n):
        grade = grades[min(i, len(grades) - 1)]
        total = max(0, 10 - i * 2)
        critical = max(0, 2 - i)
        high = max(0, 3 - i)
        rules = [f"RULE-{j:03d}" for j in range(total)]
        ts = f"2026{(i + 1):02d}01T000000Z"
        entries.append(_scan_entry(
            ts=ts,
            grade=grade,
            total=total,
            critical=critical,
            high=high,
            medium=max(0, total - critical - high),
            low=0,
            rules=rules,
        ))
    return entries


def _degrading_series(n: int = 5) -> list[dict]:
    """Build a series with degrading trend (score increasing)."""
    entries = []
    for i in range(n):
        total = 2 + i * 3
        critical = i
        high = i
        rules = [f"RULE-{j:03d}" for j in range(total)]
        ts = f"2026{(i + 1):02d}01T000000Z"
        entries.append(_scan_entry(
            ts=ts,
            grade="F" if i >= 3 else ("D" if i >= 2 else "C"),
            total=total,
            critical=critical,
            high=high,
            medium=max(0, total - critical - high),
            rules=rules,
        ))
    return entries


# ===========================================================================
# FindingRecord
# ===========================================================================

class TestFindingRecord:
    def test_defaults(self):
        fr = FindingRecord(rule_id="CMD-001")
        assert fr.first_seen == ""
        assert fr.occurrences == 0
        assert fr.resolved is False
        assert fr.severity == "MEDIUM"

    def test_time_to_resolve_not_resolved(self):
        fr = FindingRecord(rule_id="CMD-001", resolved=False)
        assert fr.time_to_resolve_days is None

    def test_time_to_resolve_resolved(self):
        fr = FindingRecord(
            rule_id="CMD-001",
            first_seen="20260101T000000Z",
            resolved=True,
            resolved_at="20260111T000000Z",
        )
        ttr = fr.time_to_resolve_days
        assert ttr is not None
        assert abs(ttr - 10.0) < 0.1

    def test_time_to_resolve_same_day(self):
        fr = FindingRecord(
            rule_id="CMD-001",
            first_seen="20260101T000000Z",
            resolved=True,
            resolved_at="20260101T000000Z",
        )
        assert fr.time_to_resolve_days == 0.0

    def test_time_to_resolve_invalid_ts(self):
        fr = FindingRecord(
            rule_id="CMD-001",
            first_seen="invalid",
            resolved=True,
            resolved_at="also-invalid",
        )
        assert fr.time_to_resolve_days is None

    def test_to_dict(self):
        fr = FindingRecord(
            rule_id="CMD-001",
            first_seen="20260101T000000Z",
            last_seen="20260110T000000Z",
            occurrences=5,
            resolved=True,
            resolved_at="20260110T000000Z",
            severity="HIGH",
        )
        d = fr.to_dict()
        assert d["rule_id"] == "CMD-001"
        assert d["occurrences"] == 5
        assert d["resolved"] is True
        assert d["time_to_resolve_days"] is not None


# ===========================================================================
# CategoryTrend
# ===========================================================================

class TestCategoryTrend:
    def test_direction_stable(self):
        ct = CategoryTrend("CRITICAL", [2, 2, 2], ["t1", "t2", "t3"])
        assert ct.direction == "stable"

    def test_direction_improving(self):
        ct = CategoryTrend("CRITICAL", [5, 4, 3, 2, 1], ["t1", "t2", "t3", "t4", "t5"])
        assert ct.direction == "improving"

    def test_direction_degrading(self):
        ct = CategoryTrend("CRITICAL", [1, 2, 3, 4, 5], ["t1", "t2", "t3", "t4", "t5"])
        assert ct.direction == "degrading"

    def test_direction_single_value(self):
        ct = CategoryTrend("CRITICAL", [5])
        assert ct.direction == "stable"

    def test_change_pct_positive(self):
        ct = CategoryTrend("HIGH", [10, 15])
        assert ct.change_pct == 50.0

    def test_change_pct_negative(self):
        ct = CategoryTrend("HIGH", [10, 5])
        assert ct.change_pct == -50.0

    def test_change_pct_zero_base(self):
        ct = CategoryTrend("HIGH", [0, 5])
        assert ct.change_pct == 100.0

    def test_change_pct_zero_to_zero(self):
        ct = CategoryTrend("HIGH", [0, 0])
        assert ct.change_pct == 0.0

    def test_change_pct_single(self):
        ct = CategoryTrend("HIGH", [5])
        assert ct.change_pct == 0.0

    def test_to_dict(self):
        ct = CategoryTrend("HIGH", [1, 2, 3], ["t1", "t2", "t3"])
        d = ct.to_dict()
        assert d["category"] == "HIGH"
        assert d["values"] == [1, 2, 3]
        assert "direction" in d
        assert "change_pct" in d


# ===========================================================================
# Prediction
# ===========================================================================

class TestPrediction:
    def test_defaults(self):
        p = Prediction(target_score=0, current_score=10)
        assert p.predicted_date == ""
        assert p.days_remaining == -1
        assert p.confidence == "low"
        assert p.achievable is True

    def test_to_dict(self):
        p = Prediction(
            target_score=0,
            current_score=10,
            predicted_date="2026-06-01",
            days_remaining=90,
            confidence="high",
            achievable=True,
            trend_slope=-0.5,
        )
        d = p.to_dict()
        assert d["target_score"] == 0
        assert d["predicted_date"] == "2026-06-01"
        assert d["days_remaining"] == 90
        assert d["trend_slope"] == -0.5


# ===========================================================================
# HistoryAnalytics
# ===========================================================================

class TestHistoryAnalytics:
    def test_defaults(self):
        ha = HistoryAnalytics()
        assert ha.total_scans == 0
        assert ha.mttr_days == 0.0
        assert ha.current_grade == "?"

    def test_top_persistent_empty(self):
        ha = HistoryAnalytics()
        assert ha.top_persistent == []

    def test_top_persistent_sorted(self):
        ha = HistoryAnalytics()
        ha.finding_records = [
            FindingRecord("R-1", occurrences=3),
            FindingRecord("R-2", occurrences=10),
            FindingRecord("R-3", occurrences=5),
        ]
        top = ha.top_persistent
        assert top[0].rule_id == "R-2"
        assert top[1].rule_id == "R-3"

    def test_top_persistent_max_10(self):
        ha = HistoryAnalytics()
        ha.finding_records = [
            FindingRecord(f"R-{i}", occurrences=20 - i)
            for i in range(15)
        ]
        assert len(ha.top_persistent) == 10

    def test_top_persistent_excludes_resolved(self):
        ha = HistoryAnalytics()
        ha.finding_records = [
            FindingRecord("R-1", occurrences=10, resolved=True),
            FindingRecord("R-2", occurrences=5),
        ]
        top = ha.top_persistent
        assert len(top) == 1
        assert top[0].rule_id == "R-2"

    def test_to_dict(self):
        ha = HistoryAnalytics(total_scans=3, mttr_days=5.5, current_grade="B")
        d = ha.to_dict()
        assert d["total_scans"] == 3
        assert d["mttr_days"] == 5.5
        assert d["current_grade"] == "B"
        assert "category_trends" in d
        assert "persistent_findings" in d
        assert "predictions" in d


# ===========================================================================
# _parse_ts
# ===========================================================================

class TestParseTs:
    def test_compact_format(self):
        dt = _parse_ts("20260101T000000Z")
        assert dt.year == 2026
        assert dt.month == 1

    def test_iso_format(self):
        dt = _parse_ts("2026-01-01T00:00:00Z")
        assert dt.year == 2026

    def test_iso_no_z(self):
        dt = _parse_ts("2026-01-01T00:00:00")
        assert dt.year == 2026

    def test_date_only(self):
        dt = _parse_ts("2026-01-01")
        assert dt.year == 2026

    def test_invalid(self):
        with pytest.raises(ValueError, match="Cannot parse"):
            _parse_ts("not-a-date")


# ===========================================================================
# Grade helpers
# ===========================================================================

class TestGradeHelpers:
    def test_grade_to_score(self):
        assert _grade_to_score("A+") == 0
        assert _grade_to_score("A") == 1
        assert _grade_to_score("F") == 5

    def test_grade_to_score_unknown(self):
        assert _grade_to_score("?") == 5

    def test_best_grade(self):
        assert _best_grade(["C", "A+", "B"]) == "A+"

    def test_best_grade_empty(self):
        assert _best_grade([]) == "?"

    def test_worst_grade(self):
        assert _worst_grade(["A", "C", "B"]) == "C"

    def test_worst_grade_empty(self):
        assert _worst_grade([]) == "?"


# ===========================================================================
# analyze_history
# ===========================================================================

class TestAnalyzeHistory:
    def test_empty(self):
        ha = analyze_history([])
        assert ha.total_scans == 0
        assert ha.current_grade == "?"

    def test_single_scan(self):
        ha = analyze_history([_scan_entry()])
        assert ha.total_scans == 1
        assert ha.current_grade == "C"
        assert ha.best_grade == "C"
        assert ha.worst_grade == "C"

    def test_multiple_scans(self):
        entries = _history_series(5)
        ha = analyze_history(entries)
        assert ha.total_scans == 5
        assert ha.date_range_start == entries[0]["timestamp"]
        assert ha.date_range_end == entries[-1]["timestamp"]

    def test_score_history(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        assert len(ha.score_history) == 3
        assert all(isinstance(s, int) for s in ha.score_history)

    def test_grade_history(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        assert len(ha.grade_history) == 3

    def test_finding_records(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        assert ha.total_unique_findings > 0
        assert len(ha.finding_records) > 0

    def test_resolved_findings(self):
        entries = _history_series(5)
        ha = analyze_history(entries)
        # Earlier rules should be resolved if not in last scan
        assert ha.total_resolved >= 0

    def test_mttr_calculated(self):
        entries = _history_series(5)
        ha = analyze_history(entries)
        assert isinstance(ha.mttr_days, float)

    def test_fix_velocity(self):
        entries = _history_series(5)
        ha = analyze_history(entries)
        assert isinstance(ha.fix_velocity, float)

    def test_recurrence_rate(self):
        # Create a series where a rule disappears and reappears
        entries = [
            _scan_entry("20260101T000000Z", rules=["CMD-001", "AUTH-001"]),
            _scan_entry("20260201T000000Z", rules=["AUTH-001"]),  # CMD-001 gone
            _scan_entry("20260301T000000Z", rules=["CMD-001", "AUTH-001"]),  # CMD-001 back
        ]
        ha = analyze_history(entries)
        assert ha.recurrence_rate > 0

    def test_recurrence_rate_zero(self):
        entries = [
            _scan_entry("20260101T000000Z", rules=["CMD-001"]),
            _scan_entry("20260201T000000Z", rules=["CMD-001"]),
        ]
        ha = analyze_history(entries)
        assert ha.recurrence_rate == 0.0

    def test_category_trends(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        assert len(ha.category_trends) >= 4  # CRITICAL, HIGH, MEDIUM, LOW, INFO

    def test_category_trend_values(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        for ct in ha.category_trends:
            assert len(ct.values) == 3
            assert ct.category in ("CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO")

    def test_improving_trend(self):
        entries = _history_series(5)
        ha = analyze_history(entries)
        # Score should be decreasing
        assert ha.score_history[-1] <= ha.score_history[0]

    def test_to_dict(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        d = ha.to_dict()
        assert d["total_scans"] == 3
        assert "category_trends" in d
        assert "persistent_findings" in d

    def test_timestamps_stored(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        assert len(ha.timestamps) == 3


# ===========================================================================
# predict_target_date
# ===========================================================================

class TestPredictTargetDate:
    def test_already_at_target(self):
        entries = [_scan_entry("20260101T000000Z", grade="A+", total=0, critical=0, high=0, medium=0, low=0)]
        ha = analyze_history(entries)
        pred = predict_target_date(ha, 0)
        assert pred.days_remaining == 0
        assert pred.achievable is True

    def test_insufficient_data(self):
        # Only one scan, target below current score: not enough data
        ha = HistoryAnalytics()
        ha.score_history = [10]
        pred = predict_target_date(ha, 0)
        assert pred.achievable is False

    def test_improving_trend(self):
        entries = _history_series(5)
        ha = analyze_history(entries)
        pred = predict_target_date(ha, 0)
        if pred.achievable:
            assert pred.days_remaining >= 0
            assert pred.predicted_date != ""

    def test_degrading_trend(self):
        entries = _degrading_series(5)
        ha = analyze_history(entries)
        pred = predict_target_date(ha, 0)
        assert pred.achievable is False

    def test_confidence_levels(self):
        entries = _history_series(10)
        ha = analyze_history(entries)
        pred = predict_target_date(ha, 0)
        assert pred.confidence in ("low", "medium", "high")

    def test_slope_stored(self):
        entries = _history_series(5)
        ha = analyze_history(entries)
        pred = predict_target_date(ha, 0)
        assert isinstance(pred.trend_slope, float)

    def test_current_score_stored(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        pred = predict_target_date(ha, 5)
        assert pred.current_score == ha.current_score

    def test_target_score_stored(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        pred = predict_target_date(ha, 5)
        assert pred.target_score == 5

    def test_single_scan(self):
        entries = [_scan_entry()]
        ha = analyze_history(entries)
        pred = predict_target_date(ha, 0)
        assert pred.confidence == "low"


# ===========================================================================
# format_analytics_text
# ===========================================================================

class TestFormatAnalyticsText:
    def test_basic_output(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        text = format_analytics_text(ha)
        assert "Scan History Analytics" in text

    def test_contains_metrics(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        text = format_analytics_text(ha)
        assert "MTTR" in text
        assert "Recurrence Rate" in text
        assert "Fix Velocity" in text

    def test_contains_trends(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        text = format_analytics_text(ha)
        assert "Category Trends" in text

    def test_contains_persistent_findings(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        text = format_analytics_text(ha)
        assert "Persistent" in text

    def test_with_predictions(self):
        entries = _history_series(5)
        ha = analyze_history(entries)
        ha.predictions = [predict_target_date(ha, 0)]
        text = format_analytics_text(ha)
        assert "Predictions" in text

    def test_empty_analytics(self):
        ha = HistoryAnalytics()
        text = format_analytics_text(ha)
        assert "0" in text


# ===========================================================================
# format_analytics_json
# ===========================================================================

class TestFormatAnalyticsJson:
    def test_valid_json(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        j = format_analytics_json(ha)
        parsed = json.loads(j)
        assert "total_scans" in parsed
        assert "category_trends" in parsed

    def test_round_trip(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        j = format_analytics_json(ha)
        parsed = json.loads(j)
        assert parsed["total_scans"] == 3

    def test_empty(self):
        ha = HistoryAnalytics()
        j = format_analytics_json(ha)
        parsed = json.loads(j)
        assert parsed["total_scans"] == 0


# ===========================================================================
# format_analytics_html
# ===========================================================================

class TestFormatAnalyticsHtml:
    def test_valid_html(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        html = format_analytics_html(ha)
        assert "<!DOCTYPE html>" in html
        assert "</html>" in html

    def test_contains_title(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        html = format_analytics_html(ha)
        assert "Scan History Analytics" in html

    def test_contains_cards(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        html = format_analytics_html(ha)
        assert "MTTR" in html
        assert "Recurrence" in html
        assert "Fix Velocity" in html

    def test_contains_score_chart(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        html = format_analytics_html(ha)
        assert "Score History" in html
        assert "bar-chart" in html

    def test_contains_grade_history(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        html = format_analytics_html(ha)
        assert "Grade History" in html
        assert "grade-chip" in html

    def test_contains_trends_table(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        html = format_analytics_html(ha)
        assert "Category Trends" in html

    def test_contains_persistent_table(self):
        entries = _history_series(3)
        ha = analyze_history(entries)
        html = format_analytics_html(ha)
        assert "Persistent Findings" in html

    def test_contains_predictions(self):
        entries = _history_series(5)
        ha = analyze_history(entries)
        ha.predictions = [predict_target_date(ha, 0)]
        html = format_analytics_html(ha)
        assert "Predictions" in html


# ===========================================================================
# Utility functions
# ===========================================================================

class TestScoreColor:
    def test_green(self):
        assert _score_color(0) == "#22c55e"
        assert _score_color(2) == "#22c55e"

    def test_yellow(self):
        assert _score_color(3) == "#eab308"
        assert _score_color(5) == "#eab308"

    def test_orange(self):
        assert _score_color(6) == "#f97316"
        assert _score_color(10) == "#f97316"

    def test_red(self):
        assert _score_color(11) == "#ef4444"
        assert _score_color(100) == "#ef4444"


class TestGradeColor:
    def test_green(self):
        assert _grade_color("A+") == "#22c55e"
        assert _grade_color("A") == "#22c55e"

    def test_yellow(self):
        assert _grade_color("B") == "#eab308"

    def test_orange(self):
        assert _grade_color("C") == "#f97316"

    def test_red(self):
        assert _grade_color("D") == "#ef4444"
        assert _grade_color("F") == "#dc2626"

    def test_unknown(self):
        assert _grade_color("?") == "#6b7280"


class TestEscapeHtmlAnalytics:
    def test_ampersand(self):
        assert _escape_html("a & b") == "a &amp; b"

    def test_lt_gt(self):
        assert _escape_html("<b>") == "&lt;b&gt;"

    def test_quote(self):
        assert _escape_html('"x"') == "&quot;x&quot;"
