"""
Coverage tests for local_deep_research/utilities/llm_utils.py.

Focuses on paths not exercised by existing test files:
- _close_base_llm: close() delegation, Ollama httpx client closing,
  non-Ollama _client skip, missing _client attr, missing httpx client
- get_model: openai_endpoint without model required, unknown type where
  Ollama import fails -> ValueError (FakeListLLM fallback removed)
- fetch_ollama_models: verifies safe_get (not requests.get) is used
"""

import builtins
from unittest.mock import MagicMock, Mock, patch

import pytest

from local_deep_research.utilities.llm_utils import (
    _close_base_llm,
    fetch_ollama_models,
    get_model,
)


# ---------------------------------------------------------------------------
# _close_base_llm
# ---------------------------------------------------------------------------


class TestCloseBaseLlmDelegation:
    """When the LLM object has its own close() method defined on the type."""

    def test_delegates_to_close_method(self):
        """Should call llm.close() when the type defines close."""

        class ClosableLLM:
            def close(self):
                pass

        llm = ClosableLLM()
        llm.close = Mock()
        _close_base_llm(llm)
        llm.close.assert_called_once()

    def test_does_not_inspect_client_after_close(self):
        """After calling close(), should return immediately without
        inspecting _client."""

        class ClosableLLM:
            def close(self):
                pass

        llm = ClosableLLM()
        llm.close = Mock()
        llm._client = Mock()
        _close_base_llm(llm)
        llm.close.assert_called_once()
        llm._client.close.assert_not_called()


class TestCloseBaseLlmOllamaClient:
    """When the LLM has an Ollama-module _client with an httpx _client."""

    def test_closes_httpx_client_on_ollama(self):
        """Should close the nested httpx client on an Ollama _client."""
        httpx_client = Mock()
        httpx_client.close = Mock()

        ollama_client = Mock()
        ollama_client._client = httpx_client
        type(ollama_client).__module__ = "ollama._client"

        llm = Mock(spec=[])
        llm._client = ollama_client

        _close_base_llm(llm)
        httpx_client.close.assert_called_once()

    def test_skips_non_ollama_client_module(self):
        """Should NOT close _client._client when module is not ollama."""
        httpx_client = Mock()
        httpx_client.close = Mock()

        non_ollama_client = Mock()
        non_ollama_client._client = httpx_client
        type(non_ollama_client).__module__ = "openai._http_client"

        llm = Mock(spec=[])
        llm._client = non_ollama_client

        _close_base_llm(llm)
        httpx_client.close.assert_not_called()

    def test_no_client_attr_is_noop(self):
        """Should do nothing when llm has no _client attribute."""
        llm = Mock(spec=[])
        assert (
            not hasattr(llm, "_client") or getattr(llm, "_client", None) is None
        )
        _close_base_llm(llm)

    def test_ollama_client_without_httpx_client(self):
        """Should handle Ollama _client that has no nested _client (httpx)."""
        ollama_client = Mock(spec=[])
        type(ollama_client).__module__ = "ollama._client"

        llm = Mock(spec=[])
        llm._client = ollama_client
        _close_base_llm(llm)

    def test_ollama_httpx_client_without_close(self):
        """Should handle Ollama httpx _client that lacks close()."""
        httpx_client = Mock(spec=[])

        ollama_client = Mock()
        ollama_client._client = httpx_client
        type(ollama_client).__module__ = "ollama.core"

        llm = Mock(spec=[])
        llm._client = ollama_client
        _close_base_llm(llm)


# ---------------------------------------------------------------------------
# get_model - openai_endpoint without model required
# ---------------------------------------------------------------------------


class TestGetModelOpenAIEndpointNoModelRequired:
    """openai_endpoint path when OPENAI_ENDPOINT_REQUIRES_MODEL is False
    and model_name is None - should omit model= from ChatOpenAI call."""

    def test_no_model_param_when_not_required(self):
        mock_chat_openai = Mock()
        with patch(
            "local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
        ) as mock_get:

            def side_effect(key, *args, **kwargs):
                if "api_key" in key:
                    return "test-key"
                return kwargs.get("default", "https://openrouter.ai/api/v1")

            mock_get.side_effect = side_effect

            with patch.dict(
                "sys.modules",
                {"langchain_openai": MagicMock(ChatOpenAI=mock_chat_openai)},
            ):
                with patch(
                    "local_deep_research.utilities.llm_utils.ChatOpenAI",
                    mock_chat_openai,
                    create=True,
                ):
                    get_model(
                        model_name=None,
                        model_type="openai_endpoint",
                        OPENAI_ENDPOINT_REQUIRES_MODEL=False,
                        DEFAULT_MODEL=None,
                    )

                    mock_chat_openai.assert_called_once()
                    call_kwargs = mock_chat_openai.call_args.kwargs
                    assert "model" not in call_kwargs
                    assert call_kwargs["api_key"] == "test-key"


# ---------------------------------------------------------------------------
# get_model - unknown type, Ollama import fails -> raises ValueError
# ---------------------------------------------------------------------------


class TestGetModelUnknownTypeRaisesValueError:
    """When model_type is unknown AND ChatOllama import fails,
    should raise ValueError (FakeListLLM fallback was removed)."""

    def test_unknown_type_ollama_fails_raises_value_error(self):
        original_import = builtins.__import__

        def custom_import(name, *args, **kwargs):
            if name == "langchain_ollama":
                raise ImportError("no langchain_ollama")
            return original_import(name, *args, **kwargs)

        with patch("builtins.__import__", side_effect=custom_import):
            with pytest.raises(
                ValueError, match="No language models available"
            ):
                get_model(model_name="test", model_type="some_weird_type")


# ---------------------------------------------------------------------------
# fetch_ollama_models - verifies safe_get usage
# ---------------------------------------------------------------------------


class TestFetchOllamaModelsUsesSafeGet:
    """Verify that fetch_ollama_models calls safe_get, not requests.get."""

    def test_calls_safe_get_not_requests_get(self):
        """safe_get should be called; requests.get should NOT."""
        mock_safe_get = Mock()
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"models": [{"name": "llama3"}]}
        mock_safe_get.return_value = mock_response

        with patch(
            "local_deep_research.utilities.llm_utils.safe_get",
            mock_safe_get,
            create=True,
        ):
            with patch("local_deep_research.security.safe_get", mock_safe_get):
                result = fetch_ollama_models("http://localhost:11434")

                mock_safe_get.assert_called()
                call_args = mock_safe_get.call_args
                assert "localhost:11434/api/tags" in call_args.args[0]
                assert call_args.kwargs["allow_localhost"] is True
                assert call_args.kwargs["allow_private_ips"] is True

        assert len(result) == 1
        assert result[0]["value"] == "llama3"

    def test_safe_get_exception_returns_empty_list(self):
        """When safe_get raises, should return [] gracefully."""
        with patch(
            "local_deep_research.security.safe_get",
            side_effect=RuntimeError("SSRF blocked"),
        ):
            result = fetch_ollama_models("http://evil.com:11434")
            assert result == []

    def test_safe_get_non_200_returns_empty(self):
        """Non-200 response from safe_get should yield empty list."""
        mock_response = Mock()
        mock_response.status_code = 503

        with patch(
            "local_deep_research.security.safe_get",
            return_value=mock_response,
        ):
            result = fetch_ollama_models("http://localhost:11434")
            assert result == []
