"""
High-value tests for EarlyStopConstrainedStrategy.

Covers pure logic paths:
- _extract_confidence_from_response: regex parsing, keyword fallback, edge cases
- _evaluate_evidence: averaging, empty evidence, missing confidence keys
- Thread-safe tracking: evaluation_lock, found_answer Event, best_candidate updates
- _evaluate_candidate_immediately: score accumulation, early critical failure, early stop trigger
"""

import threading
from unittest.mock import MagicMock, Mock, patch


from local_deep_research.advanced_search_system.strategies.early_stop_constrained_strategy import (
    EarlyStopConstrainedStrategy,
)


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


def _make_strategy(early_stop_threshold=0.99, concurrent_evaluation=True):
    """Create an EarlyStopConstrainedStrategy with mocked dependencies."""
    model = MagicMock()
    search = MagicMock()
    strategy = EarlyStopConstrainedStrategy(
        model=model,
        search=search,
        all_links_of_system=[],
        early_stop_threshold=early_stop_threshold,
        concurrent_evaluation=concurrent_evaluation,
    )
    return strategy


def _make_constraint(
    value="test constraint", weight=1.0, type_value="property"
):
    """Create a mock Constraint object."""
    constraint = Mock()
    constraint.value = value
    constraint.weight = weight
    constraint.type = Mock()
    constraint.type.value = type_value
    return constraint


def _make_candidate(name="TestCandidate"):
    """Create a mock Candidate object."""
    candidate = Mock()
    candidate.name = name
    return candidate


# ---------------------------------------------------------------------------
# _extract_confidence_from_response
# ---------------------------------------------------------------------------


class TestExtractConfidenceFromResponse:
    """Tests for regex-based confidence extraction with keyword fallback."""

    def test_extracts_decimal_number(self):
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response("Confidence: 0.85")
            == 0.85
        )

    def test_extracts_last_number_when_multiple(self):
        """Uses the last numeric match found."""
        strategy = _make_strategy()
        result = strategy._extract_confidence_from_response(
            "Score is 0.3 but revised to 0.9"
        )
        assert result == 0.9

    def test_extracts_1_0(self):
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response("Perfect match: 1.0")
            == 1.0
        )

    def test_extracts_bare_1(self):
        strategy = _make_strategy()
        assert strategy._extract_confidence_from_response("Score: 1") == 1.0

    def test_extracts_number_without_leading_zero(self):
        """Regex requires word boundary so bare '.75' does not match; falls back to keyword/default."""
        strategy = _make_strategy()
        # The regex \b0?\.\d+ requires word boundary before the optional 0
        # '.75' alone doesn't match, but '0.75' does
        assert (
            strategy._extract_confidence_from_response("Confidence 0.75")
            == 0.75
        )

    def test_keyword_definitely_returns_0_9(self):
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response(
                "This is definitely the answer"
            )
            == 0.9
        )

    def test_keyword_certainly_returns_0_9(self):
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response("I certainly believe so")
            == 0.9
        )

    def test_keyword_absolutely_returns_0_9(self):
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response("Absolutely correct")
            == 0.9
        )

    def test_keyword_likely_returns_0_7(self):
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response("This is likely correct")
            == 0.7
        )

    def test_keyword_probably_returns_0_7(self):
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response(
                "Probably the right answer"
            )
            == 0.7
        )

    def test_keyword_appears_returns_0_7(self):
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response(
                "It appears to be correct"
            )
            == 0.7
        )

    def test_keyword_possibly_returns_0_5(self):
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response("Possibly the answer")
            == 0.5
        )

    def test_keyword_maybe_returns_0_5(self):
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response("Maybe it could be this")
            == 0.5
        )

    def test_keyword_might_returns_0_5(self):
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response("It might be correct")
            == 0.5
        )

    def test_keyword_unlikely_matches_likely_first(self):
        """'unlikely' contains 'likely' substring, so 'likely' keyword matches first (0.7)."""
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response("This is unlikely")
            == 0.7
        )

    def test_keyword_doubtful_returns_0_3(self):
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response("It is doubtful") == 0.3
        )

    def test_keyword_not_returns_0_3(self):
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response("This is not the answer")
            == 0.3
        )

    def test_no_number_no_keyword_returns_0_5(self):
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response(
                "Some generic response with no indicators"
            )
            == 0.5
        )

    def test_empty_string_returns_0_5(self):
        strategy = _make_strategy()
        assert strategy._extract_confidence_from_response("") == 0.5

    def test_keyword_priority_high_confidence_first(self):
        """'definitely' keyword is checked before 'likely'."""
        strategy = _make_strategy()
        # The function checks keywords in order; 'definitely' matches first
        assert (
            strategy._extract_confidence_from_response(
                "This is definitely and likely correct"
            )
            == 0.9
        )

    def test_number_takes_priority_over_keywords(self):
        """When a number is present, it wins over keyword matching."""
        strategy = _make_strategy()
        result = strategy._extract_confidence_from_response(
            "Definitely the answer with confidence 0.42"
        )
        assert result == 0.42

    def test_case_insensitive_keywords(self):
        strategy = _make_strategy()
        assert (
            strategy._extract_confidence_from_response("DEFINITELY yes") == 0.9
        )


# ---------------------------------------------------------------------------
# _evaluate_evidence
# ---------------------------------------------------------------------------


class TestEvaluateEvidence:
    """Tests for evidence evaluation scoring."""

    def test_empty_evidence_returns_zero(self):
        strategy = _make_strategy()
        constraint = _make_constraint()
        assert strategy._evaluate_evidence([], constraint) == 0.0

    def test_single_evidence_returns_its_confidence(self):
        strategy = _make_strategy()
        constraint = _make_constraint()
        evidence = [{"confidence": 0.8, "text": "supports"}]
        assert strategy._evaluate_evidence(evidence, constraint) == 0.8

    def test_averages_multiple_evidence_confidences(self):
        strategy = _make_strategy()
        constraint = _make_constraint()
        evidence = [
            {"confidence": 0.6},
            {"confidence": 0.8},
            {"confidence": 1.0},
        ]
        result = strategy._evaluate_evidence(evidence, constraint)
        assert abs(result - 0.8) < 0.001

    def test_missing_confidence_defaults_to_0_5(self):
        strategy = _make_strategy()
        constraint = _make_constraint()
        evidence = [
            {"text": "no confidence key"},
            {"confidence": 0.9},
        ]
        result = strategy._evaluate_evidence(evidence, constraint)
        assert abs(result - 0.7) < 0.001  # (0.5 + 0.9) / 2

    def test_all_missing_confidence_returns_0_5(self):
        strategy = _make_strategy()
        constraint = _make_constraint()
        evidence = [{"text": "a"}, {"text": "b"}]
        result = strategy._evaluate_evidence(evidence, constraint)
        assert result == 0.5


# ---------------------------------------------------------------------------
# Thread-safe tracking
# ---------------------------------------------------------------------------


class TestThreadSafeTracking:
    """Tests for thread-safe best_candidate and found_answer tracking."""

    def test_initial_state(self):
        strategy = _make_strategy()
        assert strategy.best_candidate is None
        assert strategy.best_score == 0.0
        assert not strategy.found_answer.is_set()
        assert strategy.evaluated_candidates == {}
        assert strategy.evaluating_candidates == set()

    def test_evaluation_lock_is_threading_lock(self):
        strategy = _make_strategy()
        assert isinstance(strategy.evaluation_lock, type(threading.Lock()))

    def test_found_answer_is_threading_event(self):
        strategy = _make_strategy()
        assert isinstance(strategy.found_answer, threading.Event)

    def test_early_stop_threshold_stored(self):
        strategy = _make_strategy(early_stop_threshold=0.95)
        assert strategy.early_stop_threshold == 0.95

    def test_concurrent_evaluation_flag(self):
        strategy = _make_strategy(concurrent_evaluation=False)
        assert strategy.concurrent_evaluation is False


# ---------------------------------------------------------------------------
# _evaluate_candidate_immediately
# ---------------------------------------------------------------------------


class TestEvaluateCandidateImmediately:
    """Tests for immediate candidate evaluation against constraints."""

    def test_returns_zero_on_exception(self):
        """If evaluation throws, returns 0.0."""
        strategy = _make_strategy()
        strategy.constraint_ranking = [_make_constraint()]
        strategy._gather_evidence_for_constraint = Mock(
            side_effect=Exception("fail")
        )
        candidate = _make_candidate("FailCandidate")

        score = strategy._evaluate_candidate_immediately(candidate)
        assert score == 0.0

    def test_updates_best_candidate_on_higher_score(self):
        """Candidate with higher score replaces best_candidate."""
        strategy = _make_strategy()
        strategy.constraint_ranking = [_make_constraint(weight=0.5)]
        strategy._gather_evidence_for_constraint = Mock(
            return_value=[{"confidence": 0.85}]
        )
        strategy._evaluate_evidence = Mock(return_value=0.85)

        candidate = _make_candidate("GoodCandidate")
        score = strategy._evaluate_candidate_immediately(candidate)

        assert score == 0.85
        assert strategy.best_candidate == "GoodCandidate"
        assert strategy.best_score == 0.85
        assert strategy.evaluated_candidates["GoodCandidate"] == 0.85

    def test_does_not_replace_higher_score_candidate(self):
        """Lower score candidate does not replace existing best."""
        strategy = _make_strategy()
        strategy.best_score = 0.9
        strategy.best_candidate = "BetterCandidate"
        strategy.constraint_ranking = [_make_constraint(weight=0.5)]
        strategy._gather_evidence_for_constraint = Mock(
            return_value=[{"confidence": 0.5}]
        )
        strategy._evaluate_evidence = Mock(return_value=0.5)

        candidate = _make_candidate("WeakCandidate")
        score = strategy._evaluate_candidate_immediately(candidate)

        assert score == 0.5
        assert strategy.best_candidate == "BetterCandidate"
        assert strategy.best_score == 0.9

    def test_triggers_early_stop_at_threshold(self):
        """Score >= early_stop_threshold sets the found_answer Event."""
        strategy = _make_strategy(early_stop_threshold=0.8)
        strategy.constraint_ranking = [_make_constraint(weight=0.5)]
        strategy._gather_evidence_for_constraint = Mock(
            return_value=[{"confidence": 0.95}]
        )
        strategy._evaluate_evidence = Mock(return_value=0.95)

        candidate = _make_candidate("PerfectCandidate")
        strategy._evaluate_candidate_immediately(candidate)

        assert strategy.found_answer.is_set()
        assert strategy.best_candidate == "PerfectCandidate"

    def test_does_not_trigger_early_stop_below_threshold(self):
        strategy = _make_strategy(early_stop_threshold=0.99)
        strategy.constraint_ranking = [_make_constraint(weight=0.5)]
        strategy._gather_evidence_for_constraint = Mock(
            return_value=[{"confidence": 0.8}]
        )
        strategy._evaluate_evidence = Mock(return_value=0.8)

        candidate = _make_candidate("OkCandidate")
        strategy._evaluate_candidate_immediately(candidate)

        assert not strategy.found_answer.is_set()

    def test_early_critical_failure_skips_remaining_constraints(self):
        """Score < 0.3 on high-weight constraint skips remaining checks."""
        strategy = _make_strategy()
        c1 = _make_constraint(value="critical", weight=0.9)
        c2 = _make_constraint(value="secondary", weight=0.5)
        strategy.constraint_ranking = [c1, c2]

        call_count = 0

        def mock_gather(candidate, constraint):
            nonlocal call_count
            call_count += 1
            return [{"confidence": 0.1}]

        strategy._gather_evidence_for_constraint = mock_gather
        strategy._evaluate_evidence = Mock(return_value=0.1)

        candidate = _make_candidate("BadCandidate")
        score = strategy._evaluate_candidate_immediately(candidate)

        # Only the first constraint should have been checked (early break)
        assert call_count == 1
        # Score should be based on only the first constraint
        assert score == 0.1

    def test_averages_across_all_constraints_when_no_early_break(self):
        """When no early failure, averages scores across all constraints."""
        strategy = _make_strategy()
        c1 = _make_constraint(value="first", weight=0.5)
        c2 = _make_constraint(value="second", weight=0.5)
        strategy.constraint_ranking = [c1, c2]

        scores = iter([0.6, 0.8])
        strategy._gather_evidence_for_constraint = Mock(
            return_value=[{"confidence": 0.7}]
        )
        strategy._evaluate_evidence = Mock(
            side_effect=lambda e, c: next(scores)
        )

        candidate = _make_candidate("MediumCandidate")
        score = strategy._evaluate_candidate_immediately(candidate)

        assert abs(score - 0.7) < 0.001  # (0.6 + 0.8) / 2

    def test_no_constraints_returns_zero(self):
        """With no constraints, score is 0.0."""
        strategy = _make_strategy()
        strategy.constraint_ranking = []

        candidate = _make_candidate("NoConstraints")
        score = strategy._evaluate_candidate_immediately(candidate)
        assert score == 0.0

    def test_progress_callback_called_per_constraint(self):
        """Progress callback is called for each constraint evaluation."""
        strategy = _make_strategy()
        callback = Mock()
        strategy.progress_callback = callback
        strategy.constraint_ranking = [
            _make_constraint(value="c1", weight=0.5),
            _make_constraint(value="c2", weight=0.5),
        ]
        strategy._gather_evidence_for_constraint = Mock(
            return_value=[{"confidence": 0.7}]
        )
        strategy._evaluate_evidence = Mock(return_value=0.7)

        candidate = _make_candidate("CallbackTest")
        strategy._evaluate_candidate_immediately(candidate)

        assert callback.call_count == 2


# ---------------------------------------------------------------------------
# analyze_topic additions
# ---------------------------------------------------------------------------


class TestAnalyzeTopicEarlyStop:
    """Tests that analyze_topic adds early stopping metadata."""

    def test_analyze_topic_adds_early_stop_fields(self):
        """Result dict includes early_stopped, evaluated_candidates, best_candidate, best_score."""
        strategy = _make_strategy()
        # Mock parent's analyze_topic to return a base result
        with patch(
            "local_deep_research.advanced_search_system.strategies.early_stop_constrained_strategy.ParallelConstrainedStrategy.analyze_topic",
            return_value={"findings": [], "current_knowledge": "test"},
        ):
            result = strategy.analyze_topic("test query")

        assert "early_stopped" in result
        assert "evaluated_candidates" in result
        assert "best_candidate" in result
        assert "best_score" in result

    def test_early_stopped_reflects_found_answer(self):
        strategy = _make_strategy()
        strategy.found_answer.set()

        with patch(
            "local_deep_research.advanced_search_system.strategies.early_stop_constrained_strategy.ParallelConstrainedStrategy.analyze_topic",
            return_value={},
        ):
            result = strategy.analyze_topic("test")

        assert result["early_stopped"] is True

    def test_not_early_stopped_when_answer_not_found(self):
        strategy = _make_strategy()

        with patch(
            "local_deep_research.advanced_search_system.strategies.early_stop_constrained_strategy.ParallelConstrainedStrategy.analyze_topic",
            return_value={},
        ):
            result = strategy.analyze_topic("test")

        assert result["early_stopped"] is False
