"""
Tests for search_system.py - Link Deduplication and Settings Extraction

Tests cover:
- Link deduplication using object identity (id())
- Settings extraction from snapshot dictionaries

These tests address issue #301: "too many links in detailed report mode"
"""

from unittest.mock import MagicMock, Mock, patch

import pytest


class TestLinkDeduplication:
    """Tests for link deduplication behavior."""

    @pytest.fixture
    def mock_llm(self):
        """Create a mock LLM."""
        mock = MagicMock()
        mock.invoke.return_value = MagicMock(content="test response")
        return mock

    @pytest.fixture
    def mock_search_engine(self):
        """Create a mock search engine."""
        mock = MagicMock()
        mock.run.return_value = []
        return mock

    def _create_system(
        self, mock_llm, mock_search_engine, mock_strategy, **kwargs
    ):
        """Helper to create an AdvancedSearchSystem with mocked dependencies."""
        with patch(
            "local_deep_research.search_system_factory.create_strategy"
        ) as mock_create:
            mock_create.return_value = mock_strategy

            with patch(
                "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler"
            ) as mock_citation:
                mock_citation.return_value = Mock(
                    _create_documents=Mock(), _format_sources=Mock()
                )

                from local_deep_research.search_system import (
                    AdvancedSearchSystem,
                )

                system = AdvancedSearchSystem(
                    llm=mock_llm,
                    search=mock_search_engine,
                    strategy_name="standard",
                    **kwargs,
                )
                return system

    def test_same_list_object_not_duplicated(
        self, mock_llm, mock_search_engine
    ):
        """When lists are same object, don't extend."""
        mock_strategy = MagicMock()
        shared_links = [{"title": "Link1", "url": "http://example.com"}]
        mock_strategy.all_links_of_system = shared_links
        mock_strategy.questions_by_iteration = []
        mock_strategy.analyze_topic.return_value = {
            "current_knowledge": "test",
            "query": "test query",
        }

        with patch(
            "local_deep_research.search_system_factory.create_strategy"
        ) as mock_create:
            mock_create.return_value = mock_strategy

            with patch(
                "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler"
            ) as mock_citation:
                mock_citation.return_value = Mock(
                    _create_documents=Mock(), _format_sources=Mock()
                )

                from local_deep_research.search_system import (
                    AdvancedSearchSystem,
                )

                system = AdvancedSearchSystem(
                    llm=mock_llm,
                    search=mock_search_engine,
                    strategy_name="standard",
                )

                # Make the system's list the SAME object as strategy's list
                system.all_links_of_system = shared_links

                # Perform search
                system.analyze_topic("test query")

                # Links should NOT be duplicated
                # Before the fix, this would double the list
                assert len(system.all_links_of_system) == 1

    def test_different_list_objects_extended(
        self, mock_llm, mock_search_engine
    ):
        """Different objects get extended."""
        mock_strategy = MagicMock()
        strategy_links = [{"title": "Link1", "url": "http://example.com"}]
        mock_strategy.all_links_of_system = strategy_links
        mock_strategy.questions_by_iteration = []
        mock_strategy.analyze_topic.return_value = {
            "current_knowledge": "test",
            "query": "test query",
        }

        with patch(
            "local_deep_research.search_system_factory.create_strategy"
        ) as mock_create:
            mock_create.return_value = mock_strategy

            with patch(
                "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler"
            ) as mock_citation:
                mock_citation.return_value = Mock(
                    _create_documents=Mock(), _format_sources=Mock()
                )

                from local_deep_research.search_system import (
                    AdvancedSearchSystem,
                )

                system = AdvancedSearchSystem(
                    llm=mock_llm,
                    search=mock_search_engine,
                    strategy_name="standard",
                )

                # System has a DIFFERENT list object
                system.all_links_of_system = []

                system.analyze_topic("test query")

                # Links should be extended from strategy to system
                assert len(system.all_links_of_system) == 1

    def test_empty_strategy_links(self, mock_llm, mock_search_engine):
        """Empty strategy links don't cause errors."""
        mock_strategy = MagicMock()
        mock_strategy.all_links_of_system = []
        mock_strategy.questions_by_iteration = []
        mock_strategy.analyze_topic.return_value = {
            "current_knowledge": "test",
            "query": "test query",
        }

        with patch(
            "local_deep_research.search_system_factory.create_strategy"
        ) as mock_create:
            mock_create.return_value = mock_strategy

            with patch(
                "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler"
            ) as mock_citation:
                mock_citation.return_value = Mock(
                    _create_documents=Mock(), _format_sources=Mock()
                )

                from local_deep_research.search_system import (
                    AdvancedSearchSystem,
                )

                system = AdvancedSearchSystem(
                    llm=mock_llm,
                    search=mock_search_engine,
                    strategy_name="standard",
                )

                initial_links = [
                    {"title": "Existing", "url": "http://existing.com"}
                ]
                system.all_links_of_system = initial_links

                system.analyze_topic("test query")

                # Existing links should remain, nothing added
                assert len(system.all_links_of_system) == 1
                assert system.all_links_of_system[0]["title"] == "Existing"

    def test_large_link_list_performance(self, mock_llm, mock_search_engine):
        """1000+ links don't cause memory issues."""
        mock_strategy = MagicMock()
        large_links = [
            {"title": f"Link{i}", "url": f"http://example{i}.com"}
            for i in range(1000)
        ]
        mock_strategy.all_links_of_system = large_links
        mock_strategy.questions_by_iteration = []
        mock_strategy.analyze_topic.return_value = {
            "current_knowledge": "test",
            "query": "test query",
        }

        with patch(
            "local_deep_research.search_system_factory.create_strategy"
        ) as mock_create:
            mock_create.return_value = mock_strategy

            with patch(
                "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler"
            ) as mock_citation:
                mock_citation.return_value = Mock(
                    _create_documents=Mock(), _format_sources=Mock()
                )

                from local_deep_research.search_system import (
                    AdvancedSearchSystem,
                )

                system = AdvancedSearchSystem(
                    llm=mock_llm,
                    search=mock_search_engine,
                    strategy_name="standard",
                )

                system.all_links_of_system = []

                system.analyze_topic("test query")

                assert len(system.all_links_of_system) == 1000

    def test_link_dedup_preserves_order(self, mock_llm, mock_search_engine):
        """Link order preserved after dedup."""
        mock_strategy = MagicMock()
        ordered_links = [
            {"title": "First", "url": "http://first.com"},
            {"title": "Second", "url": "http://second.com"},
            {"title": "Third", "url": "http://third.com"},
        ]
        mock_strategy.all_links_of_system = ordered_links
        mock_strategy.questions_by_iteration = []
        mock_strategy.analyze_topic.return_value = {
            "current_knowledge": "test",
            "query": "test query",
        }

        with patch(
            "local_deep_research.search_system_factory.create_strategy"
        ) as mock_create:
            mock_create.return_value = mock_strategy

            with patch(
                "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler"
            ) as mock_citation:
                mock_citation.return_value = Mock(
                    _create_documents=Mock(), _format_sources=Mock()
                )

                from local_deep_research.search_system import (
                    AdvancedSearchSystem,
                )

                system = AdvancedSearchSystem(
                    llm=mock_llm,
                    search=mock_search_engine,
                    strategy_name="standard",
                )

                system.all_links_of_system = []

                system.analyze_topic("test query")

                assert system.all_links_of_system[0]["title"] == "First"
                assert system.all_links_of_system[1]["title"] == "Second"
                assert system.all_links_of_system[2]["title"] == "Third"


class TestSettingsExtraction:
    """Tests for settings extraction from snapshot."""

    @pytest.fixture
    def mock_llm(self):
        """Create a mock LLM."""
        mock = MagicMock()
        mock.invoke.return_value = MagicMock(content="test response")
        return mock

    @pytest.fixture
    def mock_search_engine(self):
        """Create a mock search engine."""
        mock = MagicMock()
        mock.run.return_value = []
        return mock

    def test_settings_dict_value_format(self, mock_llm, mock_search_engine):
        """{'value': 'actual'} extracts correctly."""
        settings = {
            "search.iterations": {"value": 5},
            "search.questions_per_iteration": {"value": 4},
        }

        with patch(
            "local_deep_research.search_system_factory.create_strategy"
        ) as mock_create:
            mock_strategy = MagicMock()
            mock_strategy.questions_by_iteration = []
            mock_strategy.all_links_of_system = []
            mock_create.return_value = mock_strategy

            with patch(
                "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler"
            ) as mock_citation:
                mock_citation.return_value = Mock(
                    _create_documents=Mock(), _format_sources=Mock()
                )

                from local_deep_research.search_system import (
                    AdvancedSearchSystem,
                )

                system = AdvancedSearchSystem(
                    llm=mock_llm,
                    search=mock_search_engine,
                    strategy_name="standard",
                    settings_snapshot=settings,
                )

                assert system.max_iterations == 5
                assert system.questions_per_iteration == 4

    def test_settings_direct_value_format(self, mock_llm, mock_search_engine):
        """Direct values work."""
        settings = {
            "search.iterations": 3,
            "search.questions_per_iteration": 2,
        }

        with patch(
            "local_deep_research.search_system_factory.create_strategy"
        ) as mock_create:
            mock_strategy = MagicMock()
            mock_strategy.questions_by_iteration = []
            mock_strategy.all_links_of_system = []
            mock_create.return_value = mock_strategy

            with patch(
                "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler"
            ) as mock_citation:
                mock_citation.return_value = Mock(
                    _create_documents=Mock(), _format_sources=Mock()
                )

                from local_deep_research.search_system import (
                    AdvancedSearchSystem,
                )

                system = AdvancedSearchSystem(
                    llm=mock_llm,
                    search=mock_search_engine,
                    strategy_name="standard",
                    settings_snapshot=settings,
                )

                assert system.max_iterations == 3
                assert system.questions_per_iteration == 2

    def test_missing_settings_use_defaults(self, mock_llm, mock_search_engine):
        """Missing settings get defaults."""
        settings = {}  # Empty settings

        with patch(
            "local_deep_research.search_system_factory.create_strategy"
        ) as mock_create:
            mock_strategy = MagicMock()
            mock_strategy.questions_by_iteration = []
            mock_strategy.all_links_of_system = []
            mock_create.return_value = mock_strategy

            with patch(
                "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler"
            ) as mock_citation:
                mock_citation.return_value = Mock(
                    _create_documents=Mock(), _format_sources=Mock()
                )

                from local_deep_research.search_system import (
                    AdvancedSearchSystem,
                )

                system = AdvancedSearchSystem(
                    llm=mock_llm,
                    search=mock_search_engine,
                    strategy_name="standard",
                    settings_snapshot=settings,
                )

                # Defaults: iterations=1, questions_per_iteration=3
                assert system.max_iterations == 1
                assert system.questions_per_iteration == 3

    def test_partial_settings_snapshot(self, mock_llm, mock_search_engine):
        """Some present, some missing."""
        settings = {
            "search.iterations": {"value": 7},
            # questions_per_iteration is missing
        }

        with patch(
            "local_deep_research.search_system_factory.create_strategy"
        ) as mock_create:
            mock_strategy = MagicMock()
            mock_strategy.questions_by_iteration = []
            mock_strategy.all_links_of_system = []
            mock_create.return_value = mock_strategy

            with patch(
                "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler"
            ) as mock_citation:
                mock_citation.return_value = Mock(
                    _create_documents=Mock(), _format_sources=Mock()
                )

                from local_deep_research.search_system import (
                    AdvancedSearchSystem,
                )

                system = AdvancedSearchSystem(
                    llm=mock_llm,
                    search=mock_search_engine,
                    strategy_name="standard",
                    settings_snapshot=settings,
                )

                assert system.max_iterations == 7
                assert system.questions_per_iteration == 3  # Default

    def test_nested_settings_structure(self, mock_llm, mock_search_engine):
        """Deeply nested dicts."""
        # The code only checks for {'value': ...} at one level
        settings = {
            "search.iterations": {"value": 2, "extra": {"nested": "data"}},
        }

        with patch(
            "local_deep_research.search_system_factory.create_strategy"
        ) as mock_create:
            mock_strategy = MagicMock()
            mock_strategy.questions_by_iteration = []
            mock_strategy.all_links_of_system = []
            mock_create.return_value = mock_strategy

            with patch(
                "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler"
            ) as mock_citation:
                mock_citation.return_value = Mock(
                    _create_documents=Mock(), _format_sources=Mock()
                )

                from local_deep_research.search_system import (
                    AdvancedSearchSystem,
                )

                system = AdvancedSearchSystem(
                    llm=mock_llm,
                    search=mock_search_engine,
                    strategy_name="standard",
                    settings_snapshot=settings,
                )

                assert system.max_iterations == 2

    def test_none_settings_snapshot(self, mock_llm, mock_search_engine):
        """None snapshot uses all defaults."""
        with patch(
            "local_deep_research.search_system_factory.create_strategy"
        ) as mock_create:
            mock_strategy = MagicMock()
            mock_strategy.questions_by_iteration = []
            mock_strategy.all_links_of_system = []
            mock_create.return_value = mock_strategy

            with patch(
                "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler"
            ) as mock_citation:
                mock_citation.return_value = Mock(
                    _create_documents=Mock(), _format_sources=Mock()
                )

                from local_deep_research.search_system import (
                    AdvancedSearchSystem,
                )

                system = AdvancedSearchSystem(
                    llm=mock_llm,
                    search=mock_search_engine,
                    strategy_name="standard",
                    settings_snapshot=None,
                )

                assert system.max_iterations == 1
                assert system.questions_per_iteration == 3

    def test_empty_settings_snapshot(self, mock_llm, mock_search_engine):
        """Empty dict uses all defaults."""
        with patch(
            "local_deep_research.search_system_factory.create_strategy"
        ) as mock_create:
            mock_strategy = MagicMock()
            mock_strategy.questions_by_iteration = []
            mock_strategy.all_links_of_system = []
            mock_create.return_value = mock_strategy

            with patch(
                "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler"
            ) as mock_citation:
                mock_citation.return_value = Mock(
                    _create_documents=Mock(), _format_sources=Mock()
                )

                from local_deep_research.search_system import (
                    AdvancedSearchSystem,
                )

                system = AdvancedSearchSystem(
                    llm=mock_llm,
                    search=mock_search_engine,
                    strategy_name="standard",
                    settings_snapshot={},
                )

                assert system.max_iterations == 1
                assert system.questions_per_iteration == 3


class TestProgressCallback:
    """Tests for progress callback functionality."""

    @pytest.fixture
    def mock_llm(self):
        """Create a mock LLM."""
        mock = MagicMock()
        return mock

    @pytest.fixture
    def mock_search_engine(self):
        """Create a mock search engine."""
        return MagicMock()

    def test_progress_callback_set_on_strategy(
        self, mock_llm, mock_search_engine
    ):
        """Progress callback is set on strategy."""
        with patch(
            "local_deep_research.search_system_factory.create_strategy"
        ) as mock_create:
            mock_strategy = MagicMock()
            mock_strategy.questions_by_iteration = []
            mock_strategy.all_links_of_system = []
            mock_create.return_value = mock_strategy

            with patch(
                "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler"
            ) as mock_citation:
                mock_citation.return_value = Mock(
                    _create_documents=Mock(), _format_sources=Mock()
                )

                from local_deep_research.search_system import (
                    AdvancedSearchSystem,
                )

                system = AdvancedSearchSystem(
                    llm=mock_llm,
                    search=mock_search_engine,
                    strategy_name="standard",
                )

                callback = MagicMock()
                system.set_progress_callback(callback)

                mock_strategy.set_progress_callback.assert_called_with(callback)

    def test_progress_callback_receives_updates(
        self, mock_llm, mock_search_engine
    ):
        """Progress callback receives progress updates during search."""
        with patch(
            "local_deep_research.search_system_factory.create_strategy"
        ) as mock_create:
            mock_strategy = MagicMock()
            mock_strategy.all_links_of_system = []
            mock_strategy.questions_by_iteration = []
            mock_strategy.analyze_topic.return_value = {
                "current_knowledge": "test",
                "query": "test query",
            }
            mock_create.return_value = mock_strategy

            with patch(
                "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler"
            ) as mock_citation:
                mock_citation.return_value = Mock(
                    _create_documents=Mock(), _format_sources=Mock()
                )

                from local_deep_research.search_system import (
                    AdvancedSearchSystem,
                )

                system = AdvancedSearchSystem(
                    llm=mock_llm,
                    search=mock_search_engine,
                    strategy_name="standard",
                    settings_snapshot={
                        "llm.provider": {"value": "test_provider"},
                        "llm.model": {"value": "test_model"},
                        "search.tool": {"value": "test_tool"},
                    },
                )

                callback = MagicMock()
                system.set_progress_callback(callback)

                system.analyze_topic("test query")

                # Callback should have been called during search
                assert callback.called
