"""System-level determinism test suite for Kanoniv.

Proves the enterprise trust contract:
    Same spec + same data = same plan + same identity graph

Uses production-scale data (202 rows, 2 sources), deep fingerprinting that
normalizes away random UUIDs and parallel iteration order, and varied
iteration counts across shuffle seeds.

Requires native extension with reconcile_local.
"""
from __future__ import annotations

import csv
import json
import subprocess
import sys
import textwrap
import time
from pathlib import Path

import pytest

# Ensure tests/ is on sys.path so determinism.helpers is importable
_tests_dir = str(Path(__file__).parents[1])
if _tests_dir not in sys.path:
    sys.path.insert(0, _tests_dir)

from determinism.helpers import (  # noqa: E402
    REPO_ROOT,
    SF_CSV_PATH,
    SPEC_PATH,
    STRIPE_CSV_PATH,
    decision_signature,
    golden_path,
    identity_fingerprint,
    reformat_yaml,
    requires_native,
    requires_reconcile,
    shuffled_csv,
)


# ═══════════════════════════════════════════════════════════════════════════
# Test 1: Input Order Randomization
# ═══════════════════════════════════════════════════════════════════════════


@requires_reconcile
class TestInputOrderRandomization:
    """Rayon parallel iterators + HashMap blocking groups could be order-sensitive."""

    @pytest.mark.parametrize("seed", range(20))
    def test_shuffled_sf_source_identical_fingerprint(
        self, seed, tmp_path, production_spec, baseline_fingerprint
    ):
        """Shuffle SF CSV with different seeds -> fingerprint matches baseline."""
        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        sf_path = shuffled_csv(str(SF_CSV_PATH), tmp_path, seed, "sf")
        sf = Source.from_csv("salesforce", sf_path, primary_key="sf_id")
        stripe = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
        result = reconcile([sf, stripe], production_spec)
        fp = identity_fingerprint(result)
        assert fp == baseline_fingerprint, (
            f"Seed {seed}: fingerprint mismatch\n"
            f"  baseline: {baseline_fingerprint}\n"
            f"  got:      {fp}"
        )

    @pytest.mark.parametrize("seed", range(20))
    def test_shuffled_both_sources_identical_fingerprint(
        self, seed, tmp_path, production_spec, baseline_fingerprint
    ):
        """Shuffle both CSVs with different seeds -> fingerprint matches baseline."""
        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        sf_path = shuffled_csv(str(SF_CSV_PATH), tmp_path, seed + 1000, "sf")
        stripe_path = shuffled_csv(str(STRIPE_CSV_PATH), tmp_path, seed + 2000, "stripe")
        sf = Source.from_csv("salesforce", sf_path, primary_key="sf_id")
        stripe = Source.from_csv("stripe", stripe_path, primary_key="cus_id")
        result = reconcile([sf, stripe], production_spec)
        fp = identity_fingerprint(result)
        assert fp == baseline_fingerprint, (
            f"Seed {seed}: fingerprint mismatch when both sources shuffled"
        )

    @pytest.mark.parametrize("seed", range(5))
    def test_shuffled_decisions_same_signatures(
        self, seed, tmp_path, production_spec, baseline_fingerprint
    ):
        """Decision signature sets are identical across shuffled inputs."""
        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        sf_path = shuffled_csv(str(SF_CSV_PATH), tmp_path, seed + 3000, "sf")
        sf = Source.from_csv("salesforce", sf_path, primary_key="sf_id")
        stripe = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
        result = reconcile([sf, stripe], production_spec)

        sf_base = Source.from_csv("salesforce", str(SF_CSV_PATH), primary_key="sf_id")
        stripe_base = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
        baseline = reconcile([sf_base, stripe_base], production_spec)

        sigs = set(decision_signature(d) for d in result.decisions)
        base_sigs = set(decision_signature(d) for d in baseline.decisions)
        assert sigs == base_sigs, f"Seed {seed}: decision signatures differ"

    @pytest.mark.parametrize("seed", range(5))
    def test_shuffled_confidence_distribution_identical(
        self, seed, tmp_path, production_spec
    ):
        """Sorted confidence lists are identical across shuffled inputs."""
        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        sf_base = Source.from_csv("salesforce", str(SF_CSV_PATH), primary_key="sf_id")
        stripe_base = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
        baseline = reconcile([sf_base, stripe_base], production_spec)

        sf_path = shuffled_csv(str(SF_CSV_PATH), tmp_path, seed + 5000, "sf")
        sf = Source.from_csv("salesforce", sf_path, primary_key="sf_id")
        stripe = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
        result = reconcile([sf, stripe], production_spec)

        base_conf = sorted(round(float(d.get("confidence", 0)), 10) for d in baseline.decisions)
        shuf_conf = sorted(round(float(d.get("confidence", 0)), 10) for d in result.decisions)
        assert base_conf == shuf_conf, f"Seed {seed}: confidence distributions differ"


# ═══════════════════════════════════════════════════════════════════════════
# Test 2: Execution Environment Variation
# ═══════════════════════════════════════════════════════════════════════════


@requires_reconcile
class TestExecutionEnvironmentVariation:
    """Rayon thread pool init, HashMap random seeds, warm vs cold engine."""

    def test_cold_vs_warm_engine_identical(self, production_spec, production_sources):
        """First call vs second call produce same fingerprint."""
        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        sf, stripe = production_sources
        result_cold = reconcile([sf, stripe], production_spec)

        sf2 = Source.from_csv("salesforce", str(SF_CSV_PATH), primary_key="sf_id")
        stripe2 = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
        result_warm = reconcile([sf2, stripe2], production_spec)

        assert identity_fingerprint(result_cold) == identity_fingerprint(result_warm), \
            "Cold vs warm engine produced different fingerprints"

    def test_fresh_object_construction_stable(self):
        """Rebuild Spec+Source from disk 3 times -> fingerprint stable."""
        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source
        from kanoniv.spec import Spec

        fingerprints = []
        for _ in range(3):
            spec = Spec.from_file(str(SPEC_PATH))
            sf = Source.from_csv("salesforce", str(SF_CSV_PATH), primary_key="sf_id")
            stripe = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
            result = reconcile([sf, stripe], spec)
            fingerprints.append(identity_fingerprint(result))

        assert len(set(fingerprints)) == 1, "Fresh construction produced varying fingerprints"

    @pytest.mark.parametrize("iteration", range(3))
    def test_subprocess_determinism(self, iteration, baseline_fingerprint):
        """Run in child process via subprocess, compare fingerprint to in-process.

        Cross-process determinism now works because the engine sorts scores
        before summing and uses lexicographic tie-breaking in survivorship.
        """
        script = textwrap.dedent(f"""\
            import sys
            sys.path.insert(0, "{REPO_ROOT / 'oss' / 'python-sdk' / 'tests'}")
            from kanoniv.reconcile import reconcile
            from kanoniv.source import Source
            from kanoniv.spec import Spec
            from determinism.helpers import identity_fingerprint

            spec = Spec.from_file("{SPEC_PATH}")
            sf = Source.from_csv("salesforce", "{SF_CSV_PATH}", primary_key="sf_id")
            stripe = Source.from_csv("stripe", "{STRIPE_CSV_PATH}", primary_key="cus_id")
            result = reconcile([sf, stripe], spec)
            print(identity_fingerprint(result))
        """)
        proc = subprocess.run(
            [sys.executable, "-c", script],
            capture_output=True,
            text=True,
            timeout=120,
        )
        assert proc.returncode == 0, f"Subprocess failed:\n{proc.stderr}"
        sub_fp = proc.stdout.strip()
        assert sub_fp == baseline_fingerprint, (
            f"Iteration {iteration}: subprocess fingerprint differs\n"
            f"  in-process: {baseline_fingerprint}\n"
            f"  subprocess: {sub_fp}"
        )

    def test_sequential_back_to_back(self, production_spec):
        """Run 3 times with 100ms gaps, all identical."""
        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        fingerprints = []
        for _ in range(3):
            sf = Source.from_csv("salesforce", str(SF_CSV_PATH), primary_key="sf_id")
            stripe = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
            result = reconcile([sf, stripe], production_spec)
            fingerprints.append(identity_fingerprint(result))
            time.sleep(0.1)

        assert len(set(fingerprints)) == 1, f"Back-to-back runs varied: {fingerprints}"


# ═══════════════════════════════════════════════════════════════════════════
# Test 3: Repeated Execution Stability
# ═══════════════════════════════════════════════════════════════════════════


@requires_reconcile
class TestRepeatedExecutionStability:
    """State leakage, floating-point accumulation, cache pollution."""

    def _run_n(self, n, production_spec):
        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        results = []
        for _ in range(n):
            sf = Source.from_csv("salesforce", str(SF_CSV_PATH), primary_key="sf_id")
            stripe = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
            results.append(reconcile([sf, stripe], production_spec))
        return results

    def test_25_runs_identical_fingerprint(self, production_spec):
        """25 runs -> all fingerprints identical."""
        results = self._run_n(25, production_spec)
        fingerprints = [identity_fingerprint(r) for r in results]
        unique = set(fingerprints)
        assert len(unique) == 1, (
            f"Fingerprints varied across 25 runs: {len(unique)} unique values"
        )

    def test_25_runs_same_metrics(self, production_spec):
        """25 runs -> cluster_count, merge_rate, decision_count all stable."""
        results = self._run_n(25, production_spec)
        counts = [r.cluster_count for r in results]
        rates = [round(r.merge_rate, 10) for r in results]
        dcounts = [len(r.decisions) for r in results]
        assert len(set(counts)) == 1, f"Cluster counts varied: {set(counts)}"
        assert len(set(rates)) == 1, f"Merge rates varied: {set(rates)}"
        assert len(set(dcounts)) == 1, f"Decision counts varied: {set(dcounts)}"

    def test_25_plan_hash_and_summary_stable(self, production_spec):
        """25 plan generations -> all plan_hash and summary() identical."""
        from kanoniv.plan import plan

        hashes = []
        summaries = []
        for _ in range(25):
            p = plan(production_spec)
            hashes.append(p.plan_hash)
            summaries.append(p.summary())
        assert len(set(hashes)) == 1, f"Plan hashes varied: {set(hashes)}"
        assert len(set(summaries)) == 1, "Plan summaries varied"


# ═══════════════════════════════════════════════════════════════════════════
# Test 4: Canonicalization Invariance
# ═══════════════════════════════════════════════════════════════════════════


@requires_reconcile
class TestCanonicalizationInvariance:
    """Column order, whitespace, casing, field aliasing affect matching."""

    def _read_csv_rows(self, path: str) -> tuple[list[str], list[dict]]:
        with open(path, newline="") as f:
            reader = csv.DictReader(f)
            fieldnames = reader.fieldnames or []
            rows = list(reader)
        return fieldnames, rows

    def _write_csv(self, path: Path, fieldnames: list[str], rows: list[dict]) -> str:
        with open(path, "w", newline="") as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()
            writer.writerows(rows)
        return str(path)

    def test_column_order_invariance(self, tmp_path, production_spec, baseline_fingerprint):
        """Reversed CSV columns -> same fingerprint."""
        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        fieldnames, rows = self._read_csv_rows(str(SF_CSV_PATH))
        reversed_fields = list(reversed(fieldnames))
        sf_path = self._write_csv(tmp_path / "sf_reversed.csv", reversed_fields, rows)

        sf = Source.from_csv("salesforce", sf_path, primary_key="sf_id")
        stripe = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
        result = reconcile([sf, stripe], production_spec)
        assert identity_fingerprint(result) == baseline_fingerprint, \
            "Column order reversal changed fingerprint"

    def test_whitespace_in_values_deterministic(self, tmp_path, production_spec):
        """Leading/trailing whitespace -> deterministic result across runs."""
        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        fieldnames, rows = self._read_csv_rows(str(SF_CSV_PATH))
        for row in rows:
            if "email" in row and row["email"]:
                row["email"] = f"  {row['email']}  "
        sf_path = self._write_csv(tmp_path / "sf_whitespace.csv", fieldnames, rows)

        fingerprints = []
        for _ in range(3):
            sf = Source.from_csv("salesforce", sf_path, primary_key="sf_id")
            stripe = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
            result = reconcile([sf, stripe], production_spec)
            fingerprints.append(identity_fingerprint(result))

        assert len(set(fingerprints)) == 1, "Whitespace data produced non-deterministic results"

    def test_email_case_deterministic(self, tmp_path, production_spec):
        """Uppercased emails -> deterministic result across runs."""
        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        fieldnames, rows = self._read_csv_rows(str(SF_CSV_PATH))
        for row in rows:
            if "email" in row and row["email"]:
                row["email"] = row["email"].upper()
        sf_path = self._write_csv(tmp_path / "sf_upper.csv", fieldnames, rows)

        fingerprints = []
        for _ in range(3):
            sf = Source.from_csv("salesforce", sf_path, primary_key="sf_id")
            stripe = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
            result = reconcile([sf, stripe], production_spec)
            fingerprints.append(identity_fingerprint(result))

        assert len(set(fingerprints)) == 1, "Uppercased emails produced non-deterministic results"

    def test_combined_canonicalization_chaos(self, tmp_path, production_spec):
        """Column reorder + whitespace combined -> deterministic result."""
        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        fieldnames, rows = self._read_csv_rows(str(SF_CSV_PATH))
        reversed_fields = list(reversed(fieldnames))
        for row in rows:
            if "email" in row and row["email"]:
                row["email"] = f" {row['email']} "

        fingerprints = []
        for i in range(3):
            sf_path = self._write_csv(
                tmp_path / f"sf_chaos_{i}.csv", reversed_fields, rows
            )
            sf = Source.from_csv("salesforce", sf_path, primary_key="sf_id")
            stripe = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
            result = reconcile([sf, stripe], production_spec)
            fingerprints.append(identity_fingerprint(result))

        assert len(set(fingerprints)) == 1, "Combined canonicalization chaos was non-deterministic"


# ═══════════════════════════════════════════════════════════════════════════
# Test 5: Tie-Breaker Stability
# ═══════════════════════════════════════════════════════════════════════════


@requires_reconcile
class TestTieBreakerStability:
    """Identical scores, multiple merge paths -> non-deterministic winner.

    Uses identity_fingerprint (including golden records) because the engine
    now has deterministic survivorship tie-breaking via lexicographic ordering.
    """

    def _make_single_source_spec(self):
        from kanoniv.spec import Spec

        return Spec.from_string(textwrap.dedent("""\
            api_version: kanoniv/v2
            identity_version: test_v1.0
            entity:
              name: customer
            sources:
              - name: main
                system: csv
                table: main
                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.4
                review: 0.2
              conflict_strategy: prefer_high_confidence
        """))

    def _write_rows(self, tmp_path, name, rows):
        csv_path = tmp_path / f"{name}.csv"
        with open(csv_path, "w", newline="") as f:
            writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
            writer.writeheader()
            writer.writerows(rows)
        return str(csv_path)

    @pytest.mark.parametrize("seed", range(20))
    def test_identical_score_pairs_stable(self, seed, tmp_path):
        """Pairs with identical matching fields -> stable full fingerprint."""
        spec = self._make_single_source_spec()
        rows = [
            {"id": "1", "email": "shared@example.com", "first_name": "Alice",
             "last_name": "Smith", "phone": "555-0001"},
            {"id": "2", "email": "shared@example.com", "first_name": "Alice",
             "last_name": "Smith", "phone": "555-0002"},
            {"id": "3", "email": "other@example.com", "first_name": "Bob",
             "last_name": "Jones", "phone": "555-0003"},
        ]
        import random as rng
        rng.Random(seed).shuffle(rows)
        csv_path = self._write_rows(tmp_path, f"pairs_{seed}", rows)

        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        src1 = Source.from_csv("main", csv_path, primary_key="id")
        r1 = reconcile([src1], spec)
        src2 = Source.from_csv("main", csv_path, primary_key="id")
        r2 = reconcile([src2], spec)
        assert identity_fingerprint(r1) == identity_fingerprint(r2), (
            f"Seed {seed}: identical score pairs produced different results"
        )

    @pytest.mark.parametrize("seed", range(20))
    def test_transitive_chain_AB_BC(self, seed, tmp_path):
        """A<->B and B<->C should transitively produce consistent cluster."""
        spec = self._make_single_source_spec()
        rows = [
            {"id": "A", "email": "shared@example.com", "first_name": "Alice",
             "last_name": "Smith", "phone": "555-1111"},
            {"id": "B", "email": "shared@example.com", "first_name": "Bob",
             "last_name": "Jones", "phone": "555-2222"},
            {"id": "C", "email": "different@example.com", "first_name": "Bob",
             "last_name": "Jones", "phone": "555-2222"},
        ]
        import random as rng
        rng.Random(seed).shuffle(rows)
        csv_path = self._write_rows(tmp_path, f"trans_{seed}", rows)

        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        src1 = Source.from_csv("main", csv_path, primary_key="id")
        r1 = reconcile([src1], spec)
        src2 = Source.from_csv("main", csv_path, primary_key="id")
        r2 = reconcile([src2], spec)
        assert identity_fingerprint(r1) == identity_fingerprint(r2), (
            f"Seed {seed}: transitive chain non-deterministic"
        )

    @pytest.mark.parametrize("seed", range(20))
    def test_diamond_merge_path(self, seed, tmp_path):
        """Diamond: A<->B, A<->C, B<->D, C<->D -> consistent cluster membership."""
        spec = self._make_single_source_spec()
        rows = [
            {"id": "A", "email": "shared1@example.com", "first_name": "Alice",
             "last_name": "Smith", "phone": "555-0001"},
            {"id": "B", "email": "shared1@example.com", "first_name": "Bob",
             "last_name": "Jones", "phone": "555-0002"},
            {"id": "C", "email": "shared2@example.com", "first_name": "Alice",
             "last_name": "Smith", "phone": "555-0001"},
            {"id": "D", "email": "shared2@example.com", "first_name": "Bob",
             "last_name": "Jones", "phone": "555-0002"},
        ]
        import random as rng
        rng.Random(seed).shuffle(rows)
        csv_path = self._write_rows(tmp_path, f"diamond_{seed}", rows)

        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        src1 = Source.from_csv("main", csv_path, primary_key="id")
        r1 = reconcile([src1], spec)
        src2 = Source.from_csv("main", csv_path, primary_key="id")
        r2 = reconcile([src2], spec)
        assert identity_fingerprint(r1) == identity_fingerprint(r2), (
            f"Seed {seed}: diamond merge path non-deterministic"
        )

    @pytest.mark.parametrize("seed", range(20))
    def test_corporate_shared_inbox_stable(self, seed, tmp_path):
        """golden/corporate_shared_inbox.csv -> stable full fingerprint."""
        spec = self._make_single_source_spec()
        csv_path = shuffled_csv(golden_path("corporate_shared_inbox.csv"), tmp_path, seed, "inbox")

        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        src1 = Source.from_csv("main", csv_path, primary_key="id")
        r1 = reconcile([src1], spec)
        src2 = Source.from_csv("main", csv_path, primary_key="id")
        r2 = reconcile([src2], spec)
        assert identity_fingerprint(r1) == identity_fingerprint(r2), (
            f"Seed {seed}: corporate shared inbox non-deterministic"
        )

    @pytest.mark.parametrize("seed", range(20))
    def test_family_shared_phone_stable(self, seed, tmp_path):
        """golden/family_shared_phone.csv -> stable full fingerprint."""
        spec = self._make_single_source_spec()
        csv_path = shuffled_csv(golden_path("family_shared_phone.csv"), tmp_path, seed, "family")

        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        src1 = Source.from_csv("main", csv_path, primary_key="id")
        r1 = reconcile([src1], spec)
        src2 = Source.from_csv("main", csv_path, primary_key="id")
        r2 = reconcile([src2], spec)
        assert identity_fingerprint(r1) == identity_fingerprint(r2), (
            f"Seed {seed}: family shared phone non-deterministic"
        )

    @pytest.mark.parametrize("seed", range(20))
    def test_near_threshold_boundary(self, seed, tmp_path):
        """Pairs at near-threshold scores -> stable classification."""
        spec = self._make_single_source_spec()
        rows = [
            {"id": "1", "email": "alice@example.com", "first_name": "Alice",
             "last_name": "Smith", "phone": "555-0001"},
            {"id": "2", "email": "alice@example.com", "first_name": "Alise",
             "last_name": "Smith", "phone": "555-0002"},
            {"id": "3", "email": "bob@example.com", "first_name": "Robert",
             "last_name": "Jones", "phone": "555-0003"},
        ]
        import random as rng
        rng.Random(seed).shuffle(rows)
        csv_path = self._write_rows(tmp_path, f"threshold_{seed}", rows)

        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        src1 = Source.from_csv("main", csv_path, primary_key="id")
        r1 = reconcile([src1], spec)
        src2 = Source.from_csv("main", csv_path, primary_key="id")
        r2 = reconcile([src2], spec)
        assert identity_fingerprint(r1) == identity_fingerprint(r2), (
            f"Seed {seed}: near-threshold boundary non-deterministic"
        )


# ═══════════════════════════════════════════════════════════════════════════
# Test 6: Plan Hash Invariance
# ═══════════════════════════════════════════════════════════════════════════


@requires_native
class TestPlanHashInvariance:
    """YAML formatting -> different plan hash despite semantic equivalence."""

    def test_yaml_reformats_same_hash(self, production_spec):
        """All YAML reformatting variants produce the same plan hash."""
        from kanoniv.plan import plan
        from kanoniv.spec import Spec

        baseline = plan(production_spec)

        for variant in ("reorder", "extra_whitespace", "quoting", "comments"):
            reformatted = reformat_yaml(SPEC_PATH.read_text(), variant)
            spec2 = Spec.from_string(reformatted)
            p2 = plan(spec2)
            assert p2.plan_hash == baseline.plan_hash, (
                f"Variant '{variant}' changed plan hash"
            )
            assert p2.to_dict() == baseline.to_dict(), (
                f"Variant '{variant}' changed plan to_dict()"
            )

    def test_from_file_vs_from_string_same_hash(self):
        """Two load paths -> same hash."""
        from kanoniv.plan import plan
        from kanoniv.spec import Spec

        spec_file = Spec.from_file(str(SPEC_PATH))
        spec_string = Spec.from_string(SPEC_PATH.read_text())
        assert plan(spec_file).plan_hash == plan(spec_string).plan_hash, \
            "from_file vs from_string produced different plan hashes"

    def test_25_plan_generations_stable(self, production_spec):
        """25 generations -> all identical hashes."""
        from kanoniv.plan import plan

        hashes = [plan(production_spec).plan_hash for _ in range(25)]
        assert len(set(hashes)) == 1, f"Plan hashes varied: {len(set(hashes))} unique"


# ═══════════════════════════════════════════════════════════════════════════
# Test 7: Shadow Mode Determinism
# ═══════════════════════════════════════════════════════════════════════════


@requires_native
class TestShadowModeDeterminism:
    """diff() reports false positives, or misses real changes."""

    def test_diff_same_spec_no_changes(self, production_spec):
        """Same spec diffed against itself -> has_changes == False (5 runs)."""
        from kanoniv.diff import diff

        for _ in range(5):
            result = diff(production_spec, production_spec)
            assert not result.has_changes, (
                f"diff of same spec reported changes: {result.summary}"
            )

    def test_diff_identical_yaml_different_objects(self):
        """Two Spec objects from same YAML -> no changes (5 runs)."""
        from kanoniv.diff import diff
        from kanoniv.spec import Spec

        yaml_text = SPEC_PATH.read_text()
        for _ in range(5):
            spec_a = Spec.from_string(yaml_text)
            spec_b = Spec.from_string(yaml_text)
            result = diff(spec_a, spec_b)
            assert not result.has_changes, \
                "identical YAML different objects reported changes"

    def test_diff_detects_real_change_consistently(self):
        """Modified threshold always detected (5 runs)."""
        from kanoniv.diff import diff
        from kanoniv.spec import Spec

        yaml_text = SPEC_PATH.read_text()
        modified = yaml_text.replace("match: 0.6", "match: 0.8")

        for _ in range(5):
            spec_a = Spec.from_string(yaml_text)
            spec_b = Spec.from_string(modified)
            result = diff(spec_a, spec_b)
            assert result.has_changes, "threshold change not detected"
            assert result.thresholds_changed, "thresholds_changed is False"

    def test_diff_symmetry(self):
        """diff(a,b) and diff(b,a) detect changes symmetrically (5 runs)."""
        from kanoniv.diff import diff
        from kanoniv.spec import Spec

        yaml_text = SPEC_PATH.read_text()
        modified = yaml_text.replace("match: 0.6", "match: 0.8")

        for _ in range(5):
            spec_a = Spec.from_string(yaml_text)
            spec_b = Spec.from_string(modified)
            result_ab = diff(spec_a, spec_b)
            result_ba = diff(spec_b, spec_a)
            assert result_ab.has_changes == result_ba.has_changes, \
                "diff symmetry broken for has_changes"
            assert result_ab.thresholds_changed == result_ba.thresholds_changed, \
                "diff symmetry broken for thresholds_changed"


# ═══════════════════════════════════════════════════════════════════════════
# Test 8: Observability Consistency
# ═══════════════════════════════════════════════════════════════════════════


@requires_reconcile
class TestObservabilityConsistency:
    """Human-readable outputs vary, operators can't baseline."""

    def test_decision_explanations_stable(self, production_spec):
        """Set of (rule_name, explanation) tuples identical across 5 runs."""
        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        def get_explanations(result):
            return frozenset(
                (rr.get("rule_name", ""), rr.get("explanation", ""))
                for d in result.decisions
                for rr in d.get("rule_results", [])
            )

        explanation_sets = []
        for _ in range(5):
            sf = Source.from_csv("salesforce", str(SF_CSV_PATH), primary_key="sf_id")
            stripe = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
            r = reconcile([sf, stripe], production_spec)
            explanation_sets.append(get_explanations(r))

        assert len(set(explanation_sets)) == 1, "Decision explanations varied across runs"

    def test_telemetry_values_stable(self, production_spec):
        """Telemetry dicts identical across 5 runs (engine now deterministic)."""
        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        def extract_telemetry(result):
            t = result.telemetry
            # Exclude timing keys
            return {
                k: v for k, v in t.items()
                if not any(w in k.lower() for w in ("duration", "elapsed", "time", "latency"))
            }

        telemetry_list = []
        for _ in range(5):
            sf = Source.from_csv("salesforce", str(SF_CSV_PATH), primary_key="sf_id")
            stripe = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
            r = reconcile([sf, stripe], production_spec)
            telemetry_list.append(json.dumps(extract_telemetry(r), sort_keys=True, default=str))

        assert len(set(telemetry_list)) == 1, "Telemetry varied across runs"

    def test_plan_observability_stable(self, production_spec):
        """Plan risk_flags, execution_stages, match_strategies all stable (5 runs)."""
        from kanoniv.plan import plan

        risk_flags = []
        stages = []
        strategies = []
        summaries = []
        for _ in range(5):
            p = plan(production_spec)
            risk_flags.append(json.dumps(p.risk_flags, sort_keys=True))
            stages.append(json.dumps(p.execution_stages, sort_keys=True))
            strategies.append(json.dumps(p.match_strategies, sort_keys=True))
            summaries.append(p.summary())

        assert len(set(risk_flags)) == 1, "risk_flags varied"
        assert len(set(stages)) == 1, "execution_stages varied"
        assert len(set(strategies)) == 1, "match_strategies varied"
        assert len(set(summaries)) == 1, "plan summaries varied"

    @pytest.mark.parametrize("iteration", range(5))
    def test_combined_observability_fingerprint(self, iteration, production_spec):
        """SHA-256 of all observability outputs stable."""
        import hashlib

        from kanoniv.plan import plan
        from kanoniv.reconcile import reconcile
        from kanoniv.source import Source

        def observability_fingerprint(spec):
            p = plan(spec)
            sf = Source.from_csv("salesforce", str(SF_CSV_PATH), primary_key="sf_id")
            stripe = Source.from_csv("stripe", str(STRIPE_CSV_PATH), primary_key="cus_id")
            r = reconcile([sf, stripe], spec)

            obs = {
                "plan_hash": p.plan_hash,
                "summary": p.summary(),
                "risk_flags": p.risk_flags,
                "execution_stages": p.execution_stages,
                "match_strategies": p.match_strategies,
                "identity_fingerprint": identity_fingerprint(r),
                "cluster_count": r.cluster_count,
                "merge_rate": round(r.merge_rate, 10),
                "decision_count": len(r.decisions),
            }
            canonical = json.dumps(obs, sort_keys=True, default=str)
            return hashlib.sha256(canonical.encode()).hexdigest()

        fp1 = observability_fingerprint(production_spec)
        fp2 = observability_fingerprint(production_spec)
        assert fp1 == fp2, (
            f"Iteration {iteration}: combined observability fingerprint differs"
        )
