"""Tests for llm_utils module."""

from unittest.mock import Mock, patch, MagicMock

import pytest

from local_deep_research.utilities.llm_utils import (
    fetch_ollama_models,
    get_model,
    get_ollama_base_url,
    get_server_url,
)


class TestGetOllamaBaseUrl:
    """Tests for get_ollama_base_url function."""

    def test_returns_default_without_settings(self):
        """Should return default URL when no settings provided."""
        with patch(
            "local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
        ) as mock_get:
            mock_get.return_value = None
            result = get_ollama_base_url()
            assert result == "http://localhost:11434"

    def test_uses_embeddings_ollama_url(self):
        """Should use embeddings.ollama.url setting."""
        with patch(
            "local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
        ) as mock_get:
            mock_get.return_value = "http://custom:11434"
            result = get_ollama_base_url()
            assert result == "http://custom:11434"

    def test_normalizes_url(self):
        """Should normalize URL without scheme."""
        with patch(
            "local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
        ) as mock_get:
            mock_get.return_value = "localhost:11434"
            result = get_ollama_base_url()
            assert result == "http://localhost:11434"

    def test_passes_settings_snapshot(self):
        """Should pass settings snapshot to get_setting_from_snapshot."""
        snapshot = {"key": "value"}
        with patch(
            "local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
        ) as mock_get:
            mock_get.return_value = "http://localhost:11434"
            get_ollama_base_url(settings_snapshot=snapshot)
            # Should have been called with snapshot
            assert any(
                call.kwargs.get("settings_snapshot") == snapshot
                for call in mock_get.call_args_list
            )


class TestGetServerUrl:
    """Tests for get_server_url function."""

    def test_returns_default_without_settings(self):
        """Should return default URL when no settings provided."""
        result = get_server_url()
        assert result == "http://127.0.0.1:5000/"

    def test_uses_server_url_from_snapshot(self):
        """Should use direct server_url from snapshot."""
        snapshot = {"server_url": "https://custom.example.com/"}
        result = get_server_url(settings_snapshot=snapshot)
        assert result == "https://custom.example.com/"

    def test_uses_system_server_url(self):
        """Should use system.server_url setting."""
        snapshot = {"system": {"server_url": "https://system.example.com/"}}
        result = get_server_url(settings_snapshot=snapshot)
        assert result == "https://system.example.com/"

    def test_constructs_url_from_web_settings(self):
        """Should construct URL from web.host, web.port, web.use_https."""
        # Need to provide snapshot so it goes through web settings path
        snapshot = {"_trigger_web_settings": True}
        with patch(
            "local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
        ) as mock_get:

            def side_effect(key, snapshot_arg=None, default=None):
                settings = {
                    "web.host": "0.0.0.0",
                    "web.port": 8080,
                    "web.use_https": True,
                }
                return settings.get(key, default)

            mock_get.side_effect = side_effect
            result = get_server_url(settings_snapshot=snapshot)
            # 0.0.0.0 should be converted to 127.0.0.1
            assert result == "https://127.0.0.1:8080/"

    def test_uses_http_when_use_https_false(self):
        """Should use http scheme when use_https is False."""
        snapshot = {"_trigger_web_settings": True}
        with patch(
            "local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
        ) as mock_get:

            def side_effect(key, snapshot_arg=None, default=None):
                settings = {
                    "web.host": "192.168.1.1",  # Not localhost, so it won't default
                    "web.port": 5000,
                    "web.use_https": False,
                }
                return settings.get(key, default)

            mock_get.side_effect = side_effect
            result = get_server_url(settings_snapshot=snapshot)
            assert result == "http://192.168.1.1:5000/"

    def test_priority_order(self):
        """Should check server_url before system before web settings."""
        # Direct server_url takes priority
        snapshot = {
            "server_url": "https://direct/",
            "system": {"server_url": "https://system/"},
        }
        result = get_server_url(settings_snapshot=snapshot)
        assert result == "https://direct/"

    def test_returns_fallback_with_trailing_slash(self):
        """Fallback URL should have trailing slash."""
        result = get_server_url()
        assert result.endswith("/")


class TestFetchOllamaModels:
    """Tests for fetch_ollama_models function."""

    def test_returns_empty_list_on_connection_error(self):
        """Should return empty list on connection error."""
        import requests

        with patch.object(requests, "get") as mock_get:
            mock_get.side_effect = Exception("Connection refused")
            result = fetch_ollama_models("http://localhost:11434")
            assert result == []

    def test_returns_empty_list_on_non_200(self):
        """Should return empty list on non-200 status."""
        import requests

        with patch.object(requests, "get") as mock_get:
            mock_response = Mock()
            mock_response.status_code = 404
            mock_get.return_value = mock_response
            result = fetch_ollama_models("http://localhost:11434")
            assert result == []

    def test_parses_models_from_response(self):
        """Should parse models from API response."""
        import requests

        with patch.object(requests, "get") as mock_get:
            mock_response = Mock()
            mock_response.status_code = 200
            mock_response.json.return_value = {
                "models": [
                    {"name": "llama2"},
                    {"name": "mistral"},
                ]
            }
            mock_get.return_value = mock_response

            result = fetch_ollama_models("http://localhost:11434")

            assert len(result) == 2
            assert {"value": "llama2", "label": "llama2"} in result
            assert {"value": "mistral", "label": "mistral"} in result

    def test_handles_older_api_format(self):
        """Should handle older API format (list directly)."""
        import requests

        with patch.object(requests, "get") as mock_get:
            mock_response = Mock()
            mock_response.status_code = 200
            mock_response.json.return_value = [
                {"name": "model1"},
                {"name": "model2"},
            ]
            mock_get.return_value = mock_response

            result = fetch_ollama_models("http://localhost:11434")

            assert len(result) == 2

    def test_skips_models_without_name(self):
        """Should skip models without name field."""
        import requests

        with patch.object(requests, "get") as mock_get:
            mock_response = Mock()
            mock_response.status_code = 200
            mock_response.json.return_value = {
                "models": [
                    {"name": "valid"},
                    {"other": "field"},  # No name
                    {"name": ""},  # Empty name
                ]
            }
            mock_get.return_value = mock_response

            result = fetch_ollama_models("http://localhost:11434")

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

    def test_uses_custom_timeout(self):
        """Should use custom timeout."""
        import requests

        with patch.object(requests, "get") as mock_get:
            mock_response = Mock()
            mock_response.status_code = 200
            mock_response.json.return_value = {"models": []}
            mock_get.return_value = mock_response

            fetch_ollama_models("http://localhost:11434", timeout=10.0)

            mock_get.assert_called_once()
            assert mock_get.call_args.kwargs["timeout"] == 10.0

    def test_uses_auth_headers(self):
        """Should pass auth headers to request."""
        import requests

        with patch.object(requests, "get") as mock_get:
            mock_response = Mock()
            mock_response.status_code = 200
            mock_response.json.return_value = {"models": []}
            mock_get.return_value = mock_response

            headers = {"Authorization": "Bearer token"}
            fetch_ollama_models("http://localhost:11434", auth_headers=headers)

            mock_get.assert_called_once()
            assert mock_get.call_args.kwargs["headers"] == headers

    def test_constructs_correct_url(self):
        """Should construct correct API URL."""
        import requests

        with patch.object(requests, "get") as mock_get:
            mock_response = Mock()
            mock_response.status_code = 200
            mock_response.json.return_value = {"models": []}
            mock_get.return_value = mock_response

            fetch_ollama_models("http://localhost:11434")

            mock_get.assert_called_once()
            assert (
                mock_get.call_args.args[0] == "http://localhost:11434/api/tags"
            )

    def test_returns_correct_format(self):
        """Should return models in correct format with value and label."""
        import requests

        with patch.object(requests, "get") as mock_get:
            mock_response = Mock()
            mock_response.status_code = 200
            mock_response.json.return_value = {
                "models": [{"name": "test-model"}]
            }
            mock_get.return_value = mock_response

            result = fetch_ollama_models("http://localhost:11434")

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

    def test_handles_empty_models_list(self):
        """Should handle empty models list."""
        import requests

        with patch.object(requests, "get") as mock_get:
            mock_response = Mock()
            mock_response.status_code = 200
            mock_response.json.return_value = {"models": []}
            mock_get.return_value = mock_response

            result = fetch_ollama_models("http://localhost:11434")

            assert result == []


class TestGetModel:
    """Tests for get_model function."""

    def test_creates_ollama_model_by_default(self):
        """Should create Ollama model by default."""
        mock_chat_ollama = Mock()
        with patch(
            "local_deep_research.utilities.llm_utils.ChatOllama",
            mock_chat_ollama,
            create=True,
        ):
            with patch.dict(
                "sys.modules",
                {"langchain_ollama": MagicMock(ChatOllama=mock_chat_ollama)},
            ):
                get_model(model_name="llama2", model_type="ollama")

                mock_chat_ollama.assert_called_once()
                call_kwargs = mock_chat_ollama.call_args.kwargs
                assert call_kwargs["model"] == "llama2"

    def test_uses_default_model_name(self):
        """Should use DEFAULT_MODEL from kwargs if model_name not provided."""
        mock_chat_ollama = Mock()
        with patch(
            "local_deep_research.utilities.llm_utils.ChatOllama",
            mock_chat_ollama,
            create=True,
        ):
            with patch.dict(
                "sys.modules",
                {"langchain_ollama": MagicMock(ChatOllama=mock_chat_ollama)},
            ):
                get_model(model_type="ollama", DEFAULT_MODEL="custom-model")

                mock_chat_ollama.assert_called_once()
                call_kwargs = mock_chat_ollama.call_args.kwargs
                assert call_kwargs["model"] == "custom-model"

    def test_uses_default_temperature(self):
        """Should use DEFAULT_TEMPERATURE from kwargs."""
        mock_chat_ollama = Mock()
        with patch(
            "local_deep_research.utilities.llm_utils.ChatOllama",
            mock_chat_ollama,
            create=True,
        ):
            with patch.dict(
                "sys.modules",
                {"langchain_ollama": MagicMock(ChatOllama=mock_chat_ollama)},
            ):
                get_model(
                    model_name="test",
                    model_type="ollama",
                    DEFAULT_TEMPERATURE=0.5,
                )

                call_kwargs = mock_chat_ollama.call_args.kwargs
                assert call_kwargs["temperature"] == 0.5

    def test_uses_max_tokens(self):
        """Should use max_tokens from kwargs."""
        mock_chat_ollama = Mock()
        with patch(
            "local_deep_research.utilities.llm_utils.ChatOllama",
            mock_chat_ollama,
            create=True,
        ):
            with patch.dict(
                "sys.modules",
                {"langchain_ollama": MagicMock(ChatOllama=mock_chat_ollama)},
            ):
                get_model(
                    model_name="test", model_type="ollama", max_tokens=5000
                )

                call_kwargs = mock_chat_ollama.call_args.kwargs
                assert call_kwargs["max_tokens"] == 5000

    def test_openai_model_requires_api_key(self):
        """Should raise error if OpenAI API key not found."""
        with patch(
            "local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
        ) as mock_get:
            mock_get.return_value = None  # No API key

            with pytest.raises(ValueError, match="OpenAI API key not found"):
                get_model(model_name="gpt-4", model_type="openai")

    def test_anthropic_model_requires_api_key(self):
        """Should raise error if Anthropic API key not found."""
        with patch(
            "local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
        ) as mock_get:
            mock_get.return_value = None  # No API key

            with pytest.raises(ValueError, match="Anthropic API key not found"):
                get_model(model_name="claude-3", model_type="anthropic")

    def test_openai_endpoint_requires_api_key(self):
        """Should raise error if OpenAI endpoint API key not found."""
        with patch(
            "local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
        ) as mock_get:
            mock_get.return_value = None  # No API key

            with pytest.raises(
                ValueError, match="OpenAI endpoint API key not found"
            ):
                get_model(model_name="model", model_type="openai_endpoint")

    def test_unknown_model_type_falls_back_to_ollama(self):
        """Should fall back to Ollama for unknown model types."""
        mock_chat_ollama = Mock()
        with patch(
            "local_deep_research.utilities.llm_utils.ChatOllama",
            mock_chat_ollama,
            create=True,
        ):
            with patch.dict(
                "sys.modules",
                {"langchain_ollama": MagicMock(ChatOllama=mock_chat_ollama)},
            ):
                with patch(
                    "local_deep_research.utilities.llm_utils.logger"
                ) as mock_logger:
                    get_model(model_name="test", model_type="unknown_type")

                    mock_logger.warning.assert_called()
                    mock_chat_ollama.assert_called_once()

    def test_passes_additional_kwargs(self):
        """Should pass additional kwargs to model."""
        mock_chat_ollama = Mock()
        with patch(
            "local_deep_research.utilities.llm_utils.ChatOllama",
            mock_chat_ollama,
            create=True,
        ):
            with patch.dict(
                "sys.modules",
                {"langchain_ollama": MagicMock(ChatOllama=mock_chat_ollama)},
            ):
                get_model(
                    model_name="test",
                    model_type="ollama",
                    custom_param="value",
                )

                call_kwargs = mock_chat_ollama.call_args.kwargs
                assert call_kwargs["custom_param"] == "value"

    def test_excludes_config_kwargs_from_model(self):
        """Should not pass config kwargs like DEFAULT_MODEL to model."""
        mock_chat_ollama = Mock()
        with patch(
            "local_deep_research.utilities.llm_utils.ChatOllama",
            mock_chat_ollama,
            create=True,
        ):
            with patch.dict(
                "sys.modules",
                {"langchain_ollama": MagicMock(ChatOllama=mock_chat_ollama)},
            ):
                get_model(
                    model_name="test",
                    model_type="ollama",
                    DEFAULT_MODEL="ignored",
                    DEFAULT_MODEL_TYPE="ignored",
                    DEFAULT_TEMPERATURE=0.5,
                    MAX_TOKENS=1000,
                )

                call_kwargs = mock_chat_ollama.call_args.kwargs
                assert "DEFAULT_MODEL" not in call_kwargs
                assert "DEFAULT_MODEL_TYPE" not in call_kwargs
                assert "DEFAULT_TEMPERATURE" not in call_kwargs
                assert "MAX_TOKENS" not in call_kwargs


class TestGetModelOpenAI:
    """Tests for get_model with OpenAI provider."""

    def test_creates_openai_model(self):
        """Should create OpenAI model with API key."""
        mock_chat_openai = Mock()
        with patch(
            "local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
        ) as mock_get:
            mock_get.return_value = "test-api-key"

            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="gpt-4", model_type="openai")

                    mock_chat_openai.assert_called_once()
                    call_kwargs = mock_chat_openai.call_args.kwargs
                    assert call_kwargs["model"] == "gpt-4"
                    assert call_kwargs["api_key"] == "test-api-key"


class TestGetModelAnthropic:
    """Tests for get_model with Anthropic provider."""

    def test_creates_anthropic_model(self):
        """Should create Anthropic model with API key."""
        mock_chat_anthropic = Mock()
        with patch(
            "local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
        ) as mock_get:
            mock_get.return_value = "test-api-key"

            with patch.dict(
                "sys.modules",
                {
                    "langchain_anthropic": MagicMock(
                        ChatAnthropic=mock_chat_anthropic
                    )
                },
            ):
                with patch(
                    "local_deep_research.utilities.llm_utils.ChatAnthropic",
                    mock_chat_anthropic,
                    create=True,
                ):
                    get_model(model_name="claude-3", model_type="anthropic")

                    mock_chat_anthropic.assert_called_once()
                    call_kwargs = mock_chat_anthropic.call_args.kwargs
                    assert call_kwargs["model"] == "claude-3"
                    assert call_kwargs["anthropic_api_key"] == "test-api-key"


class TestGetModelOpenAIEndpoint:
    """Tests for get_model with OpenAI endpoint provider."""

    def test_creates_openai_endpoint_model(self):
        """Should create OpenAI endpoint model with custom URL."""
        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-api-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="model-name",
                        model_type="openai_endpoint",
                        OPENAI_ENDPOINT_URL="https://custom-endpoint.com/v1",
                    )

                    mock_chat_openai.assert_called_once()
                    call_kwargs = mock_chat_openai.call_args.kwargs
                    assert (
                        call_kwargs["openai_api_base"]
                        == "https://custom-endpoint.com/v1"
                    )

    def test_endpoint_passes_model_name(self):
        """Should pass model name to endpoint."""
        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-api-key"
                return "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="custom-model",
                        model_type="openai_endpoint",
                    )

                    mock_chat_openai.assert_called_once()
                    call_kwargs = mock_chat_openai.call_args.kwargs
                    assert call_kwargs["model"] == "custom-model"


class TestGetModelFallback:
    """Tests for get_model fallback behavior."""

    def test_default_model_values(self):
        """Should use sensible defaults for model parameters."""
        mock_chat_ollama = Mock()
        with patch(
            "local_deep_research.utilities.llm_utils.ChatOllama",
            mock_chat_ollama,
            create=True,
        ):
            with patch.dict(
                "sys.modules",
                {"langchain_ollama": MagicMock(ChatOllama=mock_chat_ollama)},
            ):
                # Call without specifying any params
                get_model()

                mock_chat_ollama.assert_called_once()
                call_kwargs = mock_chat_ollama.call_args.kwargs
                # Should use default model name
                assert call_kwargs["model"] == "mistral"
                # Should use default temperature
                assert call_kwargs["temperature"] == 0.7
                # Should use default max_tokens
                assert call_kwargs["max_tokens"] == 30000

    def test_kwargs_override_defaults(self):
        """Should allow kwargs to override default values."""
        mock_chat_ollama = Mock()
        with patch(
            "local_deep_research.utilities.llm_utils.ChatOllama",
            mock_chat_ollama,
            create=True,
        ):
            with patch.dict(
                "sys.modules",
                {"langchain_ollama": MagicMock(ChatOllama=mock_chat_ollama)},
            ):
                get_model(
                    DEFAULT_MODEL="custom-model",
                    DEFAULT_MODEL_TYPE="ollama",
                    DEFAULT_TEMPERATURE=0.3,
                    MAX_TOKENS=10000,
                )

                call_kwargs = mock_chat_ollama.call_args.kwargs
                assert call_kwargs["model"] == "custom-model"
                assert call_kwargs["temperature"] == 0.3
                assert call_kwargs["max_tokens"] == 10000
