"""
Extended tests for Candidate - Base candidate class for tracking potential answers.

Tests cover:
- Candidate dataclass initialization
- add_evidence method
- calculate_score method
- get_unverified_constraints method
- get_weak_evidence method
- Edge cases

These tests import and test the ACTUAL Candidate class.
"""

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


class TestCandidateDataclass:
    """Tests for Candidate dataclass initialization."""

    def test_candidate_creation_with_name(self):
        """Candidate should be created with name field."""
        candidate = Candidate(name="Test Candidate")
        assert candidate.name == "Test Candidate"

    def test_candidate_has_evidence_dict(self):
        """Candidate should have evidence dict defaulting to empty."""
        candidate = Candidate(name="Test")
        assert isinstance(candidate.evidence, dict)
        assert len(candidate.evidence) == 0

    def test_candidate_evidence_default_empty(self):
        """Evidence should default to empty dict."""
        candidate = Candidate(name="Test")
        assert candidate.evidence == {}

    def test_candidate_has_score(self):
        """Candidate should have score field."""
        candidate = Candidate(name="Test", score=0.75)
        assert candidate.score == 0.75

    def test_candidate_score_default_zero(self):
        """Score should default to 0.0."""
        candidate = Candidate(name="Test")
        assert candidate.score == 0.0

    def test_candidate_has_metadata_dict(self):
        """Candidate should have metadata dict."""
        candidate = Candidate(name="Test", metadata={"key": "value"})
        assert isinstance(candidate.metadata, dict)
        assert candidate.metadata["key"] == "value"

    def test_candidate_metadata_default_empty(self):
        """Metadata should default to empty dict."""
        candidate = Candidate(name="Test")
        assert candidate.metadata == {}


class TestAddEvidence:
    """Tests for add_evidence method."""

    def test_adds_evidence_by_constraint_id(self):
        """Should add evidence by constraint_id."""
        candidate = Candidate(name="Test")
        evidence = Evidence(
            claim="Test claim",
            type=EvidenceType.DIRECT_STATEMENT,
            source="test_source",
            confidence=0.8,
        )

        candidate.add_evidence("constraint_1", evidence)

        assert "constraint_1" in candidate.evidence
        assert candidate.evidence["constraint_1"].confidence == 0.8

    def test_overwrites_existing_evidence(self):
        """Should overwrite existing evidence for same constraint."""
        candidate = Candidate(name="Test")
        evidence1 = Evidence(
            claim="First claim",
            type=EvidenceType.DIRECT_STATEMENT,
            source="source1",
            confidence=0.5,
        )
        evidence2 = Evidence(
            claim="Second claim",
            type=EvidenceType.DIRECT_STATEMENT,
            source="source2",
            confidence=0.9,
        )

        candidate.add_evidence("constraint_1", evidence1)
        candidate.add_evidence("constraint_1", evidence2)

        assert candidate.evidence["constraint_1"].confidence == 0.9
        assert candidate.evidence["constraint_1"].claim == "Second claim"

    def test_multiple_constraints(self):
        """Should handle multiple constraints."""
        candidate = Candidate(name="Test")

        for i in range(3):
            evidence = Evidence(
                claim=f"Claim {i}",
                type=EvidenceType.DIRECT_STATEMENT,
                source=f"source_{i}",
                confidence=0.5 + i * 0.1,
            )
            candidate.add_evidence(f"c{i}", evidence)

        assert len(candidate.evidence) == 3
        assert "c0" in candidate.evidence
        assert "c1" in candidate.evidence
        assert "c2" in candidate.evidence


class TestCalculateScore:
    """Tests for calculate_score method."""

    def test_returns_zero_for_no_constraints(self):
        """Should return 0.0 when no constraints."""
        candidate = Candidate(name="Test")
        constraints = []

        score = candidate.calculate_score(constraints)

        assert score == 0.0

    def test_calculates_weighted_score(self):
        """Should calculate weighted score."""
        candidate = Candidate(name="Test")

        evidence1 = Evidence(
            claim="Claim 1",
            type=EvidenceType.DIRECT_STATEMENT,
            source="source1",
            confidence=0.8,
        )
        evidence2 = Evidence(
            claim="Claim 2",
            type=EvidenceType.DIRECT_STATEMENT,
            source="source2",
            confidence=0.6,
        )

        candidate.add_evidence("c1", evidence1)
        candidate.add_evidence("c2", evidence2)

        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.PROPERTY,
                description="Constraint 1",
                value="value1",
                weight=1.0,
            ),
            Constraint(
                id="c2",
                type=ConstraintType.PROPERTY,
                description="Constraint 2",
                value="value2",
                weight=2.0,
            ),
        ]

        score = candidate.calculate_score(constraints)

        # (0.8 * 1.0 + 0.6 * 2.0) / (1.0 + 2.0) = 2.0 / 3.0 = 0.6667
        assert abs(score - 0.6667) < 0.001

    def test_handles_missing_evidence(self):
        """Should handle constraints without evidence."""
        candidate = Candidate(name="Test")

        evidence = Evidence(
            claim="Claim 1",
            type=EvidenceType.DIRECT_STATEMENT,
            source="source1",
            confidence=0.8,
        )
        candidate.add_evidence("c1", evidence)

        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.PROPERTY,
                description="Constraint 1",
                value="value1",
                weight=1.0,
            ),
            Constraint(
                id="c2",
                type=ConstraintType.PROPERTY,
                description="Constraint 2",
                value="value2",
                weight=1.0,
            ),
        ]

        score = candidate.calculate_score(constraints)

        # (0.8 * 1.0 + 0) / 2.0 = 0.4
        assert score == 0.4

    def test_handles_zero_total_weight(self):
        """Should handle zero total weight."""
        candidate = Candidate(name="Test")

        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.PROPERTY,
                description="Constraint 1",
                value="value1",
                weight=0.0,
            ),
            Constraint(
                id="c2",
                type=ConstraintType.PROPERTY,
                description="Constraint 2",
                value="value2",
                weight=0.0,
            ),
        ]

        score = candidate.calculate_score(constraints)

        assert score == 0.0

    def test_updates_candidate_score(self):
        """Should update candidate score field."""
        candidate = Candidate(name="Test")
        assert candidate.score == 0.0

        evidence = Evidence(
            claim="Claim",
            type=EvidenceType.DIRECT_STATEMENT,
            source="source",
            confidence=0.75,
        )
        candidate.add_evidence("c1", evidence)

        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.PROPERTY,
                description="Constraint 1",
                value="value1",
                weight=1.0,
            ),
        ]

        candidate.calculate_score(constraints)

        assert candidate.score == 0.75

    def test_returns_calculated_score(self):
        """Should return the calculated score."""
        candidate = Candidate(name="Test")

        evidence = Evidence(
            claim="Claim",
            type=EvidenceType.DIRECT_STATEMENT,
            source="source",
            confidence=1.0,
        )
        candidate.add_evidence("c1", evidence)

        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.PROPERTY,
                description="Constraint 1",
                value="value1",
                weight=1.0,
            ),
        ]

        score = candidate.calculate_score(constraints)

        assert score == 1.0


class TestGetUnverifiedConstraints:
    """Tests for get_unverified_constraints method."""

    def test_returns_constraints_without_evidence(self):
        """Should return constraints without evidence."""
        candidate = Candidate(name="Test")

        evidence = Evidence(
            claim="Claim",
            type=EvidenceType.DIRECT_STATEMENT,
            source="source",
            confidence=0.8,
        )
        candidate.add_evidence("c1", evidence)

        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.PROPERTY,
                description="Constraint 1",
                value="value1",
            ),
            Constraint(
                id="c2",
                type=ConstraintType.PROPERTY,
                description="Constraint 2",
                value="value2",
            ),
            Constraint(
                id="c3",
                type=ConstraintType.PROPERTY,
                description="Constraint 3",
                value="value3",
            ),
        ]

        unverified = candidate.get_unverified_constraints(constraints)

        assert len(unverified) == 2
        assert unverified[0].id == "c2"
        assert unverified[1].id == "c3"

    def test_returns_empty_when_all_verified(self):
        """Should return empty list when all constraints verified."""
        candidate = Candidate(name="Test")

        for i in range(3):
            evidence = Evidence(
                claim=f"Claim {i}",
                type=EvidenceType.DIRECT_STATEMENT,
                source=f"source{i}",
                confidence=0.8,
            )
            candidate.add_evidence(f"c{i + 1}", evidence)

        constraints = [
            Constraint(
                id=f"c{i + 1}",
                type=ConstraintType.PROPERTY,
                description=f"Constraint {i + 1}",
                value=f"value{i + 1}",
            )
            for i in range(3)
        ]

        unverified = candidate.get_unverified_constraints(constraints)

        assert len(unverified) == 0

    def test_returns_all_when_no_evidence(self):
        """Should return all constraints when no evidence."""
        candidate = Candidate(name="Test")

        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.PROPERTY,
                description="Constraint 1",
                value="value1",
            ),
            Constraint(
                id="c2",
                type=ConstraintType.PROPERTY,
                description="Constraint 2",
                value="value2",
            ),
        ]

        unverified = candidate.get_unverified_constraints(constraints)

        assert len(unverified) == 2


class TestGetWeakEvidence:
    """Tests for get_weak_evidence method."""

    def test_returns_weak_constraint_ids(self):
        """Should return constraint IDs with weak evidence."""
        candidate = Candidate(name="Test")

        evidence_strong = Evidence(
            claim="Strong",
            type=EvidenceType.DIRECT_STATEMENT,
            source="source1",
            confidence=0.8,
        )
        evidence_weak1 = Evidence(
            claim="Weak1",
            type=EvidenceType.SPECULATION,
            source="source2",
            confidence=0.3,
        )
        evidence_weak2 = Evidence(
            claim="Weak2",
            type=EvidenceType.INFERENCE,
            source="source3",
            confidence=0.4,
        )

        candidate.add_evidence("c1", evidence_strong)
        candidate.add_evidence("c2", evidence_weak1)
        candidate.add_evidence("c3", evidence_weak2)

        weak = candidate.get_weak_evidence()

        assert len(weak) == 2
        assert "c2" in weak
        assert "c3" in weak

    def test_default_threshold_is_half(self):
        """Default threshold should be 0.5."""
        candidate = Candidate(name="Test")

        evidence = Evidence(
            claim="At threshold",
            type=EvidenceType.INFERENCE,
            source="source",
            confidence=0.5,
        )
        candidate.add_evidence("c1", evidence)

        weak = candidate.get_weak_evidence()

        # 0.5 is NOT less than 0.5, so not weak
        assert len(weak) == 0

    def test_custom_threshold(self):
        """Should use custom threshold."""
        candidate = Candidate(name="Test")

        evidence1 = Evidence(
            claim="E1",
            type=EvidenceType.INFERENCE,
            source="source1",
            confidence=0.7,
        )
        evidence2 = Evidence(
            claim="E2",
            type=EvidenceType.INFERENCE,
            source="source2",
            confidence=0.6,
        )

        candidate.add_evidence("c1", evidence1)
        candidate.add_evidence("c2", evidence2)

        weak = candidate.get_weak_evidence(threshold=0.65)

        assert len(weak) == 1
        assert "c2" in weak

    def test_returns_empty_when_all_strong(self):
        """Should return empty when all evidence is strong."""
        candidate = Candidate(name="Test")

        for i in range(2):
            evidence = Evidence(
                claim=f"E{i}",
                type=EvidenceType.DIRECT_STATEMENT,
                source=f"source{i}",
                confidence=0.8 + i * 0.1,
            )
            candidate.add_evidence(f"c{i}", evidence)

        weak = candidate.get_weak_evidence()

        assert len(weak) == 0

    def test_returns_all_when_all_weak(self):
        """Should return all when all evidence is weak."""
        candidate = Candidate(name="Test")

        for i in range(3):
            evidence = Evidence(
                claim=f"E{i}",
                type=EvidenceType.SPECULATION,
                source=f"source{i}",
                confidence=0.1 + i * 0.1,
            )
            candidate.add_evidence(f"c{i}", evidence)

        weak = candidate.get_weak_evidence()

        assert len(weak) == 3


class TestScoreCalculationDetails:
    """Tests for detailed score calculation."""

    def test_equal_weights(self):
        """Should handle equal weights correctly."""
        candidate = Candidate(name="Test")

        evidence1 = Evidence(
            claim="E1",
            type=EvidenceType.DIRECT_STATEMENT,
            source="source1",
            confidence=0.6,
        )
        evidence2 = Evidence(
            claim="E2",
            type=EvidenceType.DIRECT_STATEMENT,
            source="source2",
            confidence=0.8,
        )

        candidate.add_evidence("c1", evidence1)
        candidate.add_evidence("c2", evidence2)

        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.PROPERTY,
                description="C1",
                value="v1",
                weight=1.0,
            ),
            Constraint(
                id="c2",
                type=ConstraintType.PROPERTY,
                description="C2",
                value="v2",
                weight=1.0,
            ),
        ]

        score = candidate.calculate_score(constraints)

        # (0.6 + 0.8) / 2 = 0.7
        assert score == 0.7

    def test_different_weights(self):
        """Should handle different weights correctly."""
        candidate = Candidate(name="Test")

        # Note: Evidence with confidence=0.0 triggers __post_init__ which sets base_confidence
        # So we use explicit non-zero values
        evidence1 = Evidence(
            claim="E1",
            type=EvidenceType.DIRECT_STATEMENT,
            source="source1",
            confidence=0.9,
        )
        evidence2 = Evidence(
            claim="E2",
            type=EvidenceType.DIRECT_STATEMENT,
            source="source2",
            confidence=0.3,
        )

        candidate.add_evidence("c1", evidence1)
        candidate.add_evidence("c2", evidence2)

        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.PROPERTY,
                description="C1",
                value="v1",
                weight=3.0,
            ),
            Constraint(
                id="c2",
                type=ConstraintType.PROPERTY,
                description="C2",
                value="v2",
                weight=1.0,
            ),
        ]

        score = candidate.calculate_score(constraints)

        # (0.9 * 3.0 + 0.3 * 1.0) / 4.0 = 3.0 / 4.0 = 0.75
        assert score == 0.75


class TestEdgeCases:
    """Tests for edge cases."""

    def test_empty_name(self):
        """Should handle empty name."""
        candidate = Candidate(name="")
        assert candidate.name == ""

    def test_unicode_name(self):
        """Should handle unicode name."""
        candidate = Candidate(name="Candidate 日本語")
        assert "日本語" in candidate.name

    def test_very_long_name(self):
        """Should handle very long name."""
        name = "x" * 1000
        candidate = Candidate(name=name)
        assert len(candidate.name) == 1000

    def test_confidence_exactly_threshold(self):
        """Should handle confidence exactly at threshold."""
        candidate = Candidate(name="Test")

        evidence = Evidence(
            claim="E",
            type=EvidenceType.INFERENCE,
            source="source",
            confidence=0.5,
        )
        candidate.add_evidence("c1", evidence)

        weak = candidate.get_weak_evidence()

        # Exactly at threshold is not weak (< not <=)
        assert len(weak) == 0

    def test_zero_confidence(self):
        """Should handle zero confidence."""
        candidate = Candidate(name="Test")

        evidence = Evidence(
            claim="E",
            type=EvidenceType.SPECULATION,
            source="source",
            confidence=0.0,
        )
        candidate.add_evidence("c1", evidence)

        weak = candidate.get_weak_evidence()

        assert len(weak) == 1

    def test_one_confidence(self):
        """Should handle perfect confidence."""
        candidate = Candidate(name="Test")

        evidence = Evidence(
            claim="E",
            type=EvidenceType.OFFICIAL_RECORD,
            source="source",
            confidence=1.0,
        )
        candidate.add_evidence("c1", evidence)

        weak = candidate.get_weak_evidence()

        assert len(weak) == 0

    def test_negative_weight(self):
        """Should handle negative weight (edge case)."""
        candidate = Candidate(name="Test")

        evidence = Evidence(
            claim="E",
            type=EvidenceType.DIRECT_STATEMENT,
            source="source",
            confidence=0.5,
        )
        candidate.add_evidence("c1", evidence)

        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.PROPERTY,
                description="C1",
                value="v1",
                weight=-1.0,
            ),
        ]

        # Negative weight is unusual but should not crash
        # The actual behavior with negative weights may vary
        score = candidate.calculate_score(constraints)
        # Just verify it doesn't crash and returns a float
        assert isinstance(score, float)

    def test_very_small_weight(self):
        """Should handle very small weight."""
        candidate = Candidate(name="Test")

        evidence = Evidence(
            claim="E",
            type=EvidenceType.DIRECT_STATEMENT,
            source="source",
            confidence=0.8,
        )
        candidate.add_evidence("c1", evidence)

        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.PROPERTY,
                description="C1",
                value="v1",
                weight=0.0001,
            ),
        ]

        score = candidate.calculate_score(constraints)
        assert abs(score - 0.8) < 0.001


class TestMetadataHandling:
    """Tests for metadata handling."""

    def test_stores_arbitrary_metadata(self):
        """Should store arbitrary metadata."""
        metadata = {
            "source": "web_search",
            "timestamp": "2024-01-15",
            "query": "test query",
        }
        candidate = Candidate(name="Test", metadata=metadata)

        assert candidate.metadata["source"] == "web_search"
        assert candidate.metadata["timestamp"] == "2024-01-15"

    def test_nested_metadata(self):
        """Should handle nested metadata."""
        metadata = {
            "details": {"nested": {"value": 123}},
        }
        candidate = Candidate(name="Test", metadata=metadata)

        assert candidate.metadata["details"]["nested"]["value"] == 123

    def test_metadata_with_list(self):
        """Should handle list in metadata."""
        metadata = {"tags": ["tag1", "tag2", "tag3"]}
        candidate = Candidate(name="Test", metadata=metadata)

        assert len(candidate.metadata["tags"]) == 3


class TestEvidenceTypes:
    """Tests for different evidence types."""

    def test_evidence_type_affects_base_confidence(self):
        """Evidence type should affect base confidence."""
        candidate = Candidate(name="Test")

        # Create evidence with specific types (confidence=0 triggers base_confidence)
        official = Evidence(
            claim="Official",
            type=EvidenceType.OFFICIAL_RECORD,
            source="gov",
            confidence=0.0,
        )
        speculation = Evidence(
            claim="Speculation",
            type=EvidenceType.SPECULATION,
            source="blog",
            confidence=0.0,
        )

        candidate.add_evidence("c1", official)
        candidate.add_evidence("c2", speculation)

        # OFFICIAL_RECORD has base_confidence 0.90
        # SPECULATION has base_confidence 0.10
        assert candidate.evidence["c1"].confidence == 0.90
        assert candidate.evidence["c2"].confidence == 0.10

    def test_all_evidence_types(self):
        """Should handle all evidence types."""
        candidate = Candidate(name="Test")

        types = [
            EvidenceType.DIRECT_STATEMENT,
            EvidenceType.OFFICIAL_RECORD,
            EvidenceType.RESEARCH_FINDING,
            EvidenceType.NEWS_REPORT,
            EvidenceType.STATISTICAL_DATA,
            EvidenceType.INFERENCE,
            EvidenceType.CORRELATION,
            EvidenceType.SPECULATION,
        ]

        for i, etype in enumerate(types):
            evidence = Evidence(
                claim=f"Claim {i}",
                type=etype,
                source=f"source_{i}",
                confidence=0.5,
            )
            candidate.add_evidence(f"c{i}", evidence)

        assert len(candidate.evidence) == len(types)


class TestConstraintTypes:
    """Tests for different constraint types."""

    def test_all_constraint_types(self):
        """Should handle all constraint types."""
        candidate = Candidate(name="Test")

        types = [
            ConstraintType.PROPERTY,
            ConstraintType.NAME_PATTERN,
            ConstraintType.EVENT,
            ConstraintType.STATISTIC,
            ConstraintType.TEMPORAL,
            ConstraintType.LOCATION,
            ConstraintType.COMPARISON,
            ConstraintType.EXISTENCE,
        ]

        for i, ctype in enumerate(types):
            evidence = Evidence(
                claim=f"Claim {i}",
                type=EvidenceType.DIRECT_STATEMENT,
                source=f"source_{i}",
                confidence=0.7,
            )
            candidate.add_evidence(f"c{i}", evidence)

        constraints = [
            Constraint(
                id=f"c{i}",
                type=ctype,
                description=f"Constraint {i}",
                value=f"value_{i}",
            )
            for i, ctype in enumerate(types)
        ]

        score = candidate.calculate_score(constraints)

        # All evidence has 0.7 confidence, equal weights
        assert abs(score - 0.7) < 0.0001
