"""Tests for orchestration.circuit_breaker (registry-based general-purpose CB)."""

from __future__ import annotations

import time

import pytest

from orxaq.orchestration.circuit_breaker import (
    CircuitBreaker,
    CircuitBreakerConfig,
    CircuitBreakerRegistry,
    CircuitOpenError,
    CircuitState,
    get_default_registry,
)


def _ok() -> str:
    return "ok"


def _boom() -> None:
    raise RuntimeError("boom")


def cb(*, t: int = 3, rt: float = 60.0, st: int = 1) -> CircuitBreaker:
    cfg = CircuitBreakerConfig(failure_threshold=t, reset_timeout=rt, success_threshold=st)
    return CircuitBreaker("s", cfg)


class TestConfig:
    def test_defaults(self) -> None:
        c = CircuitBreakerConfig()
        assert c.failure_threshold == 5 and c.reset_timeout == 60.0 and not c.use_rate_mode

    def test_custom(self) -> None:
        assert CircuitBreakerConfig(failure_threshold=2).failure_threshold == 2


class TestClosed:
    def test_initial(self) -> None:
        assert cb().state == CircuitState.CLOSED

    def test_success_stays_closed(self) -> None:
        c = cb()
        assert c.call(_ok) == "ok"
        assert c.state == CircuitState.CLOSED

    def test_below_threshold(self) -> None:
        c = cb(t=3)
        for _ in range(2):
            with pytest.raises(RuntimeError):
                c.call(_boom)
        assert c.state == CircuitState.CLOSED

    def test_threshold_opens(self) -> None:
        c = cb(t=3)
        for _ in range(3):
            with pytest.raises(RuntimeError):
                c.call(_boom)
        assert c.state == CircuitState.OPEN

    def test_success_resets_consecutive(self) -> None:
        c = cb(t=3)
        for _ in range(2):
            with pytest.raises(RuntimeError):
                c.call(_boom)
        c.call(_ok)
        with pytest.raises(RuntimeError):
            c.call(_boom)
        assert c.state == CircuitState.CLOSED


class TestOpen:
    def test_blocks_calls(self) -> None:
        c = cb(t=1)
        with pytest.raises(RuntimeError):
            c.call(_boom)
        with pytest.raises(CircuitOpenError) as ei:
            c.call(_ok)
        assert ei.value.name == "s"

    def test_retry_after(self) -> None:
        c = cb(t=1, rt=30.0)
        with pytest.raises(RuntimeError):
            c.call(_boom)
        with pytest.raises(CircuitOpenError) as ei:
            c.call(_ok)
        assert 0 < ei.value.retry_after <= 30.0

    def test_error_str(self) -> None:
        c = cb(t=1)
        with pytest.raises(RuntimeError):
            c.call(_boom)
        with pytest.raises(CircuitOpenError) as ei:
            c.call(_ok)
        assert "OPEN" in str(ei.value) and "s" in str(ei.value)


class TestHalfOpen:
    def _force(self, c: CircuitBreaker) -> None:
        with pytest.raises(RuntimeError):
            c.call(_boom)
        c._opened_at = time.monotonic() - c.config.reset_timeout - 1.0

    def test_after_timeout(self) -> None:
        c = cb(t=1, rt=0.001)
        with pytest.raises(RuntimeError):
            c.call(_boom)
        time.sleep(0.005)
        assert c.state == CircuitState.HALF_OPEN

    def test_success_closes(self) -> None:
        c = cb(t=1)
        self._force(c)
        c.call(_ok)
        assert c.state == CircuitState.CLOSED

    def test_failure_reopens(self) -> None:
        c = cb(t=1)
        self._force(c)
        with pytest.raises(RuntimeError):
            c.call(_boom)
        assert c.state == CircuitState.OPEN

    def test_success_threshold_2(self) -> None:
        c = cb(t=1, st=2)
        self._force(c)
        c.call(_ok)
        assert c.state == CircuitState.HALF_OPEN
        c.call(_ok)
        assert c.state == CircuitState.CLOSED


class TestManual:
    def test_record_failure_trips(self) -> None:
        c = cb(t=2)
        c.record_failure()
        assert c.state == CircuitState.CLOSED
        c.record_failure()
        assert c.state == CircuitState.OPEN

    def test_success_resets(self) -> None:
        c = cb(t=2)
        c.record_failure()
        c.record_success()
        c.record_failure()
        assert c.state == CircuitState.CLOSED

    def test_reset(self) -> None:
        c = cb(t=1)
        c.record_failure()
        c.reset()
        assert c.state == CircuitState.CLOSED


class TestStats:
    def test_initial(self) -> None:
        s = cb().stats()
        assert s.state == "CLOSED" and s.total_calls == 0

    def test_tracking(self) -> None:
        c = cb(t=5)
        c.call(_ok)
        with pytest.raises(RuntimeError):
            c.call(_boom)
        s = c.stats()
        assert s.total_calls == 2 and s.success_count == 1 and s.failure_count == 1


class TestRateMode:
    def test_opens(self) -> None:
        cfg = CircuitBreakerConfig(use_rate_mode=True, failure_rate_threshold=0.5, time_window=60.0)
        c = CircuitBreaker("r", cfg)
        c.record_success()
        for _ in range(3):
            c.record_failure()
        assert c.state == CircuitState.OPEN

    def test_below_threshold(self) -> None:
        cfg = CircuitBreakerConfig(use_rate_mode=True, failure_rate_threshold=0.6, time_window=60.0)
        c = CircuitBreaker("r2", cfg)
        for _ in range(3):
            c.record_success()
        for _ in range(2):
            c.record_failure()
        assert c.state == CircuitState.CLOSED


class TestRegistry:
    def test_register_get(self) -> None:
        reg = CircuitBreakerRegistry()
        b = reg.register("p")
        assert reg.get("p") is b

    def test_get_unknown(self) -> None:
        assert CircuitBreakerRegistry().get("x") is None

    def test_get_or_create(self) -> None:
        reg = CircuitBreakerRegistry()
        assert reg.get_or_create("p") is reg.get_or_create("p")

    def test_all_stats(self) -> None:
        reg = CircuitBreakerRegistry()
        reg.register("a")
        reg.register("b")
        assert len(reg.all_stats()) == 2

    def test_open_healthy(self) -> None:
        cfg = CircuitBreakerConfig(failure_threshold=1)
        reg = CircuitBreakerRegistry(_config=cfg)
        reg.register("p1")
        reg.register("p2")
        reg.get("p1").record_failure()  # type: ignore[union-attr]
        assert reg.open_services() == ["p1"]
        assert reg.healthy_services() == ["p2"]

    def test_reset_all(self) -> None:
        cfg = CircuitBreakerConfig(failure_threshold=1)
        reg = CircuitBreakerRegistry(_config=cfg)
        reg.register("p1")
        reg.register("p2")
        reg.get("p1").record_failure()  # type: ignore[union-attr]
        reg.get("p2").record_failure()  # type: ignore[union-attr]
        reg.reset_all()
        assert reg.open_services() == []

    def test_default_registry(self) -> None:
        assert isinstance(get_default_registry(), CircuitBreakerRegistry)


# ── Targeted coverage (lines 196, 207) ───────────────────


class TestCircuitBreakerDefensiveGuards:
    """Cover lines 196 and 207: defensive guard branches."""

    def test_seconds_until_reset_no_opened_at(self) -> None:
        """Cover line 196: _opened_at is None → return 0.0."""
        cb = CircuitBreaker("test")
        assert cb._seconds_until_reset() == 0.0

    def test_rate_exceeds_threshold_empty_log(self) -> None:
        """Cover line 207: empty call_log → return False."""
        cb = CircuitBreaker("test")
        assert cb._rate_exceeds_threshold() is False
