"""
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 TestCreateStrategyIterativeReasoning:
    """Tests for iterative-reasoning strategy."""

    PATCH_PATH = (
        f"{_STRAT_BASE}.iterative_reasoning_strategy.IterativeReasoningStrategy"
    )

    @pytest.mark.parametrize(
        "name",
        [
            "iterative-reasoning",
            "iterative_reasoning",
            "iterative_reasoning_depth",
        ],
    )
    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


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


class TestCreateStrategyIterDRAG:
    """Tests for iterdrag strategy."""

    PATCH_PATH = f"{_STRAT_BASE}.iterdrag_strategy.IterDRAGStrategy"

    def test_create_iterdrag(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name="iterdrag",
                model=mock_model,
                search=mock_search,
            )
            cls.assert_called_once()
            assert result == cls.return_value

    def test_settings_snapshot_forwarded(self, mock_model, mock_search):
        snapshot = {"key": "val"}
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="iterdrag",
                model=mock_model,
                search=mock_search,
                settings_snapshot=snapshot,
            )
            kw = cls.call_args[1]
            assert kw["settings_snapshot"] is snapshot


class TestCreateStrategyParallel:
    """Tests for parallel strategy."""

    PATCH_PATH = (
        f"{_STRAT_BASE}.parallel_search_strategy.ParallelSearchStrategy"
    )

    def test_create_parallel(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name="parallel",
                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="parallel",
                model=mock_model,
                search=mock_search,
                include_text_content=False,
                use_cross_engine_filter=False,
            )
            kw = cls.call_args[1]
            assert kw["include_text_content"] is False
            assert kw["use_cross_engine_filter"] is False


class TestCreateStrategyRapid:
    """Tests for rapid strategy."""

    PATCH_PATH = f"{_STRAT_BASE}.rapid_search_strategy.RapidSearchStrategy"

    def test_create_rapid(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name="rapid",
                model=mock_model,
                search=mock_search,
            )
            cls.assert_called_once()
            assert result == cls.return_value


class TestCreateStrategyRecursive:
    """Tests for recursive strategy."""

    PATCH_PATH = f"{_STRAT_BASE}.recursive_decomposition_strategy.RecursiveDecompositionStrategy"

    @pytest.mark.parametrize("name", ["recursive", "recursive-decomposition"])
    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


class TestCreateStrategyIterative:
    """Tests for 'iterative' strategy (distinct from iterative-reasoning)."""

    PATCH_PATH = (
        f"{_STRAT_BASE}.iterative_reasoning_strategy.IterativeReasoningStrategy"
    )

    def test_create_iterative(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name="iterative",
                model=mock_model,
                search=mock_search,
            )
            cls.assert_called_once()
            assert result == cls.return_value

    def test_iterative_passes_extra_kwargs(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="iterative",
                model=mock_model,
                search=mock_search,
                max_iterations=10,
                questions_per_iteration=5,
                confidence_threshold=0.8,
                search_iterations_per_round=2,
            )
            kw = cls.call_args[1]
            assert kw["max_iterations"] == 10
            assert kw["questions_per_search"] == 5
            assert kw["confidence_threshold"] == 0.8
            assert kw["search_iterations_per_round"] == 2


class TestCreateStrategyAdaptive:
    """Tests for adaptive strategy."""

    PATCH_PATH = f"{_STRAT_BASE}.adaptive_decomposition_strategy.AdaptiveDecompositionStrategy"

    def test_create_adaptive(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name="adaptive",
                model=mock_model,
                search=mock_search,
            )
            cls.assert_called_once()
            assert result == cls.return_value

    def test_adaptive_default_max_steps_from_max_iterations(
        self, mock_model, mock_search
    ):
        """max_steps falls back to max_iterations kwarg."""
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="adaptive",
                model=mock_model,
                search=mock_search,
                max_iterations=12,
            )
            kw = cls.call_args[1]
            assert kw["max_steps"] == 12


class TestCreateStrategySmart:
    """Tests for smart strategy."""

    PATCH_PATH = (
        f"{_STRAT_BASE}.smart_decomposition_strategy.SmartDecompositionStrategy"
    )

    def test_create_smart(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name="smart",
                model=mock_model,
                search=mock_search,
            )
            cls.assert_called_once()
            assert result == cls.return_value


class TestCreateStrategyBrowsecomp:
    """Tests for browsecomp strategy."""

    PATCH_PATH = f"{_STRAT_BASE}.browsecomp_optimized_strategy.BrowseCompOptimizedStrategy"

    def test_create_browsecomp(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name="browsecomp",
                model=mock_model,
                search=mock_search,
            )
            cls.assert_called_once()
            assert result == cls.return_value


class TestCreateStrategyEvidence:
    """Tests for evidence strategy."""

    PATCH_PATH = f"{_STRAT_BASE}.evidence_based_strategy_v2.EnhancedEvidenceBasedStrategy"

    def test_create_evidence(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name="evidence",
                model=mock_model,
                search=mock_search,
            )
            cls.assert_called_once()
            assert result == cls.return_value

    def test_evidence_kwargs(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="evidence",
                model=mock_model,
                search=mock_search,
                candidate_limit=50,
                enable_pattern_learning=False,
            )
            kw = cls.call_args[1]
            assert kw["candidate_limit"] == 50
            assert kw["enable_pattern_learning"] is False


class TestCreateStrategyConstrained:
    """Tests for constrained strategy."""

    PATCH_PATH = (
        f"{_STRAT_BASE}.constrained_search_strategy.ConstrainedSearchStrategy"
    )

    def test_create_constrained(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name="constrained",
                model=mock_model,
                search=mock_search,
            )
            cls.assert_called_once()
            assert result == cls.return_value


class TestCreateStrategyStandard:
    """Tests for standard strategy."""

    PATCH_PATH = f"{_STRAT_BASE}.standard_strategy.StandardSearchStrategy"

    def test_create_standard(self, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            result = create_strategy(
                strategy_name="standard",
                model=mock_model,
                search=mock_search,
            )
            cls.assert_called_once()
            assert result == cls.return_value

    def test_settings_snapshot_forwarded(self, mock_model, mock_search):
        snapshot = {"foo": "bar"}
        with patch(self.PATCH_PATH) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="standard",
                model=mock_model,
                search=mock_search,
                settings_snapshot=snapshot,
            )
            kw = cls.call_args[1]
            assert kw["settings_snapshot"] is snapshot


class TestCreateStrategyIterativeRefinement:
    """Tests for iterative-refinement strategy (recursive create_strategy)."""

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

    @pytest.mark.parametrize(
        "name", ["iterative-refinement", "iterative_refinement"]
    )
    def test_name_variants(self, name, mock_model, mock_search):
        with patch(self.PATCH_PATH) as cls, patch(self.SOURCE_PATCH):
            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_recursive_call_creates_initial_strategy(
        self, mock_model, mock_search
    ):
        """The factory recursively calls create_strategy for the initial_strategy."""
        with (
            patch(self.PATCH_PATH) as ref_cls,
            patch(self.SOURCE_PATCH) as src_cls,
        ):
            initial_mock = Mock(name="initial_strategy")
            src_cls.return_value = initial_mock
            ref_cls.return_value = Mock(name="refinement_strategy")

            create_strategy(
                strategy_name="iterative-refinement",
                model=mock_model,
                search=mock_search,
            )

            # Source-based strategy was created for the initial strategy
            src_cls.assert_called_once()
            # The refinement strategy got the initial strategy as a param
            ref_kw = ref_cls.call_args[1]
            assert ref_kw["initial_strategy"] is initial_mock

    def test_custom_initial_strategy(self, mock_model, mock_search):
        """Uses a custom initial_strategy name via kwargs."""
        rapid_path = f"{_STRAT_BASE}.rapid_search_strategy.RapidSearchStrategy"
        with patch(self.PATCH_PATH) as ref_cls, patch(rapid_path) as rapid_cls:
            rapid_cls.return_value = Mock(name="rapid_initial")
            ref_cls.return_value = Mock()

            create_strategy(
                strategy_name="iterative-refinement",
                model=mock_model,
                search=mock_search,
                initial_strategy="rapid",
            )

            rapid_cls.assert_called_once()
            ref_kw = ref_cls.call_args[1]
            assert ref_kw["initial_strategy"] is rapid_cls.return_value


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


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

    def test_uppercase_standard(self, mock_model, mock_search):
        with patch(
            f"{_STRAT_BASE}.standard_strategy.StandardSearchStrategy"
        ) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="STANDARD",
                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}.standard_strategy.StandardSearchStrategy"
        ) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="standard",
                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}.standard_strategy.StandardSearchStrategy"
        ) as cls:
            cls.return_value = Mock()
            create_strategy(
                strategy_name="standard",
                model=mock_model,
                search=mock_search,
                all_links_of_system=links,
            )
            kw = cls.call_args[1]
            assert kw["all_links_of_system"] is links


# ===========================================================================
# Additional strategies to confirm correct class is used
# ===========================================================================


class TestCreateStrategyParallelConstrained:
    """Tests for parallel-constrained strategy."""

    PATCH_PATH = f"{_STRAT_BASE}.parallel_constrained_strategy.ParallelConstrainedStrategy"

    @pytest.mark.parametrize(
        "name", ["parallel-constrained", "parallel_constrained"]
    )
    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


class TestCreateStrategyEarlyStopConstrained:
    """Tests for early-stop-constrained strategy."""

    PATCH_PATH = f"{_STRAT_BASE}.early_stop_constrained_strategy.EarlyStopConstrainedStrategy"

    @pytest.mark.parametrize(
        "name", ["early-stop-constrained", "early_stop_constrained"]
    )
    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


class TestCreateStrategySmartQuery:
    """Tests for smart-query strategy."""

    PATCH_PATH = f"{_STRAT_BASE}.smart_query_strategy.SmartQueryStrategy"

    @pytest.mark.parametrize("name", ["smart-query", "smart_query"])
    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


class TestCreateStrategyDualConfidence:
    """Tests for dual-confidence strategy."""

    PATCH_PATH = (
        f"{_STRAT_BASE}.dual_confidence_strategy.DualConfidenceStrategy"
    )

    @pytest.mark.parametrize("name", ["dual-confidence", "dual_confidence"])
    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


class TestCreateStrategyDualConfidenceWithRejection:
    """Tests for dual-confidence-with-rejection strategy."""

    PATCH_PATH = f"{_STRAT_BASE}.dual_confidence_with_rejection.DualConfidenceWithRejectionStrategy"

    @pytest.mark.parametrize(
        "name",
        ["dual-confidence-with-rejection", "dual_confidence_with_rejection"],
    )
    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


class TestCreateStrategyConcurrentDualConfidence:
    """Tests for concurrent-dual-confidence strategy."""

    PATCH_PATH = f"{_STRAT_BASE}.concurrent_dual_confidence_strategy.ConcurrentDualConfidenceStrategy"

    @pytest.mark.parametrize(
        "name",
        ["concurrent-dual-confidence", "concurrent_dual_confidence"],
    )
    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


class TestCreateStrategyConstraintParallel:
    """Tests for constraint-parallel strategy."""

    PATCH_PATH = (
        f"{_STRAT_BASE}.constraint_parallel_strategy.ConstraintParallelStrategy"
    )

    @pytest.mark.parametrize(
        "name", ["constraint-parallel", "constraint_parallel"]
    )
    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


class TestCreateStrategyModular:
    """Tests for modular strategy."""

    PATCH_PATH = f"{_STRAT_BASE}.modular_strategy.ModularStrategy"

    @pytest.mark.parametrize("name", ["modular", "modular-strategy"])
    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


class TestCreateStrategyModularParallel:
    """Tests for modular-parallel strategy."""

    PATCH_PATH = f"{_STRAT_BASE}.modular_strategy.ModularStrategy"

    @pytest.mark.parametrize("name", ["modular-parallel", "modular_parallel"])
    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()
            kw = cls.call_args[1]
            assert kw["exploration_strategy"] == "parallel"
            assert result == cls.return_value


class TestCreateStrategyBrowsecompEntity:
    """Tests for browsecomp-entity strategy."""

    PATCH_PATH = (
        f"{_STRAT_BASE}.browsecomp_entity_strategy.BrowseCompEntityStrategy"
    )

    @pytest.mark.parametrize("name", ["browsecomp-entity", "browsecomp_entity"])
    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


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
