"""
Gap tests for search_system.AdvancedSearchSystem.

Focuses on testable pure logic:
- Settings extraction for max_iterations / questions_per_iteration
  (dict-with-value vs plain value vs missing -> default)
- _perform_search settings extraction for llm_provider, llm_model, search_tool
- close() resource cleanup
- set_progress_callback propagation
- analyze_topic generates search_id when not provided
- all_links_of_system dedup (same-object identity check)
"""

from unittest.mock import Mock, patch

import pytest


# ---------------------------------------------------------------------------
# Factory fixture for AdvancedSearchSystem
# ---------------------------------------------------------------------------


@pytest.fixture
def make_system():
    """Factory fixture returning a function that builds an
    AdvancedSearchSystem with a mocked strategy.

    Returns ``(system, mock_strategy)`` so tests can inspect or mutate
    the strategy mock and assert on the system.
    """

    def _factory(
        settings_snapshot=None, strategy_name="source-based", **kwargs
    ):
        mock_llm = Mock()
        mock_search = Mock()

        mock_strategy = Mock()
        mock_strategy.questions_by_iteration = {}
        mock_strategy.all_links_of_system = []
        mock_strategy.set_progress_callback = Mock()
        mock_strategy.analyze_topic = Mock(
            return_value={
                "findings": [],
                "current_knowledge": "",
            }
        )

        with patch(
            "local_deep_research.search_system_factory.create_strategy",
            return_value=mock_strategy,
        ):
            from local_deep_research.search_system import AdvancedSearchSystem

            system = AdvancedSearchSystem(
                llm=mock_llm,
                search=mock_search,
                strategy_name=strategy_name,
                settings_snapshot=settings_snapshot,
                **kwargs,
            )
        return system, mock_strategy

    return _factory


# ---------------------------------------------------------------------------
# __init__ -- max_iterations extraction
# ---------------------------------------------------------------------------


class TestMaxIterationsExtraction:
    """AdvancedSearchSystem.__init__ reads max_iterations from settings."""

    def test_explicit_value_takes_precedence(self, make_system):
        system, _ = make_system(
            settings_snapshot={"search.iterations": {"value": 10}},
            max_iterations=5,
        )
        assert system.max_iterations == 5

    def test_dict_with_value_key(self, make_system):
        system, _ = make_system(
            settings_snapshot={"search.iterations": {"value": 7}},
        )
        assert system.max_iterations == 7

    def test_plain_value(self, make_system):
        system, _ = make_system(
            settings_snapshot={"search.iterations": 4},
        )
        assert system.max_iterations == 4

    def test_missing_defaults_to_1(self, make_system):
        system, _ = make_system(settings_snapshot={})
        assert system.max_iterations == 1

    def test_none_snapshot_defaults_to_1(self, make_system):
        system, _ = make_system(settings_snapshot=None)
        assert system.max_iterations == 1


# ---------------------------------------------------------------------------
# __init__ -- questions_per_iteration extraction
# ---------------------------------------------------------------------------


class TestQuestionsPerIterationExtraction:
    """AdvancedSearchSystem.__init__ reads questions_per_iteration from settings."""

    def test_explicit_value_takes_precedence(self, make_system):
        system, _ = make_system(
            settings_snapshot={"search.questions_per_iteration": {"value": 10}},
            questions_per_iteration=2,
        )
        assert system.questions_per_iteration == 2

    def test_dict_with_value_key(self, make_system):
        system, _ = make_system(
            settings_snapshot={"search.questions_per_iteration": {"value": 5}},
        )
        assert system.questions_per_iteration == 5

    def test_plain_value(self, make_system):
        system, _ = make_system(
            settings_snapshot={"search.questions_per_iteration": 8},
        )
        assert system.questions_per_iteration == 8

    def test_missing_defaults_to_3(self, make_system):
        system, _ = make_system(settings_snapshot={})
        assert system.questions_per_iteration == 3


# ---------------------------------------------------------------------------
# _perform_search -- settings extraction for progress messages
# ---------------------------------------------------------------------------


class TestPerformSearchSettingsExtraction:
    """_perform_search extracts provider/model/tool from settings snapshot."""

    def _run_perform_search(self, system):
        """Run _perform_search capturing progress callback calls."""
        calls = []
        system.progress_callback = lambda msg, pct, meta: calls.append(
            (msg, pct, meta)
        )
        with patch(
            "local_deep_research.news.core.search_integration.NewsSearchCallback",
        ):
            system._perform_search("q", "id1", True, False, "user1")
        return calls

    def test_extracts_from_dict_with_value(self, make_system):
        system, _ = make_system(
            settings_snapshot={
                "llm.provider": {"value": "openai"},
                "llm.model": {"value": "gpt-4"},
                "search.tool": {"value": "google"},
            },
        )
        calls = self._run_perform_search(system)
        assert any("openai" in c[0] or "gpt-4" in c[0] for c in calls)

    def test_extracts_plain_values(self, make_system):
        system, _ = make_system(
            settings_snapshot={
                "llm.provider": "anthropic",
                "llm.model": "claude",
                "search.tool": "searxng",
            },
        )
        calls = self._run_perform_search(system)
        assert any("anthropic" in c[0] or "claude" in c[0] for c in calls)

    def test_missing_settings_use_unknown(self, make_system):
        system, _ = make_system(settings_snapshot={})
        calls = self._run_perform_search(system)
        assert any("unknown" in c[0] for c in calls)

    def test_search_tool_default_from_dict(self, make_system):
        """search.tool dict without 'value' key defaults to 'searxng'."""
        system, _ = make_system(
            settings_snapshot={"search.tool": {"other": "x"}},
        )
        calls = self._run_perform_search(system)
        # Should use the .get("value", "searxng") default
        assert any("searxng" in c[0] for c in calls)


# ---------------------------------------------------------------------------
# all_links_of_system -- identity-based dedup
# ---------------------------------------------------------------------------


class TestAllLinksDedup:
    """_perform_search avoids extending all_links when objects are same."""

    def test_same_object_no_duplication(self, make_system):
        """When strategy.all_links_of_system IS system.all_links_of_system,
        no extension should happen (prevents issue #301)."""
        system, mock_strategy = make_system(settings_snapshot={})

        # Make them the same object
        mock_strategy.all_links_of_system = system.all_links_of_system
        system.all_links_of_system.append({"link": "https://a.com"})

        with patch(
            "local_deep_research.news.core.search_integration.NewsSearchCallback",
        ):
            system._perform_search("q", "id1", True, False, "user1")

        # Should NOT be doubled
        assert len(system.all_links_of_system) == 1

    def test_different_objects_extends(self, make_system):
        """When they are different objects, links should be extended."""
        system, mock_strategy = make_system(settings_snapshot={})

        mock_strategy.all_links_of_system = [{"link": "https://b.com"}]

        with patch(
            "local_deep_research.news.core.search_integration.NewsSearchCallback",
        ):
            system._perform_search("q", "id1", True, False, "user1")

        assert {"link": "https://b.com"} in system.all_links_of_system


# ---------------------------------------------------------------------------
# close() -- resource cleanup
# ---------------------------------------------------------------------------


class TestClose:
    """AdvancedSearchSystem.close cascades to strategy."""

    def test_close_calls_safe_close_on_strategy(self, make_system):
        system, mock_strategy = make_system(settings_snapshot={})

        with patch(
            "local_deep_research.utilities.resource_utils.safe_close"
        ) as mock_safe_close:
            system.close()
            mock_safe_close.assert_called_once_with(
                mock_strategy, "search strategy"
            )

    def test_close_without_strategy_attribute(self, make_system):
        """close() should not raise if strategy attribute is missing."""
        system, _ = make_system(settings_snapshot={})
        del system.strategy

        with patch(
            "local_deep_research.utilities.resource_utils.safe_close"
        ) as mock_safe_close:
            system.close()  # should not raise
            mock_safe_close.assert_not_called()


# ---------------------------------------------------------------------------
# set_progress_callback
# ---------------------------------------------------------------------------


class TestSetProgressCallback:
    def test_callback_set_on_system_and_strategy(self, make_system):
        system, mock_strategy = make_system(settings_snapshot={})

        cb = Mock()
        system.set_progress_callback(cb)

        assert system.progress_callback is cb
        mock_strategy.set_progress_callback.assert_called_with(cb)


# ---------------------------------------------------------------------------
# analyze_topic -- search_id auto-generation
# ---------------------------------------------------------------------------


class TestAnalyzeTopicSearchId:
    def test_generates_uuid_when_search_id_is_none(self, make_system):
        system, _ = make_system(settings_snapshot={})

        with patch.object(
            system, "_perform_search", return_value={}
        ) as mock_ps:
            system.analyze_topic("test query")
            args = mock_ps.call_args
            search_id = args[0][1]  # second positional arg
            # Should be a valid UUID string (36 chars with hyphens)
            assert len(search_id) == 36
            assert search_id.count("-") == 4

    def test_uses_provided_search_id(self, make_system):
        system, _ = make_system(settings_snapshot={})

        with patch.object(
            system, "_perform_search", return_value={}
        ) as mock_ps:
            system.analyze_topic("test query", search_id="custom-id")
            args = mock_ps.call_args
            search_id = args[0][1]
            assert search_id == "custom-id"


# ---------------------------------------------------------------------------
# programmatic_mode flag
# ---------------------------------------------------------------------------


class TestProgrammaticMode:
    def test_programmatic_mode_stored(self, make_system):
        system, _ = make_system(settings_snapshot={}, programmatic_mode=True)
        assert system.programmatic_mode is True

    def test_non_programmatic_mode_default(self, make_system):
        system, _ = make_system(settings_snapshot={})
        assert system.programmatic_mode is False
