"""
Pure-logic tests for EvidenceBasedStrategy long code paths.

Tests _score_and_prune_candidates (threshold formula, sorting, limit enforcement),
_calculate_evidence_coverage (slicing, partial coverage), and
_get_iteration_status (all 5 branches) — no LLM or search calls.
"""

from unittest.mock import Mock

from local_deep_research.advanced_search_system.candidates.base_candidate import (
    Candidate,
)
from local_deep_research.advanced_search_system.constraints.base_constraint import (
    Constraint,
    ConstraintType,
)
from local_deep_research.advanced_search_system.evidence.base_evidence import (
    Evidence,
    EvidenceType,
)
from local_deep_research.advanced_search_system.strategies.evidence_based_strategy import (
    EvidenceBasedStrategy,
)


def _constraint(id, ctype=ConstraintType.PROPERTY, weight=1.0, value="v"):
    return Constraint(
        id=id, type=ctype, description="", value=value, weight=weight
    )


def _evidence(confidence=0.8):
    return Evidence(
        claim="test",
        type=EvidenceType.DIRECT_STATEMENT,
        source="test",
        confidence=confidence,
    )


def _make_strategy(
    candidates=None,
    constraints=None,
    confidence_threshold=0.85,
    candidate_limit=10,
    progress_callback=None,
):
    """Build a minimal Mock with attributes used by pure-logic methods."""
    s = Mock(spec=[])
    s.candidates = candidates or []
    s.constraints = constraints or []
    s.confidence_threshold = confidence_threshold
    s.candidate_limit = candidate_limit
    s.progress_callback = progress_callback
    return s


# ---------------------------------------------------------------------------
# _score_and_prune_candidates
# ---------------------------------------------------------------------------


class TestScoreAndPruneCandidates:
    """Verify scoring, sorting, threshold pruning, and limit enforcement."""

    def test_empty_candidates(self):
        """Empty candidate list produces no error and stays empty."""
        s = _make_strategy(candidates=[])
        EvidenceBasedStrategy._score_and_prune_candidates(s)
        assert s.candidates == []

    def test_single_candidate_kept(self):
        """A single candidate above threshold is kept."""
        c = Candidate(name="A", score=0.5)
        cons = [_constraint("c1")]
        c.evidence = {"c1": _evidence(0.8)}
        s = _make_strategy(candidates=[c], constraints=cons)
        EvidenceBasedStrategy._score_and_prune_candidates(s)
        assert len(s.candidates) == 1
        assert s.candidates[0].name == "A"

    def test_threshold_uses_floor_when_top_score_low(self):
        """When top_score * 0.3 < 0.2, threshold is 0.2 (the floor)."""
        # top_score=0.5 -> 0.5*0.3=0.15 < 0.2, so threshold=0.2
        c1 = Candidate(name="High", score=0.5)
        c2 = Candidate(name="Low", score=0.19)
        cons = [_constraint("c1")]
        # Give c1 evidence so calculate_score keeps it high
        c1.evidence = {"c1": _evidence(0.9)}
        c2.evidence = {}
        s = _make_strategy(candidates=[c1, c2], constraints=cons)
        EvidenceBasedStrategy._score_and_prune_candidates(s)
        # c2 has score < 0.2 after calculate_score (no evidence -> score=0)
        names = [c.name for c in s.candidates]
        assert "High" in names
        # c2 should be pruned (score becomes 0.0 from calculate_score with no evidence)
        assert "Low" not in names

    def test_threshold_scales_with_top_score(self):
        """When top_score * 0.3 > 0.2, threshold scales with top score."""
        # top_score=0.9 -> 0.9*0.3=0.27, which is > 0.2, so threshold=0.27
        cons = [_constraint("c1"), _constraint("c2")]
        c1 = Candidate(name="Star")
        c1.evidence = {"c1": _evidence(0.95), "c2": _evidence(0.95)}
        c2 = Candidate(name="OK")
        c2.evidence = {"c1": _evidence(0.5)}
        c3 = Candidate(name="Weak")
        c3.evidence = {}
        s = _make_strategy(candidates=[c1, c2, c3], constraints=cons)
        EvidenceBasedStrategy._score_and_prune_candidates(s)
        # Star should be first (highest score)
        assert s.candidates[0].name == "Star"
        # Weak (score=0.0) should be pruned
        names = [c.name for c in s.candidates]
        assert "Weak" not in names

    def test_all_candidates_pruned(self):
        """When all candidates have score 0, all are pruned below min_score."""
        cons = [_constraint("c1")]
        candidates = [Candidate(name=f"C{i}") for i in range(3)]
        for c in candidates:
            c.evidence = {}  # no evidence -> score becomes 0
        s = _make_strategy(candidates=candidates, constraints=cons)
        EvidenceBasedStrategy._score_and_prune_candidates(s)
        assert s.candidates == []

    def test_candidate_limit_enforced(self):
        """Only top candidate_limit candidates are kept after pruning."""
        cons = [_constraint("c1")]
        candidates = []
        for i in range(10):
            c = Candidate(name=f"C{i}")
            c.evidence = {"c1": _evidence(0.8)}
            candidates.append(c)
        s = _make_strategy(
            candidates=candidates, constraints=cons, candidate_limit=3
        )
        EvidenceBasedStrategy._score_and_prune_candidates(s)
        assert len(s.candidates) <= 3

    def test_sorting_by_score_descending(self):
        """Candidates are sorted by score, highest first."""
        cons = [_constraint("c1"), _constraint("c2")]
        c1 = Candidate(name="Low")
        c1.evidence = {"c1": _evidence(0.3)}
        c2 = Candidate(name="High")
        c2.evidence = {"c1": _evidence(0.9), "c2": _evidence(0.9)}
        s = _make_strategy(candidates=[c1, c2], constraints=cons)
        EvidenceBasedStrategy._score_and_prune_candidates(s)
        if len(s.candidates) >= 2:
            assert s.candidates[0].score >= s.candidates[1].score

    def test_progress_callback_not_called_when_none(self):
        """No error when progress_callback is None."""
        c = Candidate(name="A")
        c.evidence = {"c1": _evidence(0.8)}
        s = _make_strategy(
            candidates=[c],
            constraints=[_constraint("c1")],
            progress_callback=None,
        )
        EvidenceBasedStrategy._score_and_prune_candidates(s)
        assert len(s.candidates) == 1


# ---------------------------------------------------------------------------
# _calculate_evidence_coverage
# ---------------------------------------------------------------------------


class TestCalculateEvidenceCoverage:
    """Verify evidence coverage calculation including edge cases and slicing."""

    def test_empty_candidates(self):
        """No candidates -> coverage 0.0."""
        s = _make_strategy(candidates=[])
        assert EvidenceBasedStrategy._calculate_evidence_coverage(s) == 0.0

    def test_empty_constraints(self):
        """No constraints -> coverage 0.0."""
        c = Candidate(name="A")
        s = _make_strategy(candidates=[c], constraints=[])
        assert EvidenceBasedStrategy._calculate_evidence_coverage(s) == 0.0

    def test_no_evidence_collected(self):
        """Candidates with no evidence -> coverage 0.0."""
        c = Candidate(name="A")
        c.evidence = {}
        s = _make_strategy(candidates=[c], constraints=[_constraint("c1")])
        assert EvidenceBasedStrategy._calculate_evidence_coverage(s) == 0.0

    def test_partial_coverage(self):
        """One candidate with evidence for 1 of 2 constraints -> 0.5."""
        c = Candidate(name="A")
        c.evidence = {"c1": _evidence()}
        cons = [_constraint("c1"), _constraint("c2")]
        s = _make_strategy(candidates=[c], constraints=cons)
        coverage = EvidenceBasedStrategy._calculate_evidence_coverage(s)
        assert coverage == 0.5

    def test_full_coverage(self):
        """All candidates have evidence for all constraints -> 1.0."""
        cons = [_constraint("c1"), _constraint("c2")]
        c = Candidate(name="A")
        c.evidence = {"c1": _evidence(), "c2": _evidence()}
        s = _make_strategy(candidates=[c], constraints=cons)
        coverage = EvidenceBasedStrategy._calculate_evidence_coverage(s)
        assert coverage == 1.0

    def test_slicing_to_top_5(self):
        """Only top 5 candidates are considered for coverage calculation."""
        cons = [_constraint("c1")]
        candidates = []
        for i in range(7):
            c = Candidate(name=f"C{i}")
            c.evidence = {"c1": _evidence()} if i < 3 else {}
            candidates.append(c)
        s = _make_strategy(candidates=candidates, constraints=cons)
        coverage = EvidenceBasedStrategy._calculate_evidence_coverage(s)
        # Only first 5 candidates: 3 have evidence, 2 don't -> 3/5 = 0.6
        assert coverage == 3 / 5


# ---------------------------------------------------------------------------
# _get_iteration_status
# ---------------------------------------------------------------------------


class TestGetIterationStatus:
    """Verify all 5 status branches based on candidates and score."""

    def test_no_candidates(self):
        """No candidates -> 'Searching for initial candidates'."""
        s = _make_strategy(candidates=[])
        result = EvidenceBasedStrategy._get_iteration_status(s)
        assert result == "Searching for initial candidates"

    def test_score_at_threshold(self):
        """Score exactly at confidence_threshold -> 'Verifying top candidate'."""
        c = Candidate(name="A", score=0.85)
        s = _make_strategy(candidates=[c], confidence_threshold=0.85)
        result = EvidenceBasedStrategy._get_iteration_status(s)
        assert result == "Verifying top candidate"

    def test_score_above_threshold(self):
        """Score above confidence_threshold -> 'Verifying top candidate'."""
        c = Candidate(name="A", score=0.95)
        s = _make_strategy(candidates=[c], confidence_threshold=0.85)
        result = EvidenceBasedStrategy._get_iteration_status(s)
        assert result == "Verifying top candidate"

    def test_score_just_below_threshold(self):
        """Score 0.84 (below 0.85 threshold, >= 0.7) -> 'Gathering final evidence'."""
        c = Candidate(name="A", score=0.84)
        s = _make_strategy(candidates=[c], confidence_threshold=0.85)
        result = EvidenceBasedStrategy._get_iteration_status(s)
        assert result == "Gathering final evidence"

    def test_score_at_070(self):
        """Score exactly 0.7 -> 'Gathering final evidence'."""
        c = Candidate(name="A", score=0.7)
        s = _make_strategy(candidates=[c], confidence_threshold=0.85)
        result = EvidenceBasedStrategy._get_iteration_status(s)
        assert result == "Gathering final evidence"

    def test_score_at_069(self):
        """Score 0.69 (below 0.7, >= 0.5) -> 'Refining candidate scores'."""
        c = Candidate(name="A", score=0.69)
        s = _make_strategy(candidates=[c], confidence_threshold=0.85)
        result = EvidenceBasedStrategy._get_iteration_status(s)
        assert result == "Refining candidate scores"

    def test_score_at_050(self):
        """Score exactly 0.5 -> 'Refining candidate scores'."""
        c = Candidate(name="A", score=0.5)
        s = _make_strategy(candidates=[c], confidence_threshold=0.85)
        result = EvidenceBasedStrategy._get_iteration_status(s)
        assert result == "Refining candidate scores"

    def test_score_below_050(self):
        """Score 0.49 (below 0.5) -> 'Exploring candidate evidence'."""
        c = Candidate(name="A", score=0.49)
        s = _make_strategy(candidates=[c], confidence_threshold=0.85)
        result = EvidenceBasedStrategy._get_iteration_status(s)
        assert result == "Exploring candidate evidence"
