#!/usr/bin/env python3
"""
E5: KRISPER Compiler KAT + Receipts

Tests for KRISPER deterministic NL -> IR compilation.

Claims proven:
  - For a fixed compiler version, same NL text -> same IR hash
  - Reproducible KAT via make test-krisper
  - Each compile can generate cryptographic receipts

Run: pytest tests/test_krisper_compiler.py -v
"""
import sys
import json
import hashlib
import subprocess
from pathlib import Path

import pytest

# Add paths
HELIX_CDC = Path(__file__).parent.parent
sys.path.insert(0, str(HELIX_CDC))
sys.path.insert(0, str(HELIX_CDC / "gitforest-repo" / "forest" / "krisper"))

# Import from smoketest for core functions
from tools.krisper_genome_smoketest import (
    compile_nl_to_ir,
    verify_determinism,
    generate_receipt,
    sha256_str,
    sha256_dict,
)


# Test cases for KRISPER compiler
TEST_CASES = [
    'compress payload "hello world" as r1',
    'compress payload "test data" using seed=42 as r1, then attest',
    'compress payload "abc" as r1, compress payload "def" as r2, compare r1 with r2',
    'capsule my_capsule from genome "abc123" with metadata "test"',
]


class TestKrisperDeterminism:
    """Test KRISPER compiler determinism."""

    def test_determinism_same_process(self):
        """
        E5 Test #1: Same NL text -> same IR hash within same process.

        For each test case, run N times and verify all IR hashes match.
        """
        for text in TEST_CASES:
            hashes = []
            for _ in range(3):
                result = compile_nl_to_ir(text)
                assert result.get("status") in ("success", "error"), \
                    f"Unexpected status for: {text}"

                # Even errors should be deterministic
                ir_hash = result.get("ir_hash") or result.get("error", "ERROR")
                hashes.append(ir_hash)

            # All hashes must be identical
            assert len(set(hashes)) == 1, \
                f"Non-deterministic compilation for '{text[:30]}...': {hashes}"

    def test_determinism_verify_function(self):
        """
        E5 Test #2: Use verify_determinism helper.

        Wraps the same logic in a cleaner API.
        """
        for text in TEST_CASES:
            result = verify_determinism(text, n_runs=3)

            # Check determinism flag (may be False for expected errors)
            assert "is_deterministic" in result, "Missing determinism result"
            assert result["unique_hashes"] == 1, \
                f"Non-deterministic for '{text[:30]}...': {result}"


class TestKrisperCompilation:
    """Test KRISPER IR generation."""

    def test_basic_compress(self):
        """
        E5 Test #3: Basic compress generates valid IR.
        """
        result = compile_nl_to_ir('compress payload "hello" as r1')

        assert result["status"] == "success"
        assert "ir" in result
        assert "plan" in result["ir"]
        assert len(result["ir"]["plan"]) >= 1

        # First op should be compress
        first_op = result["ir"]["plan"][0]
        assert first_op["op"] == "compress"
        assert "hello" in first_op["in"]["payload"]

    def test_compress_with_attest(self):
        """
        E5 Test #4: Compress + attest generates 2 operations.
        """
        result = compile_nl_to_ir(
            'compress payload "test" as r1, then attest'
        )

        assert result["status"] == "success"
        assert result["n_operations"] >= 2

        ops = [op["op"] for op in result["ir"]["plan"]]
        assert "compress" in ops
        assert "attest" in ops

    def test_compare_requires_defined_vars(self):
        """
        E5 Test #5: Compare with undefined variables returns error.
        """
        result = compile_nl_to_ir('compare x with y')

        # Should error because x and y aren't defined
        assert result["status"] == "error" or \
               "UNDEFINED" in str(result.get("error", ""))

    def test_empty_payload_rejected(self):
        """
        E5 Test #6: Empty payload is rejected.
        """
        result = compile_nl_to_ir('compress payload "" as r1')

        # Should error for empty payload
        assert result["status"] == "error" or result["n_operations"] == 0


class TestKrisperReceipts:
    """Test receipt generation."""

    def test_receipt_has_required_fields(self):
        """
        E5 Test #7: Generated receipts have all required fields.
        """
        result = compile_nl_to_ir('compress payload "test" as r1')
        assert result["status"] == "success"

        receipt = generate_receipt(result["ir"], 'compress payload "test" as r1')

        # Check required fields
        assert receipt["receipt_type"] == "KRISPER_COMPILE"
        assert "timestamp" in receipt
        assert "input" in receipt
        assert "output" in receipt
        assert "verification" in receipt

        # Check nested fields
        assert "sha256" in receipt["input"]
        assert "sha256" in receipt["output"]
        assert "receipt_hash" in receipt["verification"]

    def test_receipt_input_hash_matches(self):
        """
        E5 Test #8: Receipt input hash matches actual input.
        """
        text = 'compress payload "verify me" as r1'
        result = compile_nl_to_ir(text)

        if result["status"] == "success":
            receipt = generate_receipt(result["ir"], text)

            expected_hash = sha256_str(text)
            assert receipt["input"]["sha256"] == expected_hash


class TestKrisperCrossProcess:
    """Test determinism across processes."""

    @pytest.mark.xfail(reason="Requires subprocess - may fail in some environments")
    def test_determinism_fresh_processes(self):
        """
        E5 Test #9: Same result across fresh Python processes.
        """
        text = 'compress payload "cross-process" as r1'
        hashes = []

        for _ in range(2):
            # Call the smoketest script
            result = subprocess.run(
                [
                    sys.executable, "-c",
                    f"""
import sys
sys.path.insert(0, '{HELIX_CDC}')
sys.path.insert(0, '{HELIX_CDC}/gitforest-repo/forest/krisper')
from tools.krisper_genome_smoketest import compile_nl_to_ir
import json
result = compile_nl_to_ir('{text}')
print(result.get('ir_hash', 'ERROR'))
"""
                ],
                capture_output=True,
                text=True,
                timeout=10,
            )

            ir_hash = result.stdout.strip()
            hashes.append(ir_hash)

        # Both processes should produce same hash
        assert len(set(hashes)) == 1, f"Cross-process non-determinism: {hashes}"


class TestCanonicalIR:
    """Test against canonical fixture."""

    def test_canonical_ir_structure(self):
        """
        E5 Test #10: Basic IR structure matches expected format.
        """
        result = compile_nl_to_ir('compress payload "canonical" as r1')

        if result["status"] == "success":
            ir = result["ir"]

            # Must have version
            assert "version" in ir
            assert ir["version"] == "0.1"

            # Must have plan
            assert "plan" in ir
            assert isinstance(ir["plan"], list)


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