"""Tests for the resilient adapter wrapper.

Validates retry logic, backoff computation, rate-limit coordination,
and retryable error classification.
"""
from datetime import datetime, timezone
from typing import Any

import pytest

from services.adapters.base import AdapterResult, BaseAdapter
from services.adapters.resilient import (
    ResilientAdapter,
    RetryConfig,
    compute_delay,
)

# --- Helpers ---


def _make_result(
    ok: bool = True,
    error: str | None = None,
    http_status: int | None = None,
    metadata: dict[str, Any] | None = None,
) -> AdapterResult:
    return AdapterResult(
        source_type="market_api",
        ticker="AAPL",
        items=[{"price": 150}] if ok else [],
        raw_payload=b'{"ok":true}' if ok else b"",
        content_hash="abc" if ok else "",
        fetched_at=datetime.now(timezone.utc),
        error=error,
        http_status=http_status,
        metadata=metadata or {},
    )


class FakeAdapter(BaseAdapter):
    """Adapter that returns a sequence of pre-configured results."""

    def __init__(self, results: list[AdapterResult]) -> None:
        self._results = list(results)
        self._call_count = 0

    @property
    def call_count(self) -> int:
        return self._call_count

    async def fetch(self, ticker: str, config: dict[str, Any]) -> AdapterResult:
        idx = min(self._call_count, len(self._results) - 1)
        self._call_count += 1
        return self._results[idx]

    def source_type(self) -> str:
        return "market_api"


# --- Tests ---


class TestComputeDelay:
    def test_first_attempt_is_base_delay_plus_jitter(self):
        cfg = RetryConfig(base_delay=1.0, max_delay=60.0, jitter_factor=0.0)
        delay = compute_delay(0, cfg)
        assert delay == pytest.approx(1.0, abs=0.01)

    def test_exponential_growth(self):
        cfg = RetryConfig(base_delay=1.0, max_delay=60.0, jitter_factor=0.0)
        d0 = compute_delay(0, cfg)
        d1 = compute_delay(1, cfg)
        d2 = compute_delay(2, cfg)
        assert d1 == pytest.approx(2.0, abs=0.01)
        assert d2 == pytest.approx(4.0, abs=0.01)
        assert d2 > d1 > d0

    def test_capped_at_max_delay(self):
        cfg = RetryConfig(base_delay=1.0, max_delay=10.0, jitter_factor=0.0)
        delay = compute_delay(10, cfg)
        assert delay <= 10.0

    def test_jitter_adds_randomness(self):
        cfg = RetryConfig(base_delay=1.0, max_delay=60.0, jitter_factor=1.0)
        delays = {compute_delay(0, cfg) for _ in range(20)}
        # With jitter_factor=1.0, we should see some variation
        assert len(delays) > 1


class TestRetryableClassification:
    def setup_method(self) -> None:
        adapter = FakeAdapter([_make_result()])
        self.resilient = ResilientAdapter(adapter)

    def test_ok_result_not_retryable(self):
        result = _make_result(ok=True)
        assert self.resilient._is_retryable(result) is False

    def test_429_is_retryable(self):
        result = _make_result(ok=False, error="rate limited", http_status=429)
        assert self.resilient._is_retryable(result) is True

    def test_500_is_retryable(self):
        result = _make_result(ok=False, error="server error", http_status=500)
        assert self.resilient._is_retryable(result) is True

    def test_503_is_retryable(self):
        result = _make_result(ok=False, error="unavailable", http_status=503)
        assert self.resilient._is_retryable(result) is True

    def test_400_not_retryable(self):
        result = _make_result(ok=False, error="bad request", http_status=400)
        assert self.resilient._is_retryable(result) is False

    def test_401_not_retryable(self):
        result = _make_result(ok=False, error="unauthorized", http_status=401)
        assert self.resilient._is_retryable(result) is False

    def test_timeout_error_retryable(self):
        result = _make_result(ok=False, error="timeout: read timed out")
        assert self.resilient._is_retryable(result) is True

    def test_connection_error_retryable(self):
        result = _make_result(ok=False, error="Connection refused")
        assert self.resilient._is_retryable(result) is True

    def test_generic_error_not_retryable(self):
        result = _make_result(ok=False, error="invalid JSON response")
        assert self.resilient._is_retryable(result) is False



@pytest.mark.asyncio
class TestResilientFetch:
    async def test_success_on_first_try(self):
        adapter = FakeAdapter([_make_result(ok=True)])
        resilient = ResilientAdapter(
            adapter, retry_config=RetryConfig(max_retries=2, base_delay=0.01)
        )
        result = await resilient.fetch("AAPL", {})
        assert result.ok
        assert adapter.call_count == 1
        assert result.metadata["retry_stats"]["attempts"] == 1

    async def test_retries_on_retryable_then_succeeds(self):
        results = [
            _make_result(ok=False, error="server error", http_status=500),
            _make_result(ok=False, error="server error", http_status=500),
            _make_result(ok=True),
        ]
        adapter = FakeAdapter(results)
        resilient = ResilientAdapter(
            adapter, retry_config=RetryConfig(max_retries=3, base_delay=0.01)
        )
        result = await resilient.fetch("AAPL", {})
        assert result.ok
        assert adapter.call_count == 3
        assert result.metadata["retry_stats"]["attempts"] == 3

    async def test_exhausts_retries(self):
        fail = _make_result(ok=False, error="server error", http_status=500)
        adapter = FakeAdapter([fail, fail, fail, fail])
        resilient = ResilientAdapter(
            adapter, retry_config=RetryConfig(max_retries=2, base_delay=0.01)
        )
        result = await resilient.fetch("AAPL", {})
        assert not result.ok
        assert adapter.call_count == 3  # initial + 2 retries
        assert result.metadata["retry_stats"]["exhausted"] is True

    async def test_no_retry_on_non_retryable(self):
        fail = _make_result(ok=False, error="bad request", http_status=400)
        adapter = FakeAdapter([fail])
        resilient = ResilientAdapter(
            adapter, retry_config=RetryConfig(max_retries=3, base_delay=0.01)
        )
        result = await resilient.fetch("AAPL", {})
        assert not result.ok
        assert adapter.call_count == 1

    async def test_retry_after_respected_for_429(self):
        fail_429 = _make_result(
            ok=False, error="rate limited", http_status=429,
            metadata={"retry_after": 0.05},
        )
        results = [fail_429, _make_result(ok=True)]
        adapter = FakeAdapter(results)
        resilient = ResilientAdapter(
            adapter, retry_config=RetryConfig(max_retries=2, base_delay=0.01)
        )
        result = await resilient.fetch("AAPL", {})
        assert result.ok
        assert adapter.call_count == 2
        # Should have waited at least the retry_after amount
        assert result.metadata["retry_stats"]["total_delay"] >= 0.04

    async def test_source_type_passthrough(self):
        adapter = FakeAdapter([_make_result()])
        resilient = ResilientAdapter(adapter)
        assert resilient.source_type() == "market_api"

    async def test_default_config_for_known_source_type(self):
        adapter = FakeAdapter([_make_result()])
        resilient = ResilientAdapter(adapter)
        # market_api default is 30 rate limit max
        assert resilient.config.rate_limit_max == 30

    async def test_custom_config_overrides_default(self):
        adapter = FakeAdapter([_make_result()])
        custom = RetryConfig(max_retries=5, rate_limit_max=100)
        resilient = ResilientAdapter(adapter, retry_config=custom)
        assert resilient.config.max_retries == 5
        assert resilient.config.rate_limit_max == 100
