"""
High-value tests for SmartDecompositionStrategy and QueryType.

Covers pure logic paths:
- QueryType enum completeness and values
- _is_browsecomp_style indicator counting (threshold of 3)
- _classify_query LLM response parsing (by name, by value, default fallback)
- analyze_topic delegation to the correct sub-strategy
- Progress callback invocation with query_type metadata
"""

from unittest.mock import MagicMock, Mock, patch

import pytest

from local_deep_research.advanced_search_system.strategies.smart_decomposition_strategy import (
    QueryType,
    SmartDecompositionStrategy,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

MODULE = "local_deep_research.advanced_search_system.strategies.smart_decomposition_strategy"


def _make_strategy(progress_callback=None):
    """Create a SmartDecompositionStrategy with mocked model and search."""
    model = MagicMock()
    search = MagicMock()
    strategy = SmartDecompositionStrategy(
        model=model,
        search=search,
        all_links_of_system=[],
        settings_snapshot={},
    )
    if progress_callback is not None:
        strategy.set_progress_callback(progress_callback)
    return strategy


def _model_response(text: str):
    """Return a mock LLM response object whose .content is *text*."""
    resp = Mock()
    resp.content = text
    return resp


# ---------------------------------------------------------------------------
# QueryType enum
# ---------------------------------------------------------------------------


class TestQueryType:
    """Tests for the QueryType enum."""

    def test_all_expected_members_exist(self):
        assert QueryType.PUZZLE_LIKE.value == "puzzle_like"
        assert QueryType.HIERARCHICAL.value == "hierarchical"
        assert QueryType.EXPLORATORY.value == "exploratory"
        assert QueryType.COMPARATIVE.value == "comparative"
        assert QueryType.FACTUAL.value == "factual"
        assert QueryType.CONSTRAINT_BASED.value == "constraint_based"

    def test_member_count(self):
        assert len(QueryType) == 6

    def test_members_are_unique(self):
        values = [qt.value for qt in QueryType]
        assert len(values) == len(set(values))

    def test_lookup_by_name(self):
        assert QueryType["PUZZLE_LIKE"] is QueryType.PUZZLE_LIKE

    def test_lookup_by_name_invalid_raises(self):
        with pytest.raises(KeyError):
            QueryType["NONEXISTENT"]


# ---------------------------------------------------------------------------
# _is_browsecomp_style
# ---------------------------------------------------------------------------


class TestIsBrowsecompStyle:
    """Tests for the browsecomp indicator detection heuristic."""

    def test_returns_true_with_three_indicators(self):
        strategy = _make_strategy()
        query = "At a specific scenic location, a body part fell from the viewpoint."
        assert strategy._is_browsecomp_style(query) is True

    def test_returns_true_with_many_indicators(self):
        strategy = _make_strategy()
        query = (
            "At a specific scenic location a body part fell from a viewpoint "
            "during search and rescue operations involving SAR incidents "
            "requiring an exact answer from multiple clues."
        )
        assert strategy._is_browsecomp_style(query) is True

    def test_returns_false_with_two_indicators(self):
        strategy = _make_strategy()
        query = "A body part fell from a height."
        assert strategy._is_browsecomp_style(query) is False

    def test_returns_false_with_zero_indicators(self):
        strategy = _make_strategy()
        query = "What is the capital of France?"
        assert strategy._is_browsecomp_style(query) is False

    def test_case_insensitive_matching(self):
        strategy = _make_strategy()
        query = (
            "SPECIFIC SCENIC LOCATION with BODY PART and FELL FROM the cliff."
        )
        assert strategy._is_browsecomp_style(query) is True

    def test_mixed_case_matching(self):
        strategy = _make_strategy()
        query = "Specific Scenic Location Body Part Fell From nowhere."
        assert strategy._is_browsecomp_style(query) is True

    def test_exact_threshold_boundary_two_indicators(self):
        """Exactly 2 indicators should return False (below threshold)."""
        strategy = _make_strategy()
        query = "search and rescue at a viewpoint"
        assert strategy._is_browsecomp_style(query) is False

    def test_exact_threshold_boundary_three_indicators(self):
        """Exactly 3 indicators should return True (at threshold)."""
        strategy = _make_strategy()
        query = "search and rescue at a viewpoint with a body part"
        assert strategy._is_browsecomp_style(query) is True

    def test_empty_query(self):
        strategy = _make_strategy()
        assert strategy._is_browsecomp_style("") is False

    def test_between_and_times_more_and_what_is_the_name(self):
        """Indicators 'between', 'times more', 'what is the name' count."""
        strategy = _make_strategy()
        query = "What is the name of the thing between A and B that is 5 times more?"
        assert strategy._is_browsecomp_style(query) is True

    def test_last_ice_age_indicator(self):
        """'last ice age' is one of the recognised indicators."""
        strategy = _make_strategy()
        query = "Since the last ice age, search and rescue at a viewpoint"
        assert strategy._is_browsecomp_style(query) is True


# ---------------------------------------------------------------------------
# _classify_query
# ---------------------------------------------------------------------------


class TestClassifyQuery:
    """Tests for LLM-based query classification and response parsing."""

    def test_parses_query_type_by_enum_name(self):
        """Exact enum member name on the QUERY_TYPE: line is parsed."""
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response(
            "QUERY_TYPE: PUZZLE_LIKE\nREASONING: has clues\nKEY_CHARACTERISTICS: clues"
        )
        result = strategy._classify_query("some puzzle query")
        assert result is QueryType.PUZZLE_LIKE

    def test_parses_hierarchical(self):
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response(
            "QUERY_TYPE: HIERARCHICAL\nREASONING: multi-part"
        )
        assert (
            strategy._classify_query("multi-part question")
            is QueryType.HIERARCHICAL
        )

    def test_parses_comparative(self):
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response(
            "QUERY_TYPE: COMPARATIVE\nREASONING: comparison"
        )
        assert (
            strategy._classify_query("compare X and Y") is QueryType.COMPARATIVE
        )

    def test_parses_factual(self):
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response(
            "QUERY_TYPE: FACTUAL\nREASONING: simple fact"
        )
        assert strategy._classify_query("what year was X?") is QueryType.FACTUAL

    def test_parses_constraint_based(self):
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response(
            "QUERY_TYPE: CONSTRAINT_BASED\nREASONING: constraints"
        )
        assert (
            strategy._classify_query("find the thing with these constraints")
            is QueryType.CONSTRAINT_BASED
        )

    def test_parses_exploratory(self):
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response(
            "QUERY_TYPE: EXPLORATORY\nREASONING: open-ended"
        )
        assert (
            strategy._classify_query("tell me about X") is QueryType.EXPLORATORY
        )

    def test_falls_back_to_value_match_when_key_fails(self):
        """When the type string is not a valid enum name, try matching by value."""
        strategy = _make_strategy()
        # 'puzzle_like' is the *value*, not the enum *name* (PUZZLE_LIKE).
        strategy.model.invoke.return_value = _model_response(
            "QUERY_TYPE: puzzle_like\nREASONING: has clues"
        )
        result = strategy._classify_query("puzzle query")
        assert result is QueryType.PUZZLE_LIKE

    def test_falls_back_to_value_match_constraint_based(self):
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response(
            "QUERY_TYPE: constraint_based\nREASONING: constraints"
        )
        assert (
            strategy._classify_query("constrained query")
            is QueryType.CONSTRAINT_BASED
        )

    def test_defaults_to_exploratory_on_unparseable_response(self):
        """When the response has no QUERY_TYPE: line, default to EXPLORATORY."""
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response(
            "I think this is a factual question."
        )
        result = strategy._classify_query("any query")
        assert result is QueryType.EXPLORATORY

    def test_defaults_to_exploratory_on_unknown_type(self):
        """Unknown type string that matches no name or value defaults to EXPLORATORY."""
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response(
            "QUERY_TYPE: COMPLETELY_UNKNOWN\nREASONING: no idea"
        )
        result = strategy._classify_query("weird query")
        assert result is QueryType.EXPLORATORY

    def test_handles_think_tags_in_response(self):
        """Think tags in the LLM output are stripped before parsing."""
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response(
            "<think>Let me think about this...</think>\n"
            "QUERY_TYPE: FACTUAL\nREASONING: simple"
        )
        result = strategy._classify_query("simple fact")
        assert result is QueryType.FACTUAL

    def test_multiline_response_finds_query_type_line(self):
        """QUERY_TYPE line buried among other lines is still found."""
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response(
            "Some preamble text.\n"
            "More discussion.\n"
            "QUERY_TYPE: COMPARATIVE\n"
            "REASONING: needs comparison\n"
            "KEY_CHARACTERISTICS: two entities"
        )
        assert (
            strategy._classify_query("compare A vs B") is QueryType.COMPARATIVE
        )

    def test_empty_response_defaults_to_exploratory(self):
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response("")
        assert strategy._classify_query("anything") is QueryType.EXPLORATORY

    def test_value_match_is_case_insensitive(self):
        """Value matching uses .lower() so mixed-case values still match."""
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response(
            "QUERY_TYPE: Puzzle_Like\nREASONING: clues"
        )
        result = strategy._classify_query("puzzle")
        assert result is QueryType.PUZZLE_LIKE


# ---------------------------------------------------------------------------
# analyze_topic delegation
# ---------------------------------------------------------------------------


class TestAnalyzeTopicDelegation:
    """Tests that analyze_topic routes to the correct sub-strategy."""

    def _patch_classify(self, strategy, query_type):
        """Patch _classify_query to return a fixed QueryType."""
        strategy._classify_query = Mock(return_value=query_type)

    def _patch_browsecomp_check(self, strategy, result):
        """Patch _is_browsecomp_style to return a fixed boolean."""
        strategy._is_browsecomp_style = Mock(return_value=result)

    # -- PUZZLE_LIKE + browsecomp -> browsecomp strategy -------------------

    @patch(f"{MODULE}.BrowseCompOptimizedStrategy")
    def test_puzzle_like_browsecomp_uses_browsecomp_strategy(self, MockBrowse):
        strategy = _make_strategy()
        self._patch_classify(strategy, QueryType.PUZZLE_LIKE)
        self._patch_browsecomp_check(strategy, True)

        mock_sub = MockBrowse.return_value
        mock_sub.analyze_topic.return_value = {"result": "browsecomp"}

        result = strategy.analyze_topic("browsecomp puzzle query")

        MockBrowse.assert_called_once_with(
            model=strategy.model,
            search=strategy.search,
            all_links_of_system=strategy.all_links_of_system,
        )
        mock_sub.analyze_topic.assert_called_once_with(
            "browsecomp puzzle query"
        )
        assert result == {"result": "browsecomp"}

    # -- PUZZLE_LIKE + not browsecomp -> evidence strategy ------------------

    @patch(f"{MODULE}.EvidenceBasedStrategy")
    def test_puzzle_like_non_browsecomp_uses_evidence_strategy(
        self, MockEvidence
    ):
        strategy = _make_strategy()
        self._patch_classify(strategy, QueryType.PUZZLE_LIKE)
        self._patch_browsecomp_check(strategy, False)

        mock_sub = MockEvidence.return_value
        mock_sub.analyze_topic.return_value = {"result": "evidence"}

        result = strategy.analyze_topic("non-browsecomp puzzle")

        MockEvidence.assert_called_once()
        mock_sub.analyze_topic.assert_called_once_with("non-browsecomp puzzle")
        assert result == {"result": "evidence"}

    # -- CONSTRAINT_BASED -> evidence strategy ------------------------------

    @patch(f"{MODULE}.EvidenceBasedStrategy")
    def test_constraint_based_uses_evidence_strategy(self, MockEvidence):
        strategy = _make_strategy()
        self._patch_classify(strategy, QueryType.CONSTRAINT_BASED)

        mock_sub = MockEvidence.return_value
        mock_sub.analyze_topic.return_value = {"result": "evidence_constraint"}

        result = strategy.analyze_topic("constrained query")

        MockEvidence.assert_called_once()
        mock_sub.analyze_topic.assert_called_once_with("constrained query")
        assert result == {"result": "evidence_constraint"}

    # -- HIERARCHICAL -> recursive strategy ---------------------------------

    @patch(f"{MODULE}.RecursiveDecompositionStrategy")
    def test_hierarchical_uses_recursive_strategy(self, MockRecursive):
        strategy = _make_strategy()
        self._patch_classify(strategy, QueryType.HIERARCHICAL)

        mock_sub = MockRecursive.return_value
        mock_sub.analyze_topic.return_value = {"result": "recursive"}

        result = strategy.analyze_topic("multi-part hierarchical query")

        MockRecursive.assert_called_once()
        mock_sub.analyze_topic.assert_called_once_with(
            "multi-part hierarchical query"
        )
        assert result == {"result": "recursive"}

    # -- COMPARATIVE -> recursive strategy ----------------------------------

    @patch(f"{MODULE}.RecursiveDecompositionStrategy")
    def test_comparative_uses_recursive_strategy(self, MockRecursive):
        strategy = _make_strategy()
        self._patch_classify(strategy, QueryType.COMPARATIVE)

        mock_sub = MockRecursive.return_value
        mock_sub.analyze_topic.return_value = {"result": "recursive_comp"}

        result = strategy.analyze_topic("compare X and Y")

        MockRecursive.assert_called_once()
        mock_sub.analyze_topic.assert_called_once_with("compare X and Y")
        assert result == {"result": "recursive_comp"}

    # -- EXPLORATORY -> recursive strategy ----------------------------------

    @patch(f"{MODULE}.RecursiveDecompositionStrategy")
    def test_exploratory_uses_recursive_strategy(self, MockRecursive):
        strategy = _make_strategy()
        self._patch_classify(strategy, QueryType.EXPLORATORY)

        mock_sub = MockRecursive.return_value
        mock_sub.analyze_topic.return_value = {"result": "recursive_exp"}

        result = strategy.analyze_topic("open-ended research question")

        MockRecursive.assert_called_once()
        mock_sub.analyze_topic.assert_called_once_with(
            "open-ended research question"
        )
        assert result == {"result": "recursive_exp"}

    # -- FACTUAL -> adaptive strategy ---------------------------------------

    @patch(f"{MODULE}.AdaptiveDecompositionStrategy")
    def test_factual_uses_adaptive_strategy(self, MockAdaptive):
        strategy = _make_strategy()
        self._patch_classify(strategy, QueryType.FACTUAL)

        mock_sub = MockAdaptive.return_value
        mock_sub.analyze_topic.return_value = {"result": "adaptive"}

        result = strategy.analyze_topic("what year was X founded?")

        MockAdaptive.assert_called_once()
        mock_sub.analyze_topic.assert_called_once_with(
            "what year was X founded?"
        )
        assert result == {"result": "adaptive"}


# ---------------------------------------------------------------------------
# Progress callback
# ---------------------------------------------------------------------------


class TestProgressCallback:
    """Tests that the progress callback is invoked correctly."""

    @patch(f"{MODULE}.AdaptiveDecompositionStrategy")
    def test_progress_callback_called_with_query_type(self, MockAdaptive):
        callback = Mock()
        strategy = _make_strategy(progress_callback=callback)
        strategy._classify_query = Mock(return_value=QueryType.FACTUAL)

        mock_sub = MockAdaptive.return_value
        mock_sub.analyze_topic.return_value = {}

        strategy.analyze_topic("test query")

        callback.assert_called_once_with(
            "Analyzing query type: factual",
            5,
            {
                "phase": "query_classification",
                "query_type": "factual",
            },
        )

    @patch(f"{MODULE}.RecursiveDecompositionStrategy")
    def test_progress_callback_reports_correct_query_type_value(
        self, MockRecursive
    ):
        callback = Mock()
        strategy = _make_strategy(progress_callback=callback)
        strategy._classify_query = Mock(return_value=QueryType.HIERARCHICAL)

        mock_sub = MockRecursive.return_value
        mock_sub.analyze_topic.return_value = {}

        strategy.analyze_topic("hierarchical query")

        callback.assert_called_once()
        call_args = callback.call_args
        assert call_args[0][0] == "Analyzing query type: hierarchical"
        assert call_args[0][2]["query_type"] == "hierarchical"

    @patch(f"{MODULE}.AdaptiveDecompositionStrategy")
    def test_no_progress_callback_does_not_raise(self, MockAdaptive):
        """When no callback is set, analyze_topic completes without error."""
        strategy = _make_strategy()  # no callback
        strategy._classify_query = Mock(return_value=QueryType.FACTUAL)

        mock_sub = MockAdaptive.return_value
        mock_sub.analyze_topic.return_value = {}

        # Should not raise
        strategy.analyze_topic("test query")

    @patch(f"{MODULE}.BrowseCompOptimizedStrategy")
    def test_progress_callback_propagated_to_sub_strategy(self, MockBrowse):
        """The callback is forwarded to the sub-strategy via set_progress_callback."""
        callback = Mock()
        strategy = _make_strategy(progress_callback=callback)
        strategy._classify_query = Mock(return_value=QueryType.PUZZLE_LIKE)
        strategy._is_browsecomp_style = Mock(return_value=True)

        mock_sub = MockBrowse.return_value
        mock_sub.analyze_topic.return_value = {}

        strategy.analyze_topic("puzzle query")

        mock_sub.set_progress_callback.assert_called_once_with(callback)


# ---------------------------------------------------------------------------
# Sub-strategy kwargs propagation
# ---------------------------------------------------------------------------


class TestKwargsPassthrough:
    """Verify that extra kwargs are forwarded to sub-strategies."""

    @patch(f"{MODULE}.RecursiveDecompositionStrategy")
    def test_strategy_params_forwarded(self, MockRecursive):
        model = MagicMock()
        search = MagicMock()
        strategy = SmartDecompositionStrategy(
            model=model,
            search=search,
            all_links_of_system=[],
            settings_snapshot={},
            max_iterations=5,
            custom_param="hello",
        )
        strategy._classify_query = Mock(return_value=QueryType.HIERARCHICAL)

        mock_sub = MockRecursive.return_value
        mock_sub.analyze_topic.return_value = {}

        strategy.analyze_topic("hierarchical query")

        MockRecursive.assert_called_once_with(
            model=model,
            search=search,
            all_links_of_system=[],
            max_iterations=5,
            custom_param="hello",
        )

    @patch(f"{MODULE}.EvidenceBasedStrategy")
    def test_strategy_params_forwarded_to_evidence(self, MockEvidence):
        model = MagicMock()
        search = MagicMock()
        strategy = SmartDecompositionStrategy(
            model=model,
            search=search,
            all_links_of_system=["link1"],
            settings_snapshot={},
            depth=3,
        )
        strategy._classify_query = Mock(return_value=QueryType.CONSTRAINT_BASED)

        mock_sub = MockEvidence.return_value
        mock_sub.analyze_topic.return_value = {}

        strategy.analyze_topic("constraint query")

        MockEvidence.assert_called_once_with(
            model=model,
            search=search,
            all_links_of_system=["link1"],
            depth=3,
        )


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------


class TestEdgeCases:
    """Edge-case tests for robustness."""

    def test_classify_query_with_extra_whitespace_in_type(self):
        """Extra whitespace around the type string is stripped."""
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response(
            "QUERY_TYPE:   FACTUAL   \nREASONING: simple"
        )
        assert strategy._classify_query("fact query") is QueryType.FACTUAL

    def test_classify_query_with_colon_in_reasoning(self):
        """Colons in subsequent parts of the line don't break parsing."""
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response(
            "QUERY_TYPE: HIERARCHICAL\nREASONING: has parts: a, b, c"
        )
        assert strategy._classify_query("parts query") is QueryType.HIERARCHICAL

    def test_classify_query_first_query_type_line_wins(self):
        """If multiple QUERY_TYPE lines exist, the last one wins (loop iterates all)."""
        strategy = _make_strategy()
        strategy.model.invoke.return_value = _model_response(
            "QUERY_TYPE: FACTUAL\nQUERY_TYPE: COMPARATIVE\nREASONING: mixed"
        )
        # The loop iterates all lines, so the last QUERY_TYPE line wins
        assert (
            strategy._classify_query("ambiguous query") is QueryType.COMPARATIVE
        )

    @patch(f"{MODULE}.AdaptiveDecompositionStrategy")
    def test_analyze_topic_returns_sub_strategy_result_unchanged(
        self, MockAdaptive
    ):
        """The dict returned by the sub-strategy is returned as-is."""
        strategy = _make_strategy()
        strategy._classify_query = Mock(return_value=QueryType.FACTUAL)

        expected = {
            "findings": [{"text": "found it"}],
            "iterations": 2,
            "questions": {"1": ["q1"]},
            "formatted_findings": "formatted",
            "current_knowledge": "knowledge",
        }
        MockAdaptive.return_value.analyze_topic.return_value = expected

        result = strategy.analyze_topic("query")
        assert result is expected

    def test_browsecomp_partial_indicator_no_match(self):
        """Substring match: 'body' alone does not match 'body part'."""
        strategy = _make_strategy()
        query = "body, fell, viewpoint fragment"
        # 'body' != 'body part'; 'fell' != 'fell from'; 'viewpoint' does match
        # Only 1 indicator matches ('viewpoint'), so should be False
        assert strategy._is_browsecomp_style(query) is False
