"""
Tests for the ParallelSearchEngine class.

Tests cover:
- Initialization and configuration
- Engine selection logic
- Parallel execution
- Result aggregation
- Error handling
- Global executor management
- API key availability checking
- Engine instance caching
"""

from unittest.mock import Mock, patch

from local_deep_research.utilities.enums import SearchMode


class TestParallelSearchEngineInit:
    """Tests for ParallelSearchEngine initialization."""

    def test_init_with_defaults(self):
        """Initialize with default values."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        assert engine.llm is mock_llm
        assert engine.max_results == 10

    def test_init_with_custom_max_results(self):
        """Initialize with custom max_results."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm,
            max_results=25,
            settings_snapshot=settings,
            programmatic_mode=True,
        )

        assert engine.max_results == 25

    def test_init_stores_settings_snapshot(self):
        """Initialize stores settings snapshot."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {
            "search.max_results": {"value": 10},
            "custom_key": {"value": "test"},
        }

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        assert engine.settings_snapshot == settings

    def test_init_with_max_filtered_results(self):
        """Initialize with custom max_filtered_results."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm,
            settings_snapshot=settings,
            programmatic_mode=True,
            max_filtered_results=100,
        )

        assert engine.max_filtered_results == 100

    def test_init_default_max_filtered_results_is_50(self):
        """Default max_filtered_results is 50 for parallel search."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        assert engine.max_filtered_results == 50

    def test_init_with_use_api_key_services_false(self):
        """Initialize with use_api_key_services disabled."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm,
            settings_snapshot=settings,
            programmatic_mode=True,
            use_api_key_services=False,
        )

        assert engine.use_api_key_services is False

    def test_init_with_allow_local_engines(self):
        """Initialize with allow_local_engines enabled."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm,
            settings_snapshot=settings,
            programmatic_mode=True,
            allow_local_engines=True,
        )

        assert engine.allow_local_engines is True

    def test_init_with_scientific_search_mode(self):
        """Initialize with scientific search mode."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm,
            settings_snapshot=settings,
            programmatic_mode=True,
            search_mode=SearchMode.SCIENTIFIC,
        )

        assert engine.search_mode == SearchMode.SCIENTIFIC

    def test_init_disables_llm_relevance_filter_by_default(self):
        """LLM relevance filter is disabled by default at parallel level."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        assert engine.enable_llm_relevance_filter is False


class TestParallelSearchEngineClassAttributes:
    """Tests for ParallelSearchEngine class attributes."""

    def test_class_attributes_exist(self):
        """ParallelSearchEngine has expected class attributes."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        # Check that expected attributes exist
        assert hasattr(ParallelSearchEngine, "is_public")
        assert hasattr(ParallelSearchEngine, "is_generic")


class TestGlobalExecutorManagement:
    """Tests for global executor management."""

    def test_get_global_executor_returns_executor(self):
        """_get_global_executor returns a ThreadPoolExecutor."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            _get_global_executor,
        )

        executor = _get_global_executor()

        assert executor is not None

    def test_get_global_executor_same_instance(self):
        """_get_global_executor returns same instance on subsequent calls."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            _get_global_executor,
        )

        executor1 = _get_global_executor()
        executor2 = _get_global_executor()

        assert executor1 is executor2

    def test_shutdown_global_executor(self):
        """shutdown_global_executor shuts down the executor."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            _get_global_executor,
            shutdown_global_executor,
        )

        # Ensure executor exists
        executor = _get_global_executor()
        assert executor is not None

        # Shutdown
        shutdown_global_executor(wait=True)

        # Note: After shutdown, _global_executor should be None
        # but we can't directly test this due to module-level state


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

    def test_get_previews_returns_list(self):
        """_get_previews returns a list."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        # With no engines configured, should return empty list
        results = engine._get_previews("test query")

        assert isinstance(results, list)

    def test_get_previews_with_empty_engines(self):
        """_get_previews handles empty engines."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        # Should not raise even with no engines
        results = engine._get_previews("test query")

        assert results == []


class TestParallelSearchEngineRun:
    """Tests for run method."""

    def test_run_returns_list(self):
        """run method returns a list."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        results = engine.run("test query")

        assert isinstance(results, list)


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

    def test_get_full_content_returns_items(self):
        """_get_full_content returns the input items."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        items = [
            {"title": "Result 1", "url": "http://a.com", "snippet": "Text 1"},
            {"title": "Result 2", "url": "http://b.com", "snippet": "Text 2"},
        ]

        result = engine._get_full_content(items)

        # ParallelSearchEngine typically returns items as-is
        assert len(result) == 2


class TestParallelSearchEngineScientificMode:
    """Tests for scientific parallel search mode."""

    def test_creates_scientific_engine(self):
        """Can create scientific parallel search engine."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = create_search_engine(
            engine_name="parallel_scientific",
            llm=mock_llm,
            settings_snapshot=settings,
        )

        assert engine is not None
        assert "ParallelSearchEngine" in type(engine).__name__

    def test_creates_standard_parallel_engine(self):
        """Can create standard parallel search engine."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = create_search_engine(
            engine_name="parallel",
            llm=mock_llm,
            settings_snapshot=settings,
        )

        assert engine is not None
        assert "ParallelSearchEngine" in type(engine).__name__


class TestResolveApiKey:
    """Tests for the shared _resolve_api_key helper used by get_available_engines."""

    def test_returns_key_from_snapshot(self):
        """Returns API key found in settings snapshot."""
        from local_deep_research.web_search_engines.search_engines_config import (
            _resolve_api_key,
        )

        snapshot = {
            "search.engine.web.brave.api_key": {"value": "brave-key-123"},
        }
        result = _resolve_api_key("brave", {}, snapshot)
        assert result == "brave-key-123"

    def test_returns_key_from_engine_config_fallback(self):
        """Falls back to api_key in engine config dict."""
        from local_deep_research.web_search_engines.search_engines_config import (
            _resolve_api_key,
        )

        result = _resolve_api_key("brave", {"api_key": "config-key"}, {})
        assert result == "config-key"

    def test_returns_none_when_no_key(self):
        """Returns None when no API key is available."""
        from local_deep_research.web_search_engines.search_engines_config import (
            _resolve_api_key,
        )

        result = _resolve_api_key("brave", {}, {})
        assert result is None

    def test_rejects_placeholder_values(self):
        """Rejects common placeholder API key values."""
        from local_deep_research.web_search_engines.search_engines_config import (
            _resolve_api_key,
        )

        for placeholder in [
            "PLACEHOLDER",
            "YOUR_API_KEY_HERE",
            "null",
            "None",
            "YOUR_key",
            "BRAVE_API_KEY",
        ]:
            result = _resolve_api_key("test", {"api_key": placeholder}, {})
            assert result is None, f"Should reject placeholder: {placeholder}"

    def test_rejects_empty_and_whitespace(self):
        """Rejects empty or whitespace-only API keys."""
        from local_deep_research.web_search_engines.search_engines_config import (
            _resolve_api_key,
        )

        result = _resolve_api_key("test", {"api_key": "   "}, {})
        assert result is None


class TestGetSearchConfig:
    """Tests for _get_search_config method (now delegates to get_available_engines)."""

    def test_returns_empty_dict_without_settings_snapshot(self):
        """Returns empty dict when no settings snapshot."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=None, programmatic_mode=True
        )

        config = engine._get_search_config()

        assert config == {}


class TestSelectEngines:
    """Tests for select_engines method."""

    def test_returns_all_engines_without_llm(self):
        """Returns all available engines when no LLM."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=None, settings_snapshot=settings, programmatic_mode=True
        )
        engine.available_engines = ["engine1", "engine2", "engine3"]

        result = engine.select_engines("test query")

        assert len(result) <= engine.max_engines_to_select

    def test_returns_empty_when_no_available_engines(self):
        """Returns empty when no available engines."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )
        engine.available_engines = []

        result = engine.select_engines("test query")

        assert result == []

    def test_llm_selects_engines_from_list(self):
        """LLM selects engines from available list."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(content="[0, 1]")
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm,
            settings_snapshot=settings,
            programmatic_mode=True,
            include_generic_engines=False,
        )
        engine.available_engines = ["pubmed", "arxiv", "openalex"]

        result = engine.select_engines("cancer research")

        assert "pubmed" in result
        assert "arxiv" in result

    def test_handles_llm_error_gracefully(self):
        """Handles LLM error and returns fallback engines."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        mock_llm.invoke.side_effect = Exception("LLM error")
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )
        engine.available_engines = ["engine1", "engine2"]

        result = engine.select_engines("test query")

        # Should return some engines despite error
        assert isinstance(result, list)

    def test_handles_invalid_llm_response(self):
        """Handles invalid LLM JSON response."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(content="not valid json")
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )
        engine.available_engines = ["engine1", "engine2"]

        result = engine.select_engines("test query")

        # Should return fallback engines
        assert isinstance(result, list)


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

    def test_caches_engine_instances(self):
        """Engine instances are cached."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        mock_search_engine = Mock()
        with patch(
            "local_deep_research.web_search_engines.engines.parallel_search_engine.create_search_engine",
            return_value=mock_search_engine,
        ):
            # First call creates instance
            instance1 = engine._get_engine_instance("test_engine")
            # Second call returns cached instance
            instance2 = engine._get_engine_instance("test_engine")

            assert instance1 is instance2

    def test_returns_none_on_creation_error(self):
        """Returns None when engine creation fails."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        with patch(
            "local_deep_research.web_search_engines.engines.parallel_search_engine.create_search_engine",
            side_effect=Exception("Creation error"),
        ):
            result = engine._get_engine_instance("failing_engine")

            assert result is None


class TestExecuteSingleEngine:
    """Tests for _execute_single_engine method."""

    def test_returns_success_with_results(self):
        """Returns success result when engine returns results."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        mock_search_engine = Mock()
        mock_search_engine.run.return_value = [
            {"title": "Result 1", "link": "http://example.com"}
        ]
        engine.engine_cache["test_engine"] = mock_search_engine

        result = engine._execute_single_engine("test_engine", "test query")

        assert result["success"] is True
        assert result["engine"] == "test_engine"
        assert len(result["results"]) == 1

    def test_returns_failure_when_no_engine(self):
        """Returns failure when engine cannot be instantiated."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        with patch.object(engine, "_get_engine_instance", return_value=None):
            result = engine._execute_single_engine("missing_engine", "query")

            assert result["success"] is False
            assert "Failed to initialize" in result["error"]

    def test_returns_failure_when_no_results(self):
        """Returns failure when engine returns no results."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        mock_search_engine = Mock()
        mock_search_engine.run.return_value = []
        engine.engine_cache["test_engine"] = mock_search_engine

        result = engine._execute_single_engine("test_engine", "test query")

        assert result["success"] is False
        assert result["error"] == "No results"

    def test_handles_engine_exception(self):
        """Handles exception from engine execution."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        mock_search_engine = Mock()
        mock_search_engine.run.side_effect = Exception("Search error")
        engine.engine_cache["test_engine"] = mock_search_engine

        result = engine._execute_single_engine("test_engine", "test query")

        assert result["success"] is False
        assert "failed" in result["error"].lower()


class TestGetFullContentWithGrouping:
    """Tests for _get_full_content with engine grouping."""

    def test_groups_items_by_engine(self):
        """Groups items by their source engine."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {
            "search.max_results": {"value": 10},
            "search.snippets_only": {"value": False},
        }

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        # Create mock engines in cache
        mock_engine1 = Mock()
        mock_engine1._get_full_content.return_value = [
            {"title": "Result 1", "full_content": "Full 1"}
        ]
        mock_engine2 = Mock()
        mock_engine2._get_full_content.return_value = [
            {"title": "Result 2", "full_content": "Full 2"}
        ]

        engine.engine_cache["engine1"] = mock_engine1
        engine.engine_cache["engine2"] = mock_engine2

        items = [
            {"title": "Result 1", "search_engine": "engine1"},
            {"title": "Result 2", "search_engine": "engine2"},
        ]

        # Mock get_setting_from_snapshot to return False for snippets_only
        with patch(
            "local_deep_research.web_search_engines.engines.parallel_search_engine.get_setting_from_snapshot",
            return_value=False,
        ):
            engine._get_full_content(items)

            mock_engine1._get_full_content.assert_called_once()
            mock_engine2._get_full_content.assert_called_once()

    def test_handles_missing_engine_gracefully(self):
        """Returns items as-is when engine not in cache."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {
            "search.max_results": {"value": 10},
            "search.snippets_only": {"value": False},
        }

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        with patch.object(engine, "_get_engine_instance", return_value=None):
            items = [
                {"title": "Result 1", "search_engine": "missing_engine"},
            ]

            result = engine._get_full_content(items)

            assert len(result) == 1
            assert result[0]["title"] == "Result 1"


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

    def test_invoke_delegates_to_run(self):
        """Invoke method delegates to run."""
        from local_deep_research.web_search_engines.engines.parallel_search_engine import (
            ParallelSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        engine = ParallelSearchEngine(
            llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
        )

        with patch.object(
            engine, "run", return_value=[{"title": "Result"}]
        ) as mock_run:
            result = engine.invoke("test query")

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