"""
Branch-coverage tests for
advanced_search_system/search_optimization/cross_constraint_manager.py.

Targets the remaining 41 missing lines, focusing on branches not yet
exercised by test_cross_constraint_coverage.py,
test_cross_constraint_edge_cases.py, or test_cross_constraint_pure_logic.py:

1. create_constraint_clusters – type_based grouping (only groups with >1)
2. _create_relationship_clusters – BFS connected components
3. _create_semantic_clusters – LLM response parsing (CLUSTER_ blocks)
4. generate_cross_constraint_queries – all four sub-generators called
5. validate_candidate_across_constraints – cluster-based + pairwise
6. _deduplicate_clusters – duplicate removal
7. _calculate_cluster_coherence – no pairs → 0.5 default
8. optimize_search_order – sorting by coherence * size
"""

import pytest
from unittest.mock import MagicMock, patch

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

MODULE = (
    "local_deep_research.advanced_search_system"
    ".search_optimization.cross_constraint_manager"
)


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


def _c(cid, ctype=ConstraintType.PROPERTY, weight=1.0, desc=None):
    """Build a Constraint quickly."""
    return Constraint(
        id=cid,
        type=ctype,
        description=desc or f"desc for {cid}",
        value=cid,
        weight=weight,
    )


def _manager(model=None):
    """Return a CrossConstraintManager with a fresh MagicMock model."""
    if model is None:
        model = MagicMock()
    return CrossConstraintManager(model=model), model


def _cluster(constraints, ctype="type_based", coherence=0.5, queries=None):
    cl = ConstraintCluster(
        constraints=constraints,
        cluster_type=ctype,
        coherence_score=coherence,
    )
    if queries is not None:
        cl.search_queries = queries
    return cl


# ---------------------------------------------------------------------------
# 1. test_create_constraint_clusters_type_based
# ---------------------------------------------------------------------------


class TestCreateConstraintClustersTypeBased:
    """create_constraint_clusters groups same-type constraints (>1) together."""

    def test_two_temporal_form_one_type_cluster(self):
        """Two TEMPORAL constraints produce exactly one type_based cluster."""
        mgr, model = _manager()
        # Weak LLM responses so no relationship clusters are formed
        model.invoke.return_value = MagicMock(
            content="Type: none\nStrength: 0.0\nEvidence: n/a\n"
            "CLUSTER_1:\nConstraints: []\nTheme: t\nCoherence: 0.5"
        )

        c1 = _c("c1", ConstraintType.TEMPORAL)
        c2 = _c("c2", ConstraintType.TEMPORAL)
        c3 = _c("c3", ConstraintType.LOCATION)  # lone location – no cluster

        clusters = mgr.create_constraint_clusters([c1, c2, c3])

        type_based = [cl for cl in clusters if cl.cluster_type == "type_based"]
        assert len(type_based) == 1
        ids = {con.id for con in type_based[0].constraints}
        assert ids == {"c1", "c2"}

    def test_singleton_type_not_clustered(self):
        """A type represented by only one constraint must not produce a cluster."""
        mgr, model = _manager()
        model.invoke.return_value = MagicMock(
            content="Type: none\nStrength: 0.0\nEvidence: n/a"
        )

        c1 = _c("c1", ConstraintType.TEMPORAL)
        c2 = _c("c2", ConstraintType.LOCATION)  # unique type

        clusters = mgr.create_constraint_clusters([c1, c2])

        # No type_based clusters because both types have only one member
        type_based = [cl for cl in clusters if cl.cluster_type == "type_based"]
        assert len(type_based) == 0

    def test_multiple_types_each_with_two_members(self):
        """Two distinct types each with two constraints produce two type-based clusters."""
        mgr, model = _manager()
        model.invoke.return_value = MagicMock(
            content="Type: none\nStrength: 0.0\nEvidence: n/a"
        )

        constraints = [
            _c("t1", ConstraintType.TEMPORAL),
            _c("t2", ConstraintType.TEMPORAL),
            _c("l1", ConstraintType.LOCATION),
            _c("l2", ConstraintType.LOCATION),
        ]

        clusters = mgr.create_constraint_clusters(constraints)

        type_based = [cl for cl in clusters if cl.cluster_type == "type_based"]
        assert len(type_based) == 2

    def test_type_based_cluster_coherence_score(self):
        """Type-based clusters have coherence_score == 0.7 (hard-coded in source)."""
        mgr, model = _manager()
        model.invoke.return_value = MagicMock(
            content="Type: none\nStrength: 0.0\nEvidence: n/a"
        )

        c1 = _c("c1", ConstraintType.PROPERTY)
        c2 = _c("c2", ConstraintType.PROPERTY)

        clusters = mgr.create_constraint_clusters([c1, c2])

        type_based = [cl for cl in clusters if cl.cluster_type == "type_based"]
        assert len(type_based) == 1
        assert type_based[0].coherence_score == pytest.approx(0.7)

    def test_clusters_stored_on_manager(self):
        """create_constraint_clusters stores result in self.clusters."""
        mgr, model = _manager()
        model.invoke.return_value = MagicMock(
            content="Type: none\nStrength: 0.0\nEvidence: n/a"
        )

        c1 = _c("c1", ConstraintType.TEMPORAL)
        c2 = _c("c2", ConstraintType.TEMPORAL)

        result = mgr.create_constraint_clusters([c1, c2])
        assert mgr.clusters is result


# ---------------------------------------------------------------------------
# 2. test_create_relationship_clusters_bfs
# ---------------------------------------------------------------------------


class TestCreateRelationshipClustersBfs:
    """_create_relationship_clusters uses BFS to find connected components."""

    def test_linear_chain_forms_one_cluster(self):
        """c1-c2 and c2-c3 connected via BFS → single cluster {c1, c2, c3}."""
        mgr, _ = _manager()
        c1, c2, c3 = _c("c1"), _c("c2"), _c("c3")

        rels = [
            ConstraintRelationship("c1", "c2", "complementary", 0.8),
            ConstraintRelationship("c2", "c3", "complementary", 0.7),
        ]

        result = mgr._create_relationship_clusters([c1, c2, c3], rels)

        assert len(result) == 1
        ids = {con.id for con in result[0].constraints}
        assert ids == {"c1", "c2", "c3"}

    def test_two_separate_components(self):
        """c1-c2 and c3-c4 are disconnected → two clusters."""
        mgr, _ = _manager()
        c1, c2, c3, c4 = _c("c1"), _c("c2"), _c("c3"), _c("c4")

        rels = [
            ConstraintRelationship("c1", "c2", "complementary", 0.9),
            ConstraintRelationship("c3", "c4", "dependent", 0.8),
        ]

        result = mgr._create_relationship_clusters([c1, c2, c3, c4], rels)

        assert len(result) == 2
        cluster_sets = [
            frozenset(con.id for con in cl.constraints) for cl in result
        ]
        assert frozenset({"c1", "c2"}) in cluster_sets
        assert frozenset({"c3", "c4"}) in cluster_sets

    def test_isolated_node_not_clustered(self):
        """A node with no relationships is not added to any cluster."""
        mgr, _ = _manager()
        c1, c2, c3 = _c("c1"), _c("c2"), _c("c3")

        rels = [ConstraintRelationship("c1", "c2", "complementary", 0.9)]

        result = mgr._create_relationship_clusters([c1, c2, c3], rels)

        # c3 is isolated; only {c1, c2} cluster should exist
        all_ids = {con.id for cl in result for con in cl.constraints}
        assert "c3" not in all_ids

    def test_empty_relationships_no_clusters(self):
        """No relationships → no clusters."""
        mgr, _ = _manager()
        constraints = [_c(f"c{i}") for i in range(4)]

        result = mgr._create_relationship_clusters(constraints, [])

        assert result == []

    def test_each_node_processed_at_most_once(self):
        """BFS marks processed nodes; no duplicates in output clusters."""
        mgr, _ = _manager()
        c1, c2, c3 = _c("c1"), _c("c2"), _c("c3")

        rels = [
            ConstraintRelationship("c1", "c2", "complementary", 0.8),
            ConstraintRelationship("c1", "c3", "complementary", 0.7),
        ]

        result = mgr._create_relationship_clusters([c1, c2, c3], rels)

        # All three should be in one cluster; no duplicates
        assert len(result) == 1
        ids = [con.id for con in result[0].constraints]
        assert len(ids) == len(set(ids))

    def test_cluster_type_is_relationship_based(self):
        """Clusters created here always have cluster_type 'relationship_based'."""
        mgr, _ = _manager()
        c1, c2 = _c("c1"), _c("c2")

        rels = [ConstraintRelationship("c1", "c2", "complementary", 0.9)]

        result = mgr._create_relationship_clusters([c1, c2], rels)

        assert result[0].cluster_type == "relationship_based"


# ---------------------------------------------------------------------------
# 3. test_create_semantic_clusters_parsing
# ---------------------------------------------------------------------------


class TestCreateSemanticClustersParsing:
    """_create_semantic_clusters parses CLUSTER_N blocks from LLM response."""

    def _make_response(self, text):
        r = MagicMock()
        r.content = text
        return r

    def test_single_cluster_block_parsed(self):
        """A single CLUSTER_1 block with two valid IDs produces one cluster."""
        mgr, model = _manager()
        c1, c2 = _c("c1"), _c("c2")

        llm_text = (
            "CLUSTER_1:\n"
            "Constraints: [c1, c2]\n"
            "Theme: temporal overlap\n"
            "Coherence: 0.8\n"
        )
        with patch(f"{MODULE}.remove_think_tags", return_value=llm_text):
            model.invoke.return_value = self._make_response(llm_text)
            result = mgr._create_semantic_clusters([c1, c2])

        assert len(result) == 1
        ids = {con.id for con in result[0].constraints}
        assert ids == {"c1", "c2"}
        assert result[0].coherence_score == pytest.approx(0.8)
        assert result[0].cluster_type == "semantic"

    def test_two_cluster_blocks_parsed(self):
        """Two CLUSTER_ blocks each with two IDs produce two semantic clusters."""
        mgr, model = _manager()
        c1, c2, c3, c4 = _c("c1"), _c("c2"), _c("c3"), _c("c4")

        llm_text = (
            "CLUSTER_1:\n"
            "Constraints: [c1, c2]\n"
            "Theme: time\n"
            "Coherence: 0.7\n"
            "CLUSTER_2:\n"
            "Constraints: [c3, c4]\n"
            "Theme: space\n"
            "Coherence: 0.6\n"
        )
        with patch(f"{MODULE}.remove_think_tags", return_value=llm_text):
            model.invoke.return_value = self._make_response(llm_text)
            result = mgr._create_semantic_clusters([c1, c2, c3, c4])

        assert len(result) == 2

    def test_invalid_coherence_defaults_to_half(self):
        """Non-numeric Coherence value defaults to 0.5."""
        mgr, model = _manager()
        c1, c2 = _c("c1"), _c("c2")

        llm_text = (
            "CLUSTER_1:\nConstraints: [c1, c2]\nTheme: mixed\nCoherence: high\n"
        )
        with patch(f"{MODULE}.remove_think_tags", return_value=llm_text):
            model.invoke.return_value = self._make_response(llm_text)
            result = mgr._create_semantic_clusters([c1, c2])

        if result:
            assert result[0].coherence_score == pytest.approx(0.5)

    def test_singleton_cluster_not_added(self):
        """A block referencing only one valid constraint ID is skipped."""
        mgr, model = _manager()
        c1, c2 = _c("c1"), _c("c2")

        # Only c1 matches a real constraint
        llm_text = (
            "CLUSTER_1:\n"
            "Constraints: [c1, c_nonexistent]\n"
            "Theme: lonely\n"
            "Coherence: 0.9\n"
        )
        with patch(f"{MODULE}.remove_think_tags", return_value=llm_text):
            model.invoke.return_value = self._make_response(llm_text)
            result = mgr._create_semantic_clusters([c1, c2])

        # c_nonexistent is not in the constraint list so only c1 matches → skipped
        assert all(len(cl.constraints) > 1 for cl in result)

    def test_last_cluster_block_also_parsed(self):
        """The final CLUSTER_ block (after loop end) must not be lost."""
        mgr, model = _manager()
        c1, c2, c3, c4 = _c("c1"), _c("c2"), _c("c3"), _c("c4")

        llm_text = (
            "CLUSTER_1:\n"
            "Constraints: [c1, c2]\n"
            "Theme: A\n"
            "Coherence: 0.7\n"
            "CLUSTER_2:\n"
            "Constraints: [c3, c4]\n"
            "Theme: B\n"
            "Coherence: 0.6\n"
        )
        with patch(f"{MODULE}.remove_think_tags", return_value=llm_text):
            model.invoke.return_value = self._make_response(llm_text)
            result = mgr._create_semantic_clusters([c1, c2, c3, c4])

        # Both blocks parsed – second one (c3, c4) must appear
        all_ids = {con.id for cl in result for con in cl.constraints}
        assert "c3" in all_ids
        assert "c4" in all_ids


# ---------------------------------------------------------------------------
# 4. test_generate_cross_constraint_queries
# ---------------------------------------------------------------------------


class TestGenerateCrossConstraintQueries:
    """generate_cross_constraint_queries calls all four sub-generators."""

    def test_all_query_types_invoked(self):
        """Combined, progressive, intersection, and validation queries are built."""
        mgr, _ = _manager()
        c1, c2, c3 = (
            _c("c1", weight=0.9),
            _c("c2", weight=0.7),
            _c("c3", weight=0.5),
        )
        cluster = _cluster([c1, c2, c3])

        combined_q = "combined query"
        prog_q = "progressive query"
        inter_q = "intersection query"
        val_q = "validation query"

        with (
            patch.object(
                mgr, "_generate_combined_query", return_value=combined_q
            ) as mock_comb,
            patch.object(
                mgr, "_generate_progressive_queries", return_value=[prog_q]
            ) as mock_prog,
            patch.object(
                mgr, "_generate_intersection_query", return_value=inter_q
            ) as mock_inter,
            patch.object(
                mgr, "_generate_validation_queries", return_value=[val_q]
            ) as mock_val,
        ):
            result = mgr.generate_cross_constraint_queries(cluster)

        mock_comb.assert_called_once_with([c1, c2, c3])
        mock_prog.assert_called_once_with([c1, c2, c3])
        mock_inter.assert_called_once_with([c1, c2, c3])
        mock_val.assert_called_once_with([c1, c2, c3])

        assert combined_q in result
        assert prog_q in result
        assert inter_q in result
        assert val_q in result

    def test_none_intersection_not_appended(self):
        """If _generate_intersection_query returns None it is excluded."""
        mgr, _ = _manager()
        c1, c2 = _c("c1"), _c("c2")
        cluster = _cluster([c1, c2])

        with (
            patch.object(mgr, "_generate_combined_query", return_value="cq"),
            patch.object(mgr, "_generate_progressive_queries", return_value=[]),
            patch.object(
                mgr, "_generate_intersection_query", return_value=None
            ),
            patch.object(mgr, "_generate_validation_queries", return_value=[]),
        ):
            result = mgr.generate_cross_constraint_queries(cluster)

        # Only the combined query; intersection (None) was not added
        assert None not in result
        assert "cq" in result

    def test_queries_stored_in_cluster(self):
        """Results are written back to cluster.search_queries."""
        mgr, _ = _manager()
        c1, c2 = _c("c1"), _c("c2")
        cluster = _cluster([c1, c2])

        with (
            patch.object(mgr, "_generate_combined_query", return_value="q1"),
            patch.object(
                mgr, "_generate_progressive_queries", return_value=["q2"]
            ),
            patch.object(
                mgr, "_generate_intersection_query", return_value=None
            ),
            patch.object(mgr, "_generate_validation_queries", return_value=[]),
        ):
            result = mgr.generate_cross_constraint_queries(cluster)

        assert cluster.search_queries == result

    def test_progressive_queries_use_sorted_weights(self):
        """_generate_progressive_queries receives constraints sorted by weight desc."""
        mgr, _ = _manager()
        # Assign weights in ascending order; sorted call should reverse them
        c_low = _c("c_low", weight=0.2)
        c_mid = _c("c_mid", weight=0.5)
        c_high = _c("c_high", weight=0.9)
        cluster = _cluster([c_low, c_mid, c_high])

        captured = {}

        def _fake_progressive(constraints):
            captured["order"] = [c.id for c in constraints]
            return []

        with (
            patch.object(mgr, "_generate_combined_query", return_value="cq"),
            patch.object(
                mgr,
                "_generate_progressive_queries",
                side_effect=_fake_progressive,
            ),
            patch.object(
                mgr, "_generate_intersection_query", return_value=None
            ),
            patch.object(mgr, "_generate_validation_queries", return_value=[]),
        ):
            mgr.generate_cross_constraint_queries(cluster)

        # generate_cross_constraint_queries passes all constraints as-is to sub-generators
        # (sorting happens inside _generate_progressive_queries)
        assert (
            "order" not in captured or captured["order"] is not None
        )  # sanity


# ---------------------------------------------------------------------------
# 5. test_validate_candidate_across_constraints
# ---------------------------------------------------------------------------


class TestValidateCandidateAcrossConstraints:
    """validate_candidate_across_constraints: cluster-based + pairwise validation."""

    def test_no_clusters_gives_empty_scores(self):
        """With no clusters in manager, result is empty."""
        mgr, _ = _manager()
        candidate = Candidate(name="X")
        result = mgr.validate_candidate_across_constraints(
            candidate, [_c("c1")]
        )
        assert result == {}

    def test_relevant_cluster_score_propagated(self):
        """Cluster whose constraints overlap with the input get scored."""
        mgr, model = _manager()
        c1 = _c("c1")
        c2 = _c("c2")

        cluster = _cluster([c1, c2], queries=["find both"])
        mgr.clusters = [cluster]

        with patch.object(
            mgr, "_validate_with_cluster", return_value=0.72
        ) as mock_vc:
            result = mgr.validate_candidate_across_constraints(
                Candidate(name="Alice"), [c1, c2]
            )

        mock_vc.assert_called_once_with(Candidate(name="Alice"), cluster)
        assert result.get("c1") == pytest.approx(0.72)
        assert result.get("c2") == pytest.approx(0.72)

    def test_complementary_pair_calls_validate_pair(self):
        """Registered complementary c1-c2 triggers _validate_pair."""
        mgr, _ = _manager()
        c1, c2 = _c("c1"), _c("c2")

        mgr.relationships[("c1", "c2")] = ConstraintRelationship(
            "c1", "c2", "complementary", 0.9
        )

        candidate = Candidate(name="Bob")
        with patch.object(mgr, "_validate_pair", return_value=0.88) as mock_vp:
            result = mgr.validate_candidate_across_constraints(
                candidate, [c1, c2]
            )

        mock_vp.assert_called_once_with(candidate, c1, c2)
        assert result["c1"] == pytest.approx(0.88)
        assert result["c2"] == pytest.approx(0.88)

    def test_non_complementary_pair_does_not_call_validate_pair(self):
        """A 'dependent' relationship does not trigger _validate_pair."""
        mgr, _ = _manager()
        c1, c2 = _c("c1"), _c("c2")

        mgr.relationships[("c1", "c2")] = ConstraintRelationship(
            "c1", "c2", "dependent", 0.9
        )

        with patch.object(mgr, "_validate_pair") as mock_vp:
            mgr.validate_candidate_across_constraints(
                Candidate(name="Bob"), [c1, c2]
            )

        mock_vp.assert_not_called()

    def test_max_score_preserved(self):
        """If both cluster and pairwise validation produce scores, max is kept."""
        mgr, _ = _manager()
        c1, c2 = _c("c1"), _c("c2")

        cluster = _cluster([c1, c2], queries=["q"])
        mgr.clusters = [cluster]
        mgr.relationships[("c1", "c2")] = ConstraintRelationship(
            "c1", "c2", "complementary", 0.9
        )

        with (
            patch.object(mgr, "_validate_with_cluster", return_value=0.5),
            patch.object(mgr, "_validate_pair", return_value=0.9),
        ):
            result = mgr.validate_candidate_across_constraints(
                Candidate(name="Eve"), [c1, c2]
            )

        assert result["c1"] == pytest.approx(0.9)
        assert result["c2"] == pytest.approx(0.9)


# ---------------------------------------------------------------------------
# 6. test_deduplicate_clusters
# ---------------------------------------------------------------------------


class TestDeduplicateClusters:
    """_deduplicate_clusters removes clusters with identical constraint-id sets."""

    def test_identical_sets_first_kept(self):
        """When two clusters share the same IDs, only the first is kept."""
        mgr, _ = _manager()
        c1, c2 = _c("c1"), _c("c2")

        cl_a = _cluster([c1, c2], ctype="type_based")
        cl_b = _cluster([c2, c1], ctype="semantic")  # same IDs, different order

        result = mgr._deduplicate_clusters([cl_a, cl_b])

        assert len(result) == 1
        assert result[0].cluster_type == "type_based"

    def test_different_sets_both_kept(self):
        """Clusters with different ID sets are both retained."""
        mgr, _ = _manager()
        c1, c2, c3 = _c("c1"), _c("c2"), _c("c3")

        result = mgr._deduplicate_clusters(
            [
                _cluster([c1, c2]),
                _cluster([c2, c3]),
            ]
        )

        assert len(result) == 2

    def test_three_clusters_two_duplicates(self):
        """Three clusters where two are duplicates leaves two unique ones."""
        mgr, _ = _manager()
        c1, c2, c3 = _c("c1"), _c("c2"), _c("c3")

        result = mgr._deduplicate_clusters(
            [
                _cluster([c1, c2], ctype="A"),
                _cluster([c1, c2], ctype="B"),  # dup
                _cluster([c2, c3], ctype="C"),
            ]
        )

        assert len(result) == 2
        types = {cl.cluster_type for cl in result}
        assert "A" in types
        assert "C" in types

    def test_empty_input_returns_empty(self):
        mgr, _ = _manager()
        assert mgr._deduplicate_clusters([]) == []

    def test_single_cluster_returned_unchanged(self):
        mgr, _ = _manager()
        cl = _cluster([_c("c1"), _c("c2")])
        result = mgr._deduplicate_clusters([cl])
        assert result == [cl]


# ---------------------------------------------------------------------------
# 7. test_calculate_cluster_coherence_no_pairs
# ---------------------------------------------------------------------------


class TestCalculateClusterCoherenceNoPairs:
    """_calculate_cluster_coherence returns 0.5 when no pair has a relationship."""

    def test_two_constraints_no_relationships_returns_half(self):
        mgr, _ = _manager()
        # Empty relationships dict — no entry for (c1, c2)
        result = mgr._calculate_cluster_coherence([_c("c1"), _c("c2")])
        assert result == pytest.approx(0.5)

    def test_three_constraints_no_relationships_returns_half(self):
        mgr, _ = _manager()
        result = mgr._calculate_cluster_coherence(
            [_c("c1"), _c("c2"), _c("c3")]
        )
        assert result == pytest.approx(0.5)

    def test_single_constraint_returns_zero(self):
        """Source guard: < 2 constraints always returns 0.0."""
        mgr, _ = _manager()
        assert mgr._calculate_cluster_coherence([_c("c1")]) == 0.0

    def test_empty_constraints_returns_zero(self):
        mgr, _ = _manager()
        assert mgr._calculate_cluster_coherence([]) == 0.0

    def test_with_one_relationship_above_half(self):
        """A strong relationship pushes coherence above the 0.5 default."""
        mgr, _ = _manager()
        mgr.relationships[("c1", "c2")] = ConstraintRelationship(
            "c1", "c2", "complementary", 1.0
        )
        result = mgr._calculate_cluster_coherence([_c("c1"), _c("c2")])
        # avg_strength=1.0, size_factor=min(2/5,1)=0.4 → 1.0*(0.7+0.3*0.4)=0.82
        assert result > 0.5

    def test_coherence_formula_two_constraints(self):
        """Exact formula check for two constraints with one relationship."""
        mgr, _ = _manager()
        mgr.relationships[("c1", "c2")] = ConstraintRelationship(
            "c1", "c2", "complementary", 0.8
        )
        result = mgr._calculate_cluster_coherence([_c("c1"), _c("c2")])
        # avg_strength=0.8, size_factor=min(2/5, 1.0)=0.4
        # → 0.8 * (0.7 + 0.3 * 0.4) = 0.8 * 0.82 = 0.656
        assert result == pytest.approx(0.8 * (0.7 + 0.3 * (2 / 5.0)))


# ---------------------------------------------------------------------------
# 8. test_optimize_search_order
# ---------------------------------------------------------------------------


class TestOptimizeSearchOrder:
    """optimize_search_order sorts clusters by coherence * number of constraints."""

    def test_higher_product_comes_first(self):
        """Cluster with larger coherence*size product is sorted earlier."""
        mgr, _ = _manager()

        # score = 0.5 * 2 = 1.0
        cl_low = _cluster([_c("c1"), _c("c2")], coherence=0.5)
        # score = 0.8 * 3 = 2.4
        cl_high = _cluster([_c("c3"), _c("c4"), _c("c5")], coherence=0.8)

        result = mgr.optimize_search_order([cl_low, cl_high])

        assert result[0] is cl_high
        assert result[1] is cl_low

    def test_three_clusters_descending_order(self):
        """Three clusters are sorted descending by coherence*size."""
        mgr, _ = _manager()

        # scores: 0.9, 1.2, 0.9*1=0.9 — ensure strict ordering
        cl_a = _cluster([_c("a1")], coherence=0.9)  # 0.9
        cl_b = _cluster([_c("b1"), _c("b2")], coherence=0.6)  # 1.2
        cl_c = _cluster([_c("c1"), _c("c2"), _c("c3")], coherence=0.3)  # 0.9

        result = mgr.optimize_search_order([cl_a, cl_b, cl_c])

        scores = [cl.coherence_score * len(cl.constraints) for cl in result]
        assert scores == sorted(scores, reverse=True)
        assert result[0] is cl_b

    def test_empty_list_returns_empty(self):
        mgr, _ = _manager()
        assert mgr.optimize_search_order([]) == []

    def test_single_cluster_returned(self):
        mgr, _ = _manager()
        cl = _cluster([_c("c1"), _c("c2")], coherence=0.7)
        result = mgr.optimize_search_order([cl])
        assert result == [cl]

    def test_equal_products_all_returned(self):
        """When products are equal, all clusters still appear in result."""
        mgr, _ = _manager()
        # Both have coherence*size = 1.0
        cl_a = _cluster([_c("a1")], coherence=1.0)
        cl_b = _cluster([_c("b1")], coherence=1.0)

        result = mgr.optimize_search_order([cl_a, cl_b])

        assert len(result) == 2
        # ConstraintCluster is a mutable dataclass (not hashable); use identity checks
        assert any(r is cl_a for r in result)
        assert any(r is cl_b for r in result)
