"""
Pure-logic tests for CrossConstraintManager.

Tests cluster coherence calculation, cluster deduplication, and
search order optimization — no LLM or search calls.
"""

from unittest.mock import Mock

from local_deep_research.advanced_search_system.constraints.base_constraint import (
    Constraint,
    ConstraintType,
)
from local_deep_research.advanced_search_system.search_optimization.cross_constraint_manager import (
    ConstraintCluster,
    ConstraintRelationship,
    CrossConstraintManager,
)


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


def _cluster(constraint_ids, coherence=0.5, cluster_type="descriptive"):
    """Build a ConstraintCluster from a list of IDs."""
    constraints = [_constraint(cid) for cid in constraint_ids]
    return ConstraintCluster(
        constraints=constraints,
        cluster_type=cluster_type,
        coherence_score=coherence,
    )


def _make_manager(relationships=None):
    """Build a minimal Mock with the attributes used by pure-logic methods."""
    m = Mock(spec=[])
    m.relationships = relationships or {}
    return m


# ---------------------------------------------------------------------------
# _calculate_cluster_coherence
# ---------------------------------------------------------------------------


class TestCalculateClusterCoherence:
    """Verify pairwise-average coherence with size factor."""

    def test_single_constraint(self):
        """< 2 constraints -> 0.0."""
        m = _make_manager()
        c = [_constraint("c1")]
        result = CrossConstraintManager._calculate_cluster_coherence(m, c)
        assert result == 0.0

    def test_no_relationships(self):
        """Two constraints with no relationship -> default 0.5."""
        m = _make_manager()
        c = [_constraint("c1"), _constraint("c2")]
        result = CrossConstraintManager._calculate_cluster_coherence(m, c)
        assert result == 0.5

    def test_single_relationship(self):
        """Two constraints with one relationship."""
        rel = ConstraintRelationship(
            constraint1_id="c1",
            constraint2_id="c2",
            relationship_type="complementary",
            strength=0.8,
        )
        m = _make_manager(relationships={("c1", "c2"): rel})
        c = [_constraint("c1"), _constraint("c2")]
        result = CrossConstraintManager._calculate_cluster_coherence(m, c)
        # avg_strength = 0.8, size_factor = min(2/5, 1.0) = 0.4
        # coherence = 0.8 * (0.7 + 0.3 * 0.4) = 0.8 * 0.82 = 0.656
        assert abs(result - 0.656) < 1e-9

    def test_size_factor_capped(self):
        """Size factor capped at 1.0 for clusters >= 5."""
        rels = {}
        for i in range(5):
            for j in range(i + 1, 5):
                rels[(f"c{i}", f"c{j}")] = ConstraintRelationship(
                    constraint1_id=f"c{i}",
                    constraint2_id=f"c{j}",
                    relationship_type="complementary",
                    strength=1.0,
                )
        m = _make_manager(relationships=rels)
        c = [_constraint(f"c{i}") for i in range(5)]
        result = CrossConstraintManager._calculate_cluster_coherence(m, c)
        # avg_strength = 1.0, size_factor = 1.0
        # coherence = 1.0 * (0.7 + 0.3 * 1.0) = 1.0
        assert abs(result - 1.0) < 1e-9

    def test_partial_relationships(self):
        """Only some pairs have relationships."""
        rels = {
            ("c1", "c2"): ConstraintRelationship(
                constraint1_id="c1",
                constraint2_id="c2",
                relationship_type="complementary",
                strength=0.6,
            ),
        }
        m = _make_manager(relationships=rels)
        # 3 constraints: pairs (c1,c2), (c1,c3), (c2,c3) — only 1 has relationship
        c = [_constraint("c1"), _constraint("c2"), _constraint("c3")]
        result = CrossConstraintManager._calculate_cluster_coherence(m, c)
        # pair_count=1, avg_strength=0.6, size_factor=min(3/5,1)=0.6
        # coherence = 0.6 * (0.7 + 0.3*0.6) = 0.6 * 0.88 = 0.528
        assert abs(result - 0.528) < 1e-9


# ---------------------------------------------------------------------------
# _deduplicate_clusters
# ---------------------------------------------------------------------------


class TestDeduplicateClusters:
    """Verify set-equality based cluster deduplication."""

    def test_no_duplicates(self):
        """Distinct clusters are all kept."""
        clusters = [_cluster(["c1", "c2"]), _cluster(["c3", "c4"])]
        result = CrossConstraintManager._deduplicate_clusters(Mock(), clusters)
        assert len(result) == 2

    def test_removes_exact_duplicates(self):
        """Clusters with the same constraint IDs are deduplicated."""
        clusters = [_cluster(["c1", "c2"]), _cluster(["c1", "c2"])]
        result = CrossConstraintManager._deduplicate_clusters(Mock(), clusters)
        assert len(result) == 1

    def test_order_independent(self):
        """Constraint order doesn't matter for dedup (set comparison)."""
        clusters = [_cluster(["c1", "c2"]), _cluster(["c2", "c1"])]
        result = CrossConstraintManager._deduplicate_clusters(Mock(), clusters)
        assert len(result) == 1

    def test_preserves_first(self):
        """First occurrence is kept."""
        c1 = _cluster(["c1", "c2"], coherence=0.9)
        c2 = _cluster(["c1", "c2"], coherence=0.1)
        result = CrossConstraintManager._deduplicate_clusters(Mock(), [c1, c2])
        assert result[0].coherence_score == 0.9

    def test_empty_input(self):
        """Empty list returns empty."""
        result = CrossConstraintManager._deduplicate_clusters(Mock(), [])
        assert result == []

    def test_subsets_not_duplicates(self):
        """A subset is not a duplicate of a larger cluster."""
        clusters = [_cluster(["c1", "c2", "c3"]), _cluster(["c1", "c2"])]
        result = CrossConstraintManager._deduplicate_clusters(Mock(), clusters)
        assert len(result) == 2


# ---------------------------------------------------------------------------
# optimize_search_order
# ---------------------------------------------------------------------------


class TestOptimizeSearchOrder:
    """Verify sorting by coherence * constraint count, descending."""

    def test_sorts_by_product(self):
        """Higher coherence * size comes first."""
        c1 = _cluster(["a", "b"], coherence=0.5)  # 0.5 * 2 = 1.0
        c2 = _cluster(["a", "b", "c"], coherence=0.8)  # 0.8 * 3 = 2.4
        c3 = _cluster(["a"], coherence=1.0)  # 1.0 * 1 = 1.0
        result = CrossConstraintManager.optimize_search_order(
            Mock(), [c1, c2, c3]
        )
        assert result[0] is c2

    def test_empty_input(self):
        """Empty list returns empty."""
        result = CrossConstraintManager.optimize_search_order(Mock(), [])
        assert result == []

    def test_single_cluster(self):
        """Single cluster returned as-is."""
        c = _cluster(["a", "b"], coherence=0.5)
        result = CrossConstraintManager.optimize_search_order(Mock(), [c])
        assert result == [c]

    def test_tiebreaker(self):
        """Equal products maintain stable order."""
        c1 = _cluster(["a", "b"], coherence=0.5)  # 1.0
        c2 = _cluster(["a"], coherence=1.0)  # 1.0
        result = CrossConstraintManager.optimize_search_order(Mock(), [c1, c2])
        # Both have product 1.0 — stable sort preserves input order
        assert len(result) == 2
