"""
Tests for BaseSearchStrategy.

Tests cover:
- Initialization with default and custom parameters
- Settings access via get_setting
- Progress callback functionality
- Error handling utilities
- Progress emission helpers
"""

import pytest
from unittest.mock import Mock
from typing import Dict

from local_deep_research.advanced_search_system.strategies.base_strategy import (
    BaseSearchStrategy,
)


class ConcreteStrategy(BaseSearchStrategy):
    """Concrete implementation for testing abstract base class."""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.search = None

    def analyze_topic(self, query: str) -> Dict:
        """Simple implementation for testing."""
        return {
            "findings": [],
            "iterations": 0,
            "questions": {},
            "formatted_findings": "",
            "current_knowledge": "",
        }


class TestBaseSearchStrategyInit:
    """Tests for BaseSearchStrategy initialization."""

    def test_init_default_values(self):
        """Initialize with default values."""
        strategy = ConcreteStrategy()

        assert strategy.all_links_of_system == []
        assert strategy.questions_by_iteration == {}
        assert strategy.settings_snapshot == {}
        assert strategy.progress_callback is None
        assert strategy.search_original_query is True

    def test_init_with_all_links(self):
        """Initialize with provided all_links_of_system."""
        links = [{"url": "https://example.com", "title": "Example"}]
        strategy = ConcreteStrategy(all_links_of_system=links)

        assert strategy.all_links_of_system is links
        assert len(strategy.all_links_of_system) == 1

    def test_init_with_settings_snapshot(self):
        """Initialize with settings snapshot."""
        snapshot = {
            "search.iterations": {"value": 3},
            "llm.temperature": {"value": 0.7},
        }
        strategy = ConcreteStrategy(settings_snapshot=snapshot)

        assert strategy.settings_snapshot == snapshot

    def test_init_with_questions_by_iteration(self):
        """Initialize with questions by iteration."""
        questions = {1: ["Q1", "Q2"], 2: ["Q3"]}
        strategy = ConcreteStrategy(questions_by_iteration=questions)

        assert strategy.questions_by_iteration is questions
        assert len(strategy.questions_by_iteration) == 2

    def test_init_search_original_query_false(self):
        """Initialize with search_original_query=False."""
        strategy = ConcreteStrategy(search_original_query=False)

        assert strategy.search_original_query is False

    def test_init_creates_new_dict_for_none_questions(self):
        """Initialize creates new dict for None questions_by_iteration."""
        strategy1 = ConcreteStrategy()
        strategy2 = ConcreteStrategy()

        # Each should have its own dict
        strategy1.questions_by_iteration["test"] = ["Q1"]

        assert "test" not in strategy2.questions_by_iteration

    def test_init_creates_new_list_for_none_links(self):
        """Initialize creates new list for None all_links_of_system."""
        strategy1 = ConcreteStrategy()
        strategy2 = ConcreteStrategy()

        # Each should have its own list
        strategy1.all_links_of_system.append({"url": "test"})

        assert len(strategy2.all_links_of_system) == 0


class TestGetSetting:
    """Tests for get_setting method."""

    def test_get_setting_with_value_dict(self):
        """Get setting from dict with 'value' key."""
        snapshot = {"search.iterations": {"value": 5}}
        strategy = ConcreteStrategy(settings_snapshot=snapshot)

        result = strategy.get_setting("search.iterations")

        assert result == 5

    def test_get_setting_direct_value(self):
        """Get setting with direct value (not dict)."""
        snapshot = {"simple_key": "simple_value"}
        strategy = ConcreteStrategy(settings_snapshot=snapshot)

        result = strategy.get_setting("simple_key")

        assert result == "simple_value"

    def test_get_setting_missing_key_returns_default(self):
        """Get missing setting returns default."""
        strategy = ConcreteStrategy()

        result = strategy.get_setting("nonexistent.key", default="fallback")

        assert result == "fallback"

    def test_get_setting_missing_key_returns_none(self):
        """Get missing setting returns None by default."""
        strategy = ConcreteStrategy()

        result = strategy.get_setting("nonexistent.key")

        assert result is None

    def test_get_setting_nested_value(self):
        """Get setting with nested value."""
        snapshot = {
            "complex.setting": {
                "value": {"nested": "data"},
                "ui_element": "json",
            }
        }
        strategy = ConcreteStrategy(settings_snapshot=snapshot)

        result = strategy.get_setting("complex.setting")

        assert result == {"nested": "data"}


class TestProgressCallback:
    """Tests for progress callback functionality."""

    def test_set_progress_callback(self):
        """Set progress callback."""
        strategy = ConcreteStrategy()
        callback = Mock()

        strategy.set_progress_callback(callback)

        assert strategy.progress_callback is callback

    def test_update_progress_calls_callback(self):
        """Update progress calls callback with correct args."""
        strategy = ConcreteStrategy()
        callback = Mock()
        strategy.set_progress_callback(callback)

        strategy._update_progress("Test message", 50, {"phase": "test"})

        callback.assert_called_once_with("Test message", 50, {"phase": "test"})

    def test_update_progress_without_callback(self):
        """Update progress does nothing without callback."""
        strategy = ConcreteStrategy()

        # Should not raise
        strategy._update_progress("Test message", 50)

    def test_update_progress_none_metadata_becomes_empty_dict(self):
        """Update progress with None metadata becomes empty dict."""
        strategy = ConcreteStrategy()
        callback = Mock()
        strategy.set_progress_callback(callback)

        strategy._update_progress("Test message", 50, None)

        callback.assert_called_once_with("Test message", 50, {})

    def test_update_progress_optional_progress_percent(self):
        """Update progress with optional progress_percent."""
        strategy = ConcreteStrategy()
        callback = Mock()
        strategy.set_progress_callback(callback)

        strategy._update_progress("Test message")

        callback.assert_called_once_with("Test message", None, {})


class TestValidateSearchEngine:
    """Tests for _validate_search_engine method."""

    def test_validate_search_engine_missing(self):
        """Validate returns False when search engine missing."""
        strategy = ConcreteStrategy()
        strategy.search = None
        callback = Mock()
        strategy.set_progress_callback(callback)

        result = strategy._validate_search_engine()

        assert result is False
        callback.assert_called_once()
        call_args = callback.call_args
        assert "No search engine available" in call_args[0][0]
        assert call_args[0][1] == 100  # Progress should be 100%
        assert call_args[0][2]["phase"] == "error"

    def test_validate_search_engine_present(self):
        """Validate returns True when search engine present."""
        strategy = ConcreteStrategy()
        strategy.search = Mock()

        result = strategy._validate_search_engine()

        assert result is True

    def test_validate_search_engine_no_search_attribute(self):
        """Validate returns False when no search attribute."""
        strategy = ConcreteStrategy()
        del strategy.search

        result = strategy._validate_search_engine()

        assert result is False


class TestHandleSearchError:
    """Tests for _handle_search_error method."""

    def test_handle_search_error_updates_progress(self):
        """Handle search error updates progress."""
        strategy = ConcreteStrategy()
        callback = Mock()
        strategy.set_progress_callback(callback)

        error = ValueError("Connection failed")
        result = strategy._handle_search_error(error, "test question", 50)

        assert result == []
        callback.assert_called_once()
        call_args = callback.call_args
        assert "Connection failed" in call_args[0][0]
        assert call_args[0][1] == 52  # progress_base + 2
        assert call_args[0][2]["phase"] == "search_error"

    def test_handle_search_error_returns_empty_list(self):
        """Handle search error always returns empty list."""
        strategy = ConcreteStrategy()

        result = strategy._handle_search_error(
            Exception("Error"), "question", 0
        )

        assert result == []


class TestHandleAnalysisError:
    """Tests for _handle_analysis_error method."""

    def test_handle_analysis_error_updates_progress(self):
        """Handle analysis error updates progress."""
        strategy = ConcreteStrategy()
        callback = Mock()
        strategy.set_progress_callback(callback)

        error = ValueError("Analysis failed")
        strategy._handle_analysis_error(error, "test question", 50)

        callback.assert_called_once()
        call_args = callback.call_args
        assert "Analysis failed" in call_args[0][0]
        assert call_args[0][1] == 60  # progress_base + 10
        assert call_args[0][2]["phase"] == "analysis_error"


class TestEmitQuestionGenerationProgress:
    """Tests for _emit_question_generation_progress method."""

    def test_emit_first_iteration_progress(self):
        """Emit progress for first iteration."""
        strategy = ConcreteStrategy()
        callback = Mock()
        strategy.set_progress_callback(callback)

        strategy._emit_question_generation_progress(
            iteration=1, progress_percent=10, query="test query"
        )

        callback.assert_called_once()
        call_args = callback.call_args
        assert "test query" in call_args[0][0]
        assert call_args[0][1] == 10
        assert call_args[0][2]["iteration"] == 1
        assert call_args[0][2]["type"] == "milestone"

    def test_emit_later_iteration_progress(self):
        """Emit progress for later iterations."""
        strategy = ConcreteStrategy()
        strategy.questions_by_iteration = {
            1: ["Question 1", "Question 2", "Question 3", "Question 4"]
        }
        callback = Mock()
        strategy.set_progress_callback(callback)

        strategy._emit_question_generation_progress(
            iteration=2, progress_percent=50, source_count=10
        )

        callback.assert_called_once()
        call_args = callback.call_args
        assert "10 sources" in call_args[0][0]
        assert "and 1 more" in call_args[0][0]  # 4 questions, showing 3
        assert call_args[0][2]["source_count"] == 10


class TestEmitSearchingProgress:
    """Tests for _emit_searching_progress method."""

    def test_emit_searching_progress_basic(self):
        """Emit searching progress with questions."""
        strategy = ConcreteStrategy()
        callback = Mock()
        strategy.set_progress_callback(callback)

        questions = ["Q1", "Q2", "Q3"]
        strategy._emit_searching_progress(
            iteration=1, questions=questions, progress_percent=25
        )

        callback.assert_called_once()
        call_args = callback.call_args
        assert "Searching iteration 1" in call_args[0][0]
        assert "Q1" in call_args[0][0]
        assert call_args[0][2]["questions"] == questions

    def test_emit_searching_progress_many_questions(self):
        """Emit searching progress with many questions."""
        strategy = ConcreteStrategy()
        callback = Mock()
        strategy.set_progress_callback(callback)

        questions = ["Q1", "Q2", "Q3", "Q4", "Q5", "Q6", "Q7"]
        strategy._emit_searching_progress(
            iteration=2, questions=questions, progress_percent=50
        )

        callback.assert_called_once()
        call_args = callback.call_args
        assert "and 2 more" in call_args[0][0]  # 7 questions, showing 5


class TestAbstractMethod:
    """Tests for abstract method enforcement."""

    def test_cannot_instantiate_base_class(self):
        """Cannot instantiate BaseSearchStrategy directly."""
        with pytest.raises(TypeError, match="abstract"):
            BaseSearchStrategy()

    def test_subclass_must_implement_analyze_topic(self):
        """Subclass must implement analyze_topic."""

        class IncompleteStrategy(BaseSearchStrategy):
            pass

        with pytest.raises(TypeError, match="abstract"):
            IncompleteStrategy()


class TestStrategySubclassing:
    """Tests for strategy subclassing patterns."""

    def test_subclass_can_override_methods(self):
        """Subclass can override methods."""

        class CustomStrategy(BaseSearchStrategy):
            def analyze_topic(self, query: str) -> Dict:
                self._update_progress("Custom progress", 50)
                return {"custom": True}

            def _update_progress(self, message, progress=None, metadata=None):
                # Custom progress handling
                super()._update_progress(
                    f"[Custom] {message}", progress, metadata
                )

        strategy = CustomStrategy()
        callback = Mock()
        strategy.set_progress_callback(callback)

        result = strategy.analyze_topic("test")

        assert result["custom"] is True
        callback.assert_called_once()
        assert "[Custom]" in callback.call_args[0][0]

    def test_subclass_can_add_attributes(self):
        """Subclass can add custom attributes."""

        class ExtendedStrategy(BaseSearchStrategy):
            def __init__(self, custom_param=None, **kwargs):
                super().__init__(**kwargs)
                self.custom_param = custom_param

            def analyze_topic(self, query: str) -> Dict:
                return {"param": self.custom_param}

        strategy = ExtendedStrategy(custom_param="test_value")

        assert strategy.custom_param == "test_value"
        result = strategy.analyze_topic("query")
        assert result["param"] == "test_value"
