"""
High-value pure logic tests for ConstraintParallelStrategy and ConstraintSearchState.

Tests focus on dataclass defaults, constructor parameter storage, threading primitives,
and the _should_stop_search decision logic -- all without LLM or network calls.
"""

import threading
import time
from unittest.mock import patch


from local_deep_research.advanced_search_system.candidates.base_candidate import (
    Candidate,
)
from local_deep_research.advanced_search_system.strategies.constraint_parallel_strategy import (
    ConstraintParallelStrategy,
    ConstraintSearchState,
)


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


def _make_strategy(**overrides):
    """Create a ConstraintParallelStrategy with __init__ bypassed."""
    with patch.object(
        ConstraintParallelStrategy, "__init__", lambda self, *a, **kw: None
    ):
        strat = ConstraintParallelStrategy.__new__(ConstraintParallelStrategy)

    # Apply defaults that the real __init__ would set
    defaults = dict(
        min_good_candidates=3,
        target_candidates=5,
        max_candidates=10,
        min_score_threshold=0.65,
        exceptional_score=0.95,
        quality_plateau_threshold=0.1,
        max_search_time=30.0,
        max_evaluations=30,
        initial_search_timeout=5.0,
        state=None,
    )
    defaults.update(overrides)
    for key, val in defaults.items():
        setattr(strat, key, val)
    return strat


def _make_state(**overrides):
    """Create a ConstraintSearchState with optional overrides."""
    return ConstraintSearchState(**overrides)


# ---------------------------------------------------------------------------
# Tests: ConstraintSearchState dataclass
# ---------------------------------------------------------------------------


class TestConstraintSearchStateDefaults:
    """Verify default values of ConstraintSearchState fields."""

    def test_all_candidates_defaults_to_empty_list(self):
        state = _make_state()
        assert state.all_candidates == []

    def test_evaluated_candidates_defaults_to_empty_list(self):
        state = _make_state()
        assert state.evaluated_candidates == []

    def test_total_evaluated_defaults_to_zero(self):
        state = _make_state()
        assert state.total_evaluated == 0

    def test_entity_type_defaults_to_unknown(self):
        state = _make_state()
        assert state.entity_type == "unknown entity"

    def test_constraint_searches_defaults_to_empty_dict(self):
        state = _make_state()
        assert state.constraint_searches == {}

    def test_evaluation_futures_defaults_to_empty_list(self):
        state = _make_state()
        assert state.evaluation_futures == []

    def test_start_time_is_recent(self):
        before = time.time()
        state = _make_state()
        after = time.time()
        assert before <= state.start_time <= after


class TestConstraintSearchStateThreadingPrimitives:
    """Verify threading fields are properly initialised."""

    def test_candidates_lock_is_a_lock(self):
        state = _make_state()
        assert isinstance(state.candidates_lock, type(threading.Lock()))

    def test_lock_can_acquire_and_release(self):
        state = _make_state()
        acquired = state.candidates_lock.acquire(blocking=False)
        assert acquired is True
        state.candidates_lock.release()

    def test_stop_search_is_an_event(self):
        state = _make_state()
        assert isinstance(state.stop_search, threading.Event)

    def test_stop_search_not_set_by_default(self):
        state = _make_state()
        assert not state.stop_search.is_set()

    def test_stop_search_can_be_set(self):
        state = _make_state()
        state.stop_search.set()
        assert state.stop_search.is_set()


class TestConstraintSearchStateMutability:
    """Verify that list/dict fields are mutable as expected."""

    def test_append_to_all_candidates(self):
        state = _make_state()
        cand = Candidate(name="Alice")
        state.all_candidates.append(cand)
        assert len(state.all_candidates) == 1
        assert state.all_candidates[0].name == "Alice"

    def test_append_to_evaluated_candidates(self):
        state = _make_state()
        cand = Candidate(name="Bob")
        state.evaluated_candidates.append((cand, 0.85))
        assert len(state.evaluated_candidates) == 1
        assert state.evaluated_candidates[0][1] == 0.85

    def test_add_to_constraint_searches(self):
        state = _make_state()
        state.constraint_searches["c1"] = [Candidate(name="X")]
        assert "c1" in state.constraint_searches

    def test_independent_instances_have_separate_lists(self):
        state_a = _make_state()
        state_b = _make_state()
        state_a.all_candidates.append(Candidate(name="only_a"))
        assert len(state_b.all_candidates) == 0


# ---------------------------------------------------------------------------
# Tests: ConstraintParallelStrategy constructor defaults
# ---------------------------------------------------------------------------


class TestStrategyConstructorDefaults:
    """Verify that default parameter values match the signature."""

    def test_min_good_candidates_default(self):
        strat = _make_strategy()
        assert strat.min_good_candidates == 3

    def test_target_candidates_default(self):
        strat = _make_strategy()
        assert strat.target_candidates == 5

    def test_max_candidates_default(self):
        strat = _make_strategy()
        assert strat.max_candidates == 10

    def test_min_score_threshold_default(self):
        strat = _make_strategy()
        assert strat.min_score_threshold == 0.65

    def test_exceptional_score_default(self):
        strat = _make_strategy()
        assert strat.exceptional_score == 0.95

    def test_quality_plateau_threshold_default(self):
        strat = _make_strategy()
        assert strat.quality_plateau_threshold == 0.1

    def test_max_search_time_default(self):
        strat = _make_strategy()
        assert strat.max_search_time == 30.0

    def test_max_evaluations_default(self):
        strat = _make_strategy()
        assert strat.max_evaluations == 30

    def test_initial_search_timeout_default(self):
        strat = _make_strategy()
        assert strat.initial_search_timeout == 5.0

    def test_state_defaults_to_none(self):
        strat = _make_strategy()
        assert strat.state is None


# ---------------------------------------------------------------------------
# Tests: _should_stop_search logic
# ---------------------------------------------------------------------------


class TestShouldStopSearch:
    """Pure logic tests for the _should_stop_search method."""

    def _strategy_with_state(self, evaluated_candidates=None, **state_kw):
        strat = _make_strategy()
        strat.state = _make_state(**state_kw)
        if evaluated_candidates is not None:
            strat.state.evaluated_candidates = evaluated_candidates
        return strat

    def test_returns_false_when_no_criteria_met(self):
        strat = self._strategy_with_state(evaluated_candidates=[])
        assert strat._should_stop_search() is False

    def test_returns_true_when_stop_event_set(self):
        strat = self._strategy_with_state(evaluated_candidates=[])
        strat.state.stop_search.set()
        assert strat._should_stop_search() is True

    def test_returns_true_when_max_candidates_reached(self):
        cands = [(Candidate(name=f"c{i}"), 0.7) for i in range(10)]
        strat = self._strategy_with_state(evaluated_candidates=cands)
        assert strat._should_stop_search() is True

    def test_returns_true_target_reached_high_quality(self):
        # 5 candidates with avg score >= 0.8
        cands = [(Candidate(name=f"c{i}"), 0.85) for i in range(5)]
        strat = self._strategy_with_state(evaluated_candidates=cands)
        assert strat._should_stop_search() is True

    def test_returns_false_target_reached_low_quality(self):
        # 5 candidates but avg score < 0.8
        cands = [(Candidate(name=f"c{i}"), 0.7) for i in range(5)]
        strat = self._strategy_with_state(evaluated_candidates=cands)
        # Also need score range > plateau threshold to avoid plateau stop
        # Make scores varied enough
        cands[0] = (Candidate(name="c0"), 0.5)
        cands[4] = (Candidate(name="c4"), 0.9)
        strat.state.evaluated_candidates = cands
        assert strat._should_stop_search() is False

    def test_returns_true_min_good_with_exceptional_candidate(self):
        cands = [
            (Candidate(name="c0"), 0.7),
            (Candidate(name="c1"), 0.7),
            (Candidate(name="c2"), 0.96),
        ]
        strat = self._strategy_with_state(evaluated_candidates=cands)
        assert strat._should_stop_search() is True

    def test_returns_false_min_good_without_exceptional(self):
        cands = [
            (Candidate(name="c0"), 0.7),
            (Candidate(name="c1"), 0.7),
            (Candidate(name="c2"), 0.8),
        ]
        strat = self._strategy_with_state(evaluated_candidates=cands)
        assert strat._should_stop_search() is False

    def test_returns_true_time_limit_exceeded(self):
        strat = self._strategy_with_state(
            evaluated_candidates=[],
            start_time=time.time() - 60.0,
        )
        assert strat._should_stop_search() is True

    def test_returns_true_evaluation_limit_reached(self):
        strat = self._strategy_with_state(evaluated_candidates=[])
        strat.state.total_evaluated = 30
        assert strat._should_stop_search() is True

    def test_returns_true_quality_plateau_detected(self):
        # 5+ candidates with very close recent scores (range < 0.1)
        cands = [(Candidate(name=f"c{i}"), 0.75 + 0.01 * i) for i in range(6)]
        strat = self._strategy_with_state(evaluated_candidates=cands)
        # last 5 scores: 0.76, 0.77, 0.78, 0.79, 0.80 => range=0.04 < 0.1
        assert strat._should_stop_search() is True

    def test_returns_false_no_plateau_with_varied_scores(self):
        cands = [
            (Candidate(name="c0"), 0.5),
            (Candidate(name="c1"), 0.6),
            (Candidate(name="c2"), 0.7),
            (Candidate(name="c3"), 0.4),
            (Candidate(name="c4"), 0.9),
            (Candidate(name="c5"), 0.7),
        ]
        strat = self._strategy_with_state(evaluated_candidates=cands)
        # last 5: 0.6, 0.7, 0.4, 0.9, 0.7 => range=0.5 >= 0.1, no plateau
        # Also avg of 6 = ~0.633 < 0.8 so target not met; no exceptional
        assert strat._should_stop_search() is False
