"""PR-MacBook-RAG-Backend-1: backend dispatcher 단위 테스트.

- get_backend(None) / get_backend("gemma-macmini") → GemmaMacMiniBackend
- get_backend("qwen-macbook") → QwenMacBookBackend (config 값 반영)
- get_backend("unknown") → ValueError
- QwenMacBookBackend.generate() — mock httpx 200 OK → content 반환
- QwenMacBookBackend.generate() — dead port → BackendUnavailable("ConnectError")

목적: 정정 4 (자동 fallback 부재) 의 핵심 빌딩블럭 검증. dispatcher 자체 무결성.
"""

from __future__ import annotations

import asyncio
import os
import sys
from unittest.mock import AsyncMock, patch

import httpx
import pytest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "app"))


@pytest.fixture(autouse=True)
def _reset_dispatcher():
    """각 테스트 격리 — backend 인스턴스 캐시 초기화."""
    from services.llm import reset_backends_for_test

    reset_backends_for_test()
    yield
    reset_backends_for_test()


def test_get_backend_default_is_gemma():
    """backend 미지정 (None) = Gemma Mac mini default."""
    from services.llm import get_backend

    b = get_backend(None)
    assert b.name == "gemma-macmini"


def test_get_backend_explicit_gemma():
    """gemma-macmini 명시도 동일."""
    from services.llm import get_backend

    b = get_backend("gemma-macmini")
    assert b.name == "gemma-macmini"


def test_get_backend_qwen_macbook_uses_config():
    """qwen-macbook 은 settings.search.ask.backend 값 그대로 반영."""
    from core.config import settings
    from services.llm import QwenMacBookBackend, get_backend

    b = get_backend("qwen-macbook")
    assert isinstance(b, QwenMacBookBackend)
    assert b.name == "qwen-macbook"
    # config.yaml 의 search.ask.backend.macbook_url 그대로
    assert b.base_url == settings.search.ask.backend.macbook_url.rstrip("/")
    assert b.model == settings.search.ask.backend.macbook_model
    assert b.timeout_connect_s == settings.search.ask.backend.timeout_connect_s


def test_get_backend_unknown_raises_value_error():
    """미지원 backend 이름 → ValueError (호출자가 400 으로 매핑)."""
    from services.llm import get_backend

    with pytest.raises(ValueError, match="unknown backend"):
        get_backend("claude-opus")


def test_get_backend_cached_returns_same_instance():
    """동일 backend 재호출 시 인스턴스 캐시."""
    from services.llm import get_backend

    b1 = get_backend("qwen-macbook")
    b2 = get_backend("qwen-macbook")
    assert b1 is b2


def test_qwen_generate_success_mocked():
    """mock 200 OK → choices[0].message.content 반환."""
    from services.llm import QwenMacBookBackend

    fake_payload = {
        "choices": [{"message": {"content": "hello from qwen"}}],
    }

    class _Resp:
        status_code = 200

        def raise_for_status(self):
            return None

        def json(self):
            return fake_payload

    async def _fake_post(self, url, json=None):
        return _Resp()

    backend = QwenMacBookBackend(
        base_url="http://test:8810",
        model="test-model",
        timeout_connect_s=1,
    )

    with patch.object(httpx.AsyncClient, "post", new=_fake_post):
        result = asyncio.run(backend.generate("hi", timeout_read_s=2))

    assert result == "hello from qwen"


def test_qwen_generate_dead_port_raises_backend_unavailable():
    """실제 dead port (127.0.0.1:1) → BackendUnavailable.

    정정 4 의 핵심: 명시 Qwen 호출이 실패하면 예외가 통과돼야 한다.
    synthesis_service 가 이 예외를 잡아 status="backend_unavailable" 로 매핑.
    """
    from services.llm import BackendUnavailable, QwenMacBookBackend

    backend = QwenMacBookBackend(
        base_url="http://127.0.0.1:1",
        model="test-model",
        timeout_connect_s=1,
    )

    with pytest.raises(BackendUnavailable) as exc_info:
        asyncio.run(backend.generate("hi", timeout_read_s=2))

    assert exc_info.value.backend_name == "qwen-macbook"
    assert "ConnectError" in exc_info.value.reason or "Timeout" in exc_info.value.reason


def test_qwen_generate_http_5xx_raises_backend_unavailable():
    """5xx 응답도 BackendUnavailable 로 매핑."""
    from services.llm import BackendUnavailable, QwenMacBookBackend

    class _Resp:
        status_code = 503

        def raise_for_status(self):
            raise httpx.HTTPStatusError(
                "service unavailable",
                request=httpx.Request("POST", "http://test:8810/v1/chat/completions"),
                response=httpx.Response(503),
            )

        def json(self):
            return {}

    async def _fake_post(self, url, json=None):
        return _Resp()

    backend = QwenMacBookBackend(
        base_url="http://test:8810",
        model="test-model",
        timeout_connect_s=1,
    )

    with patch.object(httpx.AsyncClient, "post", new=_fake_post):
        with pytest.raises(BackendUnavailable) as exc_info:
            asyncio.run(backend.generate("hi", timeout_read_s=2))

    assert exc_info.value.backend_name == "qwen-macbook"
    assert "503" in exc_info.value.reason


def test_qwen_generate_http_4xx_not_backend_unavailable():
    """4xx (호출자 잘못) 은 BackendUnavailable 아님 — 일반 예외 전파."""
    from services.llm import BackendUnavailable, QwenMacBookBackend

    class _Resp:
        status_code = 400

        def raise_for_status(self):
            raise httpx.HTTPStatusError(
                "bad request",
                request=httpx.Request("POST", "http://test:8810/v1/chat/completions"),
                response=httpx.Response(400),
            )

        def json(self):
            return {}

    async def _fake_post(self, url, json=None):
        return _Resp()

    backend = QwenMacBookBackend(
        base_url="http://test:8810",
        model="test-model",
        timeout_connect_s=1,
    )

    with patch.object(httpx.AsyncClient, "post", new=_fake_post):
        with pytest.raises(httpx.HTTPStatusError):
            asyncio.run(backend.generate("hi", timeout_read_s=2))
