"""Unit tests for :mod:`llmner.llm_client`."""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch

from llmner.llm_client import BaseLLMClient, OllamaClient

# ---------------------------------------------------------------------------
# BaseLLMClient
# ---------------------------------------------------------------------------


class TestBaseLLMClient:
    def test_cannot_instantiate_directly(self) -> None:
        """BaseLLMClient is abstract."""
        import pytest

        with pytest.raises(TypeError):
            BaseLLMClient()  # type: ignore[abstract]

    def test_subclass_works(self) -> None:
        class DummyClient(BaseLLMClient):
            def generate(self, prompt: str) -> dict[str, Any] | None:
                return {"answer": prompt}

        client = DummyClient()
        assert client.generate("hello") == {"answer": "hello"}


# ---------------------------------------------------------------------------
# OllamaClient
# ---------------------------------------------------------------------------


class TestOllamaClient:
    def test_default_url(self) -> None:
        client = OllamaClient()
        assert client._url == "http://localhost:11434/api/generate"

    def test_custom_url_trailing_slash(self) -> None:
        client = OllamaClient(base_url="http://example.com:1234/")
        assert client._url == "http://example.com:1234/api/generate"

    def test_default_model(self) -> None:
        client = OllamaClient()
        assert client.model == "qwen2.5:7b-instruct"

    @patch("llmner.llm_client.requests.post")
    def test_successful_response(self, mock_post: MagicMock) -> None:
        mock_response = MagicMock()
        mock_response.json.return_value = {"response": '{"name": "John"}'}
        mock_response.raise_for_status.return_value = None
        mock_post.return_value = mock_response

        client = OllamaClient()
        result = client.generate("Extract from: John Smith")

        assert result == {"name": "John"}
        mock_post.assert_called_once()

    @patch("llmner.llm_client.requests.post")
    def test_network_error_returns_none(self, mock_post: MagicMock) -> None:
        import requests

        mock_post.side_effect = requests.ConnectionError("Connection refused")

        client = OllamaClient()
        result = client.generate("test")

        assert result is None

    @patch("llmner.llm_client.requests.post")
    def test_invalid_json_returns_none(self, mock_post: MagicMock) -> None:
        mock_response = MagicMock()
        mock_response.json.return_value = {"response": "not valid json {{{"}
        mock_response.raise_for_status.return_value = None
        mock_post.return_value = mock_response

        client = OllamaClient()
        result = client.generate("test")

        assert result is None

    @patch("llmner.llm_client.requests.post")
    def test_missing_response_key_returns_none(self, mock_post: MagicMock) -> None:
        mock_response = MagicMock()
        mock_response.json.return_value = {"other_key": "value"}
        mock_response.raise_for_status.return_value = None
        mock_post.return_value = mock_response

        client = OllamaClient()
        result = client.generate("test")

        assert result is None

    @patch("llmner.llm_client.requests.post")
    def test_payload_format(self, mock_post: MagicMock) -> None:
        mock_response = MagicMock()
        mock_response.json.return_value = {"response": "{}"}
        mock_response.raise_for_status.return_value = None
        mock_post.return_value = mock_response

        client = OllamaClient(model="test-model", timeout=60)
        client.generate("my prompt")

        call_kwargs = mock_post.call_args
        payload = call_kwargs.kwargs["json"]
        assert payload["model"] == "test-model"
        assert payload["prompt"] == "my prompt"
        assert payload["stream"] is False
        assert payload["format"] == "json"
        assert call_kwargs.kwargs["timeout"] == 60
