"""
High-value tests for rate-limited LLM wrapper.

Complements test_llm_wrapper_pure_logic.py by covering:
- AdaptiveLLMWait.__call__: retry-after extraction, tracker wait time, max selection
- RateLimitedLLMWrapper.invoke: direct passthrough without rate limiting
- __getattr__ passthrough to base LLM
- __str__ and __repr__ formatting
- _do_invoke: exception wrapping for rate limit errors
"""

import types
from unittest.mock import MagicMock, Mock, patch

import pytest

from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import (
    AdaptiveLLMWait,
    create_rate_limited_llm_wrapper,
)


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


def _make_llm(**attrs):
    return types.SimpleNamespace(**attrs)


def _wrapper(llm=None, provider=None):
    if llm is None:
        llm = _make_llm()
    return create_rate_limited_llm_wrapper(llm, provider=provider)


# ---------------------------------------------------------------------------
# AdaptiveLLMWait.__call__
# ---------------------------------------------------------------------------


class TestAdaptiveLLMWait:
    """Tests for the adaptive wait strategy."""

    def test_uses_tracker_wait_time(self):
        tracker = MagicMock()
        tracker.get_wait_time.return_value = 5.0
        wait = AdaptiveLLMWait(tracker, "test-engine")

        retry_state = Mock()
        retry_state.outcome = None

        result = wait(retry_state)
        assert result == 5.0
        tracker.get_wait_time.assert_called_once_with("test-engine")

    def test_uses_retry_after_when_larger(self):
        tracker = MagicMock()
        tracker.get_wait_time.return_value = 2.0
        wait = AdaptiveLLMWait(tracker, "test-engine")

        # Simulate a failed outcome with retry-after
        error = Exception("rate limited")
        outcome = Mock()
        outcome.failed = True
        outcome.exception.return_value = error

        retry_state = Mock()
        retry_state.outcome = outcome

        with patch(
            "local_deep_research.web_search_engines.rate_limiting.llm.wrapper.extract_retry_after",
            return_value=10.0,
        ):
            result = wait(retry_state)

        assert result == 10.0  # max(2.0, 10.0)

    def test_uses_tracker_when_retry_after_is_zero(self):
        tracker = MagicMock()
        tracker.get_wait_time.return_value = 3.0
        wait = AdaptiveLLMWait(tracker, "test-engine")

        error = Exception("rate limited")
        outcome = Mock()
        outcome.failed = True
        outcome.exception.return_value = error

        retry_state = Mock()
        retry_state.outcome = outcome

        with patch(
            "local_deep_research.web_search_engines.rate_limiting.llm.wrapper.extract_retry_after",
            return_value=0,
        ):
            result = wait(retry_state)

        assert result == 3.0

    def test_stores_last_error(self):
        tracker = MagicMock()
        tracker.get_wait_time.return_value = 1.0
        wait = AdaptiveLLMWait(tracker, "test-engine")

        error = ValueError("specific error")
        outcome = Mock()
        outcome.failed = True
        outcome.exception.return_value = error

        retry_state = Mock()
        retry_state.outcome = outcome

        with patch(
            "local_deep_research.web_search_engines.rate_limiting.llm.wrapper.extract_retry_after",
            return_value=0,
        ):
            wait(retry_state)

        assert wait.last_error is error

    def test_no_error_when_outcome_not_failed(self):
        tracker = MagicMock()
        tracker.get_wait_time.return_value = 1.0
        wait = AdaptiveLLMWait(tracker, "test-engine")
        wait.last_error = None

        retry_state = Mock()
        retry_state.outcome = Mock()
        retry_state.outcome.failed = False

        result = wait(retry_state)
        assert result == 1.0
        assert wait.last_error is None

    def test_initial_state(self):
        tracker = MagicMock()
        wait = AdaptiveLLMWait(tracker, "my-engine")
        assert wait.tracker is tracker
        assert wait.engine_type == "my-engine"
        assert wait.last_error is None


# ---------------------------------------------------------------------------
# RateLimitedLLMWrapper.invoke (without rate limiting)
# ---------------------------------------------------------------------------


class TestInvokeWithoutRateLimiting:
    """Tests for invoke when rate limiting is disabled (default)."""

    def test_invoke_passes_through_to_base_llm(self):
        llm = MagicMock()
        llm.invoke.return_value = "response"
        wrapper = _wrapper(llm=llm)

        result = wrapper.invoke("hello")
        assert result == "response"
        llm.invoke.assert_called_once_with("hello")

    def test_invoke_passes_kwargs_through(self):
        llm = MagicMock()
        llm.invoke.return_value = "resp"
        wrapper = _wrapper(llm=llm)

        wrapper.invoke("prompt", temperature=0.5, max_tokens=100)
        llm.invoke.assert_called_once_with(
            "prompt", temperature=0.5, max_tokens=100
        )

    def test_invoke_propagates_non_rate_limit_error(self):
        llm = MagicMock()
        llm.invoke.side_effect = ValueError("bad input")
        wrapper = _wrapper(llm=llm)

        with pytest.raises(ValueError, match="bad input"):
            wrapper.invoke("prompt")

    def test_invoke_wraps_rate_limit_error(self):
        """Rate limit errors are wrapped in RateLimitError."""
        from local_deep_research.web_search_engines.rate_limiting.exceptions import (
            RateLimitError,
        )

        llm = MagicMock()
        error = Exception("429 Too Many Requests")
        llm.invoke.side_effect = error
        wrapper = _wrapper(llm=llm)

        with patch(
            "local_deep_research.web_search_engines.rate_limiting.llm.wrapper.is_llm_rate_limit_error",
            return_value=True,
        ):
            with pytest.raises(RateLimitError):
                wrapper.invoke("prompt")

    def test_rate_limiter_is_none_by_default(self):
        wrapper = _wrapper()
        assert wrapper.rate_limiter is None


# ---------------------------------------------------------------------------
# __getattr__ passthrough
# ---------------------------------------------------------------------------


class TestGetAttrPassthrough:
    """Tests for attribute delegation to base LLM."""

    def test_passes_unknown_attr_to_base(self):
        llm = _make_llm(model_name="gpt-4", temperature=0.7)
        wrapper = _wrapper(llm=llm)
        assert wrapper.model_name == "gpt-4"
        assert wrapper.temperature == 0.7

    def test_raises_attribute_error_for_missing(self):
        llm = _make_llm()
        wrapper = _wrapper(llm=llm)
        with pytest.raises(AttributeError):
            _ = wrapper.nonexistent_attribute

    def test_base_llm_accessible(self):
        llm = _make_llm(name="test-llm")
        wrapper = _wrapper(llm=llm)
        assert wrapper.base_llm is llm

    def test_provider_accessible(self):
        wrapper = _wrapper(provider="anthropic")
        assert wrapper.provider == "anthropic"


# ---------------------------------------------------------------------------
# __str__ and __repr__
# ---------------------------------------------------------------------------


class TestStringRepresentation:
    """Tests for __str__ and __repr__."""

    def test_str_format(self):
        llm = _make_llm()
        wrapper = _wrapper(llm=llm)
        result = str(wrapper)
        assert result.startswith("RateLimited(")
        assert result.endswith(")")

    def test_repr_format(self):
        llm = _make_llm()
        wrapper = _wrapper(llm=llm)
        result = repr(wrapper)
        assert result.startswith("RateLimitedLLMWrapper(")
        assert result.endswith(")")

    def test_str_includes_base_llm_str(self):
        llm = types.SimpleNamespace()
        llm.__str__ = lambda self: "MyLLM"
        # SimpleNamespace doesn't support __str__ override this way,
        # so use a Mock
        llm_mock = Mock()
        llm_mock.__str__ = Mock(return_value="MockLLM")
        wrapper = create_rate_limited_llm_wrapper(llm_mock)
        assert "MockLLM" in str(wrapper)


# ---------------------------------------------------------------------------
# _should_rate_limit
# ---------------------------------------------------------------------------


class TestShouldRateLimit:
    """Tests for the rate limiting check."""

    def test_rate_limiting_disabled_by_default(self):
        wrapper = _wrapper()
        assert wrapper._should_rate_limit() is False

    def test_rate_limiter_not_set_when_disabled(self):
        wrapper = _wrapper()
        assert wrapper.rate_limiter is None
