"""High-value tests for ImprovedEvidenceBasedStrategy pure logic methods."""

import pytest
from unittest.mock import MagicMock, patch
from collections import defaultdict

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.improved_evidence_based_strategy import (
    ImprovedEvidenceBasedStrategy,
    SearchAttempt,
)


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_evidence(
    etype=EvidenceType.DIRECT_STATEMENT, confidence=0.8, source="web"
):
    ev = MagicMock(spec=Evidence)
    ev.type = etype
    ev.confidence = confidence
    ev.source = source
    return ev


@pytest.fixture
def strategy():
    """Create strategy with mocked parent init."""
    with patch.object(
        ImprovedEvidenceBasedStrategy, "__init__", lambda self, *a, **kw: None
    ):
        s = ImprovedEvidenceBasedStrategy.__new__(ImprovedEvidenceBasedStrategy)
        s.model = MagicMock()
        s.search = MagicMock()
        s.all_links_of_system = []
        s.max_iterations = 20
        s.confidence_threshold = 0.85
        s.candidate_limit = 15
        s.evidence_threshold = 0.6
        s.max_search_iterations = 3
        s.questions_per_iteration = 3
        s.min_source_diversity = 3
        s.adaptive_query_count = 3
        s.constraints = []
        s.candidates = []
        s.search_history = []
        s.search_attempts = []
        s.failed_queries = set()
        s.successful_patterns = []
        s.source_types = defaultdict(set)
        s.iteration = 0
        s.progress_callback = None
        return s


# --- SearchAttempt dataclass ---


class TestSearchAttempt:
    def test_fields(self):
        attempt = SearchAttempt(
            query="test query",
            constraint_ids=["c1", "c2"],
            results_count=5,
            candidates_found=3,
            timestamp="2024-01-01T00:00:00",
            strategy_type="single",
        )
        assert attempt.query == "test query"
        assert attempt.constraint_ids == ["c1", "c2"]
        assert attempt.results_count == 5
        assert attempt.candidates_found == 3
        assert attempt.strategy_type == "single"

    def test_strategy_types(self):
        for stype in ["single", "combined", "exploratory"]:
            attempt = SearchAttempt(
                query="q",
                constraint_ids=[],
                results_count=0,
                candidates_found=0,
                timestamp="",
                strategy_type=stype,
            )
            assert attempt.strategy_type == stype


# --- _generate_constraint_combinations ---


class TestGenerateConstraintCombinations:
    def test_single_constraint(self, strategy):
        c = [make_constraint(ConstraintType.PROPERTY, "A")]
        combos = strategy._generate_constraint_combinations(c, max_size=3)
        assert len(combos) == 1
        assert combos[0] == [c[0]]

    def test_two_constraints(self, strategy):
        constraints = [
            make_constraint(ConstraintType.PROPERTY, "A"),
            make_constraint(ConstraintType.PROPERTY, "B"),
        ]
        combos = strategy._generate_constraint_combinations(
            constraints, max_size=3
        )
        # Singles: [A], [B] + Pairs: [A,B] = 3
        assert len(combos) == 3

    def test_three_constraints(self, strategy):
        constraints = [
            make_constraint(ConstraintType.PROPERTY, "A"),
            make_constraint(ConstraintType.PROPERTY, "B"),
            make_constraint(ConstraintType.PROPERTY, "C"),
        ]
        combos = strategy._generate_constraint_combinations(
            constraints, max_size=3
        )
        # Singles: 3 + Pairs: 3 + Triples: 1 = 7
        assert len(combos) == 7

    def test_max_size_limits(self, strategy):
        constraints = [
            make_constraint(ConstraintType.PROPERTY, "A"),
            make_constraint(ConstraintType.PROPERTY, "B"),
            make_constraint(ConstraintType.PROPERTY, "C"),
        ]
        combos = strategy._generate_constraint_combinations(
            constraints, max_size=2
        )
        # Singles: 3 + Pairs: 3 = 6 (no triples)
        assert len(combos) == 6
        assert all(len(c) <= 2 for c in combos)

    def test_empty_constraints(self, strategy):
        combos = strategy._generate_constraint_combinations([], max_size=3)
        assert combos == []


# --- _format_constraints_for_prompt ---


class TestFormatConstraintsForPrompt:
    def test_single_constraint(self, strategy):
        constraints = [
            make_constraint(ConstraintType.PROPERTY, "scenic view", weight=0.75)
        ]
        result = strategy._format_constraints_for_prompt(constraints)
        assert "property" in result
        assert "scenic view" in result
        assert "0.75" in result

    def test_multiple_constraints(self, strategy):
        constraints = [
            make_constraint(ConstraintType.PROPERTY, "scenic", weight=0.5),
            make_constraint(ConstraintType.TEMPORAL, "in 2020", weight=0.8),
        ]
        result = strategy._format_constraints_for_prompt(constraints)
        assert "property" in result
        assert "temporal" in result
        lines = result.strip().split("\n")
        assert len(lines) == 2

    def test_empty_constraints(self, strategy):
        result = strategy._format_constraints_for_prompt([])
        assert result == ""


# --- _needs_diversity ---


class TestNeedsDiversity:
    def test_few_candidates_needs_diversity(self, strategy):
        strategy.candidates = [Candidate(name="A")]
        assert strategy._needs_diversity() is True

    def test_three_or_more_with_sources(self, strategy):
        strategy.candidates = [
            Candidate(name="A"),
            Candidate(name="B"),
            Candidate(name="C"),
        ]
        strategy.source_types["A"] = {"s1", "s2", "s3"}
        assert strategy._needs_diversity() is False

    def test_three_candidates_few_sources(self, strategy):
        strategy.candidates = [
            Candidate(name="A"),
            Candidate(name="B"),
            Candidate(name="C"),
        ]
        strategy.source_types["A"] = {"s1"}
        assert strategy._needs_diversity() is True

    def test_empty_candidates(self, strategy):
        strategy.candidates = []
        assert strategy._needs_diversity() is True


# --- _calculate_diversity_score ---


class TestCalculateDiversityScore:
    def test_no_evidence(self, strategy):
        c = Candidate(name="Empty")
        assert strategy._calculate_diversity_score(c) == 0.0

    def test_single_source_type(self, strategy):
        c = Candidate(name="Test")
        c.evidence = {"c1": make_evidence(EvidenceType.DIRECT_STATEMENT, 0.8)}
        strategy.source_types["Test"] = {"web"}
        score = strategy._calculate_diversity_score(c)
        assert 0.0 < score < 1.0

    def test_multiple_sources_higher_score(self, strategy):
        c1 = Candidate(name="Few")
        c1.evidence = {"c1": make_evidence()}
        strategy.source_types["Few"] = {"web"}

        c2 = Candidate(name="Many")
        c2.evidence = {
            "c1": make_evidence(EvidenceType.DIRECT_STATEMENT),
            "c2": make_evidence(EvidenceType.NEWS_REPORT),
        }
        strategy.source_types["Many"] = {"web", "academic", "news"}

        score1 = strategy._calculate_diversity_score(c1)
        score2 = strategy._calculate_diversity_score(c2)
        assert score2 > score1


# --- _adds_diversity ---


class TestAddsDiversity:
    def test_new_source_adds_diversity(self, strategy):
        c = Candidate(name="New")
        strategy.source_types["New"] = {"unique_source"}

        existing = [Candidate(name="Old")]
        strategy.source_types["Old"] = {"web"}

        assert strategy._adds_diversity(c, existing) is True

    def test_same_source_no_diversity(self, strategy):
        c = Candidate(name="New")
        c.evidence = {}
        strategy.source_types["New"] = {"web"}

        existing = [Candidate(name="Old")]
        existing[0].evidence = {}
        strategy.source_types["Old"] = {"web"}

        assert strategy._adds_diversity(c, existing) is False

    def test_new_constraint_adds_diversity(self, strategy):
        c = Candidate(name="New")
        c.evidence = {"c_new": make_evidence()}
        strategy.source_types["New"] = set()  # Same sources

        existing = [Candidate(name="Old")]
        existing[0].evidence = {"c_old": make_evidence()}
        strategy.source_types["Old"] = set()

        assert strategy._adds_diversity(c, existing) is True

    def test_empty_existing(self, strategy):
        c = Candidate(name="New")
        c.evidence = {}
        strategy.source_types["New"] = set()
        assert strategy._adds_diversity(c, []) is False


# --- _prune_with_diversity ---


class TestPruneWithDiversity:
    def test_no_pruning_if_under_limit(self, strategy):
        strategy.candidate_limit = 10
        strategy.candidates = [Candidate(name=f"C{i}") for i in range(5)]
        for c in strategy.candidates:
            c.evidence = {}
        strategy._prune_with_diversity()
        assert len(strategy.candidates) == 5

    def test_pruning_keeps_limit(self, strategy):
        strategy.candidate_limit = 4
        strategy.candidates = [Candidate(name=f"C{i}") for i in range(10)]
        for c in strategy.candidates:
            c.evidence = {}
        strategy._prune_with_diversity()
        assert len(strategy.candidates) == 4

    def test_top_half_kept(self, strategy):
        strategy.candidate_limit = 4
        candidates = [Candidate(name=f"C{i}") for i in range(10)]
        for c in candidates:
            c.evidence = {}
        strategy.candidates = candidates
        strategy._prune_with_diversity()
        # Top half (2) should be kept
        assert strategy.candidates[0].name == "C0"
        assert strategy.candidates[1].name == "C1"

    def test_diverse_candidates_preferred(self, strategy):
        strategy.candidate_limit = 3
        c0 = Candidate(name="Top1")
        c0.evidence = {}
        c1 = Candidate(name="Diverse")
        c1.evidence = {"unique_constraint": make_evidence()}
        c2 = Candidate(name="Regular")
        c2.evidence = {}

        strategy.candidates = [c0, c1, c2]
        strategy.source_types["Diverse"] = {"unique_source"}

        strategy._prune_with_diversity()
        names = [c.name for c in strategy.candidates]
        assert "Diverse" in names
