"""
Tests for pure logic helper methods across strategy files.

These methods have zero existing test coverage on their actual class
implementations. Existing tests either test the regex pattern inline
(not the full method) or test a different class's implementation.

Tested methods:
- EarlyStopConstrainedStrategy._extract_confidence_from_response()
- EarlyStopConstrainedStrategy._evaluate_evidence()
- SmartDecompositionStrategy._is_browsecomp_style()
- ParallelConstrainedStrategy._generate_query_variations()
"""

from unittest.mock import Mock

import pytest

from local_deep_research.advanced_search_system.constraints.base_constraint import (
    Constraint,
    ConstraintType,
)


# ---------------------------------------------------------------------------
# Helpers: construct strategy instances with mocked dependencies
# ---------------------------------------------------------------------------


def _make_early_stop_strategy():
    """Create an EarlyStopConstrainedStrategy with mocked model/search."""
    from local_deep_research.advanced_search_system.strategies.early_stop_constrained_strategy import (
        EarlyStopConstrainedStrategy,
    )

    model = Mock()
    search = Mock()
    return EarlyStopConstrainedStrategy(
        model=model, search=search, all_links_of_system=[]
    )


def _make_smart_decomposition_strategy():
    """Create a SmartDecompositionStrategy with mocked model/search."""
    from local_deep_research.advanced_search_system.strategies.smart_decomposition_strategy import (
        SmartDecompositionStrategy,
    )

    model = Mock()
    search = Mock()
    return SmartDecompositionStrategy(
        model=model, search=search, all_links_of_system=[]
    )


def _make_parallel_constrained_strategy():
    """Create a ParallelConstrainedStrategy with mocked model/search."""
    from local_deep_research.advanced_search_system.strategies.parallel_constrained_strategy import (
        ParallelConstrainedStrategy,
    )

    model = Mock()
    search = Mock()
    return ParallelConstrainedStrategy(
        model=model, search=search, all_links_of_system=[]
    )


def _make_constraint(ctype=ConstraintType.PROPERTY, value="test constraint"):
    """Create a Constraint for testing."""
    return Constraint(id="c1", type=ctype, description="Test", value=value)


# ---------------------------------------------------------------------------
# _extract_confidence_from_response (early_stop_constrained_strategy.py)
# ---------------------------------------------------------------------------


class TestExtractConfidenceFromResponse:
    """Tests for EarlyStopConstrainedStrategy._extract_confidence_from_response().

    This method:
    1. Uses regex to find decimal numbers between 0 and 1
    2. Returns the LAST match found
    3. Falls back to keyword-based confidence if no numeric match
    4. Returns 0.5 as ultimate default
    """

    def test_extracts_simple_decimal(self):
        """Extracts a simple decimal confidence value."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response("confidence: 0.85")
            == 0.85
        )

    def test_extracts_last_number_when_multiple(self):
        """Returns the last number found, not the first."""
        strategy = _make_early_stop_strategy()
        result = strategy._extract_confidence_from_response(
            "Initial score 0.3, revised to 0.7"
        )
        assert result == 0.7

    def test_extracts_1_0(self):
        """Extracts 1.0 as perfect confidence."""
        strategy = _make_early_stop_strategy()
        assert strategy._extract_confidence_from_response("score: 1.0") == 1.0

    def test_extracts_integer_1(self):
        """Extracts integer 1 as confidence."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response("confidence: 1") == 1.0
        )

    def test_extracts_zero_decimal(self):
        """Extracts 0.0-like values (e.g. 0.1)."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response("very low: 0.1") == 0.1
        )

    def test_keyword_definitely_returns_0_9(self):
        """'definitely' keyword returns 0.9 confidence."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response(
                "This is definitely the answer"
            )
            == 0.9
        )

    def test_keyword_certainly_returns_0_9(self):
        """'certainly' keyword returns 0.9 confidence."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response(
                "This is certainly correct"
            )
            == 0.9
        )

    def test_keyword_absolutely_returns_0_9(self):
        """'absolutely' keyword returns 0.9 confidence."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response(
                "Absolutely the right match"
            )
            == 0.9
        )

    def test_keyword_likely_returns_0_7(self):
        """'likely' keyword returns 0.7 confidence."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response(
                "This is likely the answer"
            )
            == 0.7
        )

    def test_keyword_probably_returns_0_7(self):
        """'probably' keyword returns 0.7 confidence."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response(
                "This is probably correct"
            )
            == 0.7
        )

    def test_keyword_appears_returns_0_7(self):
        """'appears' keyword returns 0.7 confidence."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response("It appears to match")
            == 0.7
        )

    def test_keyword_possibly_returns_0_5(self):
        """'possibly' keyword returns 0.5 confidence."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response(
                "This is possibly the answer"
            )
            == 0.5
        )

    def test_keyword_maybe_returns_0_5(self):
        """'maybe' keyword returns 0.5 confidence."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response("Maybe this is correct")
            == 0.5
        )

    def test_keyword_might_returns_0_5(self):
        """'might' keyword returns 0.5 confidence."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response(
                "It might be the right one"
            )
            == 0.5
        )

    def test_keyword_unlikely_matches_likely_first(self):
        """'unlikely' contains 'likely' which matches the higher-priority tier."""
        strategy = _make_early_stop_strategy()
        # "unlikely" contains "likely", and the code checks "likely" tier first
        assert (
            strategy._extract_confidence_from_response(
                "This is unlikely the answer"
            )
            == 0.7
        )

    def test_keyword_doubtful_returns_0_3(self):
        """'doubtful' keyword returns 0.3 confidence."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response(
                "It is doubtful this matches"
            )
            == 0.3
        )

    def test_keyword_not_returns_0_3(self):
        """'not' keyword returns 0.3 confidence."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response(
                "This is not the correct answer"
            )
            == 0.3
        )

    def test_no_match_returns_default_0_5(self):
        """No numeric or keyword match returns default 0.5."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response("Indeterminate result")
            == 0.5
        )

    def test_empty_string_returns_default(self):
        """Empty string returns default 0.5."""
        strategy = _make_early_stop_strategy()
        assert strategy._extract_confidence_from_response("") == 0.5

    def test_numeric_takes_priority_over_keywords(self):
        """Numeric match takes priority over keyword match."""
        strategy = _make_early_stop_strategy()
        result = strategy._extract_confidence_from_response(
            "This is definitely the answer with confidence 0.6"
        )
        assert result == 0.6

    def test_keyword_case_insensitive(self):
        """Keyword matching is case-insensitive."""
        strategy = _make_early_stop_strategy()
        assert (
            strategy._extract_confidence_from_response("DEFINITELY the answer")
            == 0.9
        )

    def test_keyword_priority_high_over_low(self):
        """First matching keyword tier wins (definitely > unlikely)."""
        strategy = _make_early_stop_strategy()
        # "definitely" is checked before "not", so 0.9 should win
        result = strategy._extract_confidence_from_response(
            "This is definitely not wrong"
        )
        assert result == 0.9


# ---------------------------------------------------------------------------
# _evaluate_evidence (early_stop_constrained_strategy.py)
# ---------------------------------------------------------------------------


class TestEvaluateEvidence:
    """Tests for EarlyStopConstrainedStrategy._evaluate_evidence().

    This method averages confidence scores from evidence items.
    Empty evidence returns 0.0.
    """

    def test_empty_evidence_returns_zero(self):
        """Empty evidence list returns 0.0."""
        strategy = _make_early_stop_strategy()
        constraint = _make_constraint()
        assert strategy._evaluate_evidence([], constraint) == 0.0

    def test_single_evidence_item(self):
        """Single evidence item returns its confidence."""
        strategy = _make_early_stop_strategy()
        constraint = _make_constraint()
        evidence = [{"confidence": 0.8, "text": "evidence text"}]
        assert strategy._evaluate_evidence(
            evidence, constraint
        ) == pytest.approx(0.8)

    def test_multiple_evidence_averaged(self):
        """Multiple evidence items are averaged."""
        strategy = _make_early_stop_strategy()
        constraint = _make_constraint()
        evidence = [
            {"confidence": 0.6},
            {"confidence": 0.8},
            {"confidence": 1.0},
        ]
        # (0.6 + 0.8 + 1.0) / 3 = 0.8
        assert strategy._evaluate_evidence(
            evidence, constraint
        ) == pytest.approx(0.8)

    def test_missing_confidence_defaults_to_0_5(self):
        """Evidence items without 'confidence' key default to 0.5."""
        strategy = _make_early_stop_strategy()
        constraint = _make_constraint()
        evidence = [{"text": "no confidence key"}]
        assert strategy._evaluate_evidence(
            evidence, constraint
        ) == pytest.approx(0.5)

    def test_mixed_present_and_missing_confidence(self):
        """Averages mix of explicit and default confidence values."""
        strategy = _make_early_stop_strategy()
        constraint = _make_constraint()
        evidence = [
            {"confidence": 0.9},
            {"text": "no confidence"},  # defaults to 0.5
        ]
        # (0.9 + 0.5) / 2 = 0.7
        assert strategy._evaluate_evidence(
            evidence, constraint
        ) == pytest.approx(0.7)

    def test_all_zero_confidence(self):
        """All zero confidence returns 0.0."""
        strategy = _make_early_stop_strategy()
        constraint = _make_constraint()
        evidence = [{"confidence": 0.0}, {"confidence": 0.0}]
        assert strategy._evaluate_evidence(evidence, constraint) == 0.0

    def test_all_perfect_confidence(self):
        """All perfect confidence returns 1.0."""
        strategy = _make_early_stop_strategy()
        constraint = _make_constraint()
        evidence = [{"confidence": 1.0}, {"confidence": 1.0}]
        assert strategy._evaluate_evidence(
            evidence, constraint
        ) == pytest.approx(1.0)

    def test_ignores_constraint_details(self):
        """Result depends only on evidence, not constraint type."""
        strategy = _make_early_stop_strategy()
        constraint_a = _make_constraint(ctype=ConstraintType.PROPERTY)
        constraint_b = _make_constraint(ctype=ConstraintType.TEMPORAL)
        evidence = [{"confidence": 0.75}]
        assert strategy._evaluate_evidence(
            evidence, constraint_a
        ) == strategy._evaluate_evidence(evidence, constraint_b)


# ---------------------------------------------------------------------------
# _is_browsecomp_style (smart_decomposition_strategy.py)
# ---------------------------------------------------------------------------


class TestIsBrowsecompStyle:
    """Tests for SmartDecompositionStrategy._is_browsecomp_style().

    Returns True when 3+ BrowseComp indicator phrases are found in the query.
    The indicators include things like "specific scenic location", "body part",
    "fell from", "search and rescue", etc.
    """

    def test_no_indicators_returns_false(self):
        """Query with no indicators returns False."""
        strategy = _make_smart_decomposition_strategy()
        assert (
            strategy._is_browsecomp_style("What is machine learning?") is False
        )

    def test_one_indicator_returns_false(self):
        """Single indicator is not enough."""
        strategy = _make_smart_decomposition_strategy()
        assert (
            strategy._is_browsecomp_style("What is the name of the capital?")
            is False
        )

    def test_two_indicators_returns_false(self):
        """Two indicators is still below threshold."""
        strategy = _make_smart_decomposition_strategy()
        assert (
            strategy._is_browsecomp_style(
                "What is the name of a specific scenic location?"
            )
            is False
        )

    def test_three_indicators_returns_true(self):
        """Three indicators meets the threshold."""
        strategy = _make_smart_decomposition_strategy()
        # Indicators: "what is the name", "body part", "fell from"
        result = strategy._is_browsecomp_style(
            "What is the name of the trail where someone fell from a body part shaped rock?"
        )
        assert result is True

    def test_four_indicators_returns_true(self):
        """Four indicators clearly returns True."""
        strategy = _make_smart_decomposition_strategy()
        # Indicators: "what is the name", "search and rescue", "fell from", "viewpoint"
        result = strategy._is_browsecomp_style(
            "What is the name of the viewpoint where hikers fell from "
            "and required search and rescue?"
        )
        assert result is True

    def test_case_insensitive_matching(self):
        """Indicator matching is case-insensitive."""
        strategy = _make_smart_decomposition_strategy()
        # Indicators: "WHAT IS THE NAME", "BODY PART", "FELL FROM"
        result = strategy._is_browsecomp_style(
            "WHAT IS THE NAME OF THE PLACE WHERE SOMEONE FELL FROM A BODY PART CLIFF?"
        )
        assert result is True

    def test_many_indicators(self):
        """Query with many indicators returns True."""
        strategy = _make_smart_decomposition_strategy()
        query = (
            "What is the name of the specific scenic location with a viewpoint "
            "where someone fell from a cliff and SAR incidents were reported "
            "since the last ice age?"
        )
        # Indicators: "what is the name", "specific scenic location", "viewpoint",
        # "fell from", "SAR incidents", "last ice age"
        assert strategy._is_browsecomp_style(query) is True

    def test_partial_indicator_not_counted(self):
        """Partial indicator phrase doesn't count."""
        strategy = _make_smart_decomposition_strategy()
        # "scenic" alone is not "specific scenic location"
        # "search" alone is not "search and rescue"
        assert (
            strategy._is_browsecomp_style("scenic search between mountains")
            is False
        )

    def test_between_indicator(self):
        """'between' counts as an indicator."""
        strategy = _make_smart_decomposition_strategy()
        # Indicators: "between", "what is the name", "times more"
        result = strategy._is_browsecomp_style(
            "What is the name of something between two peaks that is times more popular?"
        )
        assert result is True

    def test_empty_query_returns_false(self):
        """Empty query returns False."""
        strategy = _make_smart_decomposition_strategy()
        assert strategy._is_browsecomp_style("") is False

    def test_exact_answer_indicator(self):
        """'exact answer' counts as an indicator."""
        strategy = _make_smart_decomposition_strategy()
        # Indicators: "exact answer", "multiple clues", "what is the name"
        result = strategy._is_browsecomp_style(
            "What is the name? I need the exact answer using multiple clues."
        )
        assert result is True


# ---------------------------------------------------------------------------
# _generate_query_variations (parallel_constrained_strategy.py)
# ---------------------------------------------------------------------------


class TestGenerateQueryVariations:
    """Tests for ParallelConstrainedStrategy._generate_query_variations().

    Generates search query variations based on constraint type:
    - STATISTIC: "list <base>", "complete <base>", "all <base>"
    - PROPERTY: "with <base>", "featuring <base>", "known for <base>"
    - Other types: just the base value
    Always limited to 3 variations max.
    """

    def test_statistic_constraint_variations(self):
        """STATISTIC constraint adds 'list', 'complete' prefixes (capped at 3)."""
        strategy = _make_parallel_constrained_strategy()
        constraint = _make_constraint(
            ctype=ConstraintType.STATISTIC, value="84.5x ratio"
        )
        variations = strategy._generate_query_variations(constraint)
        assert variations[0] == "84.5x ratio"
        assert "list 84.5x ratio" in variations
        assert "complete 84.5x ratio" in variations
        # "all 84.5x ratio" is the 4th item, cut by [:3] cap
        assert len(variations) == 3

    def test_property_constraint_variations(self):
        """PROPERTY constraint adds 'with', 'featuring' prefixes (capped at 3)."""
        strategy = _make_parallel_constrained_strategy()
        constraint = _make_constraint(
            ctype=ConstraintType.PROPERTY, value="formed during ice age"
        )
        variations = strategy._generate_query_variations(constraint)
        assert variations[0] == "formed during ice age"
        assert "with formed during ice age" in variations
        assert "featuring formed during ice age" in variations
        # "known for formed during ice age" is the 4th item, cut by [:3] cap
        assert len(variations) == 3

    def test_event_constraint_no_extra_variations(self):
        """EVENT constraint returns only the base value."""
        strategy = _make_parallel_constrained_strategy()
        constraint = _make_constraint(
            ctype=ConstraintType.EVENT, value="fall between 2000-2021"
        )
        variations = strategy._generate_query_variations(constraint)
        assert variations == ["fall between 2000-2021"]

    def test_temporal_constraint_no_extra_variations(self):
        """TEMPORAL constraint returns only the base value."""
        strategy = _make_parallel_constrained_strategy()
        constraint = _make_constraint(
            ctype=ConstraintType.TEMPORAL, value="in 2014"
        )
        variations = strategy._generate_query_variations(constraint)
        assert variations == ["in 2014"]

    def test_location_constraint_no_extra_variations(self):
        """LOCATION constraint returns only the base value."""
        strategy = _make_parallel_constrained_strategy()
        constraint = _make_constraint(
            ctype=ConstraintType.LOCATION, value="in Colorado"
        )
        variations = strategy._generate_query_variations(constraint)
        assert variations == ["in Colorado"]

    def test_max_three_variations(self):
        """Variations list is capped at 3."""
        strategy = _make_parallel_constrained_strategy()
        constraint = _make_constraint(
            ctype=ConstraintType.STATISTIC, value="test stat"
        )
        variations = strategy._generate_query_variations(constraint)
        # base + 3 type-specific = 4 total, but capped at 3
        assert len(variations) <= 3

    def test_base_value_always_first(self):
        """Base constraint value is always the first variation."""
        strategy = _make_parallel_constrained_strategy()
        for ctype in [
            ConstraintType.STATISTIC,
            ConstraintType.PROPERTY,
            ConstraintType.EVENT,
        ]:
            constraint = _make_constraint(ctype=ctype, value="base value")
            variations = strategy._generate_query_variations(constraint)
            assert variations[0] == "base value"

    def test_name_pattern_no_extra_variations(self):
        """NAME_PATTERN constraint returns only the base value."""
        strategy = _make_parallel_constrained_strategy()
        constraint = _make_constraint(
            ctype=ConstraintType.NAME_PATTERN, value="contains body part"
        )
        variations = strategy._generate_query_variations(constraint)
        assert variations == ["contains body part"]

    def test_statistic_returns_exactly_three(self):
        """STATISTIC type: base + 3 prefixed = 4, capped to 3."""
        strategy = _make_parallel_constrained_strategy()
        constraint = _make_constraint(
            ctype=ConstraintType.STATISTIC, value="stat"
        )
        variations = strategy._generate_query_variations(constraint)
        assert len(variations) == 3

    def test_property_returns_exactly_three(self):
        """PROPERTY type: base + 3 prefixed = 4, capped to 3."""
        strategy = _make_parallel_constrained_strategy()
        constraint = _make_constraint(
            ctype=ConstraintType.PROPERTY, value="prop"
        )
        variations = strategy._generate_query_variations(constraint)
        assert len(variations) == 3
