"""Tests for LLM rate limit error detection and retry-after extraction.

Tests cover:
- is_llm_rate_limit_error: HTTP 429 detection, message pattern matching,
  exception type detection, provider-specific error handling
- extract_retry_after: header parsing, regex extraction from error messages
"""

import types

import pytest

from local_deep_research.web_search_engines.rate_limiting.llm.detection import (
    extract_retry_after,
    is_llm_rate_limit_error,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _exc_with_response(status_code, headers=None):
    """Build an Exception whose .response has a status_code and headers."""
    exc = Exception("http error")
    resp = types.SimpleNamespace(status_code=status_code)
    if headers is not None:
        resp.headers = headers
    exc.response = resp
    return exc


def _exc_with_type(type_name, msg="error", module="builtins"):
    """Build an exception whose __class__.__name__ and __module__ are set."""
    cls = type(type_name, (Exception,), {})
    cls.__module__ = module
    return cls(msg)


# ===================================================================
# is_llm_rate_limit_error
# ===================================================================


class TestIsLLMRateLimitError:
    """Tests for is_llm_rate_limit_error()."""

    # -- HTTP 429 detection ---------------------------------------------

    def test_http_429_returns_true(self):
        exc = _exc_with_response(429)
        assert is_llm_rate_limit_error(exc) is True

    def test_http_200_returns_false(self):
        exc = _exc_with_response(200)
        assert is_llm_rate_limit_error(exc) is False

    def test_http_500_returns_false(self):
        exc = _exc_with_response(500)
        assert is_llm_rate_limit_error(exc) is False

    # -- message-based detection ----------------------------------------

    @pytest.mark.parametrize(
        "msg",
        [
            "Rate limit exceeded",
            "rate_limit_error: too many tokens",
            "RateLimit hit for model",
            "Too many requests, please slow down",
            "Quota exceeded for this month",
            "quota has been exhausted",
            "Resource has been exhausted",
            "Error 429: too many requests",
            "Request threshold reached",
            "Please try again later",
            "Slow down, you're sending too fast",
        ],
    )
    def test_known_rate_limit_messages(self, msg):
        assert is_llm_rate_limit_error(Exception(msg)) is True

    def test_generic_error_returns_false(self):
        assert is_llm_rate_limit_error(Exception("connection refused")) is False

    def test_empty_message_returns_false(self):
        assert is_llm_rate_limit_error(Exception("")) is False

    # -- exception type detection ---------------------------------------

    def test_ratelimiterror_type_detected(self):
        exc = _exc_with_type("RateLimitError", msg="nope")
        assert is_llm_rate_limit_error(exc) is True

    def test_quotaexceedederror_type_detected(self):
        exc = _exc_with_type("QuotaExceededError", msg="nope")
        assert is_llm_rate_limit_error(exc) is True

    def test_valueerror_type_not_detected(self):
        assert is_llm_rate_limit_error(ValueError("bad value")) is False

    # -- OpenAI-specific detection --------------------------------------

    def test_openai_ratelimiterror_with_429(self):
        exc = _exc_with_type(
            "RateLimitError", msg="Error code: 429", module="openai"
        )
        assert is_llm_rate_limit_error(exc) is True

    def test_openai_apierror_with_429(self):
        exc = _exc_with_type("APIError", msg="status 429", module="openai")
        assert is_llm_rate_limit_error(exc) is True

    def test_openai_apierror_without_429(self):
        exc = _exc_with_type("APIError", msg="invalid api key", module="openai")
        assert is_llm_rate_limit_error(exc) is False

    # -- Anthropic-specific detection -----------------------------------

    def test_anthropic_rate_limit(self):
        exc = _exc_with_type(
            "RateLimitError", msg="rate_limit reached", module="anthropic"
        )
        assert is_llm_rate_limit_error(exc) is True

    def test_anthropic_429(self):
        exc = _exc_with_type("APIError", msg="429 too many", module="anthropic")
        assert is_llm_rate_limit_error(exc) is True

    def test_anthropic_too_many(self):
        exc = _exc_with_type(
            "APIError", msg="too many requests", module="anthropic._errors"
        )
        assert is_llm_rate_limit_error(exc) is True

    def test_anthropic_unrelated_error(self):
        exc = _exc_with_type(
            "AuthenticationError", msg="invalid key", module="anthropic"
        )
        assert is_llm_rate_limit_error(exc) is False

    # -- no response attribute -----------------------------------------

    def test_plain_exception_no_response(self):
        """Exception without .response attribute doesn't crash."""
        assert is_llm_rate_limit_error(RuntimeError("oops")) is False


# ===================================================================
# extract_retry_after
# ===================================================================


class TestExtractRetryAfter:
    """Tests for extract_retry_after()."""

    # -- Retry-After header --------------------------------------------

    def test_retry_after_header_integer(self):
        exc = _exc_with_response(429, headers={"Retry-After": "30"})
        assert extract_retry_after(exc) == 30.0

    def test_retry_after_header_float(self):
        exc = _exc_with_response(429, headers={"Retry-After": "2.5"})
        assert extract_retry_after(exc) == 2.5

    def test_retry_after_header_non_numeric(self):
        exc = _exc_with_response(
            429, headers={"Retry-After": "Wed, 21 Oct 2025"}
        )
        assert extract_retry_after(exc) == 0

    def test_retry_after_header_missing(self):
        exc = _exc_with_response(429, headers={})
        assert extract_retry_after(exc) == 0

    # -- message pattern extraction ------------------------------------

    def test_try_again_in_seconds(self):
        exc = Exception("Please try again in 15 seconds")
        assert extract_retry_after(exc) == 15.0

    def test_retry_after_seconds(self):
        exc = Exception("Retry after 3.5 seconds before next request")
        assert extract_retry_after(exc) == 3.5

    def test_wait_seconds(self):
        exc = Exception("Please wait 60 seconds")
        assert extract_retry_after(exc) == 60.0

    def test_singular_second(self):
        exc = Exception("try again in 1 second")
        assert extract_retry_after(exc) == 1.0

    def test_no_time_in_message(self):
        exc = Exception("rate limit exceeded")
        assert extract_retry_after(exc) == 0

    # -- no response attribute -----------------------------------------

    def test_plain_exception_returns_zero(self):
        assert extract_retry_after(ValueError("bad")) == 0
