"""Тесты `OpenAICompatClient` на мок-транспорте `httpx.MockTransport` (без сети).

Покрывает: успешный запрос, retry после 5xx и после сетевой ошибки → успех,
исчерпание попыток → `LlmUnavailableError`, открытие circuit breaker после
серии неудач (следующий вызов падает без HTTP-запроса) и его закрытие после
`breaker_cooldown_s`, а также явное закрытие базового `httpx.Client`
(`close()` и поддержка контекстного менеджера).
"""

import time
from collections.abc import Callable

import httpx
import pytest

from core.summarization import llm_client as llm_client_module
from core.summarization.llm_client import LlmUnavailableError, OpenAICompatClient


@pytest.fixture(autouse=True)
def _fast_backoff(monkeypatch: pytest.MonkeyPatch) -> None:
    """Свести паузы retry к нулю, чтобы тесты не ждали реальный backoff."""
    monkeypatch.setattr(llm_client_module, "_INITIAL_BACKOFF_S", 0.0)


def _make_client(
    handler: Callable[[httpx.Request], httpx.Response], **kwargs: object
) -> OpenAICompatClient:
    transport = httpx.MockTransport(handler)
    return OpenAICompatClient(
        base_url="http://llm.test/v1",
        model="qwen2.5-3b-instruct-q4_k_m",
        transport=transport,
        **kwargs,  # type: ignore[arg-type]
    )


def _ok_response(content: str = "готовое резюме") -> httpx.Response:
    return httpx.Response(
        200,
        json={"choices": [{"message": {"content": content}}]},
    )


def test_complete_returns_content_on_success() -> None:
    calls: list[httpx.Request] = []

    def handler(request: httpx.Request) -> httpx.Response:
        calls.append(request)
        assert request.url == "http://llm.test/v1/chat/completions"
        body = request.read()
        assert b'"role": "user"' in body or b'"role":"user"' in body
        return _ok_response("успех")

    client = _make_client(handler)

    result = client.complete("промпт")

    assert result == "успех"
    assert len(calls) == 1


def test_complete_uses_per_call_max_tokens_override() -> None:
    """`max_tokens` явного вызова `complete()` перекрывает дефолт конструктора клиента
    (ADR-004: разные лимиты map/reduce у `QwenLocal`, один и тот же клиент)."""
    import json

    captured: list[int] = []

    def handler(request: httpx.Request) -> httpx.Response:
        body = json.loads(request.read())
        captured.append(body["max_tokens"])
        return _ok_response("ок")

    client = _make_client(handler, max_tokens=1024)

    client.complete("промпт map", max_tokens=1024)
    client.complete("промпт reduce", max_tokens=2048)
    client.complete("промпт без переопределения")

    assert captured == [1024, 2048, 1024]


def test_complete_retries_after_5xx_then_succeeds() -> None:
    attempts = {"count": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        attempts["count"] += 1
        if attempts["count"] < 3:
            return httpx.Response(503)
        return _ok_response("успех после 503")

    client = _make_client(handler, max_attempts=3)

    result = client.complete("промпт")

    assert result == "успех после 503"
    assert attempts["count"] == 3


def test_complete_retries_after_network_error_then_succeeds() -> None:
    attempts = {"count": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        attempts["count"] += 1
        if attempts["count"] == 1:
            raise httpx.ConnectError("соединение разорвано", request=request)
        return _ok_response("успех после обрыва сети")

    client = _make_client(handler, max_attempts=3)

    result = client.complete("промпт")

    assert result == "успех после обрыва сети"
    assert attempts["count"] == 2


def test_complete_raises_llm_unavailable_after_exhausting_attempts() -> None:
    attempts = {"count": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        attempts["count"] += 1
        return httpx.Response(500)

    client = _make_client(handler, max_attempts=3, breaker_threshold=100)

    with pytest.raises(LlmUnavailableError):
        client.complete("промпт")

    assert attempts["count"] == 3


def test_breaker_opens_after_threshold_and_blocks_without_http_call() -> None:
    attempts = {"count": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        attempts["count"] += 1
        return httpx.Response(500)

    client = _make_client(handler, max_attempts=1, breaker_threshold=2, breaker_cooldown_s=60.0)

    with pytest.raises(LlmUnavailableError):
        client.complete("промпт 1")
    with pytest.raises(LlmUnavailableError):
        client.complete("промпт 2")

    assert attempts["count"] == 2  # обе неудачи дошли до сервера — порог достигнут

    with pytest.raises(LlmUnavailableError):
        client.complete("промпт 3")

    assert attempts["count"] == 2  # третий вызов не сделал HTTP-запрос — breaker открыт


def test_breaker_closes_after_cooldown_elapses(monkeypatch: pytest.MonkeyPatch) -> None:
    attempts = {"count": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        attempts["count"] += 1
        if attempts["count"] <= 2:
            return httpx.Response(500)
        return _ok_response("успех после закрытия breaker")

    client = _make_client(handler, max_attempts=1, breaker_threshold=2, breaker_cooldown_s=10.0)

    with pytest.raises(LlmUnavailableError):
        client.complete("промпт 1")
    with pytest.raises(LlmUnavailableError):
        client.complete("промпт 2")
    assert attempts["count"] == 2

    # Имитируем истечение окна отказа сдвигом монотонных часов вперёд (модуль
    # `time` — общий синглтон интерпретатора, поэтому патчим его напрямую:
    # изменение видно и внутри `core.summarization.llm_client`).
    real_monotonic = time.monotonic()
    monkeypatch.setattr(time, "monotonic", lambda: real_monotonic + 11.0)

    result = client.complete("промпт 3")

    assert result == "успех после закрытия breaker"


def test_close_closes_underlying_http_client() -> None:
    """`close()` закрывает базовый `httpx.Client` (освобождает пул соединений)."""

    def handler(request: httpx.Request) -> httpx.Response:
        return _ok_response()

    client = _make_client(handler)

    client.close()

    assert client._client.is_closed


def test_context_manager_closes_client_on_exit() -> None:
    """`OpenAICompatClient` работает как контекстный менеджер, закрывающий клиент на выходе."""

    def handler(request: httpx.Request) -> httpx.Response:
        return _ok_response("успех в контексте")

    with _make_client(handler) as client:
        assert client.complete("промпт") == "успех в контексте"
        assert not client._client.is_closed

    assert client._client.is_closed
