"""
tests/test_risk_engine.py — Unit tests for the Service Risk Index Engine.

Tests scoring components, band classification, service threshold overrides,
and full RiskReport assembly — all deterministic, no I/O required.
"""
import pytest
import sys
from pathlib import Path

# Ensure router module path is on sys.path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "services" / "router"))

from risk_engine import (
    load_risk_policy,
    _builtin_defaults,
    _reload_policy,
    score_to_band,
    get_service_thresholds,
    _score_open_incidents,
    _score_recurrence,
    _score_followups,
    _score_slo,
    _score_alerts_loop,
    _score_escalations,
    compute_service_risk,
    compute_risk_dashboard,
)


@pytest.fixture(autouse=True)
def reset_policy_cache():
    """Reset the in-memory policy cache before each test."""
    _reload_policy()
    yield
    _reload_policy()


@pytest.fixture
def policy():
    return _builtin_defaults()


@pytest.fixture
def weights(policy):
    return policy["weights"]


# ─── Band classification ──────────────────────────────────────────────────────

class TestBands:
    def test_low(self, policy):
        assert score_to_band(0, policy) == "low"
        assert score_to_band(20, policy) == "low"

    def test_medium(self, policy):
        assert score_to_band(21, policy) == "medium"
        assert score_to_band(50, policy) == "medium"

    def test_high(self, policy):
        assert score_to_band(51, policy) == "high"
        assert score_to_band(80, policy) == "high"

    def test_critical(self, policy):
        assert score_to_band(81, policy) == "critical"
        assert score_to_band(200, policy) == "critical"


# ─── Open incidents scoring ───────────────────────────────────────────────────

class TestOpenIncidents:
    def test_no_incidents(self, weights):
        pts, comp, reasons = _score_open_incidents([], weights)
        assert pts == 0
        assert comp["P0"] == 0
        assert reasons == []

    def test_single_p0(self, weights):
        incs = [{"id": "i1", "severity": "P0", "status": "open"}]
        pts, comp, reasons = _score_open_incidents(incs, weights)
        assert pts == 50
        assert comp["P0"] == 1
        assert "P0" in reasons[0]

    def test_p1_p2_combined(self, weights):
        incs = [
            {"id": "i1", "severity": "P1", "status": "open"},
            {"id": "i2", "severity": "P2", "status": "open"},
            {"id": "i3", "severity": "P2", "status": "open"},
        ]
        pts, comp, reasons = _score_open_incidents(incs, weights)
        assert pts == 25 + 10 + 10  # 45
        assert comp["P1"] == 1
        assert comp["P2"] == 2

    def test_unknown_severity(self, weights):
        incs = [{"id": "i1", "severity": "P9", "status": "open"}]
        pts, comp, reasons = _score_open_incidents(incs, weights)
        assert pts == 0


# ─── Recurrence scoring ───────────────────────────────────────────────────────

class TestRecurrence:
    def _make_rec(self, high_sigs=0, high_kinds=0, warn_sigs=0, warn_kinds=0):
        return {
            "high_recurrence": {
                "signatures": [f"sig_{i}" for i in range(high_sigs)],
                "kinds": [f"kind_{i}" for i in range(high_kinds)],
            },
            "warn_recurrence": {
                "signatures": [f"wsig_{i}" for i in range(warn_sigs)],
                "kinds": [f"wkind_{i}" for i in range(warn_kinds)],
            },
        }

    def test_no_recurrence(self, weights):
        pts, comp, reasons = _score_recurrence({}, weights)
        assert pts == 0
        assert reasons == []

    def test_one_high_signature(self, weights):
        rec = self._make_rec(high_sigs=1)
        pts, comp, reasons = _score_recurrence(rec, weights)
        assert pts == 20  # signature_high_7d = 20
        assert comp["high_signatures_7d"] == 1
        assert any("High recurrence signatures" in r for r in reasons)

    def test_high_kinds_adds_points(self, weights):
        rec = self._make_rec(high_kinds=2)
        pts, comp, _ = _score_recurrence(rec, weights)
        assert pts == 15 * 2  # kind_high_7d = 15 each

    def test_warn_signature(self, weights):
        rec = self._make_rec(warn_sigs=1)
        pts, comp, _ = _score_recurrence(rec, weights)
        assert pts == 10  # signature_warn_7d = 10


# ─── Follow-ups scoring ───────────────────────────────────────────────────────

class TestFollowups:
    def test_no_followups(self, weights):
        pts, comp, reasons = _score_followups({}, weights)
        assert pts == 0

    def test_overdue_p0(self, weights):
        data = {"overdue_followups": [{"priority": "P0"}]}
        pts, comp, reasons = _score_followups(data, weights)
        assert pts == 20
        assert comp["P0"] == 1
        assert "P0" in reasons[0]

    def test_overdue_p1(self, weights):
        data = {"overdue_followups": [{"priority": "P1"}]}
        pts, comp, reasons = _score_followups(data, weights)
        assert pts == 12
        assert comp["P1"] == 1

    def test_overdue_mixed(self, weights):
        data = {
            "overdue_followups": [
                {"priority": "P0"},
                {"priority": "P1"},
                {"priority": "P2"},
            ]
        }
        pts, comp, _ = _score_followups(data, weights)
        assert pts == 20 + 12 + 6  # 38


# ─── SLO scoring ─────────────────────────────────────────────────────────────

class TestSlo:
    def test_no_violations(self, weights):
        pts, comp, reasons = _score_slo({"violations": []}, weights)
        assert pts == 0
        assert reasons == []

    def test_one_violation(self, weights):
        pts, comp, reasons = _score_slo({"violations": [{"metric": "error_rate"}]}, weights)
        assert pts == 10
        assert comp["violations"] == 1
        assert reasons

    def test_two_violations(self, weights):
        slo = {"violations": [{"m": "latency"}, {"m": "error"}]}
        pts, comp, _ = _score_slo(slo, weights)
        assert pts == 20
        assert comp["violations"] == 2

    def test_skipped(self, weights):
        pts, comp, _ = _score_slo({"violations": [], "skipped": True}, weights)
        assert pts == 0
        assert comp["skipped"] is True


# ─── Alert-loop SLO scoring ───────────────────────────────────────────────────

class TestAlertsLoop:
    def test_no_violations(self, weights):
        pts, comp, _ = _score_alerts_loop({}, weights)
        assert pts == 0

    def test_one_loop_violation(self, weights):
        pts, comp, reasons = _score_alerts_loop({"violations": [{"type": "missed_slo"}]}, weights)
        assert pts == 10
        assert reasons


# ─── Escalation scoring ──────────────────────────────────────────────────────

class TestEscalations:
    def test_no_escalations(self, weights):
        pts, comp, _ = _score_escalations(0, weights)
        assert pts == 0

    def test_warn_level(self, weights):
        pts, comp, reasons = _score_escalations(1, weights)
        assert pts == 5  # warn level
        assert comp["count_24h"] == 1
        assert reasons

    def test_high_level(self, weights):
        pts, comp, reasons = _score_escalations(3, weights)
        assert pts == 12  # high level

    def test_high_level_more(self, weights):
        pts, comp, _ = _score_escalations(10, weights)
        assert pts == 12  # capped at high


# ─── Full compute_service_risk ────────────────────────────────────────────────

class TestComputeServiceRisk:
    def test_zero_risk_empty_inputs(self, policy):
        report = compute_service_risk(
            "gateway", "prod",
            open_incidents=[],
            recurrence_7d={},
            followups_data={},
            slo_data={"violations": []},
            alerts_loop_slo={},
            escalation_count_24h=0,
            policy=policy,
        )
        assert report["score"] == 0
        assert report["band"] == "low"
        assert report["service"] == "gateway"
        assert report["env"] == "prod"
        assert isinstance(report["reasons"], list)
        assert isinstance(report["recommendations"], list)
        assert "updated_at" in report

    def test_p0_open_incident(self, policy):
        report = compute_service_risk(
            "gateway", "prod",
            open_incidents=[{"id": "i1", "severity": "P0", "status": "open"}],
            policy=policy,
        )
        assert report["score"] == 50
        assert report["band"] == "medium"
        assert report["components"]["open_incidents"]["P0"] == 1

    def test_full_high_risk(self, policy):
        """Combining several signals pushes score into 'high' band."""
        report = compute_service_risk(
            "gateway", "prod",
            open_incidents=[
                {"id": "i1", "severity": "P1", "status": "open"},
                {"id": "i2", "severity": "P2", "status": "open"},
            ],
            recurrence_7d={
                "high_recurrence": {"signatures": ["bucket_A"], "kinds": []},
                "warn_recurrence": {"signatures": [], "kinds": []},
            },
            followups_data={"overdue_followups": [{"priority": "P1"}]},
            slo_data={"violations": [{"metric": "error_rate"}]},
            escalation_count_24h=1,
            policy=policy,
        )
        # P1=25, P2=10, high_sig_7d=20, overdue_P1=12, slo=10, esc_warn=5 → 82
        assert report["score"] >= 70
        assert report["band"] in ("high", "critical")
        assert len(report["recommendations"]) > 0

    def test_recommendations_present_for_high_score(self, policy):
        report = compute_service_risk(
            "router", "prod",
            open_incidents=[{"id": "i1", "severity": "P0", "status": "open"}],
            slo_data={"violations": [{"m": "latency"}]},
            policy=policy,
        )
        assert any("P0" in r or "SLO" in r for r in report["recommendations"])


# ─── Service threshold overrides ─────────────────────────────────────────────

class TestServiceOverrides:
    def test_gateway_fail_at_75(self):
        """risk_policy.yml defines gateway.risk_watch.fail_at = 75."""
        policy = load_risk_policy()
        thresholds = get_service_thresholds("gateway", policy)
        assert thresholds["fail_at"] == 75

    def test_router_fail_at_80(self):
        policy = load_risk_policy()
        thresholds = get_service_thresholds("router", policy)
        assert thresholds["fail_at"] == 80

    def test_unknown_service_default(self):
        policy = load_risk_policy()
        thresholds = get_service_thresholds("unknown-svc", policy)
        assert thresholds["fail_at"] >= 75  # must have a value

    def test_threshold_reflected_in_report(self):
        policy = load_risk_policy()
        report = compute_service_risk("gateway", "prod", policy=policy)
        assert report["thresholds"]["fail_at"] == 75
