"""Tests for the circuit breaker pattern."""
from __future__ import annotations

import asyncio

import pytest

from src.circuit_breaker import CircuitBreaker, CircuitState
from src.errors import CircuitBreakerError


@pytest.fixture
def breaker():
    return CircuitBreaker(failure_threshold=3, recovery_timeout=0.2)


class TestCircuitBreakerClosed:
    @pytest.mark.asyncio
    async def test_passes_through_on_success(self, breaker):
        result = await breaker.call(self._ok)
        assert result == "ok"
        assert breaker.state == CircuitState.CLOSED

    @pytest.mark.asyncio
    async def test_stays_closed_below_threshold(self, breaker):
        for _ in range(2):
            with pytest.raises(RuntimeError):
                await breaker.call(self._fail)
        assert breaker.state == CircuitState.CLOSED

    @staticmethod
    async def _ok():
        return "ok"

    @staticmethod
    async def _fail():
        raise RuntimeError("boom")


class TestCircuitBreakerOpen:
    @pytest.mark.asyncio
    async def test_trips_open_after_threshold(self, breaker):
        for _ in range(3):
            with pytest.raises(RuntimeError):
                await breaker.call(self._fail)
        assert breaker.state == CircuitState.OPEN

    @pytest.mark.asyncio
    async def test_rejects_immediately_when_open(self, breaker):
        for _ in range(3):
            with pytest.raises(RuntimeError):
                await breaker.call(self._fail)

        with pytest.raises(CircuitBreakerError) as exc_info:
            await breaker.call(self._ok)
        assert "open" in exc_info.value.message.lower()

    @pytest.mark.asyncio
    async def test_uses_fallback_when_open(self, breaker):
        for _ in range(3):
            with pytest.raises(RuntimeError):
                await breaker.call(self._fail)

        result = await breaker.call(self._ok, fallback=self._fallback)
        assert result == "fallback"

    @staticmethod
    async def _ok():
        return "ok"

    @staticmethod
    async def _fail():
        raise RuntimeError("boom")

    @staticmethod
    async def _fallback():
        return "fallback"


class TestCircuitBreakerHalfOpen:
    @pytest.mark.asyncio
    async def test_transitions_to_half_open_after_timeout(self, breaker):
        for _ in range(3):
            with pytest.raises(RuntimeError):
                await breaker.call(self._fail)
        assert breaker.state == CircuitState.OPEN

        await asyncio.sleep(0.25)
        assert breaker.state == CircuitState.HALF_OPEN

    @pytest.mark.asyncio
    async def test_recovers_on_successful_probe(self, breaker):
        for _ in range(3):
            with pytest.raises(RuntimeError):
                await breaker.call(self._fail)

        await asyncio.sleep(0.25)
        result = await breaker.call(self._ok)
        assert result == "ok"
        assert breaker.state == CircuitState.CLOSED

    @pytest.mark.asyncio
    async def test_reopens_on_failed_probe(self, breaker):
        for _ in range(3):
            with pytest.raises(RuntimeError):
                await breaker.call(self._fail)

        await asyncio.sleep(0.25)
        with pytest.raises(RuntimeError):
            await breaker.call(self._fail)
        assert breaker.state == CircuitState.OPEN

    @staticmethod
    async def _ok():
        return "ok"

    @staticmethod
    async def _fail():
        raise RuntimeError("boom")


class TestCircuitBreakerReset:
    @pytest.mark.asyncio
    async def test_manual_reset(self, breaker):
        for _ in range(3):
            with pytest.raises(RuntimeError):
                await breaker.call(self._fail)
        assert breaker.state == CircuitState.OPEN

        await breaker.reset()
        assert breaker.state == CircuitState.CLOSED

        result = await breaker.call(self._ok)
        assert result == "ok"

    @staticmethod
    async def _ok():
        return "ok"

    @staticmethod
    async def _fail():
        raise RuntimeError("boom")


class TestBatchResult:
    def test_from_response_parses_mixed_results(self):
        from src.meta_client import BatchResult

        raw = [
            {"code": 200, "body": '{"id": "1"}'},
            {"code": 400, "body": '{"error": {"message": "Bad request"}}'},
            {"code": 200, "body": '{"id": "2"}'},
        ]
        ops = [
            {"method": "GET", "relative_url": "obj_1"},
            {"method": "GET", "relative_url": "obj_2"},
            {"method": "GET", "relative_url": "obj_3"},
        ]
        result = BatchResult.from_response(raw, ops)

        assert len(result.succeeded) == 2
        assert len(result.failed) == 1
        assert result.has_failures
        assert result.failed[0]["index"] == 1
        assert result.failed[0]["error"]["message"] == "Bad request"

    def test_from_response_all_success(self):
        from src.meta_client import BatchResult

        raw = [
            {"code": 200, "body": '{"id": "1"}'},
            {"code": 200, "body": '{"id": "2"}'},
        ]
        result = BatchResult.from_response(raw)
        assert not result.has_failures
        assert len(result.succeeded) == 2

    def test_from_response_handles_non_list(self):
        from src.meta_client import BatchResult

        result = BatchResult.from_response({"error": "server error"})
        assert len(result.all) == 0
        assert not result.has_failures
