"""Tests for circuit breaker and resilience patterns."""

from __future__ import annotations

import time

from orxaq.orchestration.providers.base import LLMProvider, LLMResponse
from orxaq.orchestration.providers.resilience import (
    BackoffConfig,
    CircuitBreaker,
    CircuitConfig,
    CircuitState,
    CircuitStats,
    ResilientProvider,
    ResilientRouter,
    compute_backoff,
)


class FakeProvider(LLMProvider):
    """Test provider that can be configured to fail."""

    def __init__(self, name: str = "fake", fail: bool = False):
        self.name = name
        self.models = ["test-model"]
        self._fail = fail
        self.call_count = 0

    def generate(self, prompt, model=None, max_tokens=1024, temperature=0.7):
        self.call_count += 1
        if self._fail:
            return LLMResponse(
                text="", model=model or "test-model", provider=self.name,
                metadata={"error": "provider failure"},
            )
        return LLMResponse(
            text=f"response to: {prompt}",
            model=model or "test-model",
            provider=self.name,
        )

    def health_check(self):
        return {"healthy": not self._fail, "provider": self.name}


# ── CircuitConfig ────────────────────────────────────────


class TestCircuitConfig:
    def test_defaults(self):
        c = CircuitConfig()
        assert c.failure_threshold == 5
        assert c.recovery_timeout_s == 30.0
        assert c.success_threshold == 2

    def test_to_dict(self):
        c = CircuitConfig(failure_threshold=3)
        d = c.to_dict()
        assert d["failure_threshold"] == 3


# ── CircuitStats ─────────────────────────────────────────


class TestCircuitStats:
    def test_defaults(self):
        s = CircuitStats()
        assert s.state == CircuitState.CLOSED
        assert s.total_calls == 0

    def test_to_dict(self):
        s = CircuitStats(total_calls=5, total_failures=2)
        d = s.to_dict()
        assert d["total_calls"] == 5
        assert d["state"] == "closed"


# ── CircuitBreaker ───────────────────────────────────────


class TestCircuitBreaker:
    def test_initial_state(self):
        cb = CircuitBreaker()
        assert cb.state == CircuitState.CLOSED
        assert cb.allow_request() is True

    def test_stays_closed_on_success(self):
        cb = CircuitBreaker(CircuitConfig(failure_threshold=3))
        for _ in range(10):
            cb.record_success()
        assert cb.state == CircuitState.CLOSED
        assert cb.stats.total_successes == 10

    def test_opens_on_threshold(self):
        cb = CircuitBreaker(CircuitConfig(failure_threshold=3))
        for _ in range(3):
            cb.record_failure()
        assert cb.state == CircuitState.OPEN
        assert cb.allow_request() is False
        assert cb.stats.consecutive_failures == 3

    def test_failure_resets_success_streak(self):
        cb = CircuitBreaker(CircuitConfig(failure_threshold=3))
        cb.record_success()
        cb.record_success()
        cb.record_failure()
        assert cb.stats.consecutive_successes == 0
        assert cb.stats.consecutive_failures == 1

    def test_success_resets_failure_streak(self):
        cb = CircuitBreaker(CircuitConfig(failure_threshold=5))
        cb.record_failure()
        cb.record_failure()
        cb.record_success()
        assert cb.stats.consecutive_failures == 0
        assert cb.stats.consecutive_successes == 1

    def test_open_to_half_open_on_timeout(self):
        cfg = CircuitConfig(failure_threshold=2, recovery_timeout_s=0.01)
        cb = CircuitBreaker(cfg)
        cb.record_failure()
        cb.record_failure()
        assert cb.state == CircuitState.OPEN
        time.sleep(0.02)
        assert cb.state == CircuitState.HALF_OPEN
        assert cb.allow_request() is True

    def test_half_open_to_closed_on_success(self):
        cfg = CircuitConfig(
            failure_threshold=2,
            recovery_timeout_s=0.01,
            success_threshold=2,
        )
        cb = CircuitBreaker(cfg)
        cb.record_failure()
        cb.record_failure()
        time.sleep(0.02)
        assert cb.state == CircuitState.HALF_OPEN
        cb.record_success()
        cb.record_success()
        assert cb.state == CircuitState.CLOSED

    def test_half_open_to_open_on_failure(self):
        cfg = CircuitConfig(
            failure_threshold=2,
            recovery_timeout_s=0.01,
        )
        cb = CircuitBreaker(cfg)
        cb.record_failure()
        cb.record_failure()
        time.sleep(0.02)
        assert cb.state == CircuitState.HALF_OPEN
        cb.record_failure()
        assert cb.state == CircuitState.OPEN

    def test_half_open_limits_calls(self):
        cfg = CircuitConfig(
            failure_threshold=2,
            recovery_timeout_s=0.01,
            half_open_max_calls=1,
        )
        cb = CircuitBreaker(cfg)
        cb.record_failure()
        cb.record_failure()
        time.sleep(0.02)
        assert cb.allow_request() is True
        cb._stats.half_open_calls = 1
        assert cb.allow_request() is False

    def test_rejected_tracking(self):
        cb = CircuitBreaker(CircuitConfig(failure_threshold=1))
        cb.record_failure()
        assert cb.state == CircuitState.OPEN
        cb.record_rejected()
        cb.record_rejected()
        assert cb.stats.total_rejected == 2

    def test_reset(self):
        cb = CircuitBreaker(CircuitConfig(failure_threshold=2))
        cb.record_failure()
        cb.record_failure()
        assert cb.state == CircuitState.OPEN
        cb.reset()
        assert cb.state == CircuitState.CLOSED
        assert cb.stats.total_calls == 0


# ── BackoffConfig ────────────────────────────────────────


class TestBackoffConfig:
    def test_defaults(self):
        bc = BackoffConfig()
        assert bc.base_delay_s == 1.0
        assert bc.max_retries == 3

    def test_to_dict(self):
        bc = BackoffConfig(max_retries=5)
        d = bc.to_dict()
        assert d["max_retries"] == 5


# ── compute_backoff ──────────────────────────────────────


class TestComputeBackoff:
    def test_zero_attempt(self):
        assert compute_backoff(0) == 0.0

    def test_first_attempt(self):
        bc = BackoffConfig(base_delay_s=1.0, jitter_factor=0.0)
        delay = compute_backoff(1, bc)
        assert delay == 1.0

    def test_exponential_growth(self):
        bc = BackoffConfig(
            base_delay_s=1.0, multiplier=2.0,
            max_delay_s=100.0, jitter_factor=0.0,
        )
        assert compute_backoff(1, bc) == 1.0
        assert compute_backoff(2, bc) == 2.0
        assert compute_backoff(3, bc) == 4.0
        assert compute_backoff(4, bc) == 8.0

    def test_max_delay_cap(self):
        bc = BackoffConfig(
            base_delay_s=1.0, multiplier=10.0,
            max_delay_s=5.0, jitter_factor=0.0,
        )
        assert compute_backoff(5, bc) == 5.0

    def test_jitter_adds_randomness(self):
        bc = BackoffConfig(
            base_delay_s=1.0, jitter_factor=0.5,
        )
        delays = {compute_backoff(1, bc) for _ in range(20)}
        # With jitter, we should get varying delays
        assert len(delays) > 1


# ── ResilientProvider ────────────────────────────────────


class TestResilientProvider:
    def test_success_passthrough(self):
        fp = FakeProvider(fail=False)
        rp = ResilientProvider(
            fp,
            backoff_config=BackoffConfig(max_retries=0),
        )
        resp = rp.generate("hello")
        assert resp.text == "response to: hello"
        assert rp.circuit.state == CircuitState.CLOSED

    def test_failure_records_in_circuit(self):
        fp = FakeProvider(fail=True)
        rp = ResilientProvider(
            fp,
            circuit_config=CircuitConfig(failure_threshold=3),
            backoff_config=BackoffConfig(max_retries=0),
        )
        for _ in range(3):
            rp.generate("test")
        assert rp.circuit.state == CircuitState.OPEN

    def test_circuit_open_rejects(self):
        fp = FakeProvider(fail=True)
        rp = ResilientProvider(
            fp,
            circuit_config=CircuitConfig(failure_threshold=2),
            backoff_config=BackoffConfig(max_retries=0),
        )
        rp.generate("a")
        rp.generate("b")
        assert rp.circuit.state == CircuitState.OPEN
        resp = rp.generate("c")
        assert resp.metadata.get("error") == "circuit open"
        assert fp.call_count == 2  # no call made for "c"

    def test_retries_on_failure(self):
        fp = FakeProvider(fail=True)
        rp = ResilientProvider(
            fp,
            circuit_config=CircuitConfig(failure_threshold=10),
            backoff_config=BackoffConfig(
                max_retries=2, base_delay_s=0.001,
                jitter_factor=0.0,
            ),
        )
        resp = rp.generate("test")
        assert resp.metadata.get("retries_exhausted") is True
        assert fp.call_count == 3  # initial + 2 retries

    def test_name_and_models(self):
        fp = FakeProvider(name="test-provider")
        rp = ResilientProvider(fp)
        assert rp.name == "test-provider"
        assert rp.models == ["test-model"]
        assert rp.inner is fp

    def test_health_check_includes_circuit(self):
        fp = FakeProvider()
        rp = ResilientProvider(fp)
        health = rp.health_check()
        assert "circuit_state" in health
        assert health["circuit_state"] == "closed"


# ── ResilientRouter ──────────────────────────────────────


class TestResilientRouter:
    def test_routes_to_first_available(self):
        p1 = FakeProvider(name="p1", fail=False)
        p2 = FakeProvider(name="p2", fail=False)
        router = ResilientRouter(
            [p1, p2],
            backoff_config=BackoffConfig(max_retries=0),
        )
        resp = router.route("hello")
        assert resp.provider == "p1"
        assert p1.call_count == 1
        assert p2.call_count == 0

    def test_fallback_on_failure(self):
        p1 = FakeProvider(name="p1", fail=True)
        p2 = FakeProvider(name="p2", fail=False)
        router = ResilientRouter(
            [p1, p2],
            circuit_config=CircuitConfig(failure_threshold=10),
            backoff_config=BackoffConfig(max_retries=0),
        )
        resp = router.route("hello")
        assert resp.provider == "p2"

    def test_skips_open_circuit(self):
        p1 = FakeProvider(name="p1", fail=True)
        p2 = FakeProvider(name="p2", fail=False)
        router = ResilientRouter(
            [p1, p2],
            circuit_config=CircuitConfig(failure_threshold=2),
            backoff_config=BackoffConfig(max_retries=0),
        )
        # Trip p1's circuit
        router.route("a")
        router.route("b")
        # Now p1 is open, should go straight to p2
        p1.call_count = 0
        resp = router.route("c")
        assert resp.provider == "p2"
        assert p1.call_count == 0  # p1 was skipped

    def test_all_providers_fail(self):
        p1 = FakeProvider(name="p1", fail=True)
        p2 = FakeProvider(name="p2", fail=True)
        router = ResilientRouter(
            [p1, p2],
            circuit_config=CircuitConfig(failure_threshold=10),
            backoff_config=BackoffConfig(max_retries=0),
        )
        resp = router.route("test")
        assert resp.metadata.get("error") == "all providers failed or circuit open"

    def test_stats(self):
        p1 = FakeProvider(name="p1")
        p2 = FakeProvider(name="p2")
        router = ResilientRouter(
            [p1, p2],
            backoff_config=BackoffConfig(max_retries=0),
        )
        router.route("test")
        stats = router.stats()
        assert "p1" in stats
        assert "p2" in stats
        assert stats["p1"]["total_successes"] == 1

    def test_reset_all(self):
        p1 = FakeProvider(name="p1", fail=True)
        router = ResilientRouter(
            [p1],
            circuit_config=CircuitConfig(failure_threshold=1),
            backoff_config=BackoffConfig(max_retries=0),
        )
        router.route("test")
        assert router.providers[0].circuit.state == CircuitState.OPEN
        router.reset_all()
        assert router.providers[0].circuit.state == CircuitState.CLOSED


# ── CircuitState enum ────────────────────────────────────


class TestCircuitState:
    def test_values(self):
        assert CircuitState.CLOSED.value == "closed"
        assert CircuitState.OPEN.value == "open"
        assert CircuitState.HALF_OPEN.value == "half_open"
