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

Focuses on algorithmic behavior: coherence formula math, BFS connected
components, deduplication set logic, search-order sorting, and formatting
helpers -- all without LLM calls.
"""

from unittest.mock import patch

import pytest

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


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


def _constraint(cid, ctype=ConstraintType.PROPERTY, desc="desc", weight=1.0):
    """Shortcut to build a Constraint with minimal boilerplate."""
    return Constraint(
        id=cid,
        type=ctype,
        description=desc,
        value=cid,
        weight=weight,
    )


def _manager(**overrides):
    """Build a CrossConstraintManager with __init__ bypassed."""
    with patch.object(
        CrossConstraintManager, "__init__", lambda self, *a, **kw: None
    ):
        mgr = CrossConstraintManager.__new__(CrossConstraintManager)
    mgr.model = None
    mgr.relationships = {}
    mgr.clusters = []
    mgr.cross_validation_patterns = {}
    mgr.constraint_graph = {}
    for key, val in overrides.items():
        setattr(mgr, key, val)
    return mgr


def _relationship(c1_id, c2_id, strength=0.8, rtype="complementary"):
    return ConstraintRelationship(
        constraint1_id=c1_id,
        constraint2_id=c2_id,
        relationship_type=rtype,
        strength=strength,
    )


# ===========================================================================
# ConstraintRelationship dataclass
# ===========================================================================


class TestConstraintRelationshipDataclass:
    """Validate ConstraintRelationship field storage and defaults."""

    def test_all_relationship_types_stored(self):
        """Arbitrary relationship_type strings are preserved."""
        for rtype in (
            "complementary",
            "dependent",
            "exclusive",
            "none",
            "custom",
        ):
            rel = _relationship("a", "b", rtype=rtype)
            assert rel.relationship_type == rtype

    def test_negative_strength_stored(self):
        """Dataclass does not clamp; negative strength is preserved."""
        rel = _relationship("a", "b", strength=-0.5)
        assert rel.strength == -0.5

    def test_evidence_mutation_does_not_leak(self):
        """Appending to one instance's evidence does not affect another."""
        rel1 = _relationship("a", "b")
        rel2 = _relationship("c", "d")
        rel1.evidence.append("item")
        assert rel2.evidence == []


# ===========================================================================
# ConstraintCluster dataclass
# ===========================================================================


class TestConstraintClusterDataclass:
    """Validate ConstraintCluster field storage and defaults."""

    def test_coherence_score_zero(self):
        """Zero coherence is stored correctly."""
        cluster = ConstraintCluster(
            constraints=[],
            cluster_type="t",
            coherence_score=0.0,
        )
        assert cluster.coherence_score == 0.0

    def test_constraints_list_identity(self):
        """The constraints list is the exact object passed in."""
        items = [_constraint("c1")]
        cluster = ConstraintCluster(
            constraints=items,
            cluster_type="t",
            coherence_score=0.5,
        )
        assert cluster.constraints is items


# ===========================================================================
# _calculate_cluster_coherence
# ===========================================================================


class TestCalculateClusterCoherence:
    """Test coherence formula: avg_strength * (0.7 + 0.3 * size_factor)."""

    def test_empty_list_returns_zero(self):
        mgr = _manager()
        assert mgr._calculate_cluster_coherence([]) == 0.0

    def test_single_constraint_returns_zero(self):
        mgr = _manager()
        assert mgr._calculate_cluster_coherence([_constraint("c1")]) == 0.0

    def test_two_constraints_no_relationship_returns_default(self):
        """When no pair is found in relationships, return 0.5."""
        mgr = _manager()
        result = mgr._calculate_cluster_coherence(
            [_constraint("c1"), _constraint("c2")]
        )
        assert result == 0.5

    def test_two_constraints_with_relationship_math(self):
        """Verify exact formula for 2 constraints, 1 relationship."""
        mgr = _manager(
            relationships={
                ("c1", "c2"): _relationship("c1", "c2", strength=0.9),
            }
        )
        cons = [_constraint("c1"), _constraint("c2")]
        # avg_strength = 0.9, size_factor = min(2/5, 1.0) = 0.4
        # result = 0.9 * (0.7 + 0.3 * 0.4) = 0.9 * 0.82 = 0.738
        result = mgr._calculate_cluster_coherence(cons)
        assert result == pytest.approx(0.738)

    def test_three_constraints_partial_relationships(self):
        """Only pairs present in relationships dict contribute."""
        mgr = _manager(
            relationships={
                ("c1", "c2"): _relationship("c1", "c2", strength=0.6),
                # ("c1", "c3") missing, ("c2", "c3") missing
            }
        )
        cons = [_constraint("c1"), _constraint("c2"), _constraint("c3")]
        # pair_count=1, avg_strength=0.6, size_factor=min(3/5,1)=0.6
        # 0.6 * (0.7 + 0.3*0.6) = 0.6 * 0.88 = 0.528
        result = mgr._calculate_cluster_coherence(cons)
        assert result == pytest.approx(0.528)

    def test_five_constraints_size_factor_caps_at_one(self):
        """size_factor = min(n/5, 1.0) caps at 1.0 for n>=5."""
        rels = {}
        ids = [f"c{i}" for i in range(5)]
        # Add all 10 pairs with strength 1.0
        import itertools

        for a, b in itertools.combinations(ids, 2):
            rels[(a, b)] = _relationship(a, b, strength=1.0)
        mgr = _manager(relationships=rels)
        cons = [_constraint(cid) for cid in ids]
        # avg=1.0, size_factor=1.0, result=1.0*(0.7+0.3*1.0)=1.0
        assert mgr._calculate_cluster_coherence(cons) == pytest.approx(1.0)

    def test_six_constraints_size_factor_still_one(self):
        """For n>5 the factor stays 1.0."""
        rels = {}
        ids = [f"c{i}" for i in range(6)]
        import itertools

        for a, b in itertools.combinations(ids, 2):
            rels[(a, b)] = _relationship(a, b, strength=0.5)
        mgr = _manager(relationships=rels)
        cons = [_constraint(cid) for cid in ids]
        # avg=0.5, size_factor=min(6/5,1)=1.0, result=0.5*1.0=0.5
        assert mgr._calculate_cluster_coherence(cons) == pytest.approx(0.5)

    def test_relationship_key_order_matters(self):
        """Only (c1.id, c2.id) is checked, not (c2.id, c1.id)."""
        mgr = _manager(
            relationships={
                ("c2", "c1"): _relationship("c2", "c1", strength=0.9),
            }
        )
        cons = [_constraint("c1"), _constraint("c2")]
        # itertools.combinations yields ("c1","c2") which is not in dict
        # so pair_count=0 => returns default 0.5
        assert mgr._calculate_cluster_coherence(cons) == 0.5


# ===========================================================================
# _deduplicate_clusters
# ===========================================================================


class TestDeduplicateClusters:
    """Set-based deduplication logic."""

    def test_empty_returns_empty(self):
        mgr = _manager()
        assert mgr._deduplicate_clusters([]) == []

    def test_single_cluster_preserved(self):
        mgr = _manager()
        cluster = ConstraintCluster(
            constraints=[_constraint("c1")],
            cluster_type="t",
            coherence_score=0.5,
        )
        result = mgr._deduplicate_clusters([cluster])
        assert len(result) == 1
        assert result[0] is cluster

    def test_same_ids_different_order_are_duplicates(self):
        """Constraint sets {c1,c2} and {c2,c1} are the same."""
        mgr = _manager()
        ca, cb = _constraint("c1"), _constraint("c2")
        cl1 = ConstraintCluster(
            constraints=[ca, cb], cluster_type="a", coherence_score=0.5
        )
        cl2 = ConstraintCluster(
            constraints=[cb, ca], cluster_type="b", coherence_score=0.9
        )
        result = mgr._deduplicate_clusters([cl1, cl2])
        assert len(result) == 1

    def test_subset_is_not_duplicate(self):
        """A strict subset is kept as a separate cluster."""
        mgr = _manager()
        ca, cb, cc = _constraint("c1"), _constraint("c2"), _constraint("c3")
        cl1 = ConstraintCluster(
            constraints=[ca, cb, cc], cluster_type="a", coherence_score=0.5
        )
        cl2 = ConstraintCluster(
            constraints=[ca, cb], cluster_type="b", coherence_score=0.9
        )
        result = mgr._deduplicate_clusters([cl1, cl2])
        assert len(result) == 2

    def test_three_duplicates_keeps_first(self):
        """Out of three identical clusters, only the first survives."""
        mgr = _manager()
        con = _constraint("c1")
        clusters = [
            ConstraintCluster(
                constraints=[con],
                cluster_type=f"t{i}",
                coherence_score=float(i),
            )
            for i in range(3)
        ]
        result = mgr._deduplicate_clusters(clusters)
        assert len(result) == 1
        assert result[0].cluster_type == "t0"


# ===========================================================================
# optimize_search_order
# ===========================================================================


class TestOptimizeSearchOrder:
    """Sorting by coherence_score * len(constraints), descending."""

    def test_empty_list(self):
        mgr = _manager()
        assert mgr.optimize_search_order([]) == []

    def test_single_cluster(self):
        mgr = _manager()
        cluster = ConstraintCluster(
            constraints=[_constraint("c1")],
            cluster_type="t",
            coherence_score=0.5,
        )
        assert mgr.optimize_search_order([cluster]) == [cluster]

    def test_ordering_prefers_higher_product(self):
        mgr = _manager()
        small_high = ConstraintCluster(
            constraints=[_constraint("c1")],
            cluster_type="a",
            coherence_score=1.0,  # product=1
        )
        big_low = ConstraintCluster(
            constraints=[_constraint(f"c{i}") for i in range(4)],
            cluster_type="b",
            coherence_score=0.5,  # product=2
        )
        result = mgr.optimize_search_order([small_high, big_low])
        assert result[0] is big_low

    def test_equal_products_stable_order(self):
        """Clusters with equal product keep their original relative order (stable sort)."""
        mgr = _manager()
        cl_a = ConstraintCluster(
            constraints=[_constraint("c1"), _constraint("c2")],
            cluster_type="a",
            coherence_score=0.5,  # product=1.0
        )
        cl_b = ConstraintCluster(
            constraints=[_constraint("c3")],
            cluster_type="b",
            coherence_score=1.0,  # product=1.0
        )
        result = mgr.optimize_search_order([cl_a, cl_b])
        # Python sort is stable, so original order preserved for equal keys
        assert result[0] is cl_a
        assert result[1] is cl_b


# ===========================================================================
# _format_constraints_for_clustering
# ===========================================================================


class TestFormatConstraintsForClustering:
    """String formatting: 'id: description (Type: type, Weight: weight)'."""

    def test_empty_list(self):
        mgr = _manager()
        assert mgr._format_constraints_for_clustering([]) == ""

    def test_single_constraint_format(self):
        mgr = _manager()
        con = _constraint(
            "c1", ctype=ConstraintType.TEMPORAL, desc="In 2020", weight=0.9
        )
        result = mgr._format_constraints_for_clustering([con])
        assert result == "c1: In 2020 (Type: temporal, Weight: 0.9)"

    def test_multiple_constraints_newline_separated(self):
        mgr = _manager()
        cons = [
            _constraint("c1", desc="A"),
            _constraint("c2", desc="B"),
        ]
        result = mgr._format_constraints_for_clustering(cons)
        lines = result.split("\n")
        assert len(lines) == 2
        assert lines[0].startswith("c1:")
        assert lines[1].startswith("c2:")


# ===========================================================================
# _format_constraints_for_query
# ===========================================================================


class TestFormatConstraintsForQuery:
    """String formatting: '- description [type]'."""

    def test_empty_list(self):
        mgr = _manager()
        assert mgr._format_constraints_for_query([]) == ""

    def test_single_constraint_format(self):
        mgr = _manager()
        con = _constraint(
            "c1", ctype=ConstraintType.LOCATION, desc="In Colorado"
        )
        result = mgr._format_constraints_for_query([con])
        assert result == "- In Colorado [location]"

    def test_does_not_include_weight_or_id(self):
        mgr = _manager()
        con = _constraint("c99", desc="Test", weight=0.42)
        result = mgr._format_constraints_for_query([con])
        assert "c99" not in result
        assert "0.42" not in result


# ===========================================================================
# _create_relationship_clusters (BFS connected components)
# ===========================================================================


class TestCreateRelationshipClusters:
    """BFS connected-component logic with mocked relationships."""

    def test_no_relationships_no_clusters(self):
        """No relationships means every node is isolated -> no multi-node clusters."""
        mgr = _manager()
        cons = [_constraint("c1"), _constraint("c2")]
        result = mgr._create_relationship_clusters(cons, [])
        assert result == []

    def test_single_edge_forms_one_cluster(self):
        mgr = _manager()
        cons = [_constraint("c1"), _constraint("c2"), _constraint("c3")]
        rels = [_relationship("c1", "c2")]
        result = mgr._create_relationship_clusters(cons, rels)
        assert len(result) == 1
        ids = {c.id for c in result[0].constraints}
        assert ids == {"c1", "c2"}

    def test_chain_forms_single_component(self):
        """c1-c2-c3 chain should yield one cluster of 3."""
        mgr = _manager()
        cons = [_constraint("c1"), _constraint("c2"), _constraint("c3")]
        rels = [_relationship("c1", "c2"), _relationship("c2", "c3")]
        result = mgr._create_relationship_clusters(cons, rels)
        assert len(result) == 1
        ids = {c.id for c in result[0].constraints}
        assert ids == {"c1", "c2", "c3"}

    def test_two_disconnected_components(self):
        """Two separate edges create two clusters."""
        mgr = _manager()
        cons = [_constraint(f"c{i}") for i in range(4)]
        rels = [_relationship("c0", "c1"), _relationship("c2", "c3")]
        result = mgr._create_relationship_clusters(cons, rels)
        assert len(result) == 2
        id_sets = [frozenset(c.id for c in cl.constraints) for cl in result]
        assert frozenset({"c0", "c1"}) in id_sets
        assert frozenset({"c2", "c3"}) in id_sets

    def test_cluster_type_is_relationship_based(self):
        mgr = _manager()
        cons = [_constraint("c1"), _constraint("c2")]
        rels = [_relationship("c1", "c2")]
        result = mgr._create_relationship_clusters(cons, rels)
        assert result[0].cluster_type == "relationship_based"

    def test_constraint_not_in_list_ignored(self):
        """A relationship referencing an unknown ID does not crash."""
        mgr = _manager()
        cons = [_constraint("c1"), _constraint("c2")]
        # "c99" is not in cons
        rels = [_relationship("c1", "c99"), _relationship("c1", "c2")]
        result = mgr._create_relationship_clusters(cons, rels)
        # c1 and c2 still form a cluster; c99 is silently skipped
        assert len(result) == 1
        ids = {c.id for c in result[0].constraints}
        assert "c1" in ids
        assert "c2" in ids
