"""Chaos — Retry logic: intermittent failures, backoff timing, connection errors."""

from __future__ import annotations

from unittest.mock import patch

import httpx
import pytest
import respx

from kanoniv.client.client import KanonivAsyncClient, KanonivClient
from kanoniv.exceptions import (
    KanonivError,
    NotFoundError,
    RateLimitError,
    ServerError,
    ValidationError,
)

from .conftest import API_KEY, BASE_URL


# ---------------------------------------------------------------------------
# Retryable status codes
# ---------------------------------------------------------------------------

class TestRetryableStatusCodes:
    """Retryable codes (408/429/502/503/504) are retried; others are not."""

    @pytest.mark.parametrize("status_code", [408, 429, 502, 503, 504])
    @patch("kanoniv.client._transport.time.sleep")
    def test_retryable_then_success(
        self, mock_sleep, mock_api: respx.Router, status_code: int,
    ):
        retry_client = KanonivClient(api_key=API_KEY, base_url=BASE_URL, max_retries=2)
        try:
            mock_api.get("/v1/stats").mock(
                side_effect=[
                    httpx.Response(status_code, json={"error": "transient"}),
                    httpx.Response(200, json={"total": 42}),
                ],
            )
            result = retry_client.stats()
            assert result["total"] == 42
        finally:
            retry_client.close()

    @patch("kanoniv.client._transport.time.sleep")
    def test_400_not_retried(self, mock_sleep, mock_api: respx.Router):
        retry_client = KanonivClient(api_key=API_KEY, base_url=BASE_URL, max_retries=2)
        try:
            route = mock_api.get("/v1/stats").mock(
                return_value=httpx.Response(400, json={"error": "bad"}),
            )
            with pytest.raises(ValidationError):
                retry_client.stats()
            assert len(route.calls) == 1
        finally:
            retry_client.close()

    @patch("kanoniv.client._transport.time.sleep")
    def test_404_not_retried(self, mock_sleep, mock_api: respx.Router):
        retry_client = KanonivClient(api_key=API_KEY, base_url=BASE_URL, max_retries=2)
        try:
            route = mock_api.get("/v1/stats").mock(
                return_value=httpx.Response(404, json={"error": "nope"}),
            )
            with pytest.raises(NotFoundError):
                retry_client.stats()
            assert len(route.calls) == 1
        finally:
            retry_client.close()

    @patch("kanoniv.client._transport.time.sleep")
    def test_all_retries_exhausted(self, mock_sleep, mock_api: respx.Router):
        retry_client = KanonivClient(api_key=API_KEY, base_url=BASE_URL, max_retries=2)
        try:
            mock_api.get("/v1/stats").mock(
                return_value=httpx.Response(503, json={"error": "down"}),
            )
            with pytest.raises(ServerError):
                retry_client.stats()
        finally:
            retry_client.close()

    @pytest.mark.asyncio
    @patch("asyncio.sleep")
    async def test_async_429_retried(self, mock_sleep, mock_api: respx.Router):
        ac = KanonivAsyncClient(api_key=API_KEY, base_url=BASE_URL, max_retries=2)
        try:
            mock_api.get("/v1/stats").mock(
                side_effect=[
                    httpx.Response(429, json={"error": "rate limited"}),
                    httpx.Response(200, json={"total": 99}),
                ],
            )
            result = await ac.stats()
            assert result["total"] == 99
        finally:
            await ac.close()


# ---------------------------------------------------------------------------
# Backoff timing
# ---------------------------------------------------------------------------

class TestBackoffTiming:
    """Exponential backoff and Retry-After header."""

    @patch("kanoniv.client._transport.time.sleep")
    def test_exponential_backoff(self, mock_sleep, mock_api: respx.Router):
        retry_client = KanonivClient(api_key=API_KEY, base_url=BASE_URL, max_retries=2)
        try:
            mock_api.get("/v1/stats").mock(
                side_effect=[
                    httpx.Response(503, json={"error": "down"}),
                    httpx.Response(503, json={"error": "down"}),
                    httpx.Response(200, json={"total": 1}),
                ],
            )
            retry_client.stats()
            calls = mock_sleep.call_args_list
            assert calls[0][0][0] == pytest.approx(0.5)  # 0.5 * 2^0
            assert calls[1][0][0] == pytest.approx(1.0)  # 0.5 * 2^1
        finally:
            retry_client.close()

    @patch("kanoniv.client._transport.time.sleep")
    def test_retry_after_header_respected(self, mock_sleep, mock_api: respx.Router):
        retry_client = KanonivClient(api_key=API_KEY, base_url=BASE_URL, max_retries=2)
        try:
            mock_api.get("/v1/stats").mock(
                side_effect=[
                    httpx.Response(
                        429,
                        json={"error": "slow down"},
                        headers={"Retry-After": "3"},
                    ),
                    httpx.Response(200, json={"total": 1}),
                ],
            )
            retry_client.stats()
            assert mock_sleep.call_args_list[0][0][0] == pytest.approx(3.0)
        finally:
            retry_client.close()

    @patch("kanoniv.client._transport.time.sleep")
    def test_invalid_retry_after_falls_back(self, mock_sleep, mock_api: respx.Router):
        retry_client = KanonivClient(api_key=API_KEY, base_url=BASE_URL, max_retries=2)
        try:
            mock_api.get("/v1/stats").mock(
                side_effect=[
                    httpx.Response(
                        429,
                        json={"error": "slow down"},
                        headers={"Retry-After": "not-a-number"},
                    ),
                    httpx.Response(200, json={"total": 1}),
                ],
            )
            retry_client.stats()
            assert mock_sleep.call_args_list[0][0][0] == pytest.approx(0.5)
        finally:
            retry_client.close()


# ---------------------------------------------------------------------------
# Connection failures
# ---------------------------------------------------------------------------

class TestConnectionFailures:
    """ConnectError and ReadTimeout are retried then raise KanonivError."""

    @patch("kanoniv.client._transport.time.sleep")
    def test_connect_error_retried_then_raises(self, mock_sleep, mock_api: respx.Router):
        retry_client = KanonivClient(api_key=API_KEY, base_url=BASE_URL, max_retries=2)
        try:
            mock_api.get("/v1/stats").mock(
                side_effect=httpx.ConnectError("connection refused"),
            )
            with pytest.raises(KanonivError, match="Connection failed"):
                retry_client.stats()
            assert mock_sleep.call_count == 2
        finally:
            retry_client.close()

    @patch("kanoniv.client._transport.time.sleep")
    def test_read_timeout_retried_then_raises(self, mock_sleep, mock_api: respx.Router):
        retry_client = KanonivClient(api_key=API_KEY, base_url=BASE_URL, max_retries=2)
        try:
            mock_api.get("/v1/stats").mock(
                side_effect=httpx.ReadTimeout("read timed out"),
            )
            with pytest.raises(KanonivError, match="Connection failed"):
                retry_client.stats()
            assert mock_sleep.call_count == 2
        finally:
            retry_client.close()
