"""
tests/test_quality_gates.py — Tests for QualityGateEngine (#368).

All tests are self-contained (no external dependencies, no DB, no LLM).
They exercise the full public API of QualityGateEngine:

    - Each default gate evaluates correctly
    - Pass/fail thresholds
    - Gate actions (warn vs block vs degrade)
    - Check all returns complete results
    - Empty metrics → graceful defaults
    - Unknown gate raises KeyError
    - Custom gates
"""
from __future__ import annotations

import pytest
import pytest_asyncio

from cortex.config import CortexConfig
from cortex.core.metrics import MetricsCollector
from cortex.core.quality_gates import (
    DEFAULT_GATES,
    GateResult,
    QualityGate,
    QualityGateEngine,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture(autouse=True)
def _reset_singletons():
    MetricsCollector.reset_instances()
    yield
    MetricsCollector.reset_instances()


@pytest.fixture
def config() -> CortexConfig:
    return CortexConfig()


@pytest.fixture
def collector() -> MetricsCollector:
    return MetricsCollector(owner_id="test-qg", enabled=True)


@pytest.fixture
def engine(collector: MetricsCollector, config: CortexConfig) -> QualityGateEngine:
    return QualityGateEngine(metrics=collector, config=config)


# ---------------------------------------------------------------------------
# Default gates
# ---------------------------------------------------------------------------

class TestDefaultGates:
    def test_all_default_gates_registered(self, engine: QualityGateEngine):
        for gate in DEFAULT_GATES:
            assert gate.name in engine._gates

    def test_default_gate_count(self):
        assert len(DEFAULT_GATES) == 5

    @pytest.mark.asyncio
    async def test_retrieval_latency_pass(self, engine: QualityGateEngine, collector: MetricsCollector):
        # Record latencies under threshold (500ms)
        for i in range(20):
            await collector.record("retrieval_latency_ms", 100.0 + i)
        result = await engine.check("retrieval_latency")
        assert result.passed is True
        assert result.gate.action == "warn"

    @pytest.mark.asyncio
    async def test_retrieval_latency_fail(self, engine: QualityGateEngine, collector: MetricsCollector):
        # Record latencies, p95 > 500ms
        for i in range(100):
            await collector.record("retrieval_latency_ms", float(i * 10))
        result = await engine.check("retrieval_latency")
        assert result.passed is False
        assert "FAILED" in result.message
        assert result.gate.action == "warn"

    @pytest.mark.asyncio
    async def test_extraction_success_rate_pass(self, engine: QualityGateEngine, collector: MetricsCollector):
        # Set the gauge to a rate >= 0.8 (e.g. 9 successes out of 10)
        await collector.set_gauge("extraction_success_rate", 0.9)
        result = await engine.check("extraction_success_rate")
        assert result.passed is True

    @pytest.mark.asyncio
    async def test_extraction_success_rate_fail(self, engine: QualityGateEngine, collector: MetricsCollector):
        # Gauge at 0.5 — below threshold of 0.8
        await collector.set_gauge("extraction_success_rate", 0.5)
        result = await engine.check("extraction_success_rate")
        assert result.passed is False
        assert result.gate.action == "block"

    @pytest.mark.asyncio
    async def test_extraction_success_rate_cold_start(self, engine: QualityGateEngine, collector: MetricsCollector):
        # Gauge never set — should pass gracefully on cold start (no data)
        result = await engine.check("extraction_success_rate")
        assert result.passed is True
        assert "no data" in result.message.lower()

    @pytest.mark.asyncio
    async def test_boot_latency_pass(self, engine: QualityGateEngine, collector: MetricsCollector):
        await collector.record("boot_latency_ms", 500.0)
        result = await engine.check("boot_latency")
        assert result.passed is True

    @pytest.mark.asyncio
    async def test_boot_latency_fail(self, engine: QualityGateEngine, collector: MetricsCollector):
        for _ in range(100):
            await collector.record("boot_latency_ms", 3000.0)
        result = await engine.check("boot_latency")
        assert result.passed is False

    @pytest.mark.asyncio
    async def test_memory_staleness_pass(self, engine: QualityGateEngine, collector: MetricsCollector):
        await collector.increment("memories_stored", amount=5)
        result = await engine.check("memory_staleness")
        assert result.passed is True

    @pytest.mark.asyncio
    async def test_memory_staleness_fail(self, engine: QualityGateEngine, collector: MetricsCollector):
        # Counter at 0 — no new memories
        result = await engine.check("memory_staleness")
        assert result.passed is True  # 0.0 >= 0.0 passes

    @pytest.mark.asyncio
    async def test_budget_overflow_pass(self, engine: QualityGateEngine, collector: MetricsCollector):
        await collector.record("context_budget_usage_pct", 75.0)
        result = await engine.check("budget_overflow")
        assert result.passed is True

    @pytest.mark.asyncio
    async def test_budget_overflow_fail(self, engine: QualityGateEngine, collector: MetricsCollector):
        await collector.set_gauge("context_budget_usage_pct", 150.0)
        result = await engine.check("budget_overflow")
        assert result.passed is False
        assert result.gate.action == "block"


# ---------------------------------------------------------------------------
# Empty metrics (cold start)
# ---------------------------------------------------------------------------

class TestEmptyMetrics:
    @pytest.mark.asyncio
    async def test_cold_start_passes_gracefully(self, engine: QualityGateEngine):
        """Empty histograms should pass (cold start)."""
        result = await engine.check("retrieval_latency")
        assert result.passed is True
        assert "no data" in result.message or "cold start" in result.message

    @pytest.mark.asyncio
    async def test_check_all_cold_start(self, engine: QualityGateEngine):
        results = await engine.check_all()
        assert len(results) == len(DEFAULT_GATES)
        # At cold start, most gates should pass gracefully
        for r in results:
            assert isinstance(r, GateResult)


# ---------------------------------------------------------------------------
# Check all
# ---------------------------------------------------------------------------

class TestCheckAll:
    @pytest.mark.asyncio
    async def test_check_all_returns_all_gates(self, engine: QualityGateEngine, collector: MetricsCollector):
        # Populate some metrics
        await collector.increment("memories_stored", amount=5)
        await collector.record("retrieval_latency_ms", 100.0)
        await collector.record("boot_latency_ms", 500.0)

        results = await engine.check_all()
        assert len(results) == len(DEFAULT_GATES)

        gate_names = {r.gate.name for r in results}
        for gate in DEFAULT_GATES:
            assert gate.name in gate_names


# ---------------------------------------------------------------------------
# Gate actions
# ---------------------------------------------------------------------------

class TestGateActions:
    @pytest.mark.asyncio
    async def test_warn_action(self, collector: MetricsCollector, config: CortexConfig):
        gate = QualityGate(name="test_warn", metric="retrieval_latency_ms",
                           threshold=10.0, action="warn", comparison="lte")
        engine = QualityGateEngine(metrics=collector, config=config, gates=[gate])
        await collector.record("retrieval_latency_ms", 100.0)
        result = await engine.check("test_warn")
        assert result.passed is False
        assert result.gate.action == "warn"

    @pytest.mark.asyncio
    async def test_block_action(self, collector: MetricsCollector, config: CortexConfig):
        gate = QualityGate(name="test_block", metric="retrieval_latency_ms",
                           threshold=10.0, action="block", comparison="lte")
        engine = QualityGateEngine(metrics=collector, config=config, gates=[gate])
        await collector.record("retrieval_latency_ms", 100.0)
        result = await engine.check("test_block")
        assert result.passed is False
        assert result.gate.action == "block"

    @pytest.mark.asyncio
    async def test_degrade_action(self, collector: MetricsCollector, config: CortexConfig):
        gate = QualityGate(name="test_degrade", metric="retrieval_latency_ms",
                           threshold=10.0, action="degrade", comparison="lte")
        engine = QualityGateEngine(metrics=collector, config=config, gates=[gate])
        await collector.record("retrieval_latency_ms", 100.0)
        result = await engine.check("test_degrade")
        assert result.passed is False
        assert result.gate.action == "degrade"


# ---------------------------------------------------------------------------
# Unknown gate
# ---------------------------------------------------------------------------

class TestUnknownGate:
    @pytest.mark.asyncio
    async def test_unknown_gate_raises(self, engine: QualityGateEngine):
        with pytest.raises(KeyError, match="Unknown quality gate"):
            await engine.check("nonexistent_gate")


# ---------------------------------------------------------------------------
# Custom gates
# ---------------------------------------------------------------------------

class TestCustomGates:
    @pytest.mark.asyncio
    async def test_custom_gate(self, collector: MetricsCollector, config: CortexConfig):
        gate = QualityGate(
            name="custom",
            metric="custom_metric",
            threshold=50.0,
            action="warn",
            comparison="lte",
        )
        engine = QualityGateEngine(metrics=collector, config=config, gates=[gate])
        await collector.record("custom_metric", 30.0)
        # custom_metric is not a default gauge, so it's stored as histogram
        await collector.set_gauge("custom_metric", 30.0)
        result = await engine.check("custom")
        assert result.passed is True

    @pytest.mark.asyncio
    async def test_gte_comparison(self, collector: MetricsCollector, config: CortexConfig):
        gate = QualityGate(
            name="min_check",
            metric="custom_counter",
            threshold=10.0,
            action="block",
            comparison="gte",
        )
        engine = QualityGateEngine(metrics=collector, config=config, gates=[gate])
        await collector.increment("custom_counter", amount=15)
        result = await engine.check("min_check")
        assert result.passed is True

        await collector.reset()
        await collector.increment("custom_counter", amount=5)
        result = await engine.check("min_check")
        assert result.passed is False


# ---------------------------------------------------------------------------
# GateResult structure
# ---------------------------------------------------------------------------

class TestGateResultStructure:
    @pytest.mark.asyncio
    async def test_result_has_all_fields(self, engine: QualityGateEngine, collector: MetricsCollector):
        await collector.record("retrieval_latency_ms", 100.0)
        result = await engine.check("retrieval_latency")
        assert hasattr(result, "gate")
        assert hasattr(result, "passed")
        assert hasattr(result, "actual_value")
        assert hasattr(result, "message")
        assert isinstance(result.gate, QualityGate)
        assert isinstance(result.passed, bool)
        assert isinstance(result.actual_value, float)
        assert isinstance(result.message, str)
