"""
High-value tests for the Candidate dataclass and its methods.

Tests cover:
- Candidate dataclass construction and defaults
- add_evidence() behavior
- calculate_score() with various constraint/evidence combinations
- get_unverified_constraints() filtering
- get_weak_evidence() filtering with thresholds
"""

import pytest

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,
)


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


def _make_constraint(cid: str, weight: float = 1.0) -> Constraint:
    return Constraint(
        id=cid,
        type=ConstraintType.PROPERTY,
        description=f"constraint {cid}",
        value=f"value-{cid}",
        weight=weight,
    )


def _make_evidence(confidence: float = 0.8) -> Evidence:
    return Evidence(
        claim="test claim",
        type=EvidenceType.DIRECT_STATEMENT,
        source="test source",
        confidence=confidence,
    )


# ---------------------------------------------------------------------------
# 1. Candidate dataclass construction
# ---------------------------------------------------------------------------


class TestCandidateDataclass:
    def test_required_name_field(self):
        c = Candidate(name="answer-1")
        assert c.name == "answer-1"

    def test_default_evidence_is_empty_dict(self):
        c = Candidate(name="a")
        assert c.evidence == {}
        assert isinstance(c.evidence, dict)

    def test_default_score_is_zero(self):
        c = Candidate(name="a")
        assert c.score == 0.0

    def test_default_metadata_is_empty_dict(self):
        c = Candidate(name="a")
        assert c.metadata == {}
        assert isinstance(c.metadata, dict)

    def test_distinct_default_dicts_per_instance(self):
        c1 = Candidate(name="a")
        c2 = Candidate(name="b")
        c1.evidence["x"] = _make_evidence()
        assert "x" not in c2.evidence


# ---------------------------------------------------------------------------
# 2. add_evidence()
# ---------------------------------------------------------------------------


class TestAddEvidence:
    def test_adds_evidence_keyed_by_constraint_id(self):
        c = Candidate(name="a")
        ev = _make_evidence(0.9)
        c.add_evidence("c1", ev)
        assert "c1" in c.evidence
        assert c.evidence["c1"] is ev

    def test_overwrites_existing_evidence(self):
        c = Candidate(name="a")
        ev1 = _make_evidence(0.5)
        ev2 = _make_evidence(0.9)
        c.add_evidence("c1", ev1)
        c.add_evidence("c1", ev2)
        assert c.evidence["c1"] is ev2

    def test_multiple_constraint_ids(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence(0.3))
        c.add_evidence("c2", _make_evidence(0.7))
        assert len(c.evidence) == 2


# ---------------------------------------------------------------------------
# 3. calculate_score()
# ---------------------------------------------------------------------------


class TestCalculateScore:
    def test_empty_constraints_returns_zero(self):
        c = Candidate(name="a")
        assert c.calculate_score([]) == 0.0

    def test_single_constraint_full_confidence(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence(1.0))
        con = _make_constraint("c1", weight=1.0)
        score = c.calculate_score([con])
        assert score == pytest.approx(1.0)

    def test_weighted_average_two_constraints(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence(0.8))
        c.add_evidence("c2", _make_evidence(0.6))
        cons = [_make_constraint("c1", 1.0), _make_constraint("c2", 1.0)]
        score = c.calculate_score(cons)
        # (0.8*1 + 0.6*1) / (1+1) = 0.7
        assert score == pytest.approx(0.7)

    def test_missing_evidence_counts_as_zero(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence(1.0))
        cons = [_make_constraint("c1", 1.0), _make_constraint("c2", 1.0)]
        score = c.calculate_score(cons)
        # (1.0*1 + 0) / 2 = 0.5
        assert score == pytest.approx(0.5)

    def test_all_evidence_present(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence(0.9))
        c.add_evidence("c2", _make_evidence(0.7))
        c.add_evidence("c3", _make_evidence(0.5))
        cons = [
            _make_constraint("c1", 1.0),
            _make_constraint("c2", 1.0),
            _make_constraint("c3", 1.0),
        ]
        score = c.calculate_score(cons)
        assert score == pytest.approx((0.9 + 0.7 + 0.5) / 3.0)

    def test_different_weights(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence(1.0))
        c.add_evidence("c2", _make_evidence(0.5))
        cons = [_make_constraint("c1", 3.0), _make_constraint("c2", 1.0)]
        score = c.calculate_score(cons)
        # (1.0*3 + 0.5*1) / (3+1) = 3.5/4 = 0.875
        assert score == pytest.approx(0.875)

    def test_zero_total_weight_returns_zero(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence(0.9))
        cons = [_make_constraint("c1", 0.0)]
        score = c.calculate_score(cons)
        assert score == pytest.approx(0.0)

    def test_score_is_stored_on_instance(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence(0.6))
        cons = [_make_constraint("c1", 1.0)]
        result = c.calculate_score(cons)
        assert c.score == result


# ---------------------------------------------------------------------------
# 4. get_unverified_constraints()
# ---------------------------------------------------------------------------


class TestGetUnverifiedConstraints:
    def test_all_unverified(self):
        c = Candidate(name="a")
        cons = [_make_constraint("c1"), _make_constraint("c2")]
        unverified = c.get_unverified_constraints(cons)
        assert len(unverified) == 2

    def test_all_verified(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence())
        c.add_evidence("c2", _make_evidence())
        cons = [_make_constraint("c1"), _make_constraint("c2")]
        unverified = c.get_unverified_constraints(cons)
        assert unverified == []

    def test_mixed_verified_and_unverified(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence())
        cons = [
            _make_constraint("c1"),
            _make_constraint("c2"),
            _make_constraint("c3"),
        ]
        unverified = c.get_unverified_constraints(cons)
        ids = [u.id for u in unverified]
        assert ids == ["c2", "c3"]

    def test_empty_constraints_list(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence())
        assert c.get_unverified_constraints([]) == []


# ---------------------------------------------------------------------------
# 5. get_weak_evidence()
# ---------------------------------------------------------------------------


class TestGetWeakEvidence:
    def test_all_strong(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence(0.9))
        c.add_evidence("c2", _make_evidence(0.8))
        assert c.get_weak_evidence() == []

    def test_all_weak(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence(0.1))
        c.add_evidence("c2", _make_evidence(0.3))
        weak = c.get_weak_evidence()
        assert set(weak) == {"c1", "c2"}

    def test_mixed_strong_and_weak(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence(0.9))
        c.add_evidence("c2", _make_evidence(0.2))
        weak = c.get_weak_evidence()
        assert weak == ["c2"]

    def test_default_threshold_is_half(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence(0.5))
        c.add_evidence("c2", _make_evidence(0.49))
        weak = c.get_weak_evidence()
        # 0.5 is NOT < 0.5, so only 0.49 is weak
        assert weak == ["c2"]

    def test_custom_threshold(self):
        c = Candidate(name="a")
        c.add_evidence("c1", _make_evidence(0.7))
        c.add_evidence("c2", _make_evidence(0.9))
        weak = c.get_weak_evidence(threshold=0.8)
        assert weak == ["c1"]

    def test_empty_evidence(self):
        c = Candidate(name="a")
        assert c.get_weak_evidence() == []
