"""
Tests for IterativeRefinementStrategy.

Tests cover:
- Initialization with default and custom parameters
- Parameter validation
- LLM evaluation parsing and error handling
- Result merging logic
- Research context building
- Full analyze_topic flow with mocked dependencies
- Progress callback propagation
- Refinement history tracking
- Edge cases and error paths
"""

import pytest
from unittest.mock import Mock, patch


class TestIterativeRefinementStrategyInit:
    """Tests for IterativeRefinementStrategy initialization."""

    def test_init_with_defaults(self):
        """Initialize with default parameters."""
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        mock_initial = Mock()
        strategy = IterativeRefinementStrategy(
            model=Mock(),
            search=Mock(),
            initial_strategy=mock_initial,
        )

        assert strategy.evaluation_frequency == 1
        assert strategy.max_refinements == 3
        assert strategy.confidence_threshold == 0.8
        assert strategy.max_evaluation_tokens is None
        assert strategy.initial_strategy is mock_initial
        assert strategy.refinement_history == []

    def test_init_with_custom_params(self):
        """Initialize with custom parameters."""
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        strategy = IterativeRefinementStrategy(
            model=Mock(),
            search=Mock(),
            initial_strategy=Mock(),
            evaluation_frequency=2,
            max_refinements=5,
            confidence_threshold=0.9,
            max_evaluation_tokens=1000,
        )

        assert strategy.evaluation_frequency == 2
        assert strategy.max_refinements == 5
        assert strategy.confidence_threshold == 0.9
        assert strategy.max_evaluation_tokens == 1000

    def test_init_invalid_confidence_threshold_too_high(self):
        """Raises ValueError for confidence_threshold > 1."""
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        with pytest.raises(ValueError, match="confidence_threshold"):
            IterativeRefinementStrategy(
                model=Mock(),
                search=Mock(),
                initial_strategy=Mock(),
                confidence_threshold=1.5,
            )

    def test_init_invalid_confidence_threshold_negative(self):
        """Raises ValueError for negative confidence_threshold."""
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        with pytest.raises(ValueError, match="confidence_threshold"):
            IterativeRefinementStrategy(
                model=Mock(),
                search=Mock(),
                initial_strategy=Mock(),
                confidence_threshold=-0.1,
            )

    def test_init_invalid_max_refinements(self):
        """Raises ValueError for negative max_refinements."""
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        with pytest.raises(ValueError, match="max_refinements"):
            IterativeRefinementStrategy(
                model=Mock(),
                search=Mock(),
                initial_strategy=Mock(),
                max_refinements=-1,
            )

    def test_init_invalid_evaluation_frequency(self):
        """Raises ValueError for evaluation_frequency < 1."""
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        with pytest.raises(ValueError, match="evaluation_frequency"):
            IterativeRefinementStrategy(
                model=Mock(),
                search=Mock(),
                initial_strategy=Mock(),
                evaluation_frequency=0,
            )

    def test_init_invalid_max_evaluation_tokens(self):
        """Raises ValueError for non-positive max_evaluation_tokens."""
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        with pytest.raises(ValueError, match="max_evaluation_tokens"):
            IterativeRefinementStrategy(
                model=Mock(),
                search=Mock(),
                initial_strategy=Mock(),
                max_evaluation_tokens=0,
            )

    def test_init_boundary_values(self):
        """Accepts boundary parameter values."""
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        # All at boundaries should not raise
        strategy = IterativeRefinementStrategy(
            model=Mock(),
            search=Mock(),
            initial_strategy=Mock(),
            confidence_threshold=0.0,
            max_refinements=0,
            evaluation_frequency=1,
        )

        assert strategy.confidence_threshold == 0.0
        assert strategy.max_refinements == 0


class TestEvaluateWithLlm:
    """Tests for _evaluate_with_llm method."""

    def _make_strategy(self):
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        return IterativeRefinementStrategy(
            model=Mock(),
            search=Mock(),
            initial_strategy=Mock(),
        )

    def test_parses_valid_refine_response(self):
        """Parses valid REFINE JSON response."""
        strategy = self._make_strategy()
        strategy.model.invoke.return_value = Mock(
            content='{"action": "REFINE", "confidence": 0.6, "gaps": ["missing context"], "reasoning": "Need more data", "refinement_query": "What about X?"}'
        )

        result = strategy._evaluate_with_llm(
            {"formatted_findings": "findings", "all_links_of_system": []},
            "test query",
            0,
        )

        assert result["action"] == "REFINE"
        assert result["confidence"] == 0.6
        assert result["gaps"] == ["missing context"]
        assert result["refinement_query"] == "What about X?"

    def test_parses_valid_complete_response(self):
        """Parses valid COMPLETE JSON response."""
        strategy = self._make_strategy()
        strategy.model.invoke.return_value = Mock(
            content='{"action": "COMPLETE", "confidence": 0.95, "gaps": [], "reasoning": "All good"}'
        )

        result = strategy._evaluate_with_llm(
            {"formatted_findings": "findings", "all_links_of_system": []},
            "test query",
            0,
        )

        assert result["action"] == "COMPLETE"
        assert result["confidence"] == 0.95

    def test_clamps_confidence_to_0_1(self):
        """Clamps confidence to [0.0, 1.0] range."""
        strategy = self._make_strategy()
        strategy.model.invoke.return_value = Mock(
            content='{"action": "REFINE", "confidence": 1.5}'
        )

        result = strategy._evaluate_with_llm(
            {"formatted_findings": "", "all_links_of_system": []},
            "query",
            0,
        )

        assert result["confidence"] == 1.0

    def test_clamps_negative_confidence(self):
        """Clamps negative confidence to 0.0."""
        strategy = self._make_strategy()
        strategy.model.invoke.return_value = Mock(
            content='{"action": "REFINE", "confidence": -0.5}'
        )

        result = strategy._evaluate_with_llm(
            {"formatted_findings": "", "all_links_of_system": []},
            "query",
            0,
        )

        assert result["confidence"] == 0.0

    def test_missing_action_defaults_to_complete(self):
        """Missing action defaults to COMPLETE."""
        strategy = self._make_strategy()
        strategy.model.invoke.return_value = Mock(
            content='{"confidence": 0.7, "gaps": []}'
        )

        result = strategy._evaluate_with_llm(
            {"formatted_findings": "", "all_links_of_system": []},
            "query",
            0,
        )

        assert result["action"] == "COMPLETE"

    def test_invalid_action_defaults_to_complete(self):
        """Invalid action value defaults to COMPLETE."""
        strategy = self._make_strategy()
        strategy.model.invoke.return_value = Mock(
            content='{"action": "INVALID_ACTION", "confidence": 0.5}'
        )

        result = strategy._evaluate_with_llm(
            {"formatted_findings": "", "all_links_of_system": []},
            "query",
            0,
        )

        assert result["action"] == "COMPLETE"

    def test_non_list_gaps_replaced_with_empty(self):
        """Non-list gaps value is replaced with empty list."""
        strategy = self._make_strategy()
        strategy.model.invoke.return_value = Mock(
            content='{"action": "REFINE", "confidence": 0.5, "gaps": "not a list"}'
        )

        result = strategy._evaluate_with_llm(
            {"formatted_findings": "", "all_links_of_system": []},
            "query",
            0,
        )

        assert result["gaps"] == []

    def test_missing_confidence_defaults_to_half(self):
        """Missing confidence defaults to 0.5."""
        strategy = self._make_strategy()
        strategy.model.invoke.return_value = Mock(
            content='{"action": "COMPLETE"}'
        )

        result = strategy._evaluate_with_llm(
            {"formatted_findings": "", "all_links_of_system": []},
            "query",
            0,
        )

        assert result["confidence"] == 0.5

    def test_llm_exception_returns_complete(self):
        """LLM exception returns COMPLETE with 0.5 confidence."""
        strategy = self._make_strategy()
        strategy.model.invoke.side_effect = Exception("LLM failed")

        result = strategy._evaluate_with_llm(
            {"formatted_findings": "", "all_links_of_system": []},
            "query",
            0,
        )

        assert result["action"] == "COMPLETE"
        assert result["confidence"] == 0.5
        assert "Evaluation error" in result["reasoning"]

    def test_malformed_json_returns_complete(self):
        """Malformed JSON returns COMPLETE."""
        strategy = self._make_strategy()
        strategy.model.invoke.return_value = Mock(
            content="This is not JSON at all"
        )

        result = strategy._evaluate_with_llm(
            {"formatted_findings": "", "all_links_of_system": []},
            "query",
            0,
        )

        assert result["action"] == "COMPLETE"

    def test_truncates_findings_to_max_evaluation_tokens(self):
        """Truncates findings to max_evaluation_tokens."""
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        strategy = IterativeRefinementStrategy(
            model=Mock(),
            search=Mock(),
            initial_strategy=Mock(),
            max_evaluation_tokens=10,
        )
        strategy.model.invoke.return_value = Mock(
            content='{"action": "COMPLETE", "confidence": 0.9}'
        )

        long_findings = "x" * 1000
        strategy._evaluate_with_llm(
            {"formatted_findings": long_findings, "all_links_of_system": []},
            "query",
            0,
        )

        # The prompt should contain truncated findings
        prompt_used = strategy.model.invoke.call_args[0][0]
        # The full 1000-char findings should NOT appear in the prompt
        assert "x" * 1000 not in prompt_used
        # But some truncated version should
        assert "x" * 10 in prompt_used

    def test_includes_previous_gaps_in_prompt(self):
        """Previous gaps are included in the evaluation prompt."""
        strategy = self._make_strategy()
        strategy.refinement_history = [
            {"evaluation": {"gaps": ["gap1", "gap2"]}},
        ]
        strategy.model.invoke.return_value = Mock(
            content='{"action": "COMPLETE", "confidence": 0.9}'
        )

        strategy._evaluate_with_llm(
            {"formatted_findings": "findings", "all_links_of_system": []},
            "query",
            1,
        )

        prompt = strategy.model.invoke.call_args[0][0]
        assert "gap1" in prompt
        assert "gap2" in prompt


class TestMergeResults:
    """Tests for _merge_results method."""

    def _make_strategy(self):
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        return IterativeRefinementStrategy(
            model=Mock(),
            search=Mock(),
            initial_strategy=Mock(),
        )

    def test_appends_followup_findings(self):
        """Followup findings are appended to original."""
        strategy = self._make_strategy()

        original = {"findings": [{"phase": "initial"}]}
        followup = {"findings": [{"phase": "followup"}]}

        merged = strategy._merge_results(original, followup, "followup query")

        # Should have: original + separator + followup = 3
        assert len(merged["findings"]) == 3
        assert merged["findings"][0]["phase"] == "initial"
        assert merged["findings"][1]["type"] == "refinement_separator"
        assert merged["findings"][2]["phase"] == "followup"

    def test_creates_findings_list_if_missing(self):
        """Creates findings list if not in original."""
        strategy = self._make_strategy()

        original = {}
        followup = {"findings": [{"phase": "followup"}]}

        merged = strategy._merge_results(original, followup, "query")

        assert "findings" in merged
        assert len(merged["findings"]) >= 1

    def test_merges_formatted_findings(self):
        """Formatted findings are concatenated with separator."""
        strategy = self._make_strategy()

        original = {"formatted_findings": "Original text"}
        followup = {"formatted_findings": "Followup text"}

        merged = strategy._merge_results(original, followup, "followup query")

        assert "Original text" in merged["formatted_findings"]
        assert "Followup text" in merged["formatted_findings"]
        assert "## Refinement:" in merged["formatted_findings"]

    def test_updates_current_knowledge(self):
        """Current knowledge is updated to latest followup."""
        strategy = self._make_strategy()

        original = {"current_knowledge": "old knowledge"}
        followup = {"current_knowledge": "new knowledge"}

        merged = strategy._merge_results(original, followup, "query")

        assert merged["current_knowledge"] == "new knowledge"

    def test_merges_questions_by_iteration_with_offset(self):
        """Follow-up questions are merged with offset keys."""
        strategy = self._make_strategy()

        original = {
            "questions_by_iteration": {1: ["Q1"], 2: ["Q2"]},
        }
        followup = {
            "questions_by_iteration": {1: ["FQ1"], 2: ["FQ2"]},
        }

        merged = strategy._merge_results(original, followup, "query")

        # Original has max key 2, followup keys should be 2+1=3, 2+2=4
        assert 1 in merged["questions_by_iteration"]
        assert 2 in merged["questions_by_iteration"]
        assert 3 in merged["questions_by_iteration"]
        assert 4 in merged["questions_by_iteration"]
        assert merged["questions_by_iteration"][3] == ["FQ1"]
        assert merged["questions_by_iteration"][4] == ["FQ2"]

    def test_does_not_mutate_original(self):
        """Original results dict is not mutated."""
        strategy = self._make_strategy()

        original = {
            "findings": [{"phase": "initial"}],
            "formatted_findings": "text",
        }
        original_copy = {
            "findings": [{"phase": "initial"}],
            "formatted_findings": "text",
        }
        followup = {"findings": [{"phase": "followup"}]}

        strategy._merge_results(original, followup, "query")

        # Original dict reference should have the same findings list length
        # because .copy() is shallow - but the original variable's list shouldn't grow
        assert len(original_copy["findings"]) == 1

    def test_handles_missing_followup_data(self):
        """Handles followup results with missing keys."""
        strategy = self._make_strategy()

        original = {"formatted_findings": "text"}
        followup = {}  # No findings, no formatted_findings, no knowledge

        merged = strategy._merge_results(original, followup, "query")

        # Should not raise, original data preserved
        assert "text" in merged["formatted_findings"]


class TestBuildResearchContext:
    """Tests for _build_research_context method."""

    def _make_strategy(self):
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        mock_initial = Mock()
        mock_initial.__class__.__name__ = "FocusedIterationStrategy"
        return IterativeRefinementStrategy(
            model=Mock(),
            search=Mock(),
            initial_strategy=mock_initial,
        )

    def test_includes_required_keys(self):
        """Context includes all required keys."""
        strategy = self._make_strategy()

        results = {
            "formatted_findings": "findings text",
            "all_links_of_system": [{"url": "https://example.com"}],
        }

        context = strategy._build_research_context(results, "test query")

        assert context["original_query"] == "test query"
        assert context["past_findings"] == "findings text"
        assert len(context["past_sources"]) == 1
        assert "delegate_strategy" in context

    def test_formats_delegate_strategy_name(self):
        """Delegate strategy name is formatted correctly."""
        strategy = self._make_strategy()

        context = strategy._build_research_context(
            {"formatted_findings": "", "all_links_of_system": []},
            "query",
        )

        # "FocusedIterationStrategy" -> remove "Strategy" -> "FocusedIteration" -> lower
        assert context["delegate_strategy"] == "focusediteration"

    def test_includes_questions_if_present(self):
        """Includes questions_by_iteration when present in results."""
        strategy = self._make_strategy()

        results = {
            "formatted_findings": "",
            "all_links_of_system": [],
            "questions_by_iteration": {1: ["Q1"]},
        }

        context = strategy._build_research_context(results, "query")
        assert "questions_by_iteration" in context

    def test_includes_findings_if_present(self):
        """Includes findings when present in results."""
        strategy = self._make_strategy()

        results = {
            "formatted_findings": "",
            "all_links_of_system": [],
            "findings": [{"phase": "test"}],
        }

        context = strategy._build_research_context(results, "query")
        assert "findings" in context

    def test_omits_optional_keys_when_absent(self):
        """Does not include optional keys when not in results."""
        strategy = self._make_strategy()

        results = {"formatted_findings": "", "all_links_of_system": []}

        context = strategy._build_research_context(results, "query")
        assert "questions_by_iteration" not in context
        assert "findings" not in context


class TestSetProgressCallback:
    """Tests for set_progress_callback method."""

    def test_sets_callback_on_self(self):
        """Sets callback on the strategy itself."""
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        strategy = IterativeRefinementStrategy(
            model=Mock(),
            search=Mock(),
            initial_strategy=Mock(),
        )

        callback = Mock()
        strategy.set_progress_callback(callback)

        assert strategy.progress_callback is callback

    def test_propagates_callback_to_initial_strategy(self):
        """Propagates callback to initial strategy."""
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        mock_initial = Mock()
        strategy = IterativeRefinementStrategy(
            model=Mock(),
            search=Mock(),
            initial_strategy=mock_initial,
        )

        callback = Mock()
        strategy.set_progress_callback(callback)

        mock_initial.set_progress_callback.assert_called_once_with(callback)


class TestAnalyzeTopic:
    """Tests for analyze_topic method."""

    def _make_strategy(self, **kwargs):
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        defaults = {
            "model": Mock(),
            "search": Mock(),
            "initial_strategy": Mock(),
        }
        defaults.update(kwargs)
        return IterativeRefinementStrategy(**defaults)

    def test_returns_error_on_none_initial_results(self):
        """Returns error response when initial strategy returns None."""
        mock_initial = Mock()
        mock_initial.analyze_topic.return_value = None

        strategy = self._make_strategy(initial_strategy=mock_initial)
        result = strategy.analyze_topic("test query")

        assert "error" in result
        assert result["findings"] == []

    def test_returns_error_on_empty_initial_results(self):
        """Returns error response when initial strategy returns empty dict."""
        mock_initial = Mock()
        mock_initial.analyze_topic.return_value = {}

        strategy = self._make_strategy(initial_strategy=mock_initial)
        result = strategy.analyze_topic("test query")

        assert "error" in result

    def test_passes_through_initial_error(self):
        """Passes through error from initial strategy."""
        mock_initial = Mock()
        mock_initial.analyze_topic.return_value = {
            "error": "Initial failed",
            "findings": [],
        }

        strategy = self._make_strategy(initial_strategy=mock_initial)
        result = strategy.analyze_topic("test query")

        assert result["error"] == "Initial failed"

    def test_skips_refinement_when_no_sources(self):
        """Skips refinement when initial results have no sources."""
        mock_initial = Mock()
        mock_initial.analyze_topic.return_value = {
            "findings": [{"phase": "initial"}],
            "formatted_findings": "Some findings",
            "all_links_of_system": [],  # No sources
        }

        strategy = self._make_strategy(initial_strategy=mock_initial)
        result = strategy.analyze_topic("test query")

        assert "refinement_metadata" in result
        assert result["refinement_metadata"]["refinements_performed"] == 0
        assert (
            result["refinement_metadata"]["skipped_reason"]
            == "No initial sources found"
        )

    def test_stops_on_complete_evaluation(self):
        """Stops refinement when LLM evaluates as COMPLETE."""
        mock_initial = Mock()
        mock_initial.analyze_topic.return_value = {
            "findings": [{"phase": "initial"}],
            "formatted_findings": "Findings",
            "all_links_of_system": [{"url": "https://example.com"}],
        }
        mock_initial.__class__.__name__ = "TestStrategy"

        strategy = self._make_strategy(
            initial_strategy=mock_initial,
            max_refinements=5,
        )

        # LLM evaluates as COMPLETE on first evaluation
        with patch.object(
            strategy,
            "_evaluate_with_llm",
            return_value={
                "action": "COMPLETE",
                "confidence": 0.9,
                "gaps": [],
                "reasoning": "Research is complete",
                "refinement_query": None,
            },
        ):
            result = strategy.analyze_topic("test query")

        assert result["refinement_metadata"]["refinements_performed"] == 0

    def test_stops_on_confidence_threshold(self):
        """Stops refinement when confidence meets threshold."""
        mock_initial = Mock()
        mock_initial.analyze_topic.return_value = {
            "findings": [{"phase": "initial"}],
            "formatted_findings": "Findings",
            "all_links_of_system": [{"url": "https://example.com"}],
        }
        mock_initial.__class__.__name__ = "TestStrategy"

        strategy = self._make_strategy(
            initial_strategy=mock_initial,
            confidence_threshold=0.8,
        )

        with patch.object(
            strategy,
            "_evaluate_with_llm",
            return_value={
                "action": "REFINE",
                "confidence": 0.85,  # Above threshold
                "gaps": ["minor gap"],
                "reasoning": "Good enough",
                "refinement_query": "minor question",
            },
        ):
            result = strategy.analyze_topic("test query")

        assert result["refinement_metadata"]["refinements_performed"] == 0

    def test_stops_when_no_refinement_query(self):
        """Stops when evaluation returns no refinement query."""
        mock_initial = Mock()
        mock_initial.analyze_topic.return_value = {
            "findings": [{"phase": "initial"}],
            "formatted_findings": "Findings",
            "all_links_of_system": [{"url": "https://example.com"}],
        }
        mock_initial.__class__.__name__ = "TestStrategy"

        strategy = self._make_strategy(initial_strategy=mock_initial)

        with patch.object(
            strategy,
            "_evaluate_with_llm",
            return_value={
                "action": "REFINE",
                "confidence": 0.3,
                "gaps": ["gap"],
                "reasoning": "Needs work",
                "refinement_query": None,  # No query
            },
        ):
            result = strategy.analyze_topic("test query")

        assert result["refinement_metadata"]["refinements_performed"] == 0

    def test_max_refinements_enforced(self):
        """Max refinements limit is enforced."""
        mock_initial = Mock()
        mock_initial.analyze_topic.return_value = {
            "findings": [],
            "formatted_findings": "Findings",
            "all_links_of_system": [{"url": "https://example.com"}],
            "current_knowledge": "knowledge",
        }
        mock_initial.__class__.__name__ = "TestStrategy"

        strategy = self._make_strategy(
            initial_strategy=mock_initial,
            max_refinements=2,
        )

        # Always return REFINE with query
        with patch.object(
            strategy,
            "_evaluate_with_llm",
            return_value={
                "action": "REFINE",
                "confidence": 0.3,
                "gaps": ["gap"],
                "reasoning": "Needs work",
                "refinement_query": "Refine this",
            },
        ):
            with patch(
                "local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy.EnhancedContextualFollowUpStrategy"
            ) as mock_followup_cls:
                mock_followup = Mock()
                mock_followup.analyze_topic.return_value = {
                    "findings": [],
                    "formatted_findings": "Refined",
                    "all_links_of_system": [
                        {"url": "https://new.com"},
                        {"url": "https://new2.com"},
                    ],
                    "current_knowledge": "updated",
                }
                mock_followup_cls.return_value = mock_followup

                result = strategy.analyze_topic("test query")

        assert result["refinement_metadata"]["refinements_performed"] <= 2

    def test_stops_when_few_new_sources(self):
        """Stops when followup yields fewer than 2 new sources."""
        mock_initial = Mock()
        mock_initial.analyze_topic.return_value = {
            "findings": [],
            "formatted_findings": "Findings",
            "all_links_of_system": [{"url": "https://example.com"}],
        }
        mock_initial.__class__.__name__ = "TestStrategy"

        strategy = self._make_strategy(initial_strategy=mock_initial)

        with patch.object(
            strategy,
            "_evaluate_with_llm",
            return_value={
                "action": "REFINE",
                "confidence": 0.3,
                "gaps": ["gap"],
                "reasoning": "Needs work",
                "refinement_query": "Refine this",
            },
        ):
            with patch(
                "local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy.EnhancedContextualFollowUpStrategy"
            ) as mock_followup_cls:
                mock_followup = Mock()
                mock_followup.analyze_topic.return_value = {
                    "findings": [],
                    "formatted_findings": "Refined",
                    "all_links_of_system": [
                        {"url": "https://single.com"}
                    ],  # Only 1 source
                }
                mock_followup_cls.return_value = mock_followup

                result = strategy.analyze_topic("test query")

        # Should stop after first refinement attempt due to few sources
        assert result["refinement_metadata"]["refinements_performed"] <= 1

    def test_includes_refinement_metadata(self):
        """Result includes refinement_metadata."""
        mock_initial = Mock()
        mock_initial.analyze_topic.return_value = {
            "findings": [],
            "formatted_findings": "Findings",
            "all_links_of_system": [{"url": "https://example.com"}],
        }
        mock_initial.__class__.__name__ = "TestStrategy"

        strategy = self._make_strategy(initial_strategy=mock_initial)

        with patch.object(
            strategy,
            "_evaluate_with_llm",
            return_value={
                "action": "COMPLETE",
                "confidence": 0.9,
                "gaps": [],
                "reasoning": "Done",
            },
        ):
            result = strategy.analyze_topic("test query")

        meta = result["refinement_metadata"]
        assert meta["strategy"] == "iterative_refinement"
        assert "refinements_performed" in meta
        assert "max_refinements" in meta
        assert "final_confidence" in meta
        assert "total_sources" in meta
        assert "refinement_history" in meta

    def test_source_deduplication(self):
        """Duplicate sources are deduplicated by URL."""
        mock_initial = Mock()
        mock_initial.analyze_topic.return_value = {
            "findings": [],
            "formatted_findings": "Findings",
            "all_links_of_system": [{"url": "https://example.com"}],
        }
        mock_initial.__class__.__name__ = "TestStrategy"

        strategy = self._make_strategy(initial_strategy=mock_initial)

        with patch.object(
            strategy,
            "_evaluate_with_llm",
            return_value={
                "action": "REFINE",
                "confidence": 0.3,
                "gaps": ["gap"],
                "reasoning": "Needs work",
                "refinement_query": "Refine",
            },
        ):
            with patch(
                "local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy.EnhancedContextualFollowUpStrategy"
            ) as mock_followup_cls:
                mock_followup = Mock()
                mock_followup.analyze_topic.return_value = {
                    "findings": [],
                    "formatted_findings": "Refined",
                    "all_links_of_system": [
                        {"url": "https://example.com"},  # Duplicate
                        {"url": "https://new.com"},
                    ],
                    "current_knowledge": "updated",
                }
                mock_followup_cls.return_value = mock_followup

                # Make next evaluation return COMPLETE to stop the loop
                call_count = [0]

                def eval_side_effect(*args):
                    call_count[0] += 1
                    if call_count[0] == 1:
                        return {
                            "action": "REFINE",
                            "confidence": 0.3,
                            "gaps": ["gap"],
                            "reasoning": "Needs work",
                            "refinement_query": "Refine",
                        }
                    return {
                        "action": "COMPLETE",
                        "confidence": 0.9,
                        "gaps": [],
                        "reasoning": "Done",
                    }

                with patch.object(
                    strategy, "_evaluate_with_llm", side_effect=eval_side_effect
                ):
                    result = strategy.analyze_topic("test query")

        # Should have 2 unique URLs, not 3
        urls = [s.get("url") for s in result["all_links_of_system"]]
        assert len(set(urls)) == len(urls)


class TestRefinementHistoryTracking:
    """Tests for refinement history tracking."""

    def test_history_populated_after_evaluation(self):
        """Refinement history is populated after LLM evaluation."""
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        mock_initial = Mock()
        mock_initial.analyze_topic.return_value = {
            "findings": [],
            "formatted_findings": "Findings",
            "all_links_of_system": [{"url": "https://example.com"}],
        }
        mock_initial.__class__.__name__ = "TestStrategy"

        strategy = IterativeRefinementStrategy(
            model=Mock(),
            search=Mock(),
            initial_strategy=mock_initial,
        )

        with patch.object(
            strategy,
            "_evaluate_with_llm",
            return_value={
                "action": "COMPLETE",
                "confidence": 0.9,
                "gaps": ["gap1"],
                "reasoning": "All good",
                "refinement_query": None,
            },
        ):
            strategy.analyze_topic("test query")

        assert len(strategy.refinement_history) == 1
        entry = strategy.refinement_history[0]
        assert entry["iteration"] == 1
        assert entry["action"] == "COMPLETE"
        assert entry["confidence"] == 0.9
        assert entry["gaps"] == ["gap1"]
        assert entry["reasoning"] == "All good"

    def test_empty_history_when_no_sources(self):
        """No history when refinement skipped due to no sources."""
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        mock_initial = Mock()
        mock_initial.analyze_topic.return_value = {
            "findings": [],
            "formatted_findings": "Findings",
            "all_links_of_system": [],
        }

        strategy = IterativeRefinementStrategy(
            model=Mock(),
            search=Mock(),
            initial_strategy=mock_initial,
        )

        strategy.analyze_topic("test query")
        assert strategy.refinement_history == []

    def test_zero_max_refinements_produces_no_history(self):
        """Zero max_refinements produces no refinement history."""
        from local_deep_research.advanced_search_system.strategies.iterative_refinement_strategy import (
            IterativeRefinementStrategy,
        )

        mock_initial = Mock()
        mock_initial.analyze_topic.return_value = {
            "findings": [],
            "formatted_findings": "Findings",
            "all_links_of_system": [{"url": "https://example.com"}],
        }

        strategy = IterativeRefinementStrategy(
            model=Mock(),
            search=Mock(),
            initial_strategy=mock_initial,
            max_refinements=0,
        )

        result = strategy.analyze_topic("test query")
        assert result["refinement_metadata"]["refinements_performed"] == 0
