"""High-value tests for DualConfidenceWithRejectionStrategy pure logic."""

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

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


def make_constraint(ctype, value, weight=1.0, cid=None):
    return Constraint(
        id=cid or f"c_{value[:8]}",
        type=ctype,
        description=value,
        value=value,
        weight=weight,
    )


def make_mock_evidence(positive=0.5, negative=0.1, uncertainty=0.4):
    """Create a mock dual evidence result."""
    ev = MagicMock()
    ev.positive_confidence = positive
    ev.negative_confidence = negative
    ev.uncertainty = uncertainty
    return ev


@pytest.fixture
def strategy():
    """Create strategy with mocked parent init."""
    with patch.object(
        DualConfidenceWithRejectionStrategy,
        "__init__",
        lambda self, *a, **kw: None,
    ):
        s = DualConfidenceWithRejectionStrategy.__new__(
            DualConfidenceWithRejectionStrategy
        )
        s.rejection_threshold = 0.3
        s.positive_threshold = 0.2
        s.critical_constraint_rejection = 0.2
        s.uncertainty_penalty = 0.1
        s.negative_weight = 1.0
        s.constraint_ranking = []
        s.evaluated_candidates = {}
        s.best_score = 0.0
        s.best_candidate = None
        s.early_stop_threshold = 0.95
        s.found_answer = threading.Event()
        s.evaluation_lock = threading.Lock()
        return s


# --- Constructor defaults ---


class TestConstructorDefaults:
    def test_rejection_threshold(self, strategy):
        assert strategy.rejection_threshold == 0.3

    def test_positive_threshold(self, strategy):
        assert strategy.positive_threshold == 0.2

    def test_critical_constraint_rejection(self, strategy):
        assert strategy.critical_constraint_rejection == 0.2


# --- Symbol mapping logic ---


class TestSymbolMapping:
    """Test the symbol assignment logic: >= 0.8 -> checkmark, >= 0.5 -> circle, else cross."""

    def test_checkmark_at_0_8(self):
        score = 0.8
        symbol = "✓" if score >= 0.8 else "○" if score >= 0.5 else "✗"
        assert symbol == "✓"

    def test_checkmark_above_0_8(self):
        score = 0.95
        symbol = "✓" if score >= 0.8 else "○" if score >= 0.5 else "✗"
        assert symbol == "✓"

    def test_circle_at_0_5(self):
        score = 0.5
        symbol = "✓" if score >= 0.8 else "○" if score >= 0.5 else "✗"
        assert symbol == "○"

    def test_circle_at_0_79(self):
        score = 0.79
        symbol = "✓" if score >= 0.8 else "○" if score >= 0.5 else "✗"
        assert symbol == "○"

    def test_cross_below_0_5(self):
        score = 0.49
        symbol = "✓" if score >= 0.8 else "○" if score >= 0.5 else "✗"
        assert symbol == "✗"

    def test_cross_at_zero(self):
        score = 0.0
        symbol = "✓" if score >= 0.8 else "○" if score >= 0.5 else "✗"
        assert symbol == "✗"


# --- Early rejection logic ---


class TestEarlyRejection:
    def test_high_negative_rejects(self, strategy):
        """avg_negative > 0.25 should return 0.0."""
        candidate = MagicMock()
        candidate.name = "Test"

        constraint = make_constraint(
            ConstraintType.PROPERTY, "test", weight=1.0
        )
        strategy.constraint_ranking = [constraint]

        evidence = [MagicMock()]
        strategy._gather_evidence_for_constraint = MagicMock(
            return_value=evidence
        )

        dual_ev = make_mock_evidence(
            positive=0.1, negative=0.3, uncertainty=0.6
        )
        strategy._analyze_evidence_dual_confidence = MagicMock(
            return_value=dual_ev
        )

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

    def test_low_negative_continues(self, strategy):
        """avg_negative <= 0.25 should not trigger early rejection."""
        candidate = MagicMock()
        candidate.name = "Test"

        constraint = make_constraint(
            ConstraintType.PROPERTY, "test", weight=1.0
        )
        strategy.constraint_ranking = [constraint]

        evidence = [MagicMock()]
        strategy._gather_evidence_for_constraint = MagicMock(
            return_value=evidence
        )

        dual_ev = make_mock_evidence(
            positive=0.6, negative=0.2, uncertainty=0.2
        )
        strategy._analyze_evidence_dual_confidence = MagicMock(
            return_value=dual_ev
        )
        strategy._evaluate_evidence = MagicMock(return_value=0.7)

        result = strategy._evaluate_candidate_immediately(candidate)
        assert result > 0.0


# --- Skip remaining logic ---


class TestSkipRemaining:
    def test_poor_score_important_constraint_skips(self, strategy):
        """score < 0.2 and weight > 0.5 should skip remaining constraints."""
        candidate = MagicMock()
        candidate.name = "Test"

        c1 = make_constraint(ConstraintType.PROPERTY, "important", weight=0.8)
        c2 = make_constraint(ConstraintType.PROPERTY, "secondary", weight=0.3)
        strategy.constraint_ranking = [c1, c2]

        evidence = [MagicMock()]
        strategy._gather_evidence_for_constraint = MagicMock(
            return_value=evidence
        )

        dual_ev = make_mock_evidence(
            positive=0.1, negative=0.1, uncertainty=0.8
        )
        strategy._analyze_evidence_dual_confidence = MagicMock(
            return_value=dual_ev
        )
        strategy._evaluate_evidence = MagicMock(return_value=0.1)

        strategy._evaluate_candidate_immediately(candidate)
        # Only one constraint evaluated due to skip
        assert strategy._evaluate_evidence.call_count == 1


# --- Weighted average ---


class TestWeightedAverage:
    def test_single_constraint(self, strategy):
        candidate = MagicMock()
        candidate.name = "Test"

        c1 = make_constraint(ConstraintType.PROPERTY, "test", weight=2.0)
        strategy.constraint_ranking = [c1]

        evidence = [MagicMock()]
        strategy._gather_evidence_for_constraint = MagicMock(
            return_value=evidence
        )

        dual_ev = make_mock_evidence(
            positive=0.7, negative=0.1, uncertainty=0.2
        )
        strategy._analyze_evidence_dual_confidence = MagicMock(
            return_value=dual_ev
        )
        strategy._evaluate_evidence = MagicMock(return_value=0.8)

        result = strategy._evaluate_candidate_immediately(candidate)
        # Weighted avg: (0.8 * 2.0) / 2.0 = 0.8
        assert abs(result - 0.8) < 0.01

    def test_two_constraints_weighted(self, strategy):
        candidate = MagicMock()
        candidate.name = "Test"

        c1 = make_constraint(ConstraintType.PROPERTY, "c1", weight=1.0)
        c2 = make_constraint(ConstraintType.PROPERTY, "c2", weight=3.0)
        strategy.constraint_ranking = [c1, c2]

        evidence = [MagicMock()]
        strategy._gather_evidence_for_constraint = MagicMock(
            return_value=evidence
        )

        dual_ev = make_mock_evidence(
            positive=0.7, negative=0.1, uncertainty=0.2
        )
        strategy._analyze_evidence_dual_confidence = MagicMock(
            return_value=dual_ev
        )
        strategy._evaluate_evidence = MagicMock(side_effect=[0.6, 0.8])

        result = strategy._evaluate_candidate_immediately(candidate)
        # (0.6*1.0 + 0.8*3.0) / (1.0+3.0) = 3.0/4.0 = 0.75
        assert abs(result - 0.75) < 0.01


# --- No evidence ---


class TestNoEvidence:
    def test_no_evidence_uses_uncertainty_penalty(self, strategy):
        candidate = MagicMock()
        candidate.name = "Test"

        c1 = make_constraint(ConstraintType.PROPERTY, "test", weight=1.0)
        strategy.constraint_ranking = [c1]

        strategy._gather_evidence_for_constraint = MagicMock(return_value=[])

        result = strategy._evaluate_candidate_immediately(candidate)
        expected = 0.5 - strategy.uncertainty_penalty
        assert abs(result - expected) < 0.01


# --- Early stop ---


class TestEarlyStop:
    def test_high_score_triggers_early_stop(self, strategy):
        candidate = MagicMock()
        candidate.name = "Champion"
        strategy.early_stop_threshold = 0.9

        c1 = make_constraint(ConstraintType.PROPERTY, "test", weight=1.0)
        strategy.constraint_ranking = [c1]

        evidence = [MagicMock()]
        strategy._gather_evidence_for_constraint = MagicMock(
            return_value=evidence
        )

        dual_ev = make_mock_evidence(
            positive=0.9, negative=0.05, uncertainty=0.05
        )
        strategy._analyze_evidence_dual_confidence = MagicMock(
            return_value=dual_ev
        )
        strategy._evaluate_evidence = MagicMock(return_value=0.95)

        strategy._evaluate_candidate_immediately(candidate)
        assert strategy.found_answer.is_set()

    def test_low_score_no_early_stop(self, strategy):
        candidate = MagicMock()
        candidate.name = "Mediocre"

        c1 = make_constraint(ConstraintType.PROPERTY, "test", weight=1.0)
        strategy.constraint_ranking = [c1]

        evidence = [MagicMock()]
        strategy._gather_evidence_for_constraint = MagicMock(
            return_value=evidence
        )

        dual_ev = make_mock_evidence(
            positive=0.5, negative=0.1, uncertainty=0.4
        )
        strategy._analyze_evidence_dual_confidence = MagicMock(
            return_value=dual_ev
        )
        strategy._evaluate_evidence = MagicMock(return_value=0.5)

        strategy._evaluate_candidate_immediately(candidate)
        assert not strategy.found_answer.is_set()
