"""
High-value pure logic tests for FocusedIterationStrategy.

Tests cover constructor defaults, parameter coercion, handler_type selection,
_create_error_response, and _get_current_knowledge_summary -- all without
LLM or network calls.
"""

from unittest.mock import MagicMock, patch


# ---------------------------------------------------------------------------
# Helper: build a FocusedIterationStrategy with the constructor fully mocked
# so we can exercise individual methods / attributes without real components.
# ---------------------------------------------------------------------------

MODULE = "local_deep_research.advanced_search_system.strategies.focused_iteration_strategy"


def _make_strategy(**overrides):
    """Instantiate FocusedIterationStrategy with all heavy deps patched out."""
    from local_deep_research.advanced_search_system.strategies.focused_iteration_strategy import (
        FocusedIterationStrategy,
    )

    defaults = dict(
        model=MagicMock(),
        search=MagicMock(),
        citation_handler=MagicMock(),
        all_links_of_system=[],
    )
    defaults.update(overrides)

    with (
        patch(f"{MODULE}.BrowseCompQuestionGenerator"),
        patch(f"{MODULE}.ProgressiveExplorer"),
        patch(f"{MODULE}.FindingsRepository"),
    ):
        return FocusedIterationStrategy(**defaults)


# =========================================================================
# Constructor defaults and parameter coercion
# =========================================================================


class TestConstructorDefaults:
    """Verify default attribute values set during __init__."""

    def test_max_iterations_default_is_8(self):
        strategy = _make_strategy()
        assert strategy.max_iterations == 8

    def test_questions_per_iteration_default_is_5(self):
        strategy = _make_strategy()
        assert strategy.questions_per_iteration == 5

    def test_use_browsecomp_optimization_default_true(self):
        strategy = _make_strategy()
        assert strategy.use_browsecomp_optimization is True

    def test_enable_adaptive_questions_default_false(self):
        strategy = _make_strategy()
        assert strategy.enable_adaptive_questions is False

    def test_enable_early_termination_default_false(self):
        strategy = _make_strategy()
        assert strategy.enable_early_termination is False

    def test_knowledge_summary_limit_default(self):
        strategy = _make_strategy()
        assert strategy.knowledge_summary_limit == 10

    def test_knowledge_snippet_truncate_default(self):
        strategy = _make_strategy()
        assert strategy.knowledge_snippet_truncate == 200

    def test_prompt_knowledge_truncate_default(self):
        strategy = _make_strategy()
        assert strategy.prompt_knowledge_truncate == 1500

    def test_previous_searches_limit_default(self):
        strategy = _make_strategy()
        assert strategy.previous_searches_limit == 10


class TestParameterCoercion:
    """Verify int coercion and None-fallback logic for iteration params."""

    def test_max_iterations_coerced_from_float(self):
        strategy = _make_strategy(max_iterations=4.9)
        assert strategy.max_iterations == 4
        assert isinstance(strategy.max_iterations, int)

    def test_max_iterations_coerced_from_string(self):
        strategy = _make_strategy(max_iterations="6")
        assert strategy.max_iterations == 6
        assert isinstance(strategy.max_iterations, int)

    def test_max_iterations_none_falls_back_to_3(self):
        strategy = _make_strategy(max_iterations=None)
        assert strategy.max_iterations == 3

    def test_questions_per_iteration_coerced_from_float(self):
        strategy = _make_strategy(questions_per_iteration=3.7)
        assert strategy.questions_per_iteration == 3
        assert isinstance(strategy.questions_per_iteration, int)

    def test_questions_per_iteration_coerced_from_string(self):
        strategy = _make_strategy(questions_per_iteration="10")
        assert strategy.questions_per_iteration == 10
        assert isinstance(strategy.questions_per_iteration, int)

    def test_questions_per_iteration_none_falls_back_to_3(self):
        strategy = _make_strategy(questions_per_iteration=None)
        assert strategy.questions_per_iteration == 3


# =========================================================================
# handler_type selection logic
# =========================================================================


class TestHandlerTypeSelection:
    """Verify handler_type passed to CitationHandler depends on browsecomp flag."""

    def test_handler_type_forced_answer_when_browsecomp_true(self):
        """When use_browsecomp_optimization=True and no citation_handler provided,
        CitationHandler should be created with handler_type='forced_answer'."""
        from local_deep_research.advanced_search_system.strategies.focused_iteration_strategy import (
            FocusedIterationStrategy,
        )

        with (
            patch(f"{MODULE}.BrowseCompQuestionGenerator"),
            patch(f"{MODULE}.ProgressiveExplorer"),
            patch(f"{MODULE}.FindingsRepository"),
            patch(f"{MODULE}.CitationHandler") as mock_ch_cls,
        ):
            FocusedIterationStrategy(
                model=MagicMock(),
                search=MagicMock(),
                use_browsecomp_optimization=True,
            )
            mock_ch_cls.assert_called_once()
            call_kwargs = mock_ch_cls.call_args
            assert call_kwargs[1]["handler_type"] == "forced_answer" or (
                len(call_kwargs[0]) >= 2
                and call_kwargs[0][1] == "forced_answer"
            )

    def test_handler_type_standard_when_browsecomp_false(self):
        """When use_browsecomp_optimization=False, handler_type should be 'standard'."""
        from local_deep_research.advanced_search_system.strategies.focused_iteration_strategy import (
            FocusedIterationStrategy,
        )

        with (
            patch(f"{MODULE}.FindingsRepository"),
            patch(f"{MODULE}.CitationHandler") as mock_ch_cls,
        ):
            # browsecomp=False triggers the else branch importing StandardQuestionGenerator
            with patch(
                f"{MODULE}.StandardQuestionGenerator",
                create=True,
            ):
                FocusedIterationStrategy(
                    model=MagicMock(),
                    search=MagicMock(),
                    use_browsecomp_optimization=False,
                )
            mock_ch_cls.assert_called_once()
            assert mock_ch_cls.call_args[1]["handler_type"] == "standard"

    def test_provided_citation_handler_skips_creation(self):
        """When citation_handler is explicitly provided, CitationHandler is not created."""
        from local_deep_research.advanced_search_system.strategies.focused_iteration_strategy import (
            FocusedIterationStrategy,
        )

        custom_handler = MagicMock()
        with (
            patch(f"{MODULE}.BrowseCompQuestionGenerator"),
            patch(f"{MODULE}.ProgressiveExplorer"),
            patch(f"{MODULE}.FindingsRepository"),
            patch(f"{MODULE}.CitationHandler") as mock_ch_cls,
        ):
            strategy = FocusedIterationStrategy(
                model=MagicMock(),
                search=MagicMock(),
                citation_handler=custom_handler,
            )
            mock_ch_cls.assert_not_called()
            assert strategy.citation_handler is custom_handler


# =========================================================================
# _create_error_response
# =========================================================================


class TestCreateErrorResponse:
    """Verify the structure returned by _create_error_response."""

    def test_error_response_has_required_keys(self):
        strategy = _make_strategy()
        resp = strategy._create_error_response("something broke")
        # The shared base _create_error_response carries BOTH "questions"
        # and "questions_by_iteration" so a single helper satisfies every
        # consumer (mcp_strategy reads "questions", focused reads
        # "questions_by_iteration"). The focused error response therefore
        # now includes both keys.
        assert set(resp.keys()) == {
            "findings",
            "iterations",
            "questions",
            "questions_by_iteration",
            "formatted_findings",
            "current_knowledge",
            "error",
        }

    def test_error_response_values(self):
        strategy = _make_strategy()
        resp = strategy._create_error_response("timeout")
        assert resp["findings"] == []
        assert resp["iterations"] == 0
        assert resp["questions"] == {}
        assert resp["questions_by_iteration"] == {}
        assert resp["current_knowledge"] == ""
        assert resp["error"] == "timeout"
        assert "timeout" in resp["formatted_findings"]

    def test_error_response_formatted_findings_contains_message(self):
        strategy = _make_strategy()
        resp = strategy._create_error_response("No search engine available")
        assert resp["formatted_findings"] == "Error: No search engine available"


# =========================================================================
# _get_current_knowledge_summary
# =========================================================================


class TestGetCurrentKnowledgeSummary:
    """Verify knowledge summary building logic."""

    def test_empty_results_returns_empty_string(self):
        strategy = _make_strategy()
        strategy.all_search_results = []
        assert strategy._get_current_knowledge_summary() == ""

    def test_summary_includes_title_and_truncated_snippet(self):
        strategy = _make_strategy()
        strategy.all_search_results = [
            {"title": "Result A", "snippet": "x" * 300},
        ]
        summary = strategy._get_current_knowledge_summary()
        assert "Result A" in summary
        # Snippet should be truncated to 200 chars + "..."
        assert len(summary.split(": ", 1)[1]) == 200 + 3  # 200 chars + "..."

    def test_summary_respects_knowledge_summary_limit(self):
        strategy = _make_strategy()
        strategy.knowledge_summary_limit = 2
        strategy.all_search_results = [
            {"title": f"T{idx}", "snippet": f"S{idx}"} for idx in range(5)
        ]
        summary = strategy._get_current_knowledge_summary()
        # Only first 2 results should appear
        assert "T0" in summary
        assert "T1" in summary
        assert "T2" not in summary

    def test_summary_no_truncation_when_snippet_truncate_is_none(self):
        strategy = _make_strategy(knowledge_snippet_truncate=None)
        long_snippet = "y" * 500
        strategy.all_search_results = [
            {"title": "Full", "snippet": long_snippet},
        ]
        summary = strategy._get_current_knowledge_summary()
        # Full snippet should be present without trailing "..."
        assert long_snippet in summary
        assert not summary.endswith("......")

    def test_summary_skips_entries_without_title_or_snippet(self):
        strategy = _make_strategy()
        strategy.all_search_results = [
            {"title": "", "snippet": ""},
            {"title": "Has Title", "snippet": "Has Snippet"},
        ]
        summary = strategy._get_current_knowledge_summary()
        lines = [line for line in summary.strip().split("\n") if line]
        assert len(lines) == 1
        assert "Has Title" in lines[0]
