"""
Tests for slo_watch gate in release_check_runner.
Covers: violations → recommendations, policy strict → blocks, policy warn → pass,
policy off → skip.
"""
import asyncio
import os
import sys
from pathlib import Path
from unittest.mock import patch

ROOT = Path(__file__).resolve().parent.parent
ROUTER = ROOT / "services" / "router"
if str(ROUTER) not in sys.path:
    sys.path.insert(0, str(ROUTER))


class MockToolResult:
    def __init__(self, success, result=None, error=None):
        self.success = success
        self.result = result
        self.error = error


class MockToolManager:
    def __init__(self, slo_data=None, always_pass_others=True):
        self.slo_data = slo_data or {
            "violations": [],
            "metrics": {},
            "thresholds": {},
            "skipped": False,
        }
        self.always_pass_others = always_pass_others
        self.calls = []

    async def execute_tool(self, tool_name, args, agent_id="test"):
        self.calls.append((tool_name, args.get("action")))
        if tool_name == "observability_tool" and args.get("action") == "slo_snapshot":
            return MockToolResult(True, self.slo_data)
        if self.always_pass_others:
            return MockToolResult(True, {
                "pass": True, "blocking_count": 0, "breaking_count": 0,
                "unmitigated_high_count": 0, "summary": "ok",
                "violations": [], "open_incidents": [], "overdue_followups": [],
                "stats": {"open_incidents": 0, "overdue": 0, "total_open_followups": 0},
            })
        return MockToolResult(False, error="skipped")


def _run_check(tm, inputs, agent="test"):
    from release_check_runner import run_release_check
    return asyncio.run(run_release_check(tm, inputs, agent))


class TestSLOWatchWarnMode:
    """slo_watch in warn mode: violations → recommendations, always pass."""

    def test_violations_generate_recommendations(self):
        slo_data = {
            "violations": ["latency_p95", "error_rate"],
            "metrics": {"latency_p95_ms": 500, "error_rate_pct": 3.0},
            "thresholds": {"latency_p95_ms": 300, "error_rate_pct": 1.0},
            "skipped": False,
        }
        tm = MockToolManager(slo_data=slo_data)

        with patch("release_check_runner.load_gate_policy") as mock_policy:
            mock_policy.return_value = {
                "_profile": "dev",
                "_default_mode": "warn",
                "slo_watch": {"mode": "warn"},
                "followup_watch": {"mode": "off"},
                "privacy_watch": {"mode": "off"},
                "cost_watch": {"mode": "off"},
                "get": lambda name: {"mode": "warn"},
            }
            result = _run_check(tm, {"service_name": "gateway"})

        assert result["pass"] is True
        gate_names = [g["name"] for g in result["gates"]]
        assert "slo_watch" in gate_names

        slo_gate = next(g for g in result["gates"] if g["name"] == "slo_watch")
        assert "latency_p95" in slo_gate["violations"]
        assert any("SLO violation" in r for r in result["recommendations"])

    def test_no_violations_no_recommendations(self):
        slo_data = {
            "violations": [],
            "metrics": {"latency_p95_ms": 100, "error_rate_pct": 0.1},
            "thresholds": {"latency_p95_ms": 300, "error_rate_pct": 1.0},
            "skipped": False,
        }
        tm = MockToolManager(slo_data=slo_data)

        with patch("release_check_runner.load_gate_policy") as mock_policy:
            mock_policy.return_value = {
                "_profile": "dev",
                "_default_mode": "warn",
                "slo_watch": {"mode": "warn"},
                "followup_watch": {"mode": "off"},
                "privacy_watch": {"mode": "off"},
                "cost_watch": {"mode": "off"},
                "get": lambda name: {"mode": "warn"},
            }
            result = _run_check(tm, {"service_name": "gateway"})

        assert result["pass"] is True
        slo_recs = [r for r in result["recommendations"] if "SLO" in r]
        assert len(slo_recs) == 0


class TestSLOWatchStrictMode:
    """slo_watch in strict mode: violations block release."""

    def test_violations_block_release(self):
        slo_data = {
            "violations": ["latency_p95"],
            "metrics": {"latency_p95_ms": 500},
            "thresholds": {"latency_p95_ms": 200},
            "skipped": False,
        }
        tm = MockToolManager(slo_data=slo_data)

        with patch("release_check_runner.load_gate_policy") as mock_policy:
            mock_policy.return_value = {
                "_profile": "staging",
                "_default_mode": "warn",
                "slo_watch": {"mode": "strict"},
                "followup_watch": {"mode": "off"},
                "privacy_watch": {"mode": "off"},
                "cost_watch": {"mode": "off"},
                "get": lambda name: {"mode": "warn"},
            }
            result = _run_check(tm, {"service_name": "router", "fail_fast": True})

        assert result["pass"] is False

    def test_no_violations_does_not_block(self):
        slo_data = {
            "violations": [],
            "metrics": {"latency_p95_ms": 50},
            "thresholds": {"latency_p95_ms": 200},
            "skipped": False,
        }
        tm = MockToolManager(slo_data=slo_data)

        with patch("release_check_runner.load_gate_policy") as mock_policy:
            mock_policy.return_value = {
                "_profile": "staging",
                "_default_mode": "warn",
                "slo_watch": {"mode": "strict"},
                "followup_watch": {"mode": "off"},
                "privacy_watch": {"mode": "off"},
                "cost_watch": {"mode": "off"},
                "get": lambda name: {"mode": "warn"},
            }
            result = _run_check(tm, {"service_name": "router"})

        assert result["pass"] is True

    def test_skipped_does_not_block(self):
        slo_data = {
            "violations": ["latency_p95"],
            "skipped": True,
        }
        tm = MockToolManager(slo_data=slo_data)

        with patch("release_check_runner.load_gate_policy") as mock_policy:
            mock_policy.return_value = {
                "_profile": "staging",
                "_default_mode": "warn",
                "slo_watch": {"mode": "strict"},
                "followup_watch": {"mode": "off"},
                "privacy_watch": {"mode": "off"},
                "cost_watch": {"mode": "off"},
                "get": lambda name: {"mode": "warn"},
            }
            result = _run_check(tm, {"service_name": "router"})

        assert result["pass"] is True


class TestSLOWatchOffMode:
    """slo_watch in off mode: gate not called at all."""

    def test_gate_skipped_when_off(self):
        tm = MockToolManager()

        with patch("release_check_runner.load_gate_policy") as mock_policy:
            mock_policy.return_value = {
                "_profile": "dev",
                "_default_mode": "warn",
                "slo_watch": {"mode": "off"},
                "followup_watch": {"mode": "off"},
                "privacy_watch": {"mode": "off"},
                "cost_watch": {"mode": "off"},
                "get": lambda name: {"mode": "off"},
            }
            result = _run_check(tm, {"service_name": "gateway"})

        gate_names = [g["name"] for g in result["gates"]]
        assert "slo_watch" not in gate_names
        called_actions = [c[1] for c in tm.calls]
        assert "slo_snapshot" not in called_actions

    def test_gate_skipped_when_disabled_via_input(self):
        tm = MockToolManager()

        with patch("release_check_runner.load_gate_policy") as mock_policy:
            mock_policy.return_value = {
                "_profile": "dev",
                "_default_mode": "warn",
                "slo_watch": {"mode": "warn"},
                "followup_watch": {"mode": "off"},
                "privacy_watch": {"mode": "off"},
                "cost_watch": {"mode": "off"},
                "get": lambda name: {"mode": "warn"},
            }
            result = _run_check(tm, {"service_name": "gateway", "run_slo_watch": False})

        gate_names = [g["name"] for g in result["gates"]]
        assert "slo_watch" not in gate_names


class TestSLOWatchGatewayError:
    """slo_watch is non-fatal on gateway errors."""

    def test_gateway_error_becomes_skipped_pass(self):
        class FailingTM:
            calls = []

            async def execute_tool(self, tool_name, args, agent_id="test"):
                self.calls.append((tool_name, args.get("action")))
                if tool_name == "observability_tool" and args.get("action") == "slo_snapshot":
                    raise ConnectionError("Prometheus unreachable")
                return MockToolResult(True, {
                    "pass": True, "blocking_count": 0, "breaking_count": 0,
                    "unmitigated_high_count": 0, "summary": "ok",
                    "violations": [], "open_incidents": [], "overdue_followups": [],
                    "stats": {"open_incidents": 0, "overdue": 0, "total_open_followups": 0},
                })

        tm = FailingTM()
        with patch("release_check_runner.load_gate_policy") as mock_policy:
            mock_policy.return_value = {
                "_profile": "staging",
                "_default_mode": "warn",
                "slo_watch": {"mode": "strict"},
                "followup_watch": {"mode": "off"},
                "privacy_watch": {"mode": "off"},
                "cost_watch": {"mode": "off"},
                "get": lambda name: {"mode": "warn"},
            }
            result = _run_check(tm, {"service_name": "gateway"})

        # Even in strict mode, gateway error should not block
        assert result["pass"] is True
        slo_gate = next((g for g in result["gates"] if g["name"] == "slo_watch"), None)
        if slo_gate:
            assert slo_gate.get("skipped") is True
