"""
High-value tests for ConcurrentDualConfidenceStrategy.

Covers pure logic paths:
- SearchState dataclass defaults and field types
- _classify_constraint_difficulty: constraint type adjustment, sorting
- _build_combined_query: multi-word quoting, already-quoted passthrough, concatenation
- _should_stop_search: all 7 stopping criteria
- _is_candidate_evaluated: thread-safe duplicate check
"""

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


from local_deep_research.advanced_search_system.strategies.concurrent_dual_confidence_strategy import (
    ConcurrentDualConfidenceStrategy,
    SearchState,
)


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


def _make_strategy(**kwargs):
    """Create a ConcurrentDualConfidenceStrategy with mocked dependencies."""
    model = MagicMock()
    search = MagicMock()
    defaults = dict(
        model=model,
        search=search,
        all_links_of_system=[],
    )
    defaults.update(kwargs)
    return ConcurrentDualConfidenceStrategy(**defaults)


def _make_constraint(value="test", type_name="PROPERTY"):
    """Create a mock Constraint with a ConstraintType."""
    from local_deep_research.advanced_search_system.constraints.base_constraint import (
        ConstraintType,
    )

    constraint = Mock()
    constraint.value = value
    constraint.type = ConstraintType[type_name]
    return constraint


def _make_candidate(name="Candidate"):
    """Create a mock Candidate."""
    c = Mock()
    c.name = name
    return c


# ---------------------------------------------------------------------------
# SearchState dataclass
# ---------------------------------------------------------------------------


class TestSearchState:
    """Tests for SearchState dataclass defaults."""

    def test_default_good_candidates_empty(self):
        state = SearchState()
        assert state.good_candidates == []

    def test_default_total_evaluated_zero(self):
        state = SearchState()
        assert state.total_evaluated == 0

    def test_start_time_is_float(self):
        state = SearchState()
        assert isinstance(state.start_time, float)

    def test_remaining_constraints_empty(self):
        state = SearchState()
        assert state.remaining_constraints == []

    def test_candidates_lock_is_lock(self):
        state = SearchState()
        assert isinstance(state.candidates_lock, type(threading.Lock()))

    def test_stop_search_is_event(self):
        state = SearchState()
        assert isinstance(state.stop_search, threading.Event)
        assert not state.stop_search.is_set()

    def test_evaluation_futures_empty(self):
        state = SearchState()
        assert state.evaluation_futures == []

    def test_each_instance_gets_unique_lock(self):
        s1 = SearchState()
        s2 = SearchState()
        assert s1.candidates_lock is not s2.candidates_lock

    def test_each_instance_gets_unique_event(self):
        s1 = SearchState()
        s2 = SearchState()
        assert s1.stop_search is not s2.stop_search


# ---------------------------------------------------------------------------
# _build_combined_query
# ---------------------------------------------------------------------------


class TestBuildCombinedQuery:
    """Tests for query construction from constraints."""

    def test_single_word_no_quotes(self):
        strategy = _make_strategy()
        c = _make_constraint(value="Netflix")
        result = strategy._build_combined_query([c])
        assert result == "Netflix"

    def test_multi_word_gets_quoted(self):
        strategy = _make_strategy()
        c = _make_constraint(value="TV series")
        result = strategy._build_combined_query([c])
        assert result == '"TV series"'

    def test_already_quoted_not_double_quoted(self):
        strategy = _make_strategy()
        c = _make_constraint(value='"Breaking Bad"')
        result = strategy._build_combined_query([c])
        assert result == '"Breaking Bad"'

    def test_multiple_constraints_joined_with_space(self):
        strategy = _make_strategy()
        c1 = _make_constraint(value="HBO")
        c2 = _make_constraint(value="TV show")
        c3 = _make_constraint(value="2019")
        result = strategy._build_combined_query([c1, c2, c3])
        assert result == 'HBO "TV show" 2019'

    def test_empty_constraints_list(self):
        strategy = _make_strategy()
        result = strategy._build_combined_query([])
        assert result == ""

    def test_constraint_with_existing_quotes_preserved(self):
        strategy = _make_strategy()
        c = _make_constraint(value='aired on "HBO Max"')
        result = strategy._build_combined_query([c])
        # Contains a quote already, so it should NOT be additionally quoted
        assert result == 'aired on "HBO Max"'


# ---------------------------------------------------------------------------
# _classify_constraint_difficulty
# ---------------------------------------------------------------------------


class TestClassifyConstraintDifficulty:
    """Tests for constraint difficulty rating and sorting."""

    def test_event_type_gets_0_7(self):
        """EVENT type raises difficulty to 0.7."""
        strategy = _make_strategy()
        c = _make_constraint(
            value="complex emotional narrative", type_name="EVENT"
        )
        strategy.state = SearchState(remaining_constraints=[c])
        strategy._classify_constraint_difficulty()
        assert c.search_difficulty == 0.7

    def test_default_difficulty_is_0_5(self):
        strategy = _make_strategy()
        c = _make_constraint(value="something generic xyz")
        strategy.state = SearchState(remaining_constraints=[c])
        strategy._classify_constraint_difficulty()
        assert c.search_difficulty == 0.5

    def test_property_type_gets_0_5(self):
        """PROPERTY type gets default difficulty 0.5 (no special handling)."""
        strategy = _make_strategy()
        c = _make_constraint(value="netflix original")
        strategy.state = SearchState(remaining_constraints=[c])
        strategy._classify_constraint_difficulty()
        assert c.search_difficulty == 0.5

    def test_event_type_raises_difficulty(self):
        """EVENT constraint type raises difficulty to at least 0.7."""
        strategy = _make_strategy()
        c = _make_constraint(value="something simple", type_name="EVENT")
        strategy.state = SearchState(remaining_constraints=[c])
        strategy._classify_constraint_difficulty()
        assert c.search_difficulty >= 0.7

    def test_sorted_by_difficulty_descending(self):
        """Constraints are sorted hardest-first after classification."""
        strategy = _make_strategy()
        prop = _make_constraint(value="some property", type_name="PROPERTY")
        event = _make_constraint(value="some event", type_name="EVENT")
        strategy.state = SearchState(remaining_constraints=[prop, event])
        strategy._classify_constraint_difficulty()
        assert strategy.state.remaining_constraints[0] is event
        assert strategy.state.remaining_constraints[1] is prop

    def test_statistic_gets_default_0_5(self):
        """STATISTIC type has no special handling, gets default 0.5."""
        strategy = _make_strategy()
        c = _make_constraint(value="84.5x ratio", type_name="STATISTIC")
        strategy.state = SearchState(remaining_constraints=[c])
        strategy._classify_constraint_difficulty()
        assert c.search_difficulty == 0.5

    def test_location_gets_default_0_5(self):
        """LOCATION type has no special handling, gets default 0.5."""
        strategy = _make_strategy()
        c = _make_constraint(value="New York City", type_name="LOCATION")
        strategy.state = SearchState(remaining_constraints=[c])
        strategy._classify_constraint_difficulty()
        assert c.search_difficulty == 0.5


# ---------------------------------------------------------------------------
# _should_stop_search
# ---------------------------------------------------------------------------


class TestShouldStopSearch:
    """Tests for the 7 stopping criteria."""

    def _setup_strategy(self, **overrides):
        defaults = dict(
            max_candidates=10,
            target_candidates=5,
            min_good_candidates=3,
            exceptional_score=0.95,
            max_search_time=30.0,
            max_evaluations=30,
            quality_plateau_threshold=0.1,
        )
        defaults.update(overrides)
        strategy = _make_strategy(**defaults)
        strategy.state = SearchState(start_time=time.time())
        return strategy

    def test_returns_false_when_no_criteria_met(self):
        strategy = self._setup_strategy()
        assert strategy._should_stop_search() is False

    def test_criterion_1_stop_flag_set(self):
        """Respects the stop_search Event."""
        strategy = self._setup_strategy()
        strategy.state.stop_search.set()
        assert strategy._should_stop_search() is True

    def test_criterion_2_max_candidates_reached(self):
        """Stops when good_candidates >= max_candidates."""
        strategy = self._setup_strategy(max_candidates=2)
        strategy.state.good_candidates = [
            (_make_candidate("A"), 0.8),
            (_make_candidate("B"), 0.7),
        ]
        assert strategy._should_stop_search() is True

    def test_criterion_3_target_with_high_quality(self):
        """Stops when target_candidates reached with avg score >= 0.8."""
        strategy = self._setup_strategy(target_candidates=2)
        strategy.state.good_candidates = [
            (_make_candidate("A"), 0.85),
            (_make_candidate("B"), 0.90),
        ]
        assert strategy._should_stop_search() is True

    def test_criterion_3_target_with_low_quality_continues(self):
        """Does not stop when target reached but avg score < 0.8."""
        strategy = self._setup_strategy(target_candidates=2)
        strategy.state.good_candidates = [
            (_make_candidate("A"), 0.6),
            (_make_candidate("B"), 0.7),
        ]
        assert strategy._should_stop_search() is False

    def test_criterion_4_exceptional_candidate(self):
        """Stops when min_good met and one candidate >= exceptional_score."""
        strategy = self._setup_strategy(
            min_good_candidates=2, exceptional_score=0.95
        )
        strategy.state.good_candidates = [
            (_make_candidate("A"), 0.7),
            (_make_candidate("B"), 0.96),
        ]
        assert strategy._should_stop_search() is True

    def test_criterion_4_no_exceptional(self):
        """Does not stop when min_good met but no candidate >= exceptional_score,
        and target not yet reached with high quality."""
        strategy = self._setup_strategy(
            min_good_candidates=2,
            exceptional_score=0.95,
            target_candidates=10,  # High target so criterion 3 doesn't trigger
        )
        strategy.state.good_candidates = [
            (_make_candidate("A"), 0.7),
            (_make_candidate("B"), 0.7),
        ]
        strategy.state.remaining_constraints = [
            _make_constraint()
        ]  # Prevent criterion 7
        assert strategy._should_stop_search() is False

    def test_criterion_5_time_limit(self):
        """Stops when elapsed time exceeds max_search_time."""
        strategy = self._setup_strategy(max_search_time=0.001)
        strategy.state.start_time = time.time() - 1.0  # 1 second ago
        assert strategy._should_stop_search() is True

    def test_criterion_6_evaluation_limit(self):
        """Stops when total_evaluated >= max_evaluations."""
        strategy = self._setup_strategy(max_evaluations=5)
        strategy.state.total_evaluated = 5
        assert strategy._should_stop_search() is True

    def test_criterion_7_no_remaining_constraints_with_min_candidates(self):
        """Stops when no constraints remain and min candidates found."""
        strategy = self._setup_strategy(min_good_candidates=1)
        strategy.state.remaining_constraints = []
        strategy.state.good_candidates = [(_make_candidate("A"), 0.7)]
        assert strategy._should_stop_search() is True

    def test_criterion_7_no_constraints_but_no_candidates(self):
        """Does not stop when no constraints but also no candidates."""
        strategy = self._setup_strategy(min_good_candidates=1)
        strategy.state.remaining_constraints = []
        strategy.state.good_candidates = []
        assert strategy._should_stop_search() is False

    def test_criterion_8_quality_plateau(self):
        """Stops when last 5 scores are within plateau threshold."""
        strategy = self._setup_strategy(quality_plateau_threshold=0.1)
        strategy.state.good_candidates = [
            (_make_candidate(f"C{i}"), score)
            for i, score in enumerate([0.80, 0.81, 0.82, 0.83, 0.84])
        ]
        assert strategy._should_stop_search() is True

    def test_no_plateau_with_wide_score_range(self):
        """Does not trigger plateau when score range is wide."""
        strategy = self._setup_strategy(
            quality_plateau_threshold=0.1,
            target_candidates=20,  # Prevent criterion 3
            max_candidates=20,  # Prevent criterion 2
        )
        strategy.state.good_candidates = [
            (_make_candidate(f"C{i}"), score)
            for i, score in enumerate([0.50, 0.60, 0.70, 0.80, 0.90])
        ]
        strategy.state.remaining_constraints = [
            _make_constraint()
        ]  # Prevent criterion 7
        assert strategy._should_stop_search() is False

    def test_plateau_not_checked_under_5_candidates(self):
        """Plateau detection requires at least 5 candidates."""
        strategy = self._setup_strategy(
            quality_plateau_threshold=0.1,
            target_candidates=20,  # Prevent criterion 3
            max_candidates=20,  # Prevent criterion 2
        )
        strategy.state.good_candidates = [
            (_make_candidate(f"C{i}"), 0.8) for i in range(4)
        ]
        strategy.state.remaining_constraints = [
            _make_constraint()
        ]  # Prevent criterion 7
        assert strategy._should_stop_search() is False


# ---------------------------------------------------------------------------
# _is_candidate_evaluated
# ---------------------------------------------------------------------------


class TestIsCandidateEvaluated:
    """Tests for thread-safe duplicate candidate check."""

    def test_not_evaluated_when_empty(self):
        strategy = _make_strategy()
        strategy.state = SearchState()
        candidate = _make_candidate("NewCandidate")
        assert strategy._is_candidate_evaluated(candidate) is False

    def test_evaluated_when_in_good_candidates(self):
        strategy = _make_strategy()
        existing = _make_candidate("ExistingCandidate")
        strategy.state = SearchState(good_candidates=[(existing, 0.8)])
        check = _make_candidate("ExistingCandidate")
        assert strategy._is_candidate_evaluated(check) is True

    def test_different_name_not_evaluated(self):
        strategy = _make_strategy()
        existing = _make_candidate("CandidateA")
        strategy.state = SearchState(good_candidates=[(existing, 0.8)])
        check = _make_candidate("CandidateB")
        assert strategy._is_candidate_evaluated(check) is False

    def test_thread_safe_check(self):
        """Multiple threads can safely check without race conditions."""
        strategy = _make_strategy()
        existing = _make_candidate("SharedCandidate")
        strategy.state = SearchState(good_candidates=[(existing, 0.8)])

        results = []

        def check():
            candidate = _make_candidate("SharedCandidate")
            results.append(strategy._is_candidate_evaluated(candidate))

        threads = [threading.Thread(target=check) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert all(r is True for r in results)


# ---------------------------------------------------------------------------
# Initialization defaults
# ---------------------------------------------------------------------------


class TestInitializationDefaults:
    """Tests for constructor parameter storage."""

    def test_default_max_workers(self):
        strategy = _make_strategy()
        # Default from __init__ signature is 10
        assert strategy.evaluation_executor._max_workers == 10

    def test_custom_parameters_stored(self):
        strategy = _make_strategy(
            min_good_candidates=5,
            target_candidates=8,
            max_candidates=15,
            min_score_threshold=0.7,
            exceptional_score=0.98,
            max_search_time=60.0,
            max_evaluations=50,
        )
        assert strategy.min_good_candidates == 5
        assert strategy.target_candidates == 8
        assert strategy.max_candidates == 15
        assert strategy.min_score_threshold == 0.7
        assert strategy.exceptional_score == 0.98
        assert strategy.max_search_time == 60.0
        assert strategy.max_evaluations == 50

    def test_state_initially_none(self):
        strategy = _make_strategy()
        assert strategy.state is None
