"""Tests for MCP Security Metrics."""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from mcpaudit.security_metrics import (
    Alert,
    AlertRule,
    MetricCollector,
    MetricPoint,
    MetricsReport,
    MetricsSnapshot,
    TimeSeries,
    _aggregate_bucket,
    _approximate_day_of_year,
    _bucket_snapshots,
    _get_bucket_key,
    format_metrics_html,
    format_metrics_json,
    format_metrics_text,
    load_metrics,
    save_metrics,
)


# --- MetricPoint ---

class TestMetricPoint:
    def test_creation(self):
        point = MetricPoint(
            name="trust_score",
            value=85.0,
            timestamp="2024-01-15T10:00:00Z",
        )
        assert point.name == "trust_score"
        assert point.value == 85.0

    def test_with_labels(self):
        point = MetricPoint(
            name="findings",
            value=5.0,
            timestamp="2024-01-15T10:00:00Z",
            labels={"severity": "high"},
        )
        assert point.labels["severity"] == "high"

    def test_to_dict(self):
        point = MetricPoint("test", 42.0, "2024-01-15T10:00:00Z")
        d = point.to_dict()
        assert d["name"] == "test"
        assert d["value"] == 42.0

    def test_to_dict_no_labels(self):
        point = MetricPoint("test", 42.0, "2024-01-15T10:00:00Z")
        d = point.to_dict()
        assert "labels" not in d

    def test_from_dict(self):
        data = {"name": "test", "value": 99.0, "timestamp": "2024-01-15T10:00:00Z"}
        point = MetricPoint.from_dict(data)
        assert point.value == 99.0


# --- Alert ---

class TestAlert:
    def test_above_triggered(self):
        alert = Alert(
            metric_name="findings_per_scan",
            threshold=10.0,
            direction="above",
            current_value=15.0,
            message="Too many findings",
        )
        assert alert.is_triggered is True

    def test_above_not_triggered(self):
        alert = Alert(
            metric_name="findings_per_scan",
            threshold=10.0,
            direction="above",
            current_value=5.0,
            message="OK",
        )
        assert alert.is_triggered is False

    def test_below_triggered(self):
        alert = Alert(
            metric_name="trust_score",
            threshold=50.0,
            direction="below",
            current_value=30.0,
            message="Low trust",
        )
        assert alert.is_triggered is True

    def test_below_not_triggered(self):
        alert = Alert(
            metric_name="trust_score",
            threshold=50.0,
            direction="below",
            current_value=75.0,
            message="OK",
        )
        assert alert.is_triggered is False

    def test_to_dict(self):
        alert = Alert(
            metric_name="trust_score",
            threshold=50.0,
            direction="below",
            current_value=30.0,
            message="Low",
            severity="critical",
        )
        d = alert.to_dict()
        assert d["is_triggered"] is True
        assert d["severity"] == "critical"

    def test_auto_timestamp(self):
        alert = Alert(
            metric_name="test",
            threshold=10.0,
            direction="above",
            current_value=5.0,
            message="test",
        )
        assert alert.timestamp != ""


# --- AlertRule ---

class TestAlertRule:
    def test_evaluate_triggered(self):
        rule = AlertRule(
            metric_name="trust_score",
            threshold=50.0,
            direction="below",
            message="Low trust",
        )
        alert = rule.evaluate(30.0)
        assert alert is not None
        assert alert.is_triggered

    def test_evaluate_not_triggered(self):
        rule = AlertRule(
            metric_name="trust_score",
            threshold=50.0,
            direction="below",
        )
        alert = rule.evaluate(75.0)
        assert alert is None

    def test_evaluate_above(self):
        rule = AlertRule(
            metric_name="critical_count",
            threshold=0,
            direction="above",
        )
        alert = rule.evaluate(5.0)
        assert alert is not None

    def test_to_dict(self):
        rule = AlertRule("test", 10.0, "above", "warning", "msg")
        d = rule.to_dict()
        assert d["metric_name"] == "test"
        assert d["threshold"] == 10.0

    def test_default_message(self):
        rule = AlertRule(metric_name="test", threshold=10.0)
        alert = rule.evaluate(15.0)
        assert alert is not None
        assert "above" in alert.message


# --- TimeSeries ---

class TestTimeSeries:
    def test_empty(self):
        ts = TimeSeries(name="test")
        assert ts.latest == 0.0
        assert ts.average == 0.0
        assert ts.trend == "stable"

    def test_latest(self):
        ts = TimeSeries(name="test", points=[
            MetricPoint("test", 10.0, "2024-01-01T00:00:00Z"),
            MetricPoint("test", 20.0, "2024-01-02T00:00:00Z"),
        ])
        assert ts.latest == 20.0

    def test_average(self):
        ts = TimeSeries(name="test", points=[
            MetricPoint("test", 10.0, "2024-01-01T00:00:00Z"),
            MetricPoint("test", 30.0, "2024-01-02T00:00:00Z"),
        ])
        assert ts.average == 20.0

    def test_minimum(self):
        ts = TimeSeries(name="test", points=[
            MetricPoint("test", 10.0, "2024-01-01T00:00:00Z"),
            MetricPoint("test", 5.0, "2024-01-02T00:00:00Z"),
            MetricPoint("test", 20.0, "2024-01-03T00:00:00Z"),
        ])
        assert ts.minimum == 5.0

    def test_maximum(self):
        ts = TimeSeries(name="test", points=[
            MetricPoint("test", 10.0, "2024-01-01T00:00:00Z"),
            MetricPoint("test", 5.0, "2024-01-02T00:00:00Z"),
            MetricPoint("test", 20.0, "2024-01-03T00:00:00Z"),
        ])
        assert ts.maximum == 20.0

    def test_trend_improving(self):
        ts = TimeSeries(name="test", points=[
            MetricPoint("test", 10.0, "2024-01-01T00:00:00Z"),
            MetricPoint("test", 12.0, "2024-01-02T00:00:00Z"),
            MetricPoint("test", 50.0, "2024-01-03T00:00:00Z"),
            MetricPoint("test", 55.0, "2024-01-04T00:00:00Z"),
        ])
        assert ts.trend == "improving"

    def test_trend_declining(self):
        ts = TimeSeries(name="test", points=[
            MetricPoint("test", 90.0, "2024-01-01T00:00:00Z"),
            MetricPoint("test", 85.0, "2024-01-02T00:00:00Z"),
            MetricPoint("test", 30.0, "2024-01-03T00:00:00Z"),
            MetricPoint("test", 25.0, "2024-01-04T00:00:00Z"),
        ])
        assert ts.trend == "declining"

    def test_trend_stable(self):
        ts = TimeSeries(name="test", points=[
            MetricPoint("test", 50.0, "2024-01-01T00:00:00Z"),
            MetricPoint("test", 50.0, "2024-01-02T00:00:00Z"),
            MetricPoint("test", 50.0, "2024-01-03T00:00:00Z"),
            MetricPoint("test", 50.0, "2024-01-04T00:00:00Z"),
        ])
        assert ts.trend == "stable"

    def test_to_dict(self):
        ts = TimeSeries(name="test", points=[
            MetricPoint("test", 10.0, "2024-01-01T00:00:00Z"),
        ])
        d = ts.to_dict()
        assert d["name"] == "test"
        assert d["latest"] == 10.0
        assert d["points_count"] == 1


# --- MetricsSnapshot ---

class TestMetricsSnapshot:
    def test_creation(self):
        snapshot = MetricsSnapshot(
            timestamp="2024-01-15T10:00:00Z",
            scan_count=5,
            findings_total=25,
            trust_score=75.0,
        )
        assert snapshot.scan_count == 5
        assert snapshot.trust_score == 75.0

    def test_to_dict(self):
        snapshot = MetricsSnapshot(
            timestamp="2024-01-15T10:00:00Z",
            scan_count=1,
            findings_total=10,
            findings_per_scan=10.0,
            trust_score=80.0,
            compliance_score=90.0,
        )
        d = snapshot.to_dict()
        assert d["scan_count"] == 1
        assert d["trust_score"] == 80.0

    def test_from_dict(self):
        data = {
            "timestamp": "2024-01-15T10:00:00Z",
            "scan_count": 3,
            "trust_score": 65.0,
        }
        snapshot = MetricsSnapshot.from_dict(data)
        assert snapshot.scan_count == 3
        assert snapshot.trust_score == 65.0


# --- MetricsReport ---

class TestMetricsReport:
    def _make_report(self) -> MetricsReport:
        snapshots = [
            MetricsSnapshot("2024-01-01", scan_count=5, findings_total=20,
                          trust_score=80.0, compliance_score=90.0, fix_rate=70.0),
            MetricsSnapshot("2024-01-02", scan_count=3, findings_total=15,
                          trust_score=85.0, compliance_score=92.0, fix_rate=75.0),
        ]
        return MetricsReport(snapshots=snapshots, period="day")

    def test_total_scans(self):
        report = self._make_report()
        assert report.total_scans == 8

    def test_total_findings(self):
        report = self._make_report()
        assert report.total_findings == 35

    def test_avg_findings_per_scan(self):
        report = self._make_report()
        assert report.avg_findings_per_scan == 35 / 8

    def test_avg_trust_score(self):
        report = self._make_report()
        assert report.avg_trust_score == 82.5

    def test_avg_compliance_score(self):
        report = self._make_report()
        assert report.avg_compliance_score == 91.0

    def test_avg_fix_rate(self):
        report = self._make_report()
        assert report.avg_fix_rate == 72.5

    def test_trust_trend_stable(self):
        report = self._make_report()
        assert report.trust_trend in ("stable", "improving")

    def test_empty_report(self):
        report = MetricsReport()
        assert report.total_scans == 0
        assert report.avg_findings_per_scan == 0.0
        assert report.avg_trust_score == 0.0

    def test_triggered_alerts(self):
        report = MetricsReport(
            alerts=[
                Alert("trust_score", 50.0, "below", 30.0, "Low trust"),
                Alert("critical", 0, "above", 0, "OK"),
            ],
        )
        triggered = report.triggered_alerts
        assert len(triggered) == 1

    def test_to_dict(self):
        report = self._make_report()
        d = report.to_dict()
        assert d["total_scans"] == 8
        assert d["period"] == "day"

    def test_auto_timestamp(self):
        report = MetricsReport()
        assert report.timestamp != ""


# --- MetricCollector ---

class TestMetricCollector:
    def test_record_scan(self):
        collector = MetricCollector()
        snapshot = collector.record_scan(
            findings_total=10,
            critical=2,
            high=3,
            trust_score=70.0,
        )
        assert snapshot.findings_total == 10
        assert len(collector.snapshots) == 1

    def test_multiple_scans(self):
        collector = MetricCollector()
        collector.record_scan(findings_total=5, trust_score=80.0)
        collector.record_scan(findings_total=3, trust_score=85.0)
        assert len(collector.snapshots) == 2

    def test_get_alerts(self):
        collector = MetricCollector()
        collector.record_scan(
            findings_total=20,
            critical=5,
            trust_score=30.0,
        )
        alerts = collector.get_alerts()
        assert len(alerts) > 0
        trust_alerts = [a for a in alerts if a.metric_name == "trust_score"]
        assert len(trust_alerts) > 0

    def test_no_alerts_healthy(self):
        collector = MetricCollector()
        collector.record_scan(
            findings_total=2,
            trust_score=90.0,
            compliance_score=95.0,
            fix_rate=80.0,
        )
        alerts = collector.get_alerts()
        # Some alerts may still trigger depending on findings_per_scan
        critical_alerts = [a for a in alerts if a.severity == "critical"]
        assert len(critical_alerts) == 0

    def test_add_custom_alert_rule(self):
        collector = MetricCollector()
        collector.add_alert_rule(AlertRule(
            metric_name="scan_count",
            threshold=100,
            direction="above",
            message="Too many scans",
        ))
        assert len(collector.alert_rules) > 6  # default + custom

    def test_remove_alert_rule(self):
        collector = MetricCollector()
        initial = len(collector.alert_rules)
        collector.remove_alert_rule("trust_score")
        assert len(collector.alert_rules) < initial

    def test_aggregate_day(self):
        collector = MetricCollector()
        collector.record_scan(findings_total=5, trust_score=80.0,
                            timestamp="2024-01-15T10:00:00Z")
        collector.record_scan(findings_total=3, trust_score=85.0,
                            timestamp="2024-01-15T14:00:00Z")
        collector.record_scan(findings_total=7, trust_score=70.0,
                            timestamp="2024-01-16T09:00:00Z")
        report = collector.aggregate(period="day")
        assert len(report.snapshots) == 2
        assert report.total_scans == 3

    def test_aggregate_week(self):
        collector = MetricCollector()
        collector.record_scan(findings_total=5, trust_score=80.0,
                            timestamp="2024-01-15T10:00:00Z")
        collector.record_scan(findings_total=3, trust_score=85.0,
                            timestamp="2024-01-16T10:00:00Z")
        report = collector.aggregate(period="week")
        assert report.total_scans == 2

    def test_aggregate_month(self):
        collector = MetricCollector()
        collector.record_scan(findings_total=5, trust_score=80.0,
                            timestamp="2024-01-15T10:00:00Z")
        collector.record_scan(findings_total=3, trust_score=85.0,
                            timestamp="2024-02-15T10:00:00Z")
        report = collector.aggregate(period="month")
        assert len(report.snapshots) == 2

    def test_aggregate_empty(self):
        collector = MetricCollector()
        report = collector.aggregate()
        assert report.total_scans == 0

    def test_build_time_series(self):
        collector = MetricCollector()
        collector.record_scan(trust_score=80.0, timestamp="2024-01-01T00:00:00Z")
        collector.record_scan(trust_score=85.0, timestamp="2024-01-02T00:00:00Z")
        ts = collector.build_time_series("trust_score")
        assert ts.name == "trust_score"
        assert len(ts.points) == 2
        assert ts.latest == 85.0

    def test_to_dict(self):
        collector = MetricCollector()
        collector.record_scan(findings_total=5)
        d = collector.to_dict()
        assert d["snapshots_count"] == 1
        assert d["alert_rules_count"] > 0


# --- Aggregation Helpers ---

class TestAggregationHelpers:
    def test_get_bucket_key_day(self):
        key = _get_bucket_key("2024-01-15T10:30:00Z", "day")
        assert key == "2024-01-15"

    def test_get_bucket_key_month(self):
        key = _get_bucket_key("2024-01-15T10:30:00Z", "month")
        assert key == "2024-01"

    def test_get_bucket_key_week(self):
        key = _get_bucket_key("2024-01-15T10:30:00Z", "week")
        assert key.startswith("2024-W")

    def test_approximate_day_of_year(self):
        # Jan 15 = 15th day
        day = _approximate_day_of_year(2024, 1, 15)
        assert day == 15

    def test_approximate_day_of_year_feb(self):
        # Feb 1 = 32nd day
        day = _approximate_day_of_year(2024, 2, 1)
        assert day == 32

    def test_bucket_snapshots(self):
        snapshots = [
            MetricsSnapshot("2024-01-15T10:00:00Z", scan_count=1),
            MetricsSnapshot("2024-01-15T14:00:00Z", scan_count=1),
            MetricsSnapshot("2024-01-16T09:00:00Z", scan_count=1),
        ]
        buckets = _bucket_snapshots(snapshots, "day")
        assert len(buckets) == 2
        assert len(buckets["2024-01-15"]) == 2

    def test_aggregate_bucket(self):
        snapshots = [
            MetricsSnapshot("2024-01-15T10:00:00Z", scan_count=2,
                          findings_total=10, trust_score=80.0),
            MetricsSnapshot("2024-01-15T14:00:00Z", scan_count=1,
                          findings_total=5, trust_score=90.0),
        ]
        agg = _aggregate_bucket(snapshots, "2024-01-15")
        assert agg.scan_count == 3
        assert agg.findings_total == 15
        assert agg.trust_score == 85.0


# --- Persistence ---

class TestPersistence:
    def test_save_and_load(self, tmp_path):
        snapshots = [
            MetricsSnapshot("2024-01-15T10:00:00Z", scan_count=5,
                          findings_total=20, trust_score=75.0),
        ]
        metrics_file = tmp_path / "metrics.json"
        save_metrics(snapshots, metrics_file)

        loaded = load_metrics(metrics_file)
        assert len(loaded) == 1
        assert loaded[0].scan_count == 5
        assert loaded[0].trust_score == 75.0

    def test_load_nonexistent(self, tmp_path):
        loaded = load_metrics(tmp_path / "nonexistent.json")
        assert len(loaded) == 0

    def test_save_creates_directory(self, tmp_path):
        metrics_file = tmp_path / "subdir" / "metrics.json"
        save_metrics([], metrics_file)
        assert metrics_file.exists()


# --- Format Functions ---

class TestFormatMetricsText:
    def test_empty_report(self):
        report = MetricsReport()
        output = format_metrics_text(report)
        assert "Security Metrics" in output
        assert "Total scans: 0" in output

    def test_with_data(self):
        report = MetricsReport(
            snapshots=[
                MetricsSnapshot("2024-01-15", scan_count=5, findings_total=20,
                              trust_score=80.0, compliance_score=90.0),
            ],
            period="day",
        )
        output = format_metrics_text(report)
        assert "Total scans: 5" in output
        assert "80.0" in output

    def test_with_alerts(self):
        report = MetricsReport(
            snapshots=[MetricsSnapshot("2024-01-15", scan_count=1)],
            alerts=[
                Alert("trust_score", 50.0, "below", 30.0, "Low trust", "critical"),
            ],
        )
        output = format_metrics_text(report)
        assert "Low trust" in output
        assert "[CRIT]" in output

    def test_no_alerts(self):
        report = MetricsReport(
            snapshots=[MetricsSnapshot("2024-01-15", scan_count=1)],
        )
        output = format_metrics_text(report)
        assert "No alerts triggered" in output


class TestFormatMetricsJson:
    def test_valid_json(self):
        report = MetricsReport(
            snapshots=[MetricsSnapshot("2024-01-15", scan_count=5)],
        )
        output = format_metrics_json(report)
        data = json.loads(output)
        assert data["total_scans"] == 5

    def test_empty_report(self):
        report = MetricsReport()
        output = format_metrics_json(report)
        data = json.loads(output)
        assert data["total_scans"] == 0


class TestFormatMetricsHtml:
    def test_basic_html(self):
        report = MetricsReport(
            snapshots=[
                MetricsSnapshot("2024-01-15", scan_count=5, findings_total=20,
                              trust_score=80.0, compliance_score=90.0, fix_rate=70.0),
            ],
        )
        output = format_metrics_html(report)
        assert "<html" in output
        assert "Security Metrics" in output
        assert "80.0" in output

    def test_with_alerts(self):
        report = MetricsReport(
            snapshots=[MetricsSnapshot("2024-01-15", scan_count=1)],
            alerts=[
                Alert("trust_score", 50.0, "below", 30.0, "Low trust", "critical"),
            ],
        )
        output = format_metrics_html(report)
        assert "Low trust" in output
        assert "CRITICAL" in output

    def test_empty_report(self):
        report = MetricsReport()
        output = format_metrics_html(report)
        assert "<html" in output
        assert "No alerts triggered" in output
