"""Full integration test: imports, adapters, validators, plan, diff, reconcile.

This test exercises the complete kanoniv workflow as a user would after
``pip install kanoniv``:
  1. Import top-level API (Spec, Source, validate, plan, diff, reconcile)
  2. Create two CSV files with overlapping + unique records
  3. Validate & plan the spec
  4. Build Source objects from CSV and pandas
  5. Run reconcile() and inspect results
  6. Use diff() to compare two spec versions
"""
import csv
import json
import textwrap

import pytest

# ── Guard: skip everything if the native extension isn't built ────────────
try:
    from kanoniv._native import reconcile_local  # noqa: F401

    HAS_NATIVE = True
except ImportError:
    HAS_NATIVE = False

pytestmark = pytest.mark.skipif(not HAS_NATIVE, reason="Native extension not built")

pd = pytest.importorskip("pandas")

# ── Spec YAML ─────────────────────────────────────────────────────────────

SPEC_V1 = textwrap.dedent("""\
    api_version: kanoniv/v2
    identity_version: integration_v1.0
    entity:
      name: customer
    sources:
      - name: crm
        system: csv
        table: crm_contacts
        id: id
        attributes:
          email: email
          first_name: first_name
          last_name: last_name
          phone: phone
      - name: ecommerce
        system: csv
        table: ecommerce_orders
        id: id
        attributes:
          email: email
          first_name: first_name
          last_name: last_name
          phone: phone
    blocking:
      strategy: composite
      keys:
        - [email]
        - [phone]
    rules:
      - name: email_exact
        type: exact
        field: email
        weight: 1.0
      - name: phone_exact
        type: exact
        field: phone
        weight: 0.8
    decision:
      thresholds:
        match: 0.7
        review: 0.4
      conflict_strategy: prefer_high_confidence
""")

SPEC_V2 = textwrap.dedent("""\
    api_version: kanoniv/v2
    identity_version: integration_v2.0
    entity:
      name: customer
    sources:
      - name: crm
        system: csv
        table: crm_contacts
        id: id
        attributes:
          email: email
          first_name: first_name
          last_name: last_name
          phone: phone
      - name: ecommerce
        system: csv
        table: ecommerce_orders
        id: id
        attributes:
          email: email
          first_name: first_name
          last_name: last_name
          phone: phone
    blocking:
      strategy: composite
      keys:
        - [email]
        - [phone]
    rules:
      - name: email_exact
        type: exact
        field: email
        weight: 1.0
      - name: phone_exact
        type: exact
        field: phone
        weight: 0.9
      - name: name_similarity
        type: similarity
        field: first_name
        algorithm: jaro_winkler
        threshold: 0.7
        weight: 0.5
    decision:
      thresholds:
        match: 0.6
        review: 0.3
      conflict_strategy: prefer_high_confidence
""")


# ── CSV fixtures ──────────────────────────────────────────────────────────

CRM_ROWS = [
    {"id": "crm-001", "email": "alice@example.com", "first_name": "Alice", "last_name": "Smith", "phone": "555-0001"},
    {"id": "crm-002", "email": "bob@corp.io", "first_name": "Bob", "last_name": "Johnson", "phone": "555-0002"},
    {"id": "crm-003", "email": "carol@mail.net", "first_name": "Carol", "last_name": "Williams", "phone": "555-0003"},
    {"id": "crm-004", "email": "dave@company.com", "first_name": "Dave", "last_name": "Brown", "phone": "555-0004"},
    {"id": "crm-005", "email": "eve@startup.co", "first_name": "Eve", "last_name": "Davis", "phone": "555-0005"},
]

ECOMMERCE_ROWS = [
    {"id": "eco-001", "email": "alice@example.com", "first_name": "Alice", "last_name": "Smith", "phone": "555-0001"},
    {"id": "eco-002", "email": "bob@corp.io", "first_name": "Robert", "last_name": "Johnson", "phone": "555-0002"},
    {"id": "eco-003", "email": "frank@other.org", "first_name": "Frank", "last_name": "Miller", "phone": "555-0006"},
    {"id": "eco-004", "email": "grace@web.com", "first_name": "Grace", "last_name": "Wilson", "phone": "555-0003"},
]


def _write_csv(path, rows):
    """Write a list of dicts to a CSV file."""
    fieldnames = list(rows[0].keys())
    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(rows)


@pytest.fixture
def crm_csv(tmp_path):
    p = tmp_path / "crm_contacts.csv"
    _write_csv(str(p), CRM_ROWS)
    return str(p)


@pytest.fixture
def ecommerce_csv(tmp_path):
    p = tmp_path / "ecommerce_orders.csv"
    _write_csv(str(p), ECOMMERCE_ROWS)
    return str(p)


@pytest.fixture
def crm_df():
    return pd.DataFrame(CRM_ROWS)


@pytest.fixture
def ecommerce_df():
    return pd.DataFrame(ECOMMERCE_ROWS)


# ═══════════════════════════════════════════════════════════════════════════
# Test: top-level imports work
# ═══════════════════════════════════════════════════════════════════════════


class TestImports:
    def test_core_imports(self):
        from kanoniv import Spec, Source, validate, plan, diff, reconcile, ReconcileResult

        assert callable(validate)
        assert callable(plan)
        assert callable(diff)
        assert callable(reconcile)
        assert Spec is not None
        assert Source is not None
        assert ReconcileResult is not None

    def test_version_exists(self):
        import kanoniv

        assert hasattr(kanoniv, "__version__")
        assert isinstance(kanoniv.__version__, str)

    def test_source_factory_methods_exist(self):
        from kanoniv import Source

        assert hasattr(Source, "from_csv")
        assert hasattr(Source, "from_pandas")
        assert hasattr(Source, "from_json")
        assert hasattr(Source, "from_warehouse")
        assert hasattr(Source, "from_dbt")


# ═══════════════════════════════════════════════════════════════════════════
# Test: Spec, validate, plan
# ═══════════════════════════════════════════════════════════════════════════


class TestSpecAndValidation:
    def test_spec_from_string(self):
        from kanoniv import Spec

        spec = Spec.from_string(SPEC_V1)
        assert spec.raw == SPEC_V1

    def test_spec_entity(self):
        from kanoniv import Spec

        spec = Spec.from_string(SPEC_V1)
        entity = spec.entity
        if isinstance(entity, dict):
            assert entity["name"] == "customer"
        else:
            assert entity == "customer"

    def test_spec_sources(self):
        from kanoniv import Spec

        spec = Spec.from_string(SPEC_V1)
        assert len(spec.sources) == 2
        source_names = {s["name"] for s in spec.sources}
        assert source_names == {"crm", "ecommerce"}

    def test_spec_rules(self):
        from kanoniv import Spec

        spec = Spec.from_string(SPEC_V1)
        assert len(spec.rules) == 2
        rule_names = {r["name"] for r in spec.rules}
        assert rule_names == {"email_exact", "phone_exact"}

    def test_spec_from_file(self, tmp_path):
        from kanoniv import Spec

        p = tmp_path / "identity.yml"
        p.write_text(SPEC_V1)
        spec = Spec.from_file(str(p))
        assert spec.raw == SPEC_V1

    def test_validate_valid_spec(self):
        from kanoniv import Spec, validate

        spec = Spec.from_string(SPEC_V1)
        result = validate(spec)
        assert result.valid
        assert len(result.errors) == 0
        assert bool(result) is True

    def test_validate_invalid_spec(self):
        from kanoniv import Spec, validate

        bad_yaml = "api_version: kanoniv/v2\n"
        spec = Spec.from_string(bad_yaml)
        result = validate(spec)
        assert not result.valid
        assert len(result.errors) > 0

    def test_validate_raise_on_error(self):
        from kanoniv import Spec, validate

        bad_yaml = "api_version: kanoniv/v2\n"
        spec = Spec.from_string(bad_yaml)
        result = validate(spec)
        with pytest.raises(ValueError, match="Spec validation failed"):
            result.raise_on_error()

    def test_plan_returns_result(self):
        from kanoniv import Spec, plan

        spec = Spec.from_string(SPEC_V1)
        p = plan(spec)
        assert p is not None

    def test_plan_has_summary(self):
        from kanoniv import Spec, plan

        spec = Spec.from_string(SPEC_V1)
        p = plan(spec)
        summary = p.summary()
        assert isinstance(summary, str)
        assert len(summary) > 0

    def test_plan_has_plan_hash(self):
        from kanoniv import Spec, plan

        spec = Spec.from_string(SPEC_V1)
        p = plan(spec)
        assert isinstance(p.plan_hash, str)
        assert len(p.plan_hash) > 0

    def test_plan_has_execution_stages(self):
        from kanoniv import Spec, plan

        spec = Spec.from_string(SPEC_V1)
        p = plan(spec)
        assert isinstance(p.execution_stages, list)
        assert len(p.execution_stages) > 0

    def test_plan_has_match_strategies(self):
        from kanoniv import Spec, plan

        spec = Spec.from_string(SPEC_V1)
        p = plan(spec)
        assert isinstance(p.match_strategies, list)

    def test_plan_to_dict(self):
        from kanoniv import Spec, plan

        spec = Spec.from_string(SPEC_V1)
        p = plan(spec)
        d = p.to_dict()
        assert isinstance(d, dict)
        assert "summary" in d or "plan_hash" in d

    def test_plan_repr(self):
        from kanoniv import Spec, plan

        spec = Spec.from_string(SPEC_V1)
        p = plan(spec)
        r = repr(p)
        assert isinstance(r, str)
        assert len(r) > 0


# ═══════════════════════════════════════════════════════════════════════════
# Test: diff between two spec versions
# ═══════════════════════════════════════════════════════════════════════════


class TestDiff:
    def test_diff_detects_rule_added(self):
        from kanoniv import Spec, diff

        a = Spec.from_string(SPEC_V1)
        b = Spec.from_string(SPEC_V2)
        result = diff(a, b)
        assert "name_similarity" in result.rules_added

    def test_diff_detects_threshold_change(self):
        from kanoniv import Spec, diff

        a = Spec.from_string(SPEC_V1)
        b = Spec.from_string(SPEC_V2)
        result = diff(a, b)
        assert result.thresholds_changed is True

    def test_diff_has_summary(self):
        from kanoniv import Spec, diff

        a = Spec.from_string(SPEC_V1)
        b = Spec.from_string(SPEC_V2)
        result = diff(a, b)
        assert isinstance(result.summary, str)
        assert len(result.summary) > 0

    def test_diff_identical_specs(self):
        from kanoniv import Spec, diff

        a = Spec.from_string(SPEC_V1)
        b = Spec.from_string(SPEC_V1)
        result = diff(a, b)
        assert len(result.rules_added) == 0
        assert len(result.rules_removed) == 0
        assert result.thresholds_changed is False

    def test_diff_repr(self):
        from kanoniv import Spec, diff

        a = Spec.from_string(SPEC_V1)
        b = Spec.from_string(SPEC_V2)
        result = diff(a, b)
        r = repr(result)
        assert isinstance(r, str)


# ═══════════════════════════════════════════════════════════════════════════
# Test: CSV adapter
# ═══════════════════════════════════════════════════════════════════════════


class TestCsvAdapter:
    def test_csv_source_creation(self, crm_csv):
        from kanoniv import Source

        src = Source.from_csv("crm", crm_csv, primary_key="id")
        assert src.name == "crm"
        assert src.primary_key == "id"

    def test_csv_schema(self, crm_csv):
        from kanoniv import Source

        src = Source.from_csv("crm", crm_csv, primary_key="id")
        schema = src.schema()
        col_names = {c.name for c in schema.columns}
        assert col_names == {"id", "email", "first_name", "last_name", "phone"}

    def test_csv_iter_rows(self, crm_csv):
        from kanoniv import Source

        src = Source.from_csv("crm", crm_csv, primary_key="id")
        rows = list(src.iter_rows())
        assert len(rows) == 5
        assert rows[0]["email"] == "alice@example.com"
        assert rows[0]["first_name"] == "Alice"

    def test_csv_to_entities(self, crm_csv):
        from kanoniv import Source

        src = Source.from_csv("crm", crm_csv, primary_key="id")
        entities = src.to_entities("customer")
        assert len(entities) == 5
        for e in entities:
            assert "id" in e
            assert "entity_type" in e
            assert e["entity_type"] == "customer"
            assert e["source_name"] == "crm"
            assert "data" in e
            assert isinstance(e["data"], dict)


# ═══════════════════════════════════════════════════════════════════════════
# Test: Pandas adapter
# ═══════════════════════════════════════════════════════════════════════════


class TestPandasAdapter:
    def test_pandas_source_creation(self, crm_df):
        from kanoniv import Source

        src = Source.from_pandas("crm", crm_df, primary_key="id")
        assert src.name == "crm"

    def test_pandas_schema(self, crm_df):
        from kanoniv import Source

        src = Source.from_pandas("crm", crm_df, primary_key="id")
        schema = src.schema()
        col_names = {c.name for c in schema.columns}
        assert col_names == {"id", "email", "first_name", "last_name", "phone"}
        assert schema.row_count == 5

    def test_pandas_iter_rows(self, crm_df):
        from kanoniv import Source

        src = Source.from_pandas("crm", crm_df, primary_key="id")
        rows = list(src.iter_rows())
        assert len(rows) == 5
        assert all(isinstance(v, str) for row in rows for v in row.values())

    def test_pandas_nan_handling(self):
        import numpy as np
        from kanoniv import Source

        df = pd.DataFrame({
            "id": ["1", "2", "3"],
            "email": ["a@a.com", np.nan, "c@c.com"],
            "name": ["Alice", "Bob", None],
        })
        src = Source.from_pandas("test", df, primary_key="id")
        rows = list(src.iter_rows())
        assert rows[1]["email"] == ""
        # None can show up as "None" or "" depending on pandas behavior
        assert rows[2]["name"] in ("", "None")

    def test_pandas_to_entities(self, crm_df):
        from kanoniv import Source

        src = Source.from_pandas("crm", crm_df, primary_key="id")
        entities = src.to_entities("customer")
        assert len(entities) == 5
        assert all(e["source_name"] == "crm" for e in entities)

    def test_pandas_dtype_inference(self):
        from kanoniv import Source

        df = pd.DataFrame({
            "id": [1, 2, 3],
            "score": [0.5, 0.8, 0.9],
            "active": [True, False, True],
            "name": ["a", "b", "c"],
        })
        src = Source.from_pandas("typed", df)
        schema = src.schema()
        col_map = {c.name: c for c in schema.columns}
        assert col_map["id"].dtype == "number"
        assert col_map["score"].dtype == "number"
        assert col_map["active"].dtype == "boolean"
        assert col_map["name"].dtype == "string"

    def test_pandas_csv_parity(self, crm_csv, crm_df):
        """CSV adapter and pandas adapter should produce equivalent row data."""
        from kanoniv import Source

        csv_src = Source.from_csv("csv", crm_csv, primary_key="id")
        pd_src = Source.from_pandas("pd", crm_df, primary_key="id")

        csv_rows = sorted(list(csv_src.iter_rows()), key=lambda r: r["id"])
        pd_rows = sorted(list(pd_src.iter_rows()), key=lambda r: r["id"])

        assert len(csv_rows) == len(pd_rows)
        for cr, pr in zip(csv_rows, pd_rows):
            assert cr["id"] == pr["id"]
            assert cr["email"] == pr["email"]
            assert cr["first_name"] == pr["first_name"]


# ═══════════════════════════════════════════════════════════════════════════
# Test: JSON adapter
# ═══════════════════════════════════════════════════════════════════════════


class TestJsonAdapter:
    def test_json_source(self, tmp_path):
        from kanoniv import Source

        p = tmp_path / "data.json"
        p.write_text(json.dumps(CRM_ROWS))

        src = Source.from_json("json_src", str(p), primary_key="id")
        rows = list(src.iter_rows())
        assert len(rows) == 5
        assert rows[0]["email"] == "alice@example.com"

    def test_json_schema(self, tmp_path):
        from kanoniv import Source

        p = tmp_path / "data.json"
        p.write_text(json.dumps(CRM_ROWS))

        src = Source.from_json("json_src", str(p), primary_key="id")
        schema = src.schema()
        col_names = {c.name for c in schema.columns}
        assert "email" in col_names
        assert "first_name" in col_names

    def test_json_to_entities(self, tmp_path):
        from kanoniv import Source

        p = tmp_path / "data.json"
        p.write_text(json.dumps(ECOMMERCE_ROWS))

        src = Source.from_json("eco", str(p), primary_key="id")
        entities = src.to_entities("customer")
        assert len(entities) == 4
        assert all(e["entity_type"] == "customer" for e in entities)


# ═══════════════════════════════════════════════════════════════════════════
# Test: full reconcile pipeline (CSV)
# ═══════════════════════════════════════════════════════════════════════════


class TestReconcileCsv:
    def test_csv_reconcile(self, crm_csv, ecommerce_csv):
        from kanoniv import Spec, Source, reconcile

        crm = Source.from_csv("crm", crm_csv, primary_key="id")
        eco = Source.from_csv("ecommerce", ecommerce_csv, primary_key="id")
        spec = Spec.from_string(SPEC_V1)

        result = reconcile(sources=[crm, eco], spec=spec)

        # crm has 5 rows, ecommerce has 4 rows = 9 total entities
        # alice: email + phone match  → merge
        # bob:   email match          → merge
        # carol: phone matches grace  → merge (555-0003)
        # Remaining singletons: dave, eve, frank
        # Expected: <=6 clusters (at least 3 merges)
        assert result.cluster_count < 9
        assert result.cluster_count >= 1

    def test_csv_golden_records(self, crm_csv, ecommerce_csv):
        from kanoniv import Spec, Source, reconcile

        crm = Source.from_csv("crm", crm_csv, primary_key="id")
        eco = Source.from_csv("ecommerce", ecommerce_csv, primary_key="id")
        spec = Spec.from_string(SPEC_V1)

        result = reconcile(sources=[crm, eco], spec=spec)
        assert len(result.golden_records) == result.cluster_count
        for gr in result.golden_records:
            assert isinstance(gr, dict)

    def test_csv_merge_rate(self, crm_csv, ecommerce_csv):
        from kanoniv import Spec, Source, reconcile

        crm = Source.from_csv("crm", crm_csv, primary_key="id")
        eco = Source.from_csv("ecommerce", ecommerce_csv, primary_key="id")
        spec = Spec.from_string(SPEC_V1)

        result = reconcile(sources=[crm, eco], spec=spec)
        assert 0.0 < result.merge_rate < 1.0

    def test_csv_decisions(self, crm_csv, ecommerce_csv):
        from kanoniv import Spec, Source, reconcile

        crm = Source.from_csv("crm", crm_csv, primary_key="id")
        eco = Source.from_csv("ecommerce", ecommerce_csv, primary_key="id")
        spec = Spec.from_string(SPEC_V1)

        result = reconcile(sources=[crm, eco], spec=spec)
        assert len(result.decisions) >= 1

    def test_csv_telemetry(self, crm_csv, ecommerce_csv):
        from kanoniv import Spec, Source, reconcile

        crm = Source.from_csv("crm", crm_csv, primary_key="id")
        eco = Source.from_csv("ecommerce", ecommerce_csv, primary_key="id")
        spec = Spec.from_string(SPEC_V1)

        result = reconcile(sources=[crm, eco], spec=spec)
        assert isinstance(result.telemetry, dict)
        assert "rule_telemetry" in result.telemetry
        assert "pairs_evaluated" in result.telemetry


# ═══════════════════════════════════════════════════════════════════════════
# Test: full reconcile pipeline (pandas)
# ═══════════════════════════════════════════════════════════════════════════


class TestReconcilePandas:
    def test_pandas_reconcile(self, crm_df, ecommerce_df):
        from kanoniv import Spec, Source, reconcile

        crm = Source.from_pandas("crm", crm_df, primary_key="id")
        eco = Source.from_pandas("ecommerce", ecommerce_df, primary_key="id")
        spec = Spec.from_string(SPEC_V1)

        result = reconcile(sources=[crm, eco], spec=spec)
        assert result.cluster_count < 9
        assert result.cluster_count >= 1

    def test_pandas_to_pandas_result(self, crm_df, ecommerce_df):
        from kanoniv import Spec, Source, reconcile

        crm = Source.from_pandas("crm", crm_df, primary_key="id")
        eco = Source.from_pandas("ecommerce", ecommerce_df, primary_key="id")
        spec = Spec.from_string(SPEC_V1)

        result = reconcile(sources=[crm, eco], spec=spec)
        df = result.to_pandas()
        assert isinstance(df, pd.DataFrame)
        assert len(df) == result.cluster_count

    def test_pandas_csv_reconcile_parity(self, crm_csv, ecommerce_csv, crm_df, ecommerce_df):
        """CSV and pandas adapters should produce equivalent reconciliation results."""
        from kanoniv import Spec, Source, reconcile

        spec = Spec.from_string(SPEC_V1)

        csv_result = reconcile(
            sources=[
                Source.from_csv("crm", crm_csv, primary_key="id"),
                Source.from_csv("ecommerce", ecommerce_csv, primary_key="id"),
            ],
            spec=spec,
        )
        pd_result = reconcile(
            sources=[
                Source.from_pandas("crm", crm_df, primary_key="id"),
                Source.from_pandas("ecommerce", ecommerce_df, primary_key="id"),
            ],
            spec=spec,
        )

        # Both should produce the same number of clusters
        assert csv_result.cluster_count == pd_result.cluster_count
        assert csv_result.merge_rate == pytest.approx(pd_result.merge_rate)


# ═══════════════════════════════════════════════════════════════════════════
# Test: mixed sources (CSV + pandas in same reconcile call)
# ═══════════════════════════════════════════════════════════════════════════


class TestMixedSources:
    def test_csv_plus_pandas(self, crm_csv, ecommerce_df):
        from kanoniv import Spec, Source, reconcile

        crm = Source.from_csv("crm", crm_csv, primary_key="id")
        eco = Source.from_pandas("ecommerce", ecommerce_df, primary_key="id")
        spec = Spec.from_string(SPEC_V1)

        result = reconcile(sources=[crm, eco], spec=spec)
        assert result.cluster_count < 9
        assert len(result.golden_records) == result.cluster_count


# ═══════════════════════════════════════════════════════════════════════════
# Test: end-to-end workflow (validate → plan → reconcile → inspect)
# ═══════════════════════════════════════════════════════════════════════════


class TestEndToEndWorkflow:
    def test_full_workflow(self, crm_csv, ecommerce_csv):
        """Simulate the complete user workflow."""
        from kanoniv import Spec, Source, validate, plan, diff, reconcile

        # Step 1: Load spec
        spec = Spec.from_string(SPEC_V1)

        # Step 2: Validate
        validation = validate(spec)
        assert validation.valid, f"Spec should be valid but got: {validation.errors}"

        # Step 3: Plan
        execution_plan = plan(spec)
        summary = execution_plan.summary()
        assert len(summary) > 0, "Plan summary should not be empty"
        assert execution_plan.plan_hash, "Plan hash should not be empty"

        # Step 4: Load sources
        crm = Source.from_csv("crm", crm_csv, primary_key="id")
        eco = Source.from_csv("ecommerce", ecommerce_csv, primary_key="id")

        # Step 5: Verify schemas before reconcile
        crm_schema = crm.schema()
        eco_schema = eco.schema()
        assert len(crm_schema.columns) == 5
        assert len(eco_schema.columns) == 5

        # Step 6: Reconcile
        result = reconcile(sources=[crm, eco], spec=spec)

        # Step 7: Inspect results
        assert result.cluster_count >= 1
        assert result.cluster_count < 9
        assert len(result.golden_records) == result.cluster_count
        assert len(result.decisions) >= 1
        assert 0.0 < result.merge_rate < 1.0

        # Step 8: Convert to pandas
        df = result.to_pandas()
        assert isinstance(df, pd.DataFrame)
        assert len(df) == result.cluster_count

        # Step 9: Diff with v2
        spec_v2 = Spec.from_string(SPEC_V2)
        changes = diff(spec, spec_v2)
        assert "name_similarity" in changes.rules_added
        assert changes.thresholds_changed is True
        assert len(changes.summary) > 0

    def test_reconcile_with_v2_spec(self, crm_csv, ecommerce_csv):
        """Run reconcile with v2 spec (lower thresholds, extra rule) and compare."""
        from kanoniv import Spec, Source, validate, reconcile

        spec_v1 = Spec.from_string(SPEC_V1)
        spec_v2 = Spec.from_string(SPEC_V2)

        assert validate(spec_v2).valid

        crm = Source.from_csv("crm", crm_csv, primary_key="id")
        eco = Source.from_csv("ecommerce", ecommerce_csv, primary_key="id")

        result_v1 = reconcile(sources=[crm, eco], spec=spec_v1)
        result_v2 = reconcile(sources=[crm, eco], spec=spec_v2)

        # v2 has lower thresholds so it should merge at least as aggressively
        assert result_v2.cluster_count <= result_v1.cluster_count
