"""Tests for the SLA Monitor module."""

from __future__ import annotations

import json
from datetime import datetime, timedelta, timezone

import pytest

from mcpaudit.sla_monitor import (
    DEFAULT_POLICIES,
    EscalationLevel,
    SLAEntry,
    SLAMonitor,
    SLAPolicy,
    SLAStatus,
)


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

def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _hours_ago(hours: float) -> str:
    return (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat()


def _hours_from_now(hours: float) -> str:
    return (datetime.now(timezone.utc) + timedelta(hours=hours)).isoformat()


# ===========================================================================
# SLAStatus
# ===========================================================================

class TestSLAStatus:
    def test_all_values(self):
        assert len(SLAStatus) == 5

    def test_values(self):
        assert SLAStatus.ON_TRACK.value == "ON_TRACK"
        assert SLAStatus.AT_RISK.value == "AT_RISK"
        assert SLAStatus.BREACHED.value == "BREACHED"


# ===========================================================================
# EscalationLevel
# ===========================================================================

class TestEscalationLevel:
    def test_all_values(self):
        assert len(EscalationLevel) == 4

    def test_ordering(self):
        assert EscalationLevel.NONE.value == "NONE"
        assert EscalationLevel.BLOCK_DEPLOY.value == "BLOCK_DEPLOY"


# ===========================================================================
# SLAPolicy
# ===========================================================================

class TestSLAPolicy:
    def test_defaults(self):
        p = SLAPolicy(severity="HIGH", max_hours=72)
        assert p.warning_threshold == 0.75
        assert p.escalation == EscalationLevel.NOTIFY

    def test_to_dict(self):
        p = SLAPolicy("CRITICAL", 24, 0.5, EscalationLevel.BLOCK_DEPLOY)
        d = p.to_dict()
        assert d["severity"] == "CRITICAL"
        assert d["max_hours"] == 24
        assert d["escalation"] == "BLOCK_DEPLOY"

    def test_from_dict(self):
        data = {"severity": "HIGH", "max_hours": 72, "escalation": "ASSIGN"}
        p = SLAPolicy.from_dict(data)
        assert p.severity == "HIGH"
        assert p.max_hours == 72
        assert p.escalation == EscalationLevel.ASSIGN

    def test_roundtrip(self):
        p = SLAPolicy("MEDIUM", 168, 0.8, EscalationLevel.NOTIFY)
        p2 = SLAPolicy.from_dict(p.to_dict())
        assert p == p2


# ===========================================================================
# SLAEntry
# ===========================================================================

class TestSLAEntry:
    def test_hours_elapsed(self):
        entry = SLAEntry(
            finding_id="f1",
            severity="HIGH",
            title="test",
            created_at=_hours_ago(5),
            sla_deadline=_hours_from_now(67),
        )
        assert 4.9 < entry.hours_elapsed < 5.1

    def test_hours_remaining_positive(self):
        entry = SLAEntry(
            finding_id="f1",
            severity="HIGH",
            title="test",
            created_at=_hours_ago(1),
            sla_deadline=_hours_from_now(10),
        )
        assert 9.9 < entry.hours_remaining < 10.1

    def test_hours_remaining_negative(self):
        entry = SLAEntry(
            finding_id="f1",
            severity="HIGH",
            title="test",
            created_at=_hours_ago(100),
            sla_deadline=_hours_ago(28),
        )
        assert entry.hours_remaining < 0

    def test_elapsed_percentage_early(self):
        entry = SLAEntry(
            finding_id="f1",
            severity="HIGH",
            title="test",
            created_at=_hours_ago(10),
            sla_deadline=_hours_from_now(90),
        )
        pct = entry.elapsed_percentage
        assert 0.05 < pct < 0.15

    def test_compute_status_on_track(self):
        entry = SLAEntry(
            finding_id="f1",
            severity="HIGH",
            title="test",
            created_at=_hours_ago(1),
            sla_deadline=_hours_from_now(71),
        )
        assert entry.compute_status() == SLAStatus.ON_TRACK

    def test_compute_status_breached(self):
        entry = SLAEntry(
            finding_id="f1",
            severity="HIGH",
            title="test",
            created_at=_hours_ago(100),
            sla_deadline=_hours_ago(28),
        )
        assert entry.compute_status() == SLAStatus.BREACHED

    def test_compute_status_resolved(self):
        entry = SLAEntry(
            finding_id="f1",
            severity="HIGH",
            title="test",
            created_at=_hours_ago(10),
            sla_deadline=_hours_from_now(62),
            resolved_at=_now_iso(),
        )
        assert entry.compute_status() == SLAStatus.RESOLVED

    def test_compute_status_waived(self):
        entry = SLAEntry(
            finding_id="f1",
            severity="HIGH",
            title="test",
            created_at=_hours_ago(100),
            sla_deadline=_hours_ago(28),
            waived=True,
        )
        assert entry.compute_status() == SLAStatus.WAIVED

    def test_to_dict(self):
        entry = SLAEntry(
            finding_id="f1",
            severity="HIGH",
            title="injection",
            created_at="2026-01-01T00:00:00+00:00",
            sla_deadline="2026-01-04T00:00:00+00:00",
            owner="alice",
        )
        d = entry.to_dict()
        assert d["finding_id"] == "f1"
        assert d["owner"] == "alice"

    def test_from_dict(self):
        data = {
            "finding_id": "f1",
            "severity": "CRITICAL",
            "title": "test",
            "created_at": "2026-01-01T00:00:00+00:00",
            "sla_deadline": "2026-01-02T00:00:00+00:00",
            "waived": True,
            "waive_reason": "false positive",
        }
        e = SLAEntry.from_dict(data)
        assert e.severity == "CRITICAL"
        assert e.waived
        assert e.waive_reason == "false positive"

    def test_roundtrip(self):
        entry = SLAEntry(
            finding_id="f1",
            severity="HIGH",
            title="test",
            created_at="2026-01-01T00:00:00+00:00",
            sla_deadline="2026-01-04T00:00:00+00:00",
        )
        e2 = SLAEntry.from_dict(entry.to_dict())
        assert e2.finding_id == entry.finding_id
        assert e2.sla_deadline == entry.sla_deadline


# ===========================================================================
# SLAMonitor
# ===========================================================================

class TestSLAMonitor:
    def test_empty(self):
        m = SLAMonitor()
        assert m.count == 0
        assert m.entries == []

    def test_default_policies(self):
        m = SLAMonitor()
        assert len(m.policies) == len(DEFAULT_POLICIES)
        assert m.get_policy("CRITICAL") is not None
        assert m.get_policy("CRITICAL").max_hours == 24

    def test_custom_policies(self):
        policies = [SLAPolicy("CRITICAL", 4)]
        m = SLAMonitor(policies=policies)
        assert m.get_policy("CRITICAL").max_hours == 4

    def test_set_policy(self):
        m = SLAMonitor()
        m.set_policy(SLAPolicy("CRITICAL", 12))
        assert m.get_policy("CRITICAL").max_hours == 12

    def test_add_finding(self):
        m = SLAMonitor()
        entry = m.add_finding("f1", "HIGH", "test finding")
        assert m.count == 1
        assert entry.severity == "HIGH"
        assert entry.sla_deadline != ""

    def test_add_finding_duplicate(self):
        m = SLAMonitor()
        m.add_finding("f1", "HIGH", "test")
        with pytest.raises(ValueError, match="already tracked"):
            m.add_finding("f1", "HIGH", "test")

    def test_add_finding_with_owner(self):
        m = SLAMonitor()
        entry = m.add_finding("f1", "HIGH", "test", owner="alice")
        assert entry.owner == "alice"

    def test_resolve(self):
        m = SLAMonitor()
        m.add_finding("f1", "HIGH", "test")
        m.resolve("f1")
        assert m.entries[0].status == SLAStatus.RESOLVED
        assert m.entries[0].resolved_at != ""

    def test_resolve_nonexistent(self):
        m = SLAMonitor()
        with pytest.raises(KeyError):
            m.resolve("xyz")

    def test_waive(self):
        m = SLAMonitor()
        m.add_finding("f1", "HIGH", "test")
        m.waive("f1", "false positive")
        assert m.entries[0].status == SLAStatus.WAIVED
        assert m.entries[0].waive_reason == "false positive"

    def test_waive_nonexistent(self):
        m = SLAMonitor()
        with pytest.raises(KeyError):
            m.waive("xyz", "reason")

    def test_breached_empty(self):
        m = SLAMonitor()
        assert m.breached() == []

    def test_on_track(self):
        m = SLAMonitor()
        m.add_finding("f1", "LOW", "test")  # LOW has 720h SLA
        assert len(m.on_track()) == 1

    def test_by_severity(self):
        m = SLAMonitor()
        m.add_finding("f1", "HIGH", "a")
        m.add_finding("f2", "LOW", "b")
        m.add_finding("f3", "HIGH", "c")
        assert len(m.by_severity("HIGH")) == 2
        assert len(m.by_severity("LOW")) == 1

    def test_compliance_rate_empty(self):
        m = SLAMonitor()
        assert m.compliance_rate() == 1.0

    def test_compliance_rate_all_on_track(self):
        m = SLAMonitor()
        m.add_finding("f1", "LOW", "test")
        assert m.compliance_rate() == 1.0

    def test_breach_count(self):
        m = SLAMonitor()
        m.add_finding("f1", "LOW", "test")
        assert m.breach_count() == 0

    def test_severity_compliance(self):
        m = SLAMonitor()
        m.add_finding("f1", "HIGH", "test")
        m.add_finding("f2", "LOW", "test")
        comp = m.severity_compliance()
        assert "HIGH" in comp
        assert "LOW" in comp

    def test_needs_escalation_empty(self):
        m = SLAMonitor()
        assert m.needs_escalation() == []

    # -- Persistence --

    def test_to_json(self):
        m = SLAMonitor()
        m.add_finding("f1", "HIGH", "test")
        j = m.to_json()
        data = json.loads(j)
        assert data["version"] == "1.0"
        assert len(data["entries"]) == 1

    def test_from_json(self):
        m = SLAMonitor()
        m.add_finding("f1", "HIGH", "test")
        j = m.to_json()
        m2 = SLAMonitor.from_json(j)
        assert m2.count == 1

    def test_roundtrip(self):
        m = SLAMonitor()
        m.add_finding("f1", "CRITICAL", "vuln")
        m.add_finding("f2", "LOW", "info")
        m.resolve("f2")
        j = m.to_json()
        m2 = SLAMonitor.from_json(j)
        assert m2.count == 2

    # -- Report --

    def test_markdown_report_empty(self):
        m = SLAMonitor()
        report = m.markdown_report()
        assert "# SLA Compliance Report" in report
        assert "Tracked findings**: 0" in report

    def test_markdown_report_with_data(self):
        m = SLAMonitor()
        m.add_finding("f1", "HIGH", "injection")
        m.add_finding("f2", "LOW", "info leak")
        report = m.markdown_report()
        assert "Compliance by Severity" in report


# ===========================================================================
# DEFAULT_POLICIES
# ===========================================================================

class TestDefaultPolicies:
    def test_critical_24h(self):
        p = [p for p in DEFAULT_POLICIES if p.severity == "CRITICAL"][0]
        assert p.max_hours == 24
        assert p.escalation == EscalationLevel.BLOCK_DEPLOY

    def test_high_72h(self):
        p = [p for p in DEFAULT_POLICIES if p.severity == "HIGH"][0]
        assert p.max_hours == 72
        assert p.escalation == EscalationLevel.ASSIGN

    def test_medium_168h(self):
        p = [p for p in DEFAULT_POLICIES if p.severity == "MEDIUM"][0]
        assert p.max_hours == 168

    def test_low_720h(self):
        p = [p for p in DEFAULT_POLICIES if p.severity == "LOW"][0]
        assert p.max_hours == 720
        assert p.escalation == EscalationLevel.NONE
