"""Tests for provider implementations."""

import json
from unittest.mock import AsyncMock, MagicMock

import httpx
import pytest

from oclawma.providers import (
    CompletionRequest,
    Message,
    ModelNotFoundError,
    OllamaProvider,
    ProviderConnectionError,
    UsageStats,
)


class TestBaseProviderClasses:
    """Test base provider data classes."""

    def test_message_creation(self) -> None:
        """Test Message dataclass creation."""
        msg = Message(role="user", content="Hello")
        assert msg.role == "user"
        assert msg.content == "Hello"
        assert msg.name is None

    def test_message_with_name(self) -> None:
        """Test Message with name field."""
        msg = Message(role="assistant", content="Hi there", name="Claude")
        assert msg.name == "Claude"

    def test_completion_request_creation(self) -> None:
        """Test CompletionRequest creation."""
        msg = Message(role="user", content="Hello")
        req = CompletionRequest(messages=[msg], model="llama2")
        assert req.messages == [msg]
        assert req.model == "llama2"
        assert req.temperature == 0.7
        assert req.max_tokens is None
        assert req.stream is False

    def test_completion_request_custom_params(self) -> None:
        """Test CompletionRequest with custom parameters."""
        msg = Message(role="user", content="Hello")
        req = CompletionRequest(
            messages=[msg],
            model="llama2",
            temperature=0.5,
            max_tokens=100,
            stream=True,
        )
        assert req.temperature == 0.5
        assert req.max_tokens == 100
        assert req.stream is True


class TestOllamaProviderTokenCounting:
    """Test OllamaProvider token counting."""

    def test_count_tokens_empty(self) -> None:
        """Test token counting for empty string."""
        provider = OllamaProvider()
        assert provider.count_tokens("") == 0

    def test_count_tokens_short(self) -> None:
        """Test token counting for short text."""
        provider = OllamaProvider()
        # Minimum 1 token
        assert provider.count_tokens("Hi") == 1

    def test_count_tokens_long(self) -> None:
        """Test token counting for longer text."""
        provider = OllamaProvider()
        text = "a" * 100  # 100 characters
        # ~4 chars per token = 25 tokens
        assert provider.count_tokens(text) == 25

    def test_count_message_tokens(self) -> None:
        """Test token counting for messages."""
        provider = OllamaProvider()
        messages = [
            Message(role="system", content="You are helpful"),
            Message(role="user", content="Hello there"),
        ]
        tokens = provider.count_message_tokens(messages)
        # 4 base per message + content tokens + 2 completion primer
        assert tokens > 10

    def test_count_message_tokens_empty(self) -> None:
        """Test token counting for empty message list."""
        provider = OllamaProvider()
        tokens = provider.count_message_tokens([])
        # Just completion primer
        assert tokens == 2


class TestOllamaProviderConvertMessages:
    """Test OllamaProvider message conversion."""

    def test_convert_messages_basic(self) -> None:
        """Test basic message conversion."""
        provider = OllamaProvider()
        messages = [
            Message(role="user", content="Hello"),
            Message(role="assistant", content="Hi there"),
        ]
        system, ollama_msgs = provider._convert_messages(messages)
        assert system is None
        assert len(ollama_msgs) == 2
        assert ollama_msgs[0]["role"] == "user"
        assert ollama_msgs[0]["content"] == "Hello"

    def test_convert_messages_with_system(self) -> None:
        """Test message conversion with system prompt."""
        provider = OllamaProvider()
        messages = [
            Message(role="system", content="You are helpful"),
            Message(role="user", content="Hello"),
        ]
        system, ollama_msgs = provider._convert_messages(messages)
        assert system == "You are helpful"
        assert len(ollama_msgs) == 1
        assert ollama_msgs[0]["role"] == "user"

    def test_convert_messages_multiple_system(self) -> None:
        """Test that last system message wins."""
        provider = OllamaProvider()
        messages = [
            Message(role="system", content="First"),
            Message(role="system", content="Second"),
            Message(role="user", content="Hello"),
        ]
        system, ollama_msgs = provider._convert_messages(messages)
        assert system == "Second"


@pytest.mark.asyncio
class TestOllamaProviderComplete:
    """Test OllamaProvider complete method."""

    async def test_complete_success(self) -> None:
        """Test successful completion."""
        provider = OllamaProvider()

        mock_response = MagicMock()
        mock_response.json.return_value = {
            "message": {"content": "Hello! How can I help?"},
            "done": True,
        }
        mock_response.raise_for_status = MagicMock()

        mock_client = AsyncMock()
        mock_client.post = AsyncMock(return_value=mock_response)
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="llama2",
        )

        response = await provider.complete(request)

        assert response.content == "Hello! How can I help?"
        assert response.model == "llama2"
        assert response.finish_reason == "stop"
        assert isinstance(response.usage, UsageStats)
        assert response.usage.prompt_tokens > 0
        assert response.usage.completion_tokens > 0

    async def test_complete_with_system(self) -> None:
        """Test completion with system message."""
        provider = OllamaProvider()

        mock_response = MagicMock()
        mock_response.json.return_value = {
            "message": {"content": "Response"},
            "done": True,
        }
        mock_response.raise_for_status = MagicMock()

        mock_client = AsyncMock()
        mock_client.post = AsyncMock(return_value=mock_response)
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[
                Message(role="system", content="Be helpful"),
                Message(role="user", content="Hi"),
            ],
            model="llama2",
        )

        response = await provider.complete(request)
        assert response.content == "Response"

        # Verify system was passed in payload
        call_args = mock_client.post.call_args
        payload = call_args[1]["json"]
        assert payload["system"] == "Be helpful"

    async def test_complete_with_max_tokens(self) -> None:
        """Test completion with max_tokens."""
        provider = OllamaProvider()

        mock_response = MagicMock()
        mock_response.json.return_value = {
            "message": {"content": "Response"},
            "done": True,
        }
        mock_response.raise_for_status = MagicMock()

        mock_client = AsyncMock()
        mock_client.post = AsyncMock(return_value=mock_response)
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="llama2",
            max_tokens=100,
        )

        await provider.complete(request)

        call_args = mock_client.post.call_args
        payload = call_args[1]["json"]
        assert payload["options"]["num_predict"] == 100

    async def test_complete_connection_error(self) -> None:
        """Test connection error handling."""
        provider = OllamaProvider()

        mock_client = AsyncMock()
        mock_client.post = AsyncMock(side_effect=httpx.ConnectError("Connection refused"))
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="llama2",
        )

        with pytest.raises(ProviderConnectionError) as exc_info:
            await provider.complete(request)

        assert "Cannot connect to Ollama" in str(exc_info.value)

    async def test_complete_model_not_found(self) -> None:
        """Test model not found error handling."""
        provider = OllamaProvider()

        mock_response = MagicMock()
        mock_response.status_code = 404
        mock_response.text = "model not found"

        mock_client = AsyncMock()
        mock_client.post = AsyncMock(
            side_effect=httpx.HTTPStatusError(
                "404",
                request=MagicMock(),
                response=mock_response,
            )
        )
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="nonexistent",
        )

        with pytest.raises(ModelNotFoundError) as exc_info:
            await provider.complete(request)

        assert "Model not found" in str(exc_info.value)


@pytest.mark.asyncio
class TestOllamaProviderStreamComplete:
    """Test OllamaProvider streaming completion."""

    async def test_stream_complete_success(self) -> None:
        """Test successful streaming completion."""
        provider = OllamaProvider()

        # Create mock stream data
        chunks = [
            json.dumps({"message": {"content": "Hello"}, "done": False}),
            json.dumps({"message": {"content": " there"}, "done": False}),
            json.dumps({"message": {"content": "!"}, "done": True}),
        ]

        # Create an async iterator for the chunks
        async def async_chunk_iterator():
            for chunk in chunks:
                yield chunk

        mock_stream = MagicMock()
        mock_stream.aiter_lines = MagicMock(return_value=async_chunk_iterator())
        mock_stream.__aenter__ = AsyncMock(return_value=mock_stream)
        mock_stream.__aexit__ = AsyncMock(return_value=None)

        mock_client = AsyncMock()
        mock_client.stream = MagicMock(return_value=mock_stream)
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="llama2",
        )

        responses = []
        async for response in provider.stream_complete(request):
            responses.append(response)

        assert len(responses) == 4  # 3 content chunks + 1 final chunk with finish_reason
        assert responses[0].content == "Hello"
        assert responses[1].content == " there"
        assert responses[2].content == "!"
        assert responses[3].content == ""  # Final chunk is empty
        assert responses[3].finish_reason == "stop"

    async def test_stream_empty_chunks_filtered(self) -> None:
        """Test that empty chunks are filtered in streaming."""
        provider = OllamaProvider()

        chunks = [
            json.dumps({"message": {"content": "Hello"}, "done": False}),
            json.dumps({"message": {"content": ""}, "done": False}),
            json.dumps({"message": {"content": "World"}, "done": True}),
        ]

        # Create an async iterator for the chunks
        async def async_chunk_iterator():
            for chunk in chunks:
                yield chunk

        mock_stream = MagicMock()
        mock_stream.aiter_lines = MagicMock(return_value=async_chunk_iterator())
        mock_stream.__aenter__ = AsyncMock(return_value=mock_stream)
        mock_stream.__aexit__ = AsyncMock(return_value=None)

        mock_client = AsyncMock()
        mock_client.stream = MagicMock(return_value=mock_stream)
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="llama2",
        )

        responses = []
        async for response in provider.stream_complete(request):
            responses.append(response)

        # Empty content chunk is filtered, but we get 2 content chunks + 1 final chunk
        assert len(responses) == 3
        assert responses[0].content == "Hello"
        assert responses[1].content == "World"
        assert responses[2].finish_reason == "stop"

    async def test_stream_connection_error(self) -> None:
        """Test streaming connection error."""
        provider = OllamaProvider()

        mock_client = AsyncMock()
        mock_client.stream = MagicMock(side_effect=httpx.ConnectError("Connection refused"))
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="llama2",
        )

        with pytest.raises(ProviderConnectionError):
            async for _ in provider.stream_complete(request):
                pass


@pytest.mark.asyncio
class TestOllamaProviderListModels:
    """Test OllamaProvider list_models method."""

    async def test_list_models_success(self) -> None:
        """Test successful model listing."""
        provider = OllamaProvider()

        mock_response = MagicMock()
        mock_response.json.return_value = {
            "models": [
                {"name": "llama2:latest"},
                {"name": "mistral:7b"},
                {"name": "codellama:13b"},
            ]
        }
        mock_response.raise_for_status = MagicMock()

        mock_client = AsyncMock()
        mock_client.get = AsyncMock(return_value=mock_response)
        mock_client.is_closed = False

        provider._client = mock_client

        models = await provider.list_models()

        assert models == ["codellama:13b", "llama2:latest", "mistral:7b"]

    async def test_list_models_empty(self) -> None:
        """Test empty model list."""
        provider = OllamaProvider()

        mock_response = MagicMock()
        mock_response.json.return_value = {"models": []}
        mock_response.raise_for_status = MagicMock()

        mock_client = AsyncMock()
        mock_client.get = AsyncMock(return_value=mock_response)
        mock_client.is_closed = False

        provider._client = mock_client

        models = await provider.list_models()

        assert models == []

    async def test_list_models_connection_error(self) -> None:
        """Test connection error during model listing."""
        provider = OllamaProvider()

        mock_client = AsyncMock()
        mock_client.get = AsyncMock(side_effect=httpx.ConnectError("Connection refused"))
        mock_client.is_closed = False

        provider._client = mock_client

        with pytest.raises(ProviderConnectionError):
            await provider.list_models()


@pytest.mark.asyncio
class TestOllamaProviderHealthCheck:
    """Test OllamaProvider health_check method."""

    async def test_health_check_healthy(self) -> None:
        """Test healthy status."""
        provider = OllamaProvider()

        mock_response = MagicMock()
        mock_response.json.return_value = {"models": []}
        mock_response.raise_for_status = MagicMock()

        mock_client = AsyncMock()
        mock_client.get = AsyncMock(return_value=mock_response)
        mock_client.is_closed = False

        provider._client = mock_client

        health = await provider.health_check()

        assert health["status"] == "healthy"
        assert health["base_url"] == "http://localhost:11434"

    async def test_health_check_unhealthy(self) -> None:
        """Test unhealthy status."""
        provider = OllamaProvider()

        mock_client = AsyncMock()
        mock_client.get = AsyncMock(side_effect=httpx.ConnectError("Connection refused"))
        mock_client.is_closed = False

        provider._client = mock_client

        health = await provider.health_check()

        assert health["status"] == "unhealthy"
        assert "Cannot connect to Ollama" in health["error"]


class TestOllamaProviderClose:
    """Test OllamaProvider close method."""

    async def test_close_client(self) -> None:
        """Test closing the client."""
        provider = OllamaProvider()

        mock_client = AsyncMock()
        mock_client.is_closed = False
        mock_client.aclose = AsyncMock()

        provider._client = mock_client

        await provider.close()

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