"""
Comprehensive tests for web_search_engines/engines/meta_search_engine.py

Tests cover:
- MetaSearchEngine initialization
- Available engines detection
- Query analysis
- Engine selection
- Preview and full content retrieval
- Engine caching
- Error handling
"""

import pytest
from unittest.mock import Mock, patch


class TestMetaSearchEngineInit:
    """Tests for MetaSearchEngine initialization."""

    def test_init_with_llm(self):
        """Test initialization with LLM."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()

        with patch.object(
            MetaSearchEngine, "_get_available_engines", return_value=["searxng"]
        ):
            engine = MetaSearchEngine(llm=mock_llm)

            assert engine.llm == mock_llm
            assert engine.max_results == 10  # default
            assert engine.use_api_key_services is True
            assert engine.max_engines_to_try == 3

    def test_init_custom_parameters(self):
        """Test initialization with custom parameters."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()

        with patch.object(
            MetaSearchEngine, "_get_available_engines", return_value=["searxng"]
        ):
            engine = MetaSearchEngine(
                llm=mock_llm,
                max_results=20,
                use_api_key_services=False,
                max_engines_to_try=5,
                max_filtered_results=10,
            )

            assert engine.max_results == 20
            assert engine.use_api_key_services is False
            assert engine.max_engines_to_try == 5
            assert engine.max_filtered_results == 10

    def test_init_with_settings_snapshot(self):
        """Test initialization with settings snapshot."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        snapshot = {"search.engine.web.searxng.use_in_auto_search": True}

        with patch.object(
            MetaSearchEngine, "_get_available_engines", return_value=["searxng"]
        ):
            engine = MetaSearchEngine(
                llm=mock_llm,
                settings_snapshot=snapshot,
            )

            assert engine.settings_snapshot == snapshot

    def test_init_creates_fallback_engine(self):
        """Test that fallback Wikipedia engine is created."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()

        with patch.object(
            MetaSearchEngine, "_get_available_engines", return_value=["searxng"]
        ):
            engine = MetaSearchEngine(llm=mock_llm)

            assert engine.fallback_engine is not None


class TestGetAvailableEngines:
    """Tests for _get_available_engines method."""

    def test_get_available_engines_from_snapshot(self):
        """Test getting available engines from settings snapshot."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        snapshot = {
            "search.engine.web.searxng.use_in_auto_search": True,
            "search.engine.web.wikipedia.use_in_auto_search": True,
        }

        with patch(
            "local_deep_research.config.search_config.get_setting_from_snapshot",
            return_value=True,
        ):
            with patch.object(
                MetaSearchEngine,
                "_get_search_config",
                return_value={"searxng": {}, "wikipedia": {}},
            ):
                engine = MetaSearchEngine(
                    llm=mock_llm, settings_snapshot=snapshot
                )

                # Available engines should include those with use_in_auto_search=True
                assert len(engine.available_engines) >= 0

    def test_get_available_engines_excludes_meta(self):
        """Test that 'meta' and 'auto' are excluded."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        # Provide settings_snapshot to avoid recursion
        snapshot = {"search.engine.web.searxng.use_in_auto_search": True}

        with patch(
            "local_deep_research.config.search_config.get_setting_from_snapshot",
            return_value=True,
        ):
            with patch.object(
                MetaSearchEngine,
                "_get_search_config",
                return_value={"meta": {}, "auto": {}, "searxng": {}},
            ):
                engine = MetaSearchEngine(
                    llm=mock_llm, settings_snapshot=snapshot
                )

                assert "meta" not in engine.available_engines
                assert "auto" not in engine.available_engines

    def test_get_available_engines_no_engines_raises(self):
        """Test that RuntimeError is raised when no engines available."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()

        with patch(
            "local_deep_research.config.search_config.get_setting_from_snapshot",
            return_value=False,  # All engines disabled
        ):
            with patch.object(
                MetaSearchEngine,
                "_get_search_config",
                return_value={"searxng": {}},
            ):
                with pytest.raises(
                    RuntimeError, match="No search engines enabled"
                ):
                    MetaSearchEngine(llm=mock_llm)


class TestAnalyzeQuery:
    """Tests for analyze_query method."""

    @pytest.fixture
    def engine(self):
        """Create a MetaSearchEngine instance for testing."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        # Provide settings_snapshot to avoid recursion in _get_search_config
        snapshot = {"search.engine.web.searxng.use_in_auto_search": True}

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["searxng", "arxiv", "pubmed", "github"],
        ):
            return MetaSearchEngine(llm=mock_llm, settings_snapshot=snapshot)

    def test_analyze_scientific_query(self, engine):
        """Test that scientific queries prefer arxiv/pubmed."""
        result = engine.analyze_query(
            "latest scientific paper on machine learning"
        )

        # Should prioritize arxiv or pubmed
        assert any(e in result for e in ["arxiv", "pubmed"])

    def test_analyze_medical_query(self, engine):
        """Test that medical queries prefer pubmed."""
        result = engine.analyze_query(
            "clinical trial results for diabetes treatment"
        )

        assert "pubmed" in result

    def test_analyze_code_query(self, engine):
        """Test that code queries prefer github."""
        result = engine.analyze_query("github repository for machine learning")

        assert "github" in result

    def test_analyze_query_with_searxng_available(self, engine):
        """Test that SearXNG is used for general queries."""
        result = engine.analyze_query("latest news about technology")

        assert "searxng" in result

    def test_analyze_query_no_llm(self, engine):
        """Test query analysis without LLM."""
        engine.llm = None

        result = engine.analyze_query("test query")

        # Should fall back to reliability-based ordering
        assert isinstance(result, list)

    def test_analyze_query_llm_error(self, engine):
        """Test error handling in query analysis."""
        engine.llm.invoke.side_effect = Exception("LLM error")

        result = engine.analyze_query("test query")

        # Should fall back gracefully
        assert isinstance(result, list)
        assert len(result) > 0


class TestGetPreviews:
    """Tests for _get_previews method."""

    @pytest.fixture
    def engine(self):
        """Create a MetaSearchEngine instance for testing."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()

        with patch.object(
            MetaSearchEngine, "_get_available_engines", return_value=["searxng"]
        ):
            return MetaSearchEngine(llm=mock_llm)

    def test_get_previews_success(self, engine):
        """Test successful preview retrieval."""
        mock_search_engine = Mock()
        mock_search_engine._get_previews.return_value = [
            {"id": "1", "title": "Result 1"},
            {"id": "2", "title": "Result 2"},
        ]

        with patch.object(engine, "analyze_query", return_value=["searxng"]):
            with patch.object(
                engine, "_get_engine_instance", return_value=mock_search_engine
            ):
                results = engine._get_previews("test query")

                assert len(results) == 2
                assert engine._selected_engine == mock_search_engine

    def test_get_previews_tries_multiple_engines(self, engine):
        """Test that multiple engines are tried on failure."""
        engine.available_engines = ["engine1", "engine2"]

        mock_engine1 = Mock()
        mock_engine1._get_previews.return_value = []  # No results

        mock_engine2 = Mock()
        mock_engine2._get_previews.return_value = [{"id": "1"}]

        def get_engine(name):
            if name == "engine1":
                return mock_engine1
            return mock_engine2

        with patch.object(
            engine, "analyze_query", return_value=["engine1", "engine2"]
        ):
            with patch.object(
                engine, "_get_engine_instance", side_effect=get_engine
            ):
                results = engine._get_previews("test")

                assert len(results) == 1

    def test_get_previews_fallback_to_wikipedia(self, engine):
        """Test fallback to Wikipedia when all engines fail."""
        mock_engine = Mock()
        mock_engine._get_previews.return_value = []

        engine.fallback_engine._get_previews = Mock(
            return_value=[{"id": "wiki1"}]
        )

        with patch.object(engine, "analyze_query", return_value=["searxng"]):
            with patch.object(
                engine, "_get_engine_instance", return_value=mock_engine
            ):
                results = engine._get_previews("test")

                assert len(results) == 1
                assert engine._selected_engine == engine.fallback_engine

    def test_get_previews_engine_error(self, engine):
        """Test handling of engine errors."""
        mock_engine = Mock()
        mock_engine._get_previews.side_effect = Exception("Engine error")

        engine.fallback_engine._get_previews = Mock(return_value=[{"id": "1"}])

        with patch.object(engine, "analyze_query", return_value=["searxng"]):
            with patch.object(
                engine, "_get_engine_instance", return_value=mock_engine
            ):
                results = engine._get_previews("test")

                # Should use fallback
                assert len(results) == 1


class TestGetFullContent:
    """Tests for _get_full_content method."""

    @pytest.fixture
    def engine(self):
        """Create a MetaSearchEngine instance for testing."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()

        with patch.object(
            MetaSearchEngine, "_get_available_engines", return_value=["searxng"]
        ):
            return MetaSearchEngine(llm=mock_llm)

    def test_get_full_content_uses_selected_engine(self, engine):
        """Test that full content uses selected engine."""
        mock_selected = Mock()
        mock_selected._get_full_content.return_value = [
            {"id": "1", "full_content": "Content here"}
        ]

        engine._selected_engine = mock_selected
        engine._selected_engine_name = "test_engine"

        items = [{"id": "1"}]

        # Patch at the module level where get_setting_from_snapshot is called
        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_setting_from_snapshot",
            return_value=False,  # Not snippets only
        ):
            results = engine._get_full_content(items)

            mock_selected._get_full_content.assert_called_once_with(items)
            assert results[0]["full_content"] == "Content here"

    def test_get_full_content_snippets_only(self, engine):
        """Test snippets-only mode."""
        items = [{"id": "1", "title": "Test"}]

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_setting_from_snapshot",
            return_value=True,  # Snippets only
        ):
            results = engine._get_full_content(items)

            assert results == items

    def test_get_full_content_no_selected_engine(self, engine):
        """Test handling when no engine was selected."""
        items = [{"id": "1"}]

        # Remove selected engine
        if hasattr(engine, "_selected_engine"):
            delattr(engine, "_selected_engine")

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_setting_from_snapshot",
            return_value=False,
        ):
            results = engine._get_full_content(items)

            assert results == items


class TestGetEngineInstance:
    """Tests for _get_engine_instance method."""

    @pytest.fixture
    def engine(self):
        """Create a MetaSearchEngine instance for testing."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()

        with patch.object(
            MetaSearchEngine, "_get_available_engines", return_value=["searxng"]
        ):
            return MetaSearchEngine(llm=mock_llm)

    def test_get_engine_instance_creates_new(self, engine):
        """Test creating new engine instance."""
        mock_created_engine = Mock()

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.create_search_engine",
            return_value=mock_created_engine,
        ):
            result = engine._get_engine_instance("wikipedia")

            assert result == mock_created_engine
            assert "wikipedia" in engine.engine_cache

    def test_get_engine_instance_returns_cached(self, engine):
        """Test returning cached engine instance."""
        mock_cached = Mock()
        engine.engine_cache["wikipedia"] = mock_cached

        result = engine._get_engine_instance("wikipedia")

        assert result == mock_cached

    def test_get_engine_instance_error(self, engine):
        """Test error handling in engine creation."""
        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.create_search_engine",
            side_effect=Exception("Creation error"),
        ):
            result = engine._get_engine_instance("failing_engine")

            assert result is None


class TestInvoke:
    """Tests for invoke method."""

    @pytest.fixture
    def engine(self):
        """Create a MetaSearchEngine instance for testing."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()

        with patch.object(
            MetaSearchEngine, "_get_available_engines", return_value=["searxng"]
        ):
            return MetaSearchEngine(llm=mock_llm)

    def test_invoke_calls_run(self, engine):
        """Test that invoke calls run method."""
        with patch.object(
            engine, "run", return_value=[{"id": "1"}]
        ) as mock_run:
            result = engine.invoke("test query")

            mock_run.assert_called_once_with("test query")
            assert result == [{"id": "1"}]


class TestGetSearchConfig:
    """Tests for _get_search_config method."""

    @pytest.fixture
    def engine(self):
        """Create a MetaSearchEngine instance for testing."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        snapshot = {
            "search.engine.web.searxng.description": "SearXNG search",
            "search.engine.web.searxng.reliability": 0.9,
        }

        with patch.object(
            MetaSearchEngine, "_get_available_engines", return_value=["searxng"]
        ):
            return MetaSearchEngine(llm=mock_llm, settings_snapshot=snapshot)

    def test_get_search_config_from_snapshot(self, engine):
        """Test getting search config from settings snapshot."""
        config = engine._get_search_config()

        assert isinstance(config, dict)


class TestSpecializedDomainDetection:
    """Tests for specialized domain detection in analyze_query."""

    @pytest.fixture
    def engine(self):
        """Create a MetaSearchEngine instance for testing."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["searxng", "arxiv", "pubmed", "github", "wikipedia"],
        ):
            return MetaSearchEngine(llm=mock_llm)

    def test_detect_arxiv_keyword(self, engine):
        """Test arxiv keyword detection."""
        result = engine.analyze_query("find arxiv papers on transformers")

        assert result[0] == "arxiv"

    def test_detect_pubmed_keyword(self, engine):
        """Test pubmed keyword detection."""
        result = engine.analyze_query("pubmed articles on cancer research")

        assert result[0] == "pubmed"

    def test_detect_programming_query(self, engine):
        """Test programming query detection."""
        result = engine.analyze_query(
            "programming tutorial for web development"
        )

        assert "github" in result


class TestEngineSelectionCallback:
    """Tests for engine selection callback."""

    def test_engine_selection_emits_socket_event(self):
        """Test that engine selection emits socket event."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()

        with patch.object(
            MetaSearchEngine, "_get_available_engines", return_value=["searxng"]
        ):
            engine = MetaSearchEngine(llm=mock_llm)

        mock_search_engine = Mock()
        mock_search_engine._get_previews.return_value = [{"id": "1"}]

        with patch.object(engine, "analyze_query", return_value=["searxng"]):
            with patch.object(
                engine, "_get_engine_instance", return_value=mock_search_engine
            ):
                with patch(
                    "local_deep_research.web_search_engines.engines.meta_search_engine.SocketIOService"
                ) as mock_socket:
                    mock_socket_instance = Mock()
                    mock_socket.return_value = mock_socket_instance

                    engine._get_previews("test")

                    # Socket event should have been emitted
                    mock_socket_instance.emit_socket_event.assert_called()


class TestProgrammaticMode:
    """Tests for programmatic mode."""

    def test_programmatic_mode_initialization(self):
        """Test initialization in programmatic mode."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()

        with patch.object(
            MetaSearchEngine, "_get_available_engines", return_value=["searxng"]
        ):
            engine = MetaSearchEngine(llm=mock_llm, programmatic_mode=True)

            assert engine.programmatic_mode is True
