"""Tests for the Remediation Tracker module."""

from __future__ import annotations

import json
from datetime import datetime, timedelta, timezone

import pytest

from mcpaudit.remediation_tracker import (
    DEFAULT_SLA_HOURS,
    RemediationItem,
    RemediationStatus,
    RemediationTracker,
    StatusChange,
    _TERMINAL_STATES,
    _VALID_TRANSITIONS,
)


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

def _make_item(
    finding_id: str = "abc123",
    rule_id: str = "SEC-001",
    severity: str = "HIGH",
    file: str = "server.py",
    line: int = 42,
    title: str = "Command injection",
    **kwargs,
) -> RemediationItem:
    return RemediationItem(
        finding_id=finding_id,
        rule_id=rule_id,
        severity=severity,
        file=file,
        line=line,
        title=title,
        **kwargs,
    )


def _make_tracker(*items: RemediationItem) -> RemediationTracker:
    t = RemediationTracker()
    for item in items:
        t.add(item)
    return t


# ===========================================================================
# RemediationStatus
# ===========================================================================

class TestRemediationStatus:
    def test_all_values(self):
        assert len(RemediationStatus) == 7

    def test_terminal_states(self):
        assert RemediationStatus.VERIFIED in _TERMINAL_STATES
        assert RemediationStatus.WONTFIX in _TERMINAL_STATES
        assert RemediationStatus.FALSE_POSITIVE in _TERMINAL_STATES
        assert RemediationStatus.OPEN not in _TERMINAL_STATES

    def test_valid_transitions_exist_for_all(self):
        for status in RemediationStatus:
            assert status in _VALID_TRANSITIONS


# ===========================================================================
# StatusChange
# ===========================================================================

class TestStatusChange:
    def test_to_dict(self):
        sc = StatusChange(
            from_status=RemediationStatus.OPEN,
            to_status=RemediationStatus.IN_PROGRESS,
            timestamp="2026-01-01T00:00:00+00:00",
            comment="started work",
        )
        d = sc.to_dict()
        assert d["from"] == "OPEN"
        assert d["to"] == "IN_PROGRESS"
        assert d["timestamp"] == "2026-01-01T00:00:00+00:00"
        assert d["comment"] == "started work"

    def test_from_dict(self):
        data = {
            "from": "FIXED",
            "to": "VERIFIED",
            "timestamp": "2026-01-02T00:00:00+00:00",
        }
        sc = StatusChange.from_dict(data)
        assert sc.from_status == RemediationStatus.FIXED
        assert sc.to_status == RemediationStatus.VERIFIED
        assert sc.comment == ""

    def test_roundtrip(self):
        sc = StatusChange(
            from_status=RemediationStatus.IN_PROGRESS,
            to_status=RemediationStatus.FIXED,
            timestamp="2026-01-01T12:00:00+00:00",
            comment="patched",
        )
        sc2 = StatusChange.from_dict(sc.to_dict())
        assert sc2.from_status == sc.from_status
        assert sc2.to_status == sc.to_status
        assert sc2.comment == sc.comment


# ===========================================================================
# RemediationItem
# ===========================================================================

class TestRemediationItem:
    def test_defaults(self):
        item = _make_item()
        assert item.status == RemediationStatus.OPEN
        assert item.owner == ""
        assert item.created_at != ""
        assert item.updated_at != ""
        assert item.history == []

    def test_is_terminal_open(self):
        item = _make_item()
        assert not item.is_terminal

    def test_is_terminal_verified(self):
        item = _make_item(status=RemediationStatus.VERIFIED)
        assert item.is_terminal

    def test_is_terminal_wontfix(self):
        item = _make_item(status=RemediationStatus.WONTFIX)
        assert item.is_terminal

    def test_is_terminal_false_positive(self):
        item = _make_item(status=RemediationStatus.FALSE_POSITIVE)
        assert item.is_terminal

    def test_is_overdue_no_due_date(self):
        item = _make_item()
        assert not item.is_overdue

    def test_is_overdue_future(self):
        future = (datetime.now(timezone.utc) + timedelta(days=30)).isoformat()
        item = _make_item(due_date=future)
        assert not item.is_overdue

    def test_is_overdue_past(self):
        past = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
        item = _make_item(due_date=past)
        assert item.is_overdue

    def test_is_overdue_terminal_not_overdue(self):
        past = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
        item = _make_item(due_date=past, status=RemediationStatus.VERIFIED)
        assert not item.is_overdue

    def test_age_hours(self):
        item = _make_item()
        assert item.age_hours >= 0
        assert item.age_hours < 1  # just created

    def test_time_to_remediation_none(self):
        item = _make_item()
        assert item.time_to_remediation_hours is None

    def test_transition_valid(self):
        item = _make_item()
        item.transition(RemediationStatus.IN_PROGRESS, "starting")
        assert item.status == RemediationStatus.IN_PROGRESS
        assert len(item.history) == 1
        assert item.history[0].comment == "starting"

    def test_transition_invalid(self):
        item = _make_item()
        with pytest.raises(ValueError, match="Cannot transition"):
            item.transition(RemediationStatus.VERIFIED)

    def test_transition_chain(self):
        item = _make_item()
        item.transition(RemediationStatus.IN_PROGRESS)
        item.transition(RemediationStatus.FIXED)
        item.transition(RemediationStatus.VERIFIED)
        assert item.status == RemediationStatus.VERIFIED
        assert len(item.history) == 3

    def test_transition_reopen(self):
        item = _make_item()
        item.transition(RemediationStatus.IN_PROGRESS)
        item.transition(RemediationStatus.OPEN)  # re-open
        assert item.status == RemediationStatus.OPEN

    def test_transition_from_terminal(self):
        item = _make_item(status=RemediationStatus.VERIFIED)
        with pytest.raises(ValueError):
            item.transition(RemediationStatus.OPEN)

    def test_transition_wontfix_reopen(self):
        item = _make_item(status=RemediationStatus.WONTFIX)
        item.transition(RemediationStatus.OPEN)
        assert item.status == RemediationStatus.OPEN

    def test_to_dict(self):
        item = _make_item(owner="alice")
        d = item.to_dict()
        assert d["finding_id"] == "abc123"
        assert d["rule_id"] == "SEC-001"
        assert d["severity"] == "HIGH"
        assert d["owner"] == "alice"
        assert d["status"] == "OPEN"
        assert isinstance(d["history"], list)

    def test_from_dict(self):
        item = _make_item(owner="bob", notes="test note")
        item.transition(RemediationStatus.IN_PROGRESS)
        d = item.to_dict()
        item2 = RemediationItem.from_dict(d)
        assert item2.finding_id == "abc123"
        assert item2.owner == "bob"
        assert item2.notes == "test note"
        assert item2.status == RemediationStatus.IN_PROGRESS
        assert len(item2.history) == 1

    def test_roundtrip(self):
        item = _make_item()
        item.transition(RemediationStatus.IN_PROGRESS, "wip")
        item.transition(RemediationStatus.FIXED, "done")
        d = item.to_dict()
        item2 = RemediationItem.from_dict(d)
        assert item2.status == item.status
        assert len(item2.history) == len(item.history)


# ===========================================================================
# RemediationTracker
# ===========================================================================

class TestRemediationTracker:
    def test_empty(self):
        t = RemediationTracker()
        assert t.count == 0
        assert t.items == []

    def test_add(self):
        t = RemediationTracker()
        t.add(_make_item())
        assert t.count == 1

    def test_add_duplicate(self):
        t = RemediationTracker()
        t.add(_make_item())
        with pytest.raises(ValueError, match="already tracked"):
            t.add(_make_item())

    def test_get(self):
        t = _make_tracker(_make_item())
        assert t.get("abc123") is not None
        assert t.get("nonexistent") is None

    def test_remove(self):
        t = _make_tracker(_make_item())
        assert t.remove("abc123")
        assert t.count == 0

    def test_remove_nonexistent(self):
        t = RemediationTracker()
        assert not t.remove("xyz")

    def test_transition(self):
        t = _make_tracker(_make_item())
        t.transition("abc123", RemediationStatus.IN_PROGRESS, "starting")
        assert t.get("abc123").status == RemediationStatus.IN_PROGRESS

    def test_transition_nonexistent(self):
        t = RemediationTracker()
        with pytest.raises(KeyError):
            t.transition("xyz", RemediationStatus.IN_PROGRESS)

    def test_assign(self):
        t = _make_tracker(_make_item())
        t.assign("abc123", "alice")
        assert t.get("abc123").owner == "alice"

    def test_assign_nonexistent(self):
        t = RemediationTracker()
        with pytest.raises(KeyError):
            t.assign("xyz", "alice")

    def test_by_status(self):
        t = _make_tracker(
            _make_item("a1"),
            _make_item("a2"),
        )
        t.transition("a1", RemediationStatus.IN_PROGRESS)
        assert len(t.by_status(RemediationStatus.OPEN)) == 1
        assert len(t.by_status(RemediationStatus.IN_PROGRESS)) == 1

    def test_by_severity(self):
        t = _make_tracker(
            _make_item("a1", severity="HIGH"),
            _make_item("a2", severity="LOW"),
            _make_item("a3", severity="HIGH"),
        )
        assert len(t.by_severity("HIGH")) == 2
        assert len(t.by_severity("LOW")) == 1

    def test_by_owner(self):
        t = _make_tracker(
            _make_item("a1"),
            _make_item("a2"),
        )
        t.assign("a1", "alice")
        t.assign("a2", "bob")
        assert len(t.by_owner("alice")) == 1
        assert len(t.by_owner("carol")) == 0

    def test_open_closed_count(self):
        t = _make_tracker(
            _make_item("a1"),
            _make_item("a2"),
            _make_item("a3", status=RemediationStatus.VERIFIED),
        )
        assert t.open_count() == 2
        assert t.closed_count() == 1

    def test_overdue(self):
        past = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
        t = _make_tracker(
            _make_item("a1", due_date=past),
            _make_item("a2"),
        )
        assert len(t.overdue()) == 1

    def test_mttr_none(self):
        t = _make_tracker(_make_item())
        assert t.mttr_hours() is None

    def test_sla_compliance_empty(self):
        t = RemediationTracker()
        assert t.sla_compliance_rate() == 1.0

    def test_sla_compliance_all_open_within(self):
        future = (datetime.now(timezone.utc) + timedelta(days=30)).isoformat()
        t = _make_tracker(
            _make_item("a1", severity="HIGH", due_date=future),
        )
        assert t.sla_compliance_rate() == 1.0

    def test_severity_breakdown(self):
        t = _make_tracker(
            _make_item("a1", severity="HIGH"),
            _make_item("a2", severity="HIGH", status=RemediationStatus.VERIFIED),
            _make_item("a3", severity="LOW"),
        )
        bd = t.severity_breakdown()
        assert bd["HIGH"]["open"] == 1
        assert bd["HIGH"]["closed"] == 1
        assert bd["HIGH"]["total"] == 2
        assert bd["LOW"]["total"] == 1

    def test_status_counts(self):
        t = _make_tracker(
            _make_item("a1"),
            _make_item("a2"),
            _make_item("a3", status=RemediationStatus.VERIFIED),
        )
        counts = t.status_counts()
        assert counts["OPEN"] == 2
        assert counts["VERIFIED"] == 1

    def test_custom_sla(self):
        custom = {"CRITICAL": 4, "HIGH": 8}
        t = RemediationTracker(sla_hours=custom)
        assert t._sla_hours["CRITICAL"] == 4

    # -- Persistence --

    def test_to_json(self):
        t = _make_tracker(_make_item())
        j = t.to_json()
        data = json.loads(j)
        assert data["version"] == "1.0"
        assert len(data["items"]) == 1

    def test_from_json(self):
        t = _make_tracker(_make_item())
        j = t.to_json()
        t2 = RemediationTracker.from_json(j)
        assert t2.count == 1
        assert t2.get("abc123") is not None

    def test_roundtrip_json(self):
        t = _make_tracker(
            _make_item("a1", owner="alice"),
            _make_item("a2", severity="CRITICAL"),
        )
        t.transition("a1", RemediationStatus.IN_PROGRESS)
        j = t.to_json()
        t2 = RemediationTracker.from_json(j)
        assert t2.count == 2
        assert t2.get("a1").status == RemediationStatus.IN_PROGRESS
        assert t2.get("a2").severity == "CRITICAL"

    def test_to_dict(self):
        t = _make_tracker(_make_item())
        d = t.to_dict()
        assert "version" in d
        assert "sla_hours" in d
        assert "items" in d

    def test_from_dict(self):
        t = _make_tracker(_make_item())
        d = t.to_dict()
        t2 = RemediationTracker.from_dict(d)
        assert t2.count == t.count

    # -- Report --

    def test_markdown_report_empty(self):
        t = RemediationTracker()
        report = t.markdown_report()
        assert "# Remediation Report" in report
        assert "Total items**: 0" in report

    def test_markdown_report_with_items(self):
        past = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
        t = _make_tracker(
            _make_item("a1", severity="CRITICAL", owner="alice", due_date=past),
            _make_item("a2", severity="LOW", status=RemediationStatus.VERIFIED),
        )
        report = t.markdown_report()
        assert "CRITICAL" in report
        assert "alice" in report
        assert "Overdue" in report
        assert "Open Items" in report

    def test_markdown_report_no_overdue(self):
        t = _make_tracker(
            _make_item("a1", status=RemediationStatus.VERIFIED),
        )
        report = t.markdown_report()
        assert "Overdue" not in report

    def test_markdown_report_severity_table(self):
        t = _make_tracker(
            _make_item("a1", severity="HIGH"),
            _make_item("a2", severity="HIGH"),
        )
        report = t.markdown_report()
        assert "By Severity" in report
        assert "HIGH" in report


# ===========================================================================
# DEFAULT_SLA_HOURS
# ===========================================================================

class TestDefaultSLA:
    def test_critical_24h(self):
        assert DEFAULT_SLA_HOURS["CRITICAL"] == 24

    def test_high_72h(self):
        assert DEFAULT_SLA_HOURS["HIGH"] == 72

    def test_info_no_sla(self):
        assert DEFAULT_SLA_HOURS["INFO"] == 0

    def test_all_severities_present(self):
        for sev in ("CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"):
            assert sev in DEFAULT_SLA_HOURS
