"""Tests for llm_utils module."""

from unittest.mock import Mock, patch

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


class _FakeModelName:
    def __init__(self, model_name):
        self.model_name = model_name


class _FakeModelOnly:
    def __init__(self, model):
        self.model = model


class _FakeBoth:
    def __init__(self, model_name, model):
        self.model_name = model_name
        self.model = model


class _FakeNeither:
    pass


class _FakeWrapper:
    def __init__(self, base_llm):
        self.base_llm = base_llm


class TestGetModelIdentifier:
    """Regression tests for the identifier helper that drives the
    Tier 4 LLM cache predicate. Earlier code used ``getattr(llm, "name",
    str(llm))`` which always fell back to ``str(wrapper)`` — a repr
    string that poisoned the quality_model column."""

    def test_prefers_model_name(self):
        assert get_model_identifier(_FakeModelName("gpt-4")) == "gpt-4"

    def test_falls_back_to_model(self):
        assert (
            get_model_identifier(_FakeModelOnly("claude-opus-4-7"))
            == "claude-opus-4-7"
        )

    def test_prefers_model_name_over_model(self):
        # If both exist, model_name wins. ChatOpenAI / ChatAnthropic
        # expose model_name; ChatOllama exposes model.
        llm = _FakeBoth(model_name="gpt-4", model="ignored")
        assert get_model_identifier(llm) == "gpt-4"

    def test_class_name_fallback(self):
        # Never return a repr() with object address — fall back cleanly.
        result = get_model_identifier(_FakeNeither())
        assert result == "_FakeNeither"
        assert "object at 0x" not in result

    def test_unwraps_processing_wrapper(self):
        wrapped = _FakeWrapper(_FakeModelOnly("mistral"))
        assert get_model_identifier(wrapped) == "mistral"

    def test_none_values_skipped(self):
        class _NonePair:
            model_name = None
            model = "real-model"

        assert get_model_identifier(_NonePair()) == "real-model"


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()
            # safe_get wraps requests.get and injects a project User-Agent
            # alongside caller-provided headers, so we check the auth
            # header is present rather than doing an exact dict match.
            sent_headers = mock_get.call_args.kwargs["headers"]
            assert sent_headers["Authorization"] == "Bearer token"

    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 == []
