"""
Tests for search_system_factory.py

Tests cover:
- _get_setting helper function (all edge cases)
- create_strategy factory function (all strategy types)
- Strategy name normalization (case-insensitive, alternative forms)
- kwargs pass-through and settings_snapshot forwarding
- Focused-iteration special behaviors (zero-to-None, flexible generator)
- Iterative-refinement recursive create_strategy call
- Unknown strategy fallback with warning
"""

from unittest.mock import MagicMock, Mock, patch

import pytest
from langchain_core.language_models import BaseChatModel

from local_deep_research.search_system_factory import (
    _get_setting,
    create_strategy,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def mock_model():
    """Return a MagicMock with BaseChatModel spec."""
    return MagicMock(spec=BaseChatModel)


@pytest.fixture
def mock_search():
    """Return a plain MagicMock for the search engine."""
    return MagicMock()


# ===========================================================================
# _get_setting tests
# ===========================================================================


class TestGetSetting:
    """Tests for _get_setting helper function."""

    def test_returns_default_when_snapshot_is_none(self):
        assert _get_setting(None, "any.key", "default_val") == "default_val"

    def test_returns_default_when_snapshot_is_empty_dict(self):
        # Empty dict is falsy; the guard `if not settings_snapshot` catches it.
        assert _get_setting({}, "missing.key", 42) == 42

    def test_returns_default_when_key_not_in_snapshot(self):
        snapshot = {"other.key": "other_val"}
        assert _get_setting(snapshot, "missing.key", "fallback") == "fallback"

    def test_returns_raw_value_when_value_is_not_dict(self):
        snapshot = {"my.key": "plain_string"}
        assert _get_setting(snapshot, "my.key", "default") == "plain_string"

    def test_returns_integer_value_directly(self):
        snapshot = {"iterations": 7}
        assert _get_setting(snapshot, "iterations", 10) == 7

    def test_returns_boolean_value_directly(self):
        snapshot = {"enabled": True}
        assert _get_setting(snapshot, "enabled", False) is True

    def test_extracts_value_from_dict_with_value_key(self):
        snapshot = {"my.key": {"value": "nested_result", "type": "string"}}
        assert _get_setting(snapshot, "my.key", "default") == "nested_result"

    def test_returns_dict_itself_when_no_value_key(self):
        inner = {"type": "string", "description": "no value key here"}
        snapshot = {"my.key": inner}
        assert _get_setting(snapshot, "my.key", "default") == inner

    def test_extracts_none_from_value_key(self):
        """Even if value['value'] is None, it should be returned (not default)."""
        snapshot = {"my.key": {"value": None}}
        assert _get_setting(snapshot, "my.key", "default") is None

    def test_extracts_zero_from_value_key(self):
        """Zero is a valid value and should not be replaced by default."""
        snapshot = {"limit": {"value": 0}}
        assert _get_setting(snapshot, "limit", 10) == 0

    def test_returns_list_value_directly(self):
        snapshot = {"tags": ["a", "b", "c"]}
        assert _get_setting(snapshot, "tags", []) == ["a", "b", "c"]


# ===========================================================================
# create_strategy tests – individual strategies
# ===========================================================================

# Helpers: common patch paths
_STRAT_BASE = "local_deep_research.advanced_search_system.strategies"


class TestCreateStrategySourceBased:
    """Tests for source-based strategy and its name variants."""

    PATCH_PATH = (
        f"{_STRAT_BASE}.source_based_strategy.SourceBasedSearchStrategy"
    )

    @pytest.mark.parametrize(
        "name",
        ["source-based", "source_based", "source_based_search"],
    )
    def test_all_name_variants_create_source_based(
        self, name, mock_model, mock_search
    ):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name=name, model=mock_model, search=mock_search
            )
            cls.assert_called_once()
            assert result == cls.return_value

    def test_kwargs_passed_through(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="source-based",
                model=mock_model,
                search=mock_search,
                include_text_content=False,
                use_cross_engine_filter=False,
                use_atomic_facts=True,
                search_original_query=False,
            )
            kw = cls.call_args[1]
            assert kw["include_text_content"] is False
            assert kw["use_cross_engine_filter"] is False
            assert kw["use_atomic_facts"] is True
            assert kw["search_original_query"] is False

    def test_settings_snapshot_forwarded(self, mock_model, mock_search):
        snapshot = {"some.setting": 123}
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="source-based",
                model=mock_model,
                search=mock_search,
                settings_snapshot=snapshot,
            )
            kw = cls.call_args[1]
            assert kw["settings_snapshot"] is snapshot


class TestCreateStrategyFocusedIteration:
    """Tests for focused-iteration strategy and its special behaviors."""

    PATCH_PATH = (
        f"{_STRAT_BASE}.focused_iteration_strategy.FocusedIterationStrategy"
    )

    @pytest.mark.parametrize("name", ["focused-iteration", "focused_iteration"])
    def test_name_variants(self, name, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name=name, model=mock_model, search=mock_search
            )
            cls.assert_called_once()
            assert result == cls.return_value

    def test_knowledge_limit_zero_converts_to_none(
        self, mock_model, mock_search
    ):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="focused-iteration",
                model=mock_model,
                search=mock_search,
                knowledge_summary_limit=0,
            )
            kw = cls.call_args[1]
            assert kw["knowledge_summary_limit"] is None

    def test_snippet_truncate_zero_converts_to_none(
        self, mock_model, mock_search
    ):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="focused-iteration",
                model=mock_model,
                search=mock_search,
                knowledge_snippet_truncate=0,
            )
            kw = cls.call_args[1]
            assert kw["knowledge_snippet_truncate"] is None

    def test_prompt_knowledge_truncate_zero_converts_to_none(
        self, mock_model, mock_search
    ):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="focused-iteration",
                model=mock_model,
                search=mock_search,
                prompt_knowledge_truncate=0,
            )
            kw = cls.call_args[1]
            assert kw["prompt_knowledge_truncate"] is None

    def test_previous_searches_limit_zero_converts_to_none(
        self, mock_model, mock_search
    ):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="focused-iteration",
                model=mock_model,
                search=mock_search,
                previous_searches_limit=0,
            )
            kw = cls.call_args[1]
            assert kw["previous_searches_limit"] is None

    def test_nonzero_limits_stay_unchanged(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="focused-iteration",
                model=mock_model,
                search=mock_search,
                knowledge_summary_limit=5,
                knowledge_snippet_truncate=200,
            )
            kw = cls.call_args[1]
            assert kw["knowledge_summary_limit"] == 5
            assert kw["knowledge_snippet_truncate"] == 200

    def test_flexible_question_generator_overrides(
        self, mock_model, mock_search
    ):
        flex_gen_path = "local_deep_research.advanced_search_system.questions.flexible_browsecomp_question.FlexibleBrowseCompQuestionGenerator"
        with patch(self.PATCH_PATH) as cls, patch(flex_gen_path) as flex_cls:
            strategy_instance = Mock()
            cls.return_value = strategy_instance
            flex_cls.return_value = Mock()

            create_strategy(
                strategy_name="focused-iteration",
                model=mock_model,
                search=mock_search,
                question_generator="flexible",
            )

            flex_cls.assert_called_once()
            # The generator is assigned to the strategy's question_generator
            assert strategy_instance.question_generator == flex_cls.return_value

    def test_non_flexible_generator_does_not_override(
        self, mock_model, mock_search
    ):
        flex_gen_path = "local_deep_research.advanced_search_system.questions.flexible_browsecomp_question.FlexibleBrowseCompQuestionGenerator"
        with patch(self.PATCH_PATH) as cls, patch(flex_gen_path) as flex_cls:
            strategy_instance = Mock()
            cls.return_value = strategy_instance

            create_strategy(
                strategy_name="focused-iteration",
                model=mock_model,
                search=mock_search,
                question_generator="browsecomp",
            )

            flex_cls.assert_not_called()

    def test_settings_snapshot_read_for_focused_iteration(
        self, mock_model, mock_search
    ):
        settings = {
            "focused_iteration.adaptive_questions": 1,
            "focused_iteration.knowledge_summary_limit": 20,
            "focused_iteration.snippet_truncate": 300,
        }
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="focused-iteration",
                model=mock_model,
                search=mock_search,
                settings_snapshot=settings,
            )
            kw = cls.call_args[1]
            assert kw["enable_adaptive_questions"] is True
            assert kw["knowledge_summary_limit"] == 20
            assert kw["knowledge_snippet_truncate"] == 300

    def test_kwargs_override_settings_snapshot(self, mock_model, mock_search):
        settings = {
            "focused_iteration.knowledge_summary_limit": 20,
        }
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="focused-iteration",
                model=mock_model,
                search=mock_search,
                settings_snapshot=settings,
                knowledge_summary_limit=99,
            )
            kw = cls.call_args[1]
            assert kw["knowledge_summary_limit"] == 99


class TestCreateStrategyFocusedIterationStandard:
    """Tests for focused-iteration-standard (with standard citation handler)."""

    PATCH_PATH = (
        f"{_STRAT_BASE}.focused_iteration_strategy.FocusedIterationStrategy"
    )
    CITATION_PATH = "local_deep_research.citation_handler.CitationHandler"

    @pytest.mark.parametrize(
        "name",
        ["focused-iteration-standard", "focused_iteration_standard"],
    )
    def test_name_variants(self, name, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls, patch(self.CITATION_PATH):
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name=name, model=mock_model, search=mock_search
            )
            cls.assert_called_once()
            assert result == cls.return_value

    def test_citation_handler_created_with_standard_type(
        self, mock_model, mock_search
    ):
        with (
            patch(self.PATCH_PATH) as cls,
            patch(self.CITATION_PATH) as cite_cls,
        ):
            cls.return_value = Mock()
            create_strategy(
                strategy_name="focused-iteration-standard",
                model=mock_model,
                search=mock_search,
            )
            cite_cls.assert_called_once()
            cite_kw = cite_cls.call_args
            assert cite_kw[1]["handler_type"] == "standard"


class TestCreateStrategyNews:
    """Tests for news strategy."""

    PATCH_PATH = f"{_STRAT_BASE}.news_strategy.NewsAggregationStrategy"

    @pytest.mark.parametrize(
        "name", ["news", "news_aggregation", "news-aggregation"]
    )
    def test_name_variants(self, name, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name=name, model=mock_model, search=mock_search
            )
            cls.assert_called_once()
            assert result == cls.return_value


# ===========================================================================
# Case-insensitivity tests
# ===========================================================================


class TestCaseInsensitivity:
    """Strategy name matching is case-insensitive via .lower()."""

    def test_uppercase_topic_organization(self, mock_model, mock_search):
        with patch(
            f"{_STRAT_BASE}.topic_organization_strategy.TopicOrganizationStrategy"
        ) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="TOPIC-ORGANIZATION",
                model=mock_model,
                search=mock_search,
            )
            cls.assert_called_once()

    def test_mixed_case_source_based(self, mock_model, mock_search):
        with patch(
            f"{_STRAT_BASE}.source_based_strategy.SourceBasedSearchStrategy"
        ) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="Source-Based",
                model=mock_model,
                search=mock_search,
            )
            cls.assert_called_once()

    def test_uppercase_news(self, mock_model, mock_search):
        with patch(
            f"{_STRAT_BASE}.news_strategy.NewsAggregationStrategy"
        ) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="NEWS",
                model=mock_model,
                search=mock_search,
            )
            cls.assert_called_once()

    def test_mixed_case_focused_iteration(self, mock_model, mock_search):
        with patch(
            f"{_STRAT_BASE}.focused_iteration_strategy.FocusedIterationStrategy"
        ) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="Focused-Iteration",
                model=mock_model,
                search=mock_search,
            )
            cls.assert_called_once()


# ===========================================================================
# Unknown strategy fallback
# ===========================================================================


class TestUnknownStrategyFallback:
    """Unknown strategy names fall back to SourceBasedSearchStrategy."""

    PATCH_PATH = (
        f"{_STRAT_BASE}.source_based_strategy.SourceBasedSearchStrategy"
    )

    def test_unknown_strategy_falls_back_to_source_based(
        self, mock_model, mock_search
    ):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name="unknown-strategy-xyz",
                model=mock_model,
                search=mock_search,
            )
            cls.assert_called_once()
            assert result == cls.return_value

    def test_unknown_strategy_logs_warning(self, mock_model, mock_search):
        with (
            patch(self.PATCH_PATH) as cls,
            patch(
                "local_deep_research.search_system_factory.logger"
            ) as mock_logger,
        ):
            cls.return_value = Mock()
            create_strategy(
                strategy_name="unknown-strategy-xyz",
                model=mock_model,
                search=mock_search,
            )
            mock_logger.warning.assert_called_once()
            warning_msg = mock_logger.warning.call_args[0][0]
            assert "unknown-strategy-xyz" in warning_msg.lower()

    def test_another_unknown_name(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="totally-made-up",
                model=mock_model,
                search=mock_search,
            )
            cls.assert_called_once()

    def test_fallback_uses_hardcoded_defaults(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="nonexistent",
                model=mock_model,
                search=mock_search,
            )
            kw = cls.call_args[1]
            assert kw["include_text_content"] is True
            assert kw["use_cross_engine_filter"] is True
            assert kw["use_atomic_facts"] is False


# ===========================================================================
# all_links_of_system handling
# ===========================================================================


class TestAllLinksHandling:
    """Tests for all_links_of_system parameter normalization."""

    def test_none_becomes_empty_list(self, mock_model, mock_search):
        with patch(
            f"{_STRAT_BASE}.source_based_strategy.SourceBasedSearchStrategy"
        ) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="source-based",
                model=mock_model,
                search=mock_search,
                all_links_of_system=None,
            )
            kw = cls.call_args[1]
            assert kw["all_links_of_system"] == []

    def test_existing_links_passed_through(self, mock_model, mock_search):
        links = [{"link": "http://a.com"}, {"link": "http://b.com"}]
        with patch(
            f"{_STRAT_BASE}.source_based_strategy.SourceBasedSearchStrategy"
        ) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="source-based",
                model=mock_model,
                search=mock_search,
                all_links_of_system=links,
            )
            kw = cls.call_args[1]
            assert kw["all_links_of_system"] is links


class TestCreateStrategyTopicOrganization:
    """Tests for topic-organization strategy."""

    PATCH_PATH = (
        f"{_STRAT_BASE}.topic_organization_strategy.TopicOrganizationStrategy"
    )

    @pytest.mark.parametrize(
        "name", ["topic-organization", "topic_organization", "topic"]
    )
    def test_name_variants(self, name, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name=name, model=mock_model, search=mock_search
            )
            cls.assert_called_once()
            assert result == cls.return_value
