#!/usr/bin/env python3
"""
R2: Centroid RNG Equivalence Tests

Tests that prove the C backend (helix_regen_cli) now uses the same
RNG context semantics as Python: sha256(dna_hex|entropy_hex|label|offset)

After R1 fix, these tests should PASS:
  - Different labels → different outputs
  - Different offsets → different outputs
  - Same context → same output (deterministic)

These tests replace the xfail markers from test_centroid_regrow.py.
"""
import sys
import os
import json
import hashlib
import subprocess
import tempfile
from pathlib import Path

import pytest

# Add paths
HELIX_CDC = Path(__file__).parent.parent
sys.path.insert(0, str(HELIX_CDC))


# =============================================================================
# Helpers
# =============================================================================
def run_c_backend(dna_hex: str, entropy_hex: str, label: str, offset: int, size: int) -> tuple:
    """
    Run helix_regen_cli with context mode and return (output_hash, receipt).
    """
    c_binary = HELIX_CDC / "tools" / "helix_regen_cli"
    if not c_binary.exists():
        pytest.skip(f"C binary not found at {c_binary}")

    with tempfile.NamedTemporaryFile(delete=False, suffix=".bin") as f:
        out_path = f.name

    try:
        cmd = [
            str(c_binary),
            "--dna", dna_hex,
            "--entropy", entropy_hex,
            "--label", label,
            "--offset", str(offset),
            "--size", str(size),
            "--out", out_path
        ]

        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=30
        )

        if result.returncode != 0:
            raise RuntimeError(f"C regen failed: {result.stderr}")

        # Parse JSON receipt from stdout
        receipt = json.loads(result.stdout)

        # Read and hash output
        with open(out_path, "rb") as f:
            data = f.read()
        output_hash = hashlib.sha256(data).hexdigest()

        return output_hash, receipt

    finally:
        if os.path.exists(out_path):
            os.unlink(out_path)


def python_context_hash(dna_hex: str, entropy_hex: str, label: str, offset: int) -> str:
    """
    Compute the Python RNG context hash.
    Matches: f"{dna_hex}|{entropy_hex}|{label}|{offset}"
    """
    context = f"{dna_hex}|{entropy_hex}|{label}|{offset}".encode()
    return hashlib.sha256(context).hexdigest()


# =============================================================================
# Test: C backend uses context mode
# =============================================================================
class TestCBackendContextMode:
    """Tests for C backend context mode (dna|entropy|label|offset)."""

    def test_c_backend_runs_in_context_mode(self):
        """
        Claim: C backend runs successfully in context mode.
        """
        dna = "abc123"
        entropy = "def456"
        label = "test_layer"
        offset = 0
        size = 1024

        output_hash, receipt = run_c_backend(dna, entropy, label, offset, size)

        # Should use context mode
        assert receipt["regen_core"]["use_context_mode"] is True
        assert "context" in receipt
        assert receipt["context"]["label"] == label
        assert receipt["context"]["offset"] == offset

    def test_c_context_hash_matches_python(self):
        """
        Claim: C backend's context_hash matches Python's sha256(dna|entropy|label|offset).
        """
        dna = "dna_abc123"
        entropy = "entropy_xyz789"
        label = "blk.00.attn.q"
        offset = 42

        # Get C backend's context hash
        _, receipt = run_c_backend(dna, entropy, label, offset, 1024)
        c_context_hash = receipt["context"]["context_hash"]

        # Compute Python's context hash
        py_context_hash = python_context_hash(dna, entropy, label, offset)

        assert c_context_hash == py_context_hash, \
            f"Context hash mismatch: C={c_context_hash[:16]}... vs Python={py_context_hash[:16]}..."


# =============================================================================
# Test: Different labels → different outputs (R1 FIX)
# =============================================================================
class TestDifferentLabelsProduceDifferentOutput:
    """
    Tests that replace xfail: test_different_names_different_rng

    After R1 fix, different labels must produce different outputs.
    """

    def test_different_labels_different_output(self):
        """
        Claim: Same dna/entropy, different label → different output.
        """
        dna = "fixed_dna_hex"
        entropy = "fixed_entropy_hex"
        size = 4096

        # Two different labels
        hash1, _ = run_c_backend(dna, entropy, "blk.00.attn.q", 0, size)
        hash2, _ = run_c_backend(dna, entropy, "blk.00.attn.k", 0, size)

        assert hash1 != hash2, "Different labels should produce different outputs"

    def test_same_label_same_output(self):
        """
        Claim: Same context → same output (deterministic).
        """
        dna = "fixed_dna"
        entropy = "fixed_entropy"
        label = "same_label"
        offset = 0
        size = 4096

        hash1, _ = run_c_backend(dna, entropy, label, offset, size)
        hash2, _ = run_c_backend(dna, entropy, label, offset, size)

        assert hash1 == hash2, "Same context should produce identical output"

    def test_empty_vs_nonempty_label(self):
        """
        Claim: Empty label and non-empty label produce different outputs.
        """
        dna = "fixed_dna"
        entropy = "fixed_entropy"
        size = 4096

        hash_empty, _ = run_c_backend(dna, entropy, "", 0, size)
        hash_named, _ = run_c_backend(dna, entropy, "layer_name", 0, size)

        assert hash_empty != hash_named, "Empty vs non-empty label should differ"


# =============================================================================
# Test: Different offsets → different outputs (R1 FIX)
# =============================================================================
class TestDifferentOffsetsProduceDifferentOutput:
    """
    Tests that replace xfail: test_explicit_offset_changes_output

    After R1 fix, different offsets must produce different outputs.
    """

    def test_different_offsets_different_output(self):
        """
        Claim: Same dna/entropy/label, different offset → different output.
        """
        dna = "fixed_dna"
        entropy = "fixed_entropy"
        label = "fixed_label"
        size = 4096

        hash0, _ = run_c_backend(dna, entropy, label, 0, size)
        hash1, _ = run_c_backend(dna, entropy, label, 1, size)

        assert hash0 != hash1, "Different offsets should produce different outputs"

    def test_offset_0_vs_large_offset(self):
        """
        Claim: Offset 0 and offset 1000000 produce different outputs.
        """
        dna = "fixed_dna"
        entropy = "fixed_entropy"
        label = "tensor"
        size = 4096

        hash_0, _ = run_c_backend(dna, entropy, label, 0, size)
        hash_big, _ = run_c_backend(dna, entropy, label, 1000000, size)

        assert hash_0 != hash_big, "Offset 0 vs large offset should differ"


# =============================================================================
# Test: Context mode vs legacy mode produce different outputs
# =============================================================================
class TestContextModeVsLegacyMode:
    """
    Tests that context mode differs from legacy mode.
    """

    def test_context_mode_differs_from_legacy(self):
        """
        Claim: Context mode with label/offset differs from legacy --seed mode.
        """
        c_binary = HELIX_CDC / "tools" / "helix_regen_cli"
        if not c_binary.exists():
            pytest.skip("C binary not found")

        dna = "abc"
        entropy = "xyz"
        label = "test"
        offset = 0
        size = 1024

        # Context mode
        hash_context, _ = run_c_backend(dna, entropy, label, offset, size)

        # Legacy mode with combined seed (old behavior)
        legacy_seed = f"{dna}{entropy}"
        with tempfile.NamedTemporaryFile(delete=False, suffix=".bin") as f:
            out_path = f.name

        try:
            cmd = [
                str(c_binary),
                "--seed", legacy_seed,
                "--size", str(size),
                "--out", out_path
            ]
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
            if result.returncode != 0:
                pytest.skip("Legacy mode failed")

            with open(out_path, "rb") as f:
                data = f.read()
            hash_legacy = hashlib.sha256(data).hexdigest()
        finally:
            if os.path.exists(out_path):
                os.unlink(out_path)

        # Context mode should differ from legacy mode
        assert hash_context != hash_legacy, \
            "Context mode should produce different output than legacy mode"


# =============================================================================
# Test: Multiple contexts produce unique outputs
# =============================================================================
class TestMultipleContextsUnique:
    """
    Tests with multiple contexts to verify uniqueness.
    """

    CONTEXTS = [
        ("dnaA", "entA", "blk.00.attn.q", 0),
        ("dnaA", "entA", "blk.00.attn.k", 0),
        ("dnaA", "entA", "blk.00.attn.q", 1),
        ("dnaA", "entB", "blk.00.attn.q", 0),
        ("dnaB", "entA", "blk.00.attn.q", 0),
    ]

    def test_all_contexts_produce_unique_hashes(self):
        """
        Claim: Each unique context produces a unique output hash.
        """
        size = 4096
        hashes = set()

        for dna, entropy, label, offset in self.CONTEXTS:
            h, _ = run_c_backend(dna, entropy, label, offset, size)
            assert h not in hashes, f"Collision for ({dna}, {entropy}, {label}, {offset})"
            hashes.add(h)

        assert len(hashes) == len(self.CONTEXTS), "All contexts should produce unique hashes"


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
