"""
Unit tests for LLM Adapter.
[AC-AISVC-02, AC-AISVC-06] Tests for LLM client interface.

Tests cover:
- Non-streaming generation
- Streaming generation
- Error handling
- Retry logic
"""

import json
from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest

from app.services.llm.base import LLMConfig, LLMResponse, LLMStreamChunk
from app.services.llm.openai_client import (
    LLMException,
    OpenAIClient,
    TimeoutException,
)


@pytest.fixture
def mock_settings():
    """Mock settings for testing."""
    settings = MagicMock()
    settings.llm_api_key = "test-api-key"
    settings.llm_base_url = "https://api.openai.com/v1"
    settings.llm_model = "gpt-4o-mini"
    settings.llm_max_tokens = 2048
    settings.llm_temperature = 0.7
    settings.llm_timeout_seconds = 30
    settings.llm_max_retries = 3
    return settings


@pytest.fixture
def llm_client(mock_settings):
    """Create LLM client with mocked settings."""
    with patch("app.services.llm.openai_client.get_settings", return_value=mock_settings):
        client = OpenAIClient()
        yield client


@pytest.fixture
def mock_messages():
    """Sample chat messages for testing."""
    return [
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Hello, how are you?"},
    ]


@pytest.fixture
def mock_generate_response():
    """Sample non-streaming response from OpenAI API."""
    return {
        "id": "chatcmpl-123",
        "object": "chat.completion",
        "created": 1677652288,
        "model": "gpt-4o-mini",
        "choices": [
            {
                "index": 0,
                "message": {
                    "role": "assistant",
                    "content": "Hello! I'm doing well, thank you for asking!",
                },
                "finish_reason": "stop",
            }
        ],
        "usage": {
            "prompt_tokens": 20,
            "completion_tokens": 15,
            "total_tokens": 35,
        },
    }


@pytest.fixture
def mock_stream_chunks():
    """Sample streaming chunks from OpenAI API."""
    return [
        "data: {\"id\":\"chatcmpl-123\",\"choices\":[{\"delta\":{\"content\":\"Hello\"},\"finish_reason\":null}]}\n",
        "data: {\"id\":\"chatcmpl-123\",\"choices\":[{\"delta\":{\"content\":\"!\"},\"finish_reason\":null}]}\n",
        "data: {\"id\":\"chatcmpl-123\",\"choices\":[{\"delta\":{\"content\":\" How\"},\"finish_reason\":null}]}\n",
        "data: {\"id\":\"chatcmpl-123\",\"choices\":[{\"delta\":{\"content\":\" can I help?\"},\"finish_reason\":\"stop\"}]}\n",
        "data: [DONE]\n",
    ]


class TestOpenAIClientGenerate:
    """Tests for non-streaming generation. [AC-AISVC-02]"""

    @pytest.mark.asyncio
    async def test_generate_success(self, llm_client, mock_messages, mock_generate_response):
        """[AC-AISVC-02] Test successful non-streaming generation."""
        mock_response = MagicMock()
        mock_response.json.return_value = mock_generate_response
        mock_response.raise_for_status = MagicMock()

        with patch.object(
            llm_client, "_get_client"
        ) as mock_get_client:
            mock_client = AsyncMock()
            mock_client.post = AsyncMock(return_value=mock_response)
            mock_get_client.return_value = mock_client

            result = await llm_client.generate(mock_messages)

            assert isinstance(result, LLMResponse)
            assert result.content == "Hello! I'm doing well, thank you for asking!"
            assert result.model == "gpt-4o-mini"
            assert result.finish_reason == "stop"
            assert result.usage["total_tokens"] == 35

    @pytest.mark.asyncio
    async def test_generate_with_custom_config(self, llm_client, mock_messages, mock_generate_response):
        """[AC-AISVC-02] Test generation with custom configuration."""
        custom_config = LLMConfig(
            model="gpt-4",
            max_tokens=1024,
            temperature=0.5,
        )

        mock_response = MagicMock()
        mock_response.json.return_value = {**mock_generate_response, "model": "gpt-4"}
        mock_response.raise_for_status = MagicMock()

        with patch.object(llm_client, "_get_client") as mock_get_client:
            mock_client = AsyncMock()
            mock_client.post = AsyncMock(return_value=mock_response)
            mock_get_client.return_value = mock_client

            result = await llm_client.generate(mock_messages, config=custom_config)

            assert result.model == "gpt-4"

    @pytest.mark.asyncio
    async def test_generate_timeout_error(self, llm_client, mock_messages):
        """[AC-AISVC-02] Test timeout error handling."""
        with patch.object(llm_client, "_get_client") as mock_get_client:
            mock_client = AsyncMock()
            mock_client.post = AsyncMock(side_effect=httpx.TimeoutException("Timeout"))
            mock_get_client.return_value = mock_client

            with pytest.raises(TimeoutException):
                await llm_client.generate(mock_messages)

    @pytest.mark.asyncio
    async def test_generate_api_error(self, llm_client, mock_messages):
        """[AC-AISVC-02] Test API error handling."""
        mock_response = MagicMock()
        mock_response.status_code = 401
        mock_response.text = '{"error": {"message": "Invalid API key"}}'
        mock_response.json.return_value = {"error": {"message": "Invalid API key"}}

        http_error = httpx.HTTPStatusError(
            "Unauthorized",
            request=MagicMock(),
            response=mock_response,
        )

        with patch.object(llm_client, "_get_client") as mock_get_client:
            mock_client = AsyncMock()
            mock_client.post = AsyncMock(side_effect=http_error)
            mock_get_client.return_value = mock_client

            with pytest.raises(LLMException) as exc_info:
                await llm_client.generate(mock_messages)

            assert "Invalid API key" in str(exc_info.value.message)

    @pytest.mark.asyncio
    async def test_generate_malformed_response(self, llm_client, mock_messages):
        """[AC-AISVC-02] Test handling of malformed response."""
        mock_response = MagicMock()
        mock_response.json.return_value = {"invalid": "response"}
        mock_response.raise_for_status = MagicMock()

        with patch.object(llm_client, "_get_client") as mock_get_client:
            mock_client = AsyncMock()
            mock_client.post = AsyncMock(return_value=mock_response)
            mock_get_client.return_value = mock_client

            with pytest.raises(LLMException):
                await llm_client.generate(mock_messages)


class MockAsyncStreamContext:
    """Mock async context manager for streaming."""

    def __init__(self, response):
        self._response = response

    async def __aenter__(self):
        return self._response

    async def __aexit__(self, *args):
        pass


class TestOpenAIClientStreamGenerate:
    """Tests for streaming generation. [AC-AISVC-06, AC-AISVC-07]"""

    @pytest.mark.asyncio
    async def test_stream_generate_success(self, llm_client, mock_messages, mock_stream_chunks):
        """[AC-AISVC-06, AC-AISVC-07] Test successful streaming generation."""
        async def mock_aiter_lines():
            for chunk in mock_stream_chunks:
                yield chunk

        mock_response = MagicMock()
        mock_response.raise_for_status = MagicMock()
        mock_response.aiter_lines = mock_aiter_lines

        mock_client = AsyncMock()
        mock_client.stream = MagicMock(return_value=MockAsyncStreamContext(mock_response))

        with patch.object(llm_client, "_get_client", return_value=mock_client):
            chunks = []
            async for chunk in llm_client.stream_generate(mock_messages):
                chunks.append(chunk)

            assert len(chunks) == 4
            assert chunks[0].delta == "Hello"
            assert chunks[-1].finish_reason == "stop"

    @pytest.mark.asyncio
    async def test_stream_generate_timeout_error(self, llm_client, mock_messages):
        """[AC-AISVC-06] Test streaming timeout error handling."""
        mock_client = AsyncMock()

        class TimeoutContext:
            async def __aenter__(self):
                raise httpx.TimeoutException("Timeout")
            async def __aexit__(self, *args):
                pass

        mock_client.stream = MagicMock(return_value=TimeoutContext())

        with patch.object(llm_client, "_get_client", return_value=mock_client):
            with pytest.raises(TimeoutException):
                async for _ in llm_client.stream_generate(mock_messages):
                    pass

    @pytest.mark.asyncio
    async def test_stream_generate_api_error(self, llm_client, mock_messages):
        """[AC-AISVC-06] Test streaming API error handling."""
        mock_response = MagicMock()
        mock_response.status_code = 500
        mock_response.text = "Internal Server Error"
        mock_response.json.return_value = {"error": {"message": "Internal Server Error"}}

        http_error = httpx.HTTPStatusError(
            "Internal Server Error",
            request=MagicMock(),
            response=mock_response,
        )

        mock_client = AsyncMock()

        class ErrorContext:
            async def __aenter__(self):
                raise http_error
            async def __aexit__(self, *args):
                pass

        mock_client.stream = MagicMock(return_value=ErrorContext())

        with patch.object(llm_client, "_get_client", return_value=mock_client):
            with pytest.raises(LLMException):
                async for _ in llm_client.stream_generate(mock_messages):
                    pass


class TestOpenAIClientConfig:
    """Tests for LLM configuration."""

    def test_default_config(self, mock_settings):
        """Test default configuration from settings."""
        with patch("app.services.llm.openai_client.get_settings", return_value=mock_settings):
            client = OpenAIClient()

            assert client._model == "gpt-4o-mini"
            assert client._default_config.max_tokens == 2048
            assert client._default_config.temperature == 0.7

    def test_custom_config_override(self, mock_settings):
        """Test custom configuration override."""
        with patch("app.services.llm.openai_client.get_settings", return_value=mock_settings):
            client = OpenAIClient(
                api_key="custom-key",
                base_url="https://custom.api.com/v1",
                model="gpt-4",
            )

            assert client._api_key == "custom-key"
            assert client._base_url == "https://custom.api.com/v1"
            assert client._model == "gpt-4"


class TestOpenAIClientClose:
    """Tests for client cleanup."""

    @pytest.mark.asyncio
    async def test_close_client(self, llm_client):
        """Test client close releases resources."""
        mock_client = AsyncMock()
        mock_client.aclose = AsyncMock()
        llm_client._client = mock_client

        await llm_client.close()

        mock_client.aclose.assert_called_once()
        assert llm_client._client is None
