"""Core integration tests for FIPS Phase C crypto wrapper.

Validates the integration of the FIPS crypto primitives through
real-world workflows and standards compliance. Focuses on gaps
not covered by the existing unit, integration, or extended tests:

- RFC/NIST known-answer test vectors (HMAC-SHA256, PBKDF2, SHA-256)
- FIPS success-path routing (OpenSSL backend mock succeeds, not just fails)
- Chained crypto composition (output-of-one → input-to-another)
- Secret rotation and key migration workflows
- Multi-node authenticated sync simulation
- Encryption version backward-compatibility matrix
- Error propagation through the full crypto stack
- ID generator distribution uniformity validation
"""

from __future__ import annotations

import base64
import hashlib
import hmac as _hmac
import json
import math
import os
import time
from collections import Counter
from unittest import mock

import pytest

from greymatter_core.security.fips import (
    _FIPS_MODE,
    _HAS_CRYPTO,
    fips_choice,
    fips_hmac_sha256,
    fips_pbkdf2,
    fips_random_bytes,
    fips_sha256,
    fips_token_bytes,
    fips_token_urlsafe,
    is_fips_mode,
)
from greymatter_core.security.tokens import (
    generate_node_token,
    verify_node_token,
    TokenStore,
)
from greymatter_core.security.encryption import (
    encrypt_sync_payload,
    decrypt_sync_payload,
    _derive_key,
    get_sqlcipher_key,
    MLKEM_AVAILABLE,
)
from greymatter_core.ids import (
    generate_pattern_id,
    generate_knowledge_id,
    generate_work_item_id,
    generate_session_id,
    generate_id,
    ALPHABET,
)
from greymatter_core.db import derive_key_from_password


# Shared test constants
SECRET = b"test-cluster-secret-for-fips-int"
ALT_SECRET = b"alternate-secret-for-fips-tests!"


# =========================================================================
# RFC / NIST known-answer test vectors
# =========================================================================

class TestRFCTestVectors:
    """Validate FIPS wrapper output against published standards.

    These tests ensure the wrapper doesn't silently alter cryptographic
    behavior during migration. If any of these fail, the wrapper is
    producing non-standard output.
    """

    # --- SHA-256 (FIPS 180-4 / NIST examples) ---

    def test_sha256_empty_string_nist_vector(self):
        """SHA-256('') = e3b0c44298fc1c149afbf4c8996fb924...

        Reference: FIPS 180-4, Section B.1 (empty message).
        """
        expected_hex = (
            "e3b0c44298fc1c149afbf4c8996fb924"
            "27ae41e4649b934ca495991b7852b855"
        )
        result = fips_sha256(b"")
        assert result.hex() == expected_hex

    def test_sha256_abc_nist_vector(self):
        """SHA-256('abc') = ba7816bf8f01cfea414140de5dae2223...

        Reference: FIPS 180-4, Section B.1, Example 1.
        """
        expected_hex = (
            "ba7816bf8f01cfea414140de5dae2223"
            "b00361a396177a9cb410ff61f20015ad"
        )
        result = fips_sha256(b"abc")
        assert result.hex() == expected_hex

    def test_sha256_448bit_nist_vector(self):
        """SHA-256('abcdbcde...nopq') — the 448-bit NIST test vector.

        Reference: FIPS 180-4, Section B.1, Example 2.
        """
        msg = b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
        expected_hex = (
            "248d6a61d20638b8e5c026930c3e6039"
            "a33ce45964ff2167f6ecedd419db06c1"
        )
        result = fips_sha256(msg)
        assert result.hex() == expected_hex

    # --- HMAC-SHA256 (RFC 4231) ---

    def test_hmac_sha256_rfc4231_test_case_1(self):
        """RFC 4231 Test Case 1: 20-byte key, 'Hi There'.

        Key  = 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b (20 bytes)
        Data = 'Hi There'
        HMAC = b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7
        """
        key = bytes.fromhex("0b" * 20)
        data = b"Hi There"
        expected = "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
        result = fips_hmac_sha256(key, data)
        assert result.hex() == expected

    def test_hmac_sha256_rfc4231_test_case_2(self):
        """RFC 4231 Test Case 2: 'Jefe' key, 'what do ya want...'.

        Key  = 'Jefe'
        Data = 'what do ya want for nothing?'
        HMAC = 5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843
        """
        key = b"Jefe"
        data = b"what do ya want for nothing?"
        expected = "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
        result = fips_hmac_sha256(key, data)
        assert result.hex() == expected

    def test_hmac_sha256_rfc4231_test_case_3(self):
        """RFC 4231 Test Case 3: 20-byte 0xaa key, 50-byte 0xdd data.

        Key  = aaaa...aa (20 bytes)
        Data = dddd...dd (50 bytes)
        HMAC = 773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe
        """
        key = bytes.fromhex("aa" * 20)
        data = bytes.fromhex("dd" * 50)
        expected = "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe"
        result = fips_hmac_sha256(key, data)
        assert result.hex() == expected

    def test_hmac_sha256_rfc4231_test_case_4(self):
        """RFC 4231 Test Case 4: incrementing key, incrementing data.

        Key  = 0102030405060708090a0b0c0d0e0f10111213141516171819 (25 bytes)
        Data = cdcdcd...cd (50 bytes)
        HMAC = 82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b
        """
        key = bytes(range(1, 26))
        data = bytes.fromhex("cd" * 50)
        expected = "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b"
        result = fips_hmac_sha256(key, data)
        assert result.hex() == expected

    def test_hmac_sha256_rfc4231_test_case_6(self):
        """RFC 4231 Test Case 6: 131-byte key (longer than block size).

        Key  = aaaa...aa (131 bytes)
        Data = 'Test Using Larger Than Block-Size Key - Hash Key First'
        HMAC = 60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54
        """
        key = bytes.fromhex("aa" * 131)
        data = b"Test Using Larger Than Block-Size Key - Hash Key First"
        expected = "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"
        result = fips_hmac_sha256(key, data)
        assert result.hex() == expected

    def test_hmac_sha256_rfc4231_test_case_7(self):
        """RFC 4231 Test Case 7: 131-byte key, longer data message.

        Key  = aaaa...aa (131 bytes)
        Data = 'This is a test u]sing a larger than block-size key and a
                larger than block-size data...'
        HMAC = 9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2
        """
        key = bytes.fromhex("aa" * 131)
        data = (
            b"This is a test using a larger than block-size key "
            b"and a larger than block-size data. The key needs to "
            b"be hashed before being used by the HMAC algorithm."
        )
        expected = "9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2"
        result = fips_hmac_sha256(key, data)
        assert result.hex() == expected

    # --- PBKDF2-HMAC-SHA256 (RFC 6070 / RFC 7914 vectors) ---

    def test_pbkdf2_rfc7914_vector_1(self):
        """PBKDF2-HMAC-SHA256 with 'passwd', 'salt', 1 iteration, dkLen=64.

        Reference: RFC 7914, Section 11 (test vector 1 for PBKDF2).
        Only the first 32 bytes are checked (our wrapper returns dklen bytes).
        """
        # RFC 7914 vector: password='passwd', salt='salt', c=1, dkLen=64
        # Full 64-byte output starts with:
        # 55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc
        # 49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783
        result = fips_pbkdf2(b"passwd", b"salt", iterations=1, dklen=64)
        expected_hex = (
            "55ac046e56e3089fec1691c22544b605"
            "f94185216dde0465e68b9d57c20dacbc"
            "49ca9cccf179b645991664b39d77ef31"
            "7c71b845b1e30bd509112041d3a19783"
        )
        assert result.hex() == expected_hex

    def test_pbkdf2_rfc7914_vector_2(self):
        """PBKDF2-HMAC-SHA256 with 'Password', 'NaCl', 80000 iterations, dkLen=64.

        Reference: RFC 7914, Section 11 (test vector 2).
        """
        result = fips_pbkdf2(b"Password", b"NaCl", iterations=80000, dklen=64)
        expected_hex = (
            "4ddcd8f60b98be21830cee5ef22701f9"
            "641a4418d04c0414aeff08876b34ab56"
            "a1d425a1225833549adb841b51c9b317"
            "6a272bdebba1d078478f62b397f33c8d"
        )
        assert result.hex() == expected_hex


# =========================================================================
# FIPS success-path routing (OpenSSL backend succeeds)
# =========================================================================

class TestFipsSuccessPathRouting:
    """Verify that when FIPS mode is active and OpenSSL succeeds,
    the wrapper uses the OpenSSL path (not stdlib).

    The extended tests mock OpenSSL *failures*; these tests mock
    OpenSSL *successes* to verify routing correctness.
    """

    def test_random_bytes_routes_through_openssl_when_fips_active(self):
        """When FIPS mode is active, fips_random_bytes should call RAND_bytes."""
        mock_backend = mock.MagicMock()
        sentinel = b"\xaa" * 32

        def fake_rand_bytes(buf, n):
            # Write sentinel bytes into the CFFI buffer
            return 1  # success

        mock_backend._lib.RAND_bytes.side_effect = fake_rand_bytes
        mock_backend._ffi.new.return_value = mock.MagicMock()
        mock_backend._ffi.buffer.return_value = sentinel

        with mock.patch.multiple("greymatter_core.security.fips", **{
            "_FIPS_MODE": True,
            "_HAS_CRYPTO": True,
            "_ossl_backend": mock_backend,
        }):
            result = fips_random_bytes(32)
            # Verify OpenSSL was actually called
            mock_backend._lib.RAND_bytes.assert_called_once()
            assert isinstance(result, bytes)
            assert len(result) == 32

    def test_sha256_routes_through_crypto_when_fips_active(self):
        """When FIPS mode is active, fips_sha256 should use cryptography.Hash."""
        mock_hashes = mock.MagicMock()
        mock_hash_instance = mock.MagicMock()
        mock_hashes.Hash.return_value = mock_hash_instance
        expected = hashlib.sha256(b"test").digest()
        mock_hash_instance.finalize.return_value = expected

        with mock.patch.multiple("greymatter_core.security.fips", **{
            "_FIPS_MODE": True,
            "_HAS_CRYPTO": True,
            "_crypto_hashes": mock_hashes,
        }):
            result = fips_sha256(b"test")
            mock_hashes.Hash.assert_called_once()
            mock_hash_instance.update.assert_called_once_with(b"test")
            mock_hash_instance.finalize.assert_called_once()
            assert result == expected

    def test_hmac_routes_through_crypto_when_fips_active(self):
        """When FIPS mode is active, fips_hmac_sha256 should use cryptography.HMAC."""
        expected = _hmac.new(b"key", b"data", hashlib.sha256).digest()
        mock_hmac_instance = mock.MagicMock()
        mock_hmac_instance.finalize.return_value = expected
        mock_hmac_cls = mock.MagicMock(return_value=mock_hmac_instance)
        mock_hashes = mock.MagicMock()

        with mock.patch.multiple("greymatter_core.security.fips", **{
            "_FIPS_MODE": True,
            "_HAS_CRYPTO": True,
            "_CryptoHMAC": mock_hmac_cls,
            "_crypto_hashes": mock_hashes,
        }):
            result = fips_hmac_sha256(b"key", b"data")
            mock_hmac_cls.assert_called_once()
            mock_hmac_instance.update.assert_called_once_with(b"data")
            mock_hmac_instance.finalize.assert_called_once()
            assert result == expected

    def test_pbkdf2_routes_through_crypto_when_fips_active(self):
        """When FIPS mode is active, fips_pbkdf2 should use cryptography.PBKDF2HMAC."""
        expected = hashlib.pbkdf2_hmac("sha256", b"pw", b"salt", 1000, dklen=32)
        mock_kdf_instance = mock.MagicMock()
        mock_kdf_instance.derive.return_value = expected
        mock_pbkdf2_cls = mock.MagicMock(return_value=mock_kdf_instance)
        mock_hashes = mock.MagicMock()

        with mock.patch.multiple("greymatter_core.security.fips", **{
            "_FIPS_MODE": True,
            "_HAS_CRYPTO": True,
            "_PBKDF2HMAC": mock_pbkdf2_cls,
            "_crypto_hashes": mock_hashes,
        }):
            result = fips_pbkdf2(b"pw", b"salt", iterations=1000, dklen=32)
            mock_pbkdf2_cls.assert_called_once()
            mock_kdf_instance.derive.assert_called_once_with(b"pw")
            assert result == expected

    def test_stdlib_path_not_called_when_openssl_succeeds(self):
        """When FIPS OpenSSL path succeeds, os.urandom should NOT be called."""
        mock_backend = mock.MagicMock()
        mock_backend._lib.RAND_bytes.return_value = 1
        mock_backend._ffi.new.return_value = mock.MagicMock()
        mock_backend._ffi.buffer.return_value = b"\x00" * 32

        with mock.patch.multiple("greymatter_core.security.fips", **{
            "_FIPS_MODE": True,
            "_HAS_CRYPTO": True,
            "_ossl_backend": mock_backend,
        }):
            with mock.patch("os.urandom") as mock_urandom:
                fips_random_bytes(32)
                mock_urandom.assert_not_called()


# =========================================================================
# Chained crypto composition
# =========================================================================

class TestChainedCryptoComposition:
    """Tests using the output of one FIPS function as input to another.

    This validates that the wrapper functions compose correctly — a
    critical property for real key derivation and signing pipelines.
    """

    def test_pbkdf2_output_as_hmac_key(self):
        """Derive a key via PBKDF2, then use it for HMAC signing."""
        derived_key = fips_pbkdf2(b"master-secret", b"hmac-key-salt", iterations=1000)
        assert len(derived_key) == 32

        mac = fips_hmac_sha256(derived_key, b"message to sign")
        assert len(mac) == 32

        # Verify determinism: same key + message → same MAC
        mac2 = fips_hmac_sha256(derived_key, b"message to sign")
        assert mac == mac2

        # Different message → different MAC
        mac3 = fips_hmac_sha256(derived_key, b"different message")
        assert mac != mac3

    def test_sha256_then_hmac_composition(self):
        """Hash a document, then HMAC-sign the hash."""
        document = b"This is the document content to sign."
        doc_hash = fips_sha256(document)
        assert len(doc_hash) == 32

        signing_key = fips_random_bytes(32)
        signature = fips_hmac_sha256(signing_key, doc_hash)
        assert len(signature) == 32

        # Verify: re-hash + re-sign produces same signature
        doc_hash_again = fips_sha256(document)
        signature_again = fips_hmac_sha256(signing_key, doc_hash_again)
        assert signature == signature_again

    def test_random_bytes_as_pbkdf2_salt(self):
        """Generate a random salt, then use it for key derivation."""
        salt = fips_random_bytes(16)
        assert len(salt) == 16

        key = fips_pbkdf2(b"my-password", salt, iterations=1000)
        assert len(key) == 32

        # Same salt → same key (deterministic KDF)
        key2 = fips_pbkdf2(b"my-password", salt, iterations=1000)
        assert key == key2

        # Different salt → different key
        other_salt = fips_random_bytes(16)
        key3 = fips_pbkdf2(b"my-password", other_salt, iterations=1000)
        assert key != key3

    def test_token_urlsafe_as_hmac_data(self):
        """Use a URL-safe token as HMAC-signed data (e.g., signed download link)."""
        token = fips_token_urlsafe(32)
        signing_key = fips_pbkdf2(b"link-signer", b"download-salt", iterations=1000)

        signature = fips_hmac_sha256(signing_key, token.encode())
        signed_url = f"https://example.com/dl?t={token}&sig={signature.hex()}"

        # Verify: extract token and signature, re-compute and compare
        extracted_token = token
        extracted_sig = signature.hex()
        recomputed = fips_hmac_sha256(signing_key, extracted_token.encode()).hex()
        assert _hmac.compare_digest(extracted_sig, recomputed)

    def test_multi_step_key_derivation_chain(self):
        """Derive key → derive sub-key → sign → hash the signature.

        Models a realistic hierarchical key derivation pattern.
        """
        # Master key derivation
        master_key = fips_pbkdf2(b"root-secret", b"master-salt", iterations=10000)
        assert len(master_key) == 32

        # Sub-key derivation (uses master key as password)
        sub_key = fips_pbkdf2(master_key, b"sub-key-salt", iterations=1000)
        assert len(sub_key) == 32
        assert sub_key != master_key

        # Sign with sub-key
        signature = fips_hmac_sha256(sub_key, b"protected content")
        assert len(signature) == 32

        # Hash the signature (e.g., for compact storage)
        sig_hash = fips_sha256(signature)
        assert len(sig_hash) == 32

        # Entire chain is deterministic
        master2 = fips_pbkdf2(b"root-secret", b"master-salt", iterations=10000)
        sub2 = fips_pbkdf2(master2, b"sub-key-salt", iterations=1000)
        sig2 = fips_hmac_sha256(sub2, b"protected content")
        hash2 = fips_sha256(sig2)
        assert hash2 == sig_hash


# =========================================================================
# Secret rotation and key migration
# =========================================================================

class TestSecretRotationWorkflow:
    """Tests the critical operational pattern of rotating cluster secrets.

    In production, secrets must be rotated periodically. Data encrypted
    with the old secret must remain decryptable during migration.
    """

    OLD_SECRET = b"old-cluster-secret-rotate-test!!"
    NEW_SECRET = b"new-cluster-secret-rotate-test!!"

    def test_encrypted_with_old_secret_not_decryptable_with_new(self):
        """After rotation, old-secret payloads cannot be decrypted with new secret."""
        payload = {"data": "encrypted before rotation"}
        encrypted = encrypt_sync_payload(payload, secret=self.OLD_SECRET)
        with pytest.raises(ValueError):
            decrypt_sync_payload(encrypted, secret=self.NEW_SECRET)

    def test_re_encrypt_migration_workflow(self):
        """Decrypt with old secret → re-encrypt with new secret → verify.

        This is the standard migration pattern.
        """
        original = {"table": "patterns", "migrating": True}

        # Encrypt with old secret
        encrypted_old = encrypt_sync_payload(original, secret=self.OLD_SECRET)

        # Migration: decrypt with old, re-encrypt with new
        decrypted = decrypt_sync_payload(encrypted_old, secret=self.OLD_SECRET)
        assert decrypted == original

        encrypted_new = encrypt_sync_payload(decrypted, secret=self.NEW_SECRET)

        # Verify new encryption works
        result = decrypt_sync_payload(encrypted_new, secret=self.NEW_SECRET)
        assert result == original

        # Old encrypted payload still works with old secret
        result_old = decrypt_sync_payload(encrypted_old, secret=self.OLD_SECRET)
        assert result_old == original

    def test_token_rotation_with_new_secret(self):
        """Tokens generated with old secret fail with new secret."""
        old_token = generate_node_token("node-1", secret=self.OLD_SECRET)

        # Old secret verifies
        valid, nid = verify_node_token(old_token, secret=self.OLD_SECRET)
        assert valid is True
        assert nid == "node-1"

        # New secret rejects
        valid, nid = verify_node_token(old_token, secret=self.NEW_SECRET)
        assert valid is False

        # Generate new token with new secret
        new_token = generate_node_token("node-1", secret=self.NEW_SECRET)
        valid, nid = verify_node_token(new_token, secret=self.NEW_SECRET)
        assert valid is True
        assert nid == "node-1"

    def test_sqlcipher_key_changes_with_secret_rotation(self):
        """SQLCipher keys derived from different secrets are different."""
        key_old = get_sqlcipher_key("node-1", secret=self.OLD_SECRET)
        key_new = get_sqlcipher_key("node-1", secret=self.NEW_SECRET)
        assert key_old != key_new

        # Same secret + same node → same key (deterministic)
        key_old2 = get_sqlcipher_key("node-1", secret=self.OLD_SECRET)
        assert key_old == key_old2

    def test_batch_migration_of_multiple_payloads(self):
        """Migrate a batch of payloads from old to new secret."""
        payloads = [{"id": i, "data": f"record-{i}"} for i in range(20)]
        encrypted_old = [encrypt_sync_payload(p, secret=self.OLD_SECRET) for p in payloads]

        # Migrate all
        migrated = []
        for enc in encrypted_old:
            decrypted = decrypt_sync_payload(enc, secret=self.OLD_SECRET)
            re_encrypted = encrypt_sync_payload(decrypted, secret=self.NEW_SECRET)
            migrated.append(re_encrypted)

        # Verify all migrated payloads
        for i, enc in enumerate(migrated):
            result = decrypt_sync_payload(enc, secret=self.NEW_SECRET)
            assert result == payloads[i], f"Mismatch at payload {i}"

    def test_token_store_rotation_workflow(self, tmp_path):
        """TokenStore rotation: issue → rotate → verify old and new."""
        store = TokenStore(str(tmp_path / "rotation.db"))

        # Issue initial token
        t1 = store.issue("rotate-node")
        nodes = store.list_nodes()
        assert len(nodes) == 1
        assert nodes[0]["rotated_at"] is None

        # Rotate (issue new token for same node)
        t2 = store.issue("rotate-node")
        assert t1 != t2
        nodes = store.list_nodes()
        assert len(nodes) == 1
        assert nodes[0]["rotated_at"] is not None

        # Rotate again
        t3 = store.issue("rotate-node")
        assert t3 != t2
        assert t3 != t1

        store.close()


# =========================================================================
# Multi-node authenticated sync simulation
# =========================================================================

class TestMultiNodeSyncSimulation:
    """Simulates a realistic multi-node cluster exchanging authenticated
    encrypted data through the full FIPS crypto stack."""

    CLUSTER_SECRET = b"multi-node-cluster-secret-32b!!"

    def test_three_node_mesh_sync(self):
        """Three nodes register, each generates tokens, then exchange encrypted data."""
        nodes = ["alpha", "beta", "gamma"]

        # Step 1: Each node registers
        tokens = {}
        for node in nodes:
            tokens[node] = generate_node_token(node, secret=self.CLUSTER_SECRET)

        # Step 2: Verify all tokens
        for node in nodes:
            valid, nid = verify_node_token(tokens[node], secret=self.CLUSTER_SECRET)
            assert valid, f"Token for {node} should be valid"
            assert nid == node

        # Step 3: Each node encrypts different data
        payloads = {
            "alpha": {"table": "observations", "rows": [{"id": "obs-a1"}]},
            "beta":  {"table": "patterns", "rows": [{"id": "pat-b1"}]},
            "gamma": {"table": "knowledge", "rows": [{"id": "kn-g1"}]},
        }
        encrypted = {}
        for node in nodes:
            encrypted[node] = encrypt_sync_payload(
                payloads[node], secret=self.CLUSTER_SECRET
            )

        # Step 4: Simulate JSON serialization (network transit)
        in_transit = {k: json.loads(json.dumps(v)) for k, v in encrypted.items()}

        # Step 5: Each node can decrypt any other node's payload
        for sender in nodes:
            for receiver in nodes:
                decrypted = decrypt_sync_payload(
                    in_transit[sender], secret=self.CLUSTER_SECRET
                )
                assert decrypted == payloads[sender], \
                    f"{receiver} failed to decrypt {sender}'s payload"

    def test_node_generates_unique_ids_from_fips_rng(self):
        """Multiple nodes generating IDs concurrently should never collide."""
        # Simulate 5 nodes each generating 100 IDs
        all_ids = set()
        for node_idx in range(5):
            for _ in range(100):
                all_ids.add(generate_pattern_id())
        assert len(all_ids) == 500, "ID collision detected across simulated nodes"

    def test_node_isolation_different_sqlcipher_keys(self):
        """Each node gets a unique SQLCipher key from the same cluster secret."""
        nodes = ["node-a", "node-b", "node-c", "node-d"]
        keys = {n: get_sqlcipher_key(n, secret=self.CLUSTER_SECRET) for n in nodes}

        # All keys are different
        assert len(set(keys.values())) == len(nodes)

        # Each key is 64-char hex (32 bytes)
        for key in keys.values():
            assert len(key) == 64
            bytes.fromhex(key)  # validates hex format

    def test_cross_node_sync_with_derived_keys(self):
        """Two nodes derive the same sync key from the shared secret,
        enabling encrypt-on-one, decrypt-on-other."""
        node_a_key = _derive_key(self.CLUSTER_SECRET, b"gm-sync-payload", length=32)
        node_b_key = _derive_key(self.CLUSTER_SECRET, b"gm-sync-payload", length=32)
        assert node_a_key == node_b_key, "Nodes must derive identical sync keys"


# =========================================================================
# Encryption version backward-compatibility matrix
# =========================================================================

class TestEncryptionVersionCompatibility:
    """Validate that different encryption versions interoperate correctly
    and that version boundaries are enforced."""

    SECRET = b"version-compat-test-secret-32b!!"

    def test_v1_payload_structure_is_complete(self):
        """v1 encrypted payloads have all required fields with correct types."""
        enc = encrypt_sync_payload({"test": True}, secret=self.SECRET)
        assert enc["v"] == 1
        assert isinstance(enc["ct"], str)
        assert isinstance(enc["nonce"], str)
        assert isinstance(enc["hmac"], str)
        # ct and nonce are base64
        base64.b64decode(enc["ct"])
        base64.b64decode(enc["nonce"])
        # hmac is 64-char hex (32 bytes)
        assert len(enc["hmac"]) == 64
        bytes.fromhex(enc["hmac"])

    def test_v1_different_payloads_produce_different_ciphertexts(self):
        """Same secret but different plaintext → different ciphertext."""
        enc1 = encrypt_sync_payload({"a": 1}, secret=self.SECRET)
        enc2 = encrypt_sync_payload({"b": 2}, secret=self.SECRET)
        assert enc1["ct"] != enc2["ct"]
        assert enc1["nonce"] != enc2["nonce"]
        assert enc1["hmac"] != enc2["hmac"]

    def test_v1_same_payload_produces_different_ciphertexts(self):
        """Same plaintext encrypted twice → different ciphertext (random nonce)."""
        payload = {"same": "data"}
        enc1 = encrypt_sync_payload(payload, secret=self.SECRET)
        enc2 = encrypt_sync_payload(payload, secret=self.SECRET)
        assert enc1["ct"] != enc2["ct"]
        assert enc1["nonce"] != enc2["nonce"]
        # But both decrypt to the same payload
        assert decrypt_sync_payload(enc1, secret=self.SECRET) == payload
        assert decrypt_sync_payload(enc2, secret=self.SECRET) == payload

    def test_version_field_preserved_through_serialization(self):
        """Version field survives JSON serialization (int, not string)."""
        enc = encrypt_sync_payload({"x": 1}, secret=self.SECRET)
        serialized = json.dumps(enc)
        deserialized = json.loads(serialized)
        assert deserialized["v"] == 1
        assert isinstance(deserialized["v"], int)
        # Decrypt from deserialized
        result = decrypt_sync_payload(deserialized, secret=self.SECRET)
        assert result == {"x": 1}

    def test_future_version_rejected_with_clear_error(self):
        """Attempting to decrypt a payload with a future version raises ValueError."""
        for future_version in [3, 10, 100, 999]:
            with pytest.raises(ValueError, match="Unknown encryption version"):
                decrypt_sync_payload(
                    {"v": future_version, "ct": "", "hmac": ""},
                    secret=self.SECRET,
                )

    @pytest.mark.skipif(not MLKEM_AVAILABLE, reason="kyber-py not installed")
    def test_v2_not_decryptable_as_v1(self):
        """A v2 payload cannot be decrypted as v1 (version enforced)."""
        from greymatter_core.security.encryption import generate_mlkem_keypair
        ek, dk = generate_mlkem_keypair()
        enc = encrypt_sync_payload({"pq": True}, secret=self.SECRET, peer_ek=ek)
        assert enc["v"] == 2
        # Force version to 1 to simulate downgrade attack
        downgraded = dict(enc)
        downgraded["v"] = 1
        with pytest.raises(Exception):
            decrypt_sync_payload(downgraded, secret=self.SECRET)


# =========================================================================
# Error propagation through the full stack
# =========================================================================

class TestErrorPropagation:
    """Verify that errors propagate correctly through the multi-layer
    crypto stack, not silently swallowed or misattributed."""

    SECRET = b"error-propagation-test-secret!!!"

    def test_tampered_token_in_full_auth_flow(self):
        """A tampered token should fail verification even in a full workflow."""
        # Generate valid token
        token = generate_node_token("legit-node", secret=self.SECRET)
        valid, _ = verify_node_token(token, secret=self.SECRET)
        assert valid is True

        # Tamper with each part and verify rejection
        parts = token.split(".")
        assert len(parts) == 4

        # Tamper node_id
        tampered = f"evil-node.{parts[1]}.{parts[2]}.{parts[3]}"
        assert verify_node_token(tampered, secret=self.SECRET) == (False, "")

        # Tamper timestamp
        tampered = f"{parts[0]}.0.{parts[2]}.{parts[3]}"
        assert verify_node_token(tampered, secret=self.SECRET) == (False, "")

        # Tamper nonce
        tampered = f"{parts[0]}.{parts[1]}.{'00' * 8}.{parts[3]}"
        assert verify_node_token(tampered, secret=self.SECRET) == (False, "")

        # Tamper signature
        tampered = f"{parts[0]}.{parts[1]}.{parts[2]}.{'ff' * 32}"
        assert verify_node_token(tampered, secret=self.SECRET) == (False, "")

    def test_double_encryption_produces_valid_nested_payload(self):
        """Encrypting an already-encrypted payload works (defense-in-depth)."""
        inner_payload = {"sensitive": "data"}
        inner_enc = encrypt_sync_payload(inner_payload, secret=self.SECRET)

        # Encrypt the encrypted dict again (e.g., for transport wrapping)
        outer_enc = encrypt_sync_payload(inner_enc, secret=self.SECRET)

        # Decrypt outer layer
        inner_recovered = decrypt_sync_payload(outer_enc, secret=self.SECRET)
        assert inner_recovered == inner_enc

        # Decrypt inner layer
        final = decrypt_sync_payload(inner_recovered, secret=self.SECRET)
        assert final == inner_payload

    def test_corrupted_base64_ciphertext_raises(self):
        """Corrupted base64 in ciphertext triggers a clear error."""
        enc = encrypt_sync_payload({"x": 1}, secret=self.SECRET)
        enc["ct"] = "!!!invalid-base64==="
        with pytest.raises(Exception):
            decrypt_sync_payload(enc, secret=self.SECRET)

    def test_truncated_hmac_rejected(self):
        """A truncated HMAC should fail verification."""
        enc = encrypt_sync_payload({"x": 1}, secret=self.SECRET)
        enc["hmac"] = enc["hmac"][:32]  # Truncate to 16 bytes (32 hex chars)
        with pytest.raises(ValueError, match="HMAC verification failed"):
            decrypt_sync_payload(enc, secret=self.SECRET)

    def test_swapped_nonce_between_payloads_rejected(self):
        """Swapping nonces between two payloads should fail HMAC check."""
        enc1 = encrypt_sync_payload({"id": 1}, secret=self.SECRET)
        enc2 = encrypt_sync_payload({"id": 2}, secret=self.SECRET)

        # Swap nonces
        enc1["nonce"], enc2["nonce"] = enc2["nonce"], enc1["nonce"]

        with pytest.raises(ValueError, match="HMAC verification failed"):
            decrypt_sync_payload(enc1, secret=self.SECRET)
        with pytest.raises(ValueError, match="HMAC verification failed"):
            decrypt_sync_payload(enc2, secret=self.SECRET)

    def test_hmac_covers_all_fields(self):
        """Modifying any encrypted field independently should break HMAC."""
        enc = encrypt_sync_payload({"data": "test"}, secret=self.SECRET)
        original_hmac = enc["hmac"]

        # Modify ct only → HMAC mismatch
        enc_mod_ct = dict(enc)
        ct_bytes = base64.b64decode(enc_mod_ct["ct"])
        enc_mod_ct["ct"] = base64.b64encode(ct_bytes[::-1]).decode()
        with pytest.raises(ValueError, match="HMAC verification failed"):
            decrypt_sync_payload(enc_mod_ct, secret=self.SECRET)

        # Modify nonce only → HMAC mismatch
        enc_mod_nonce = dict(enc)
        nonce_bytes = base64.b64decode(enc_mod_nonce["nonce"])
        enc_mod_nonce["nonce"] = base64.b64encode(bytes(b ^ 0xFF for b in nonce_bytes)).decode()
        with pytest.raises(ValueError, match="HMAC verification failed"):
            decrypt_sync_payload(enc_mod_nonce, secret=self.SECRET)


# =========================================================================
# ID generator distribution uniformity
# =========================================================================

class TestIDGeneratorDistribution:
    """Validates that the FIPS-backed ID generator produces uniform
    character distribution, which is critical for collision resistance.

    Uses a chi-squared goodness-of-fit test to detect bias.
    """

    def test_character_distribution_chi_squared(self):
        """Chi-squared test: character frequencies should not deviate
        significantly from uniform over 50,000 samples.

        H0: characters are uniformly distributed over ALPHABET.
        Reject if p < 0.001 (very conservative threshold).
        """
        n_ids = 5000
        id_suffix_len = 8
        total_chars = n_ids * id_suffix_len  # 40,000 characters

        counts = Counter()
        for _ in range(n_ids):
            pid = generate_pattern_id()
            for c in pid[4:]:  # skip "pat-" prefix
                counts[c] += 1

        # Verify we got the expected total
        assert sum(counts.values()) == total_chars

        # Chi-squared test
        expected_per_char = total_chars / len(ALPHABET)
        chi_squared = sum(
            (counts.get(c, 0) - expected_per_char) ** 2 / expected_per_char
            for c in ALPHABET
        )

        # Degrees of freedom = len(ALPHABET) - 1 = 35
        # Critical value for p=0.001, df=35 is ~66.6
        # We use a generous threshold to avoid flaky tests
        critical_value = 80.0  # well above chi-squared(35, 0.001)
        assert chi_squared < critical_value, (
            f"Character distribution is non-uniform: chi²={chi_squared:.1f} > {critical_value}. "
            f"Counts: {dict(sorted(counts.items()))}"
        )

    def test_all_id_generators_use_same_alphabet(self):
        """All ID generators should produce characters from ALPHABET."""
        generators = [
            ("pat-", generate_pattern_id),
            ("kn-", generate_knowledge_id),
            ("wi-", generate_work_item_id),
        ]
        for prefix, gen_fn in generators:
            for _ in range(100):
                gid = gen_fn()
                suffix = gid[len(prefix):]
                assert all(c in ALPHABET for c in suffix), \
                    f"ID '{gid}' contains char outside ALPHABET"

    def test_session_id_date_prefix_format(self):
        """Session IDs have date prefix matching YYYY-MM-DD format."""
        for _ in range(10):
            sid = generate_session_id()
            # Format: YYYY-MM-DD-xxxxxxxx
            parts = sid.rsplit("-", 1)
            assert len(parts) == 2
            date_part = parts[0]
            suffix = parts[1]
            # Validate date format
            assert len(date_part) == 10  # YYYY-MM-DD
            assert len(suffix) == 8
            assert all(c in ALPHABET for c in suffix)

    def test_generic_id_prefixes_do_not_collide(self):
        """IDs from different generators never collide, even ignoring prefixes."""
        pat_ids = {generate_pattern_id() for _ in range(500)}
        kn_ids = {generate_knowledge_id() for _ in range(500)}
        wi_ids = {generate_work_item_id() for _ in range(500)}

        # Different prefixes guarantee no collision
        all_ids = pat_ids | kn_ids | wi_ids
        assert len(all_ids) == 1500


# =========================================================================
# Token expiry boundary conditions
# =========================================================================

class TestTokenExpiryBoundaries:
    """Tests token expiry at exact boundary conditions using mocked time."""

    SECRET = b"token-expiry-boundary-test-key!!"

    def test_token_valid_just_before_expiry(self):
        """Token should be valid 1 second before max_age."""
        with mock.patch("greymatter_core.security.tokens.time") as mock_time:
            # Generation time
            mock_time.time.return_value = 1000.0
            token = generate_node_token("node-1", secret=self.SECRET)

            # Verify at max_age - 1
            mock_time.time.return_value = 1000.0 + 3599  # max_age=3600, 1s remaining
            valid, nid = verify_node_token(token, secret=self.SECRET, max_age=3600)
            assert valid is True
            assert nid == "node-1"

    def test_token_invalid_just_after_expiry(self):
        """Token should be invalid 1 second after max_age."""
        with mock.patch("greymatter_core.security.tokens.time") as mock_time:
            # Generation time
            mock_time.time.return_value = 1000.0
            token = generate_node_token("node-1", secret=self.SECRET)

            # Verify at max_age + 1
            mock_time.time.return_value = 1000.0 + 3601  # max_age=3600, 1s past
            valid, _ = verify_node_token(token, secret=self.SECRET, max_age=3600)
            assert valid is False

    def test_token_invalid_at_exact_expiry(self):
        """Token at exactly max_age seconds should be invalid (> not >=)."""
        with mock.patch("greymatter_core.security.tokens.time") as mock_time:
            mock_time.time.return_value = 1000.0
            token = generate_node_token("node-1", secret=self.SECRET)

            # The check is: time.time() - token_time > max_age
            # At exactly max_age, time.time() - 1000 = 3600, which is NOT > 3600
            # So the token should still be valid at exact boundary
            mock_time.time.return_value = 1000.0 + 3600
            valid, nid = verify_node_token(token, secret=self.SECRET, max_age=3600)
            assert valid is True  # boundary is NOT exceeded (> check, not >=)

    def test_token_with_zero_max_age(self):
        """max_age=0 means token expires immediately after creation."""
        token = generate_node_token("node-1", secret=self.SECRET)
        # Even a few milliseconds later, the token should be expired
        valid, _ = verify_node_token(token, secret=self.SECRET, max_age=0)
        assert valid is False

    def test_token_with_very_large_max_age(self):
        """Token with max_age=10 years should still be valid."""
        token = generate_node_token("long-lived", secret=self.SECRET)
        ten_years_seconds = 10 * 365 * 24 * 3600
        valid, nid = verify_node_token(
            token, secret=self.SECRET, max_age=ten_years_seconds
        )
        assert valid is True
        assert nid == "long-lived"


# =========================================================================
# FIPS wrapper API contract
# =========================================================================

class TestFipsAPIContract:
    """Formal contract tests for the FIPS wrapper public API.

    These tests validate the function signatures, return types, and
    error behaviors as a stable contract — ensuring that any refactor
    or backend change doesn't break consumers.
    """

    def test_fips_random_bytes_contract(self):
        """fips_random_bytes(n) → bytes of length n, for all valid n."""
        for n in [0, 1, 16, 32, 64, 256, 1024]:
            result = fips_random_bytes(n)
            assert isinstance(result, bytes)
            assert len(result) == n

    def test_fips_sha256_contract(self):
        """fips_sha256(data) → 32-byte digest for any bytes input."""
        inputs = [b"", b"\x00", b"test", b"\xff" * 1000, os.urandom(10000)]
        for data in inputs:
            result = fips_sha256(data)
            assert isinstance(result, bytes)
            assert len(result) == 32

    def test_fips_hmac_sha256_contract(self):
        """fips_hmac_sha256(key, data) → 32-byte MAC for any bytes inputs."""
        cases = [
            (b"", b""),
            (b"key", b"data"),
            (b"k" * 128, b"d" * 10000),
        ]
        for key, data in cases:
            result = fips_hmac_sha256(key, data)
            assert isinstance(result, bytes)
            assert len(result) == 32

    def test_fips_pbkdf2_contract(self):
        """fips_pbkdf2(pw, salt, iters, dklen) → bytes of length dklen."""
        for dklen in [1, 16, 32, 64, 128]:
            result = fips_pbkdf2(b"pw", b"salt", iterations=1, dklen=dklen)
            assert isinstance(result, bytes)
            assert len(result) == dklen

    def test_fips_token_urlsafe_contract(self):
        """fips_token_urlsafe(n) → str, URL-safe, no padding."""
        for nbytes in [8, 16, 32, 64]:
            result = fips_token_urlsafe(nbytes)
            assert isinstance(result, str)
            assert len(result) > 0
            # URL-safe base64 alphabet only
            for char in result:
                assert char in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"
            # No padding
            assert "=" not in result

    def test_fips_token_bytes_contract(self):
        """fips_token_bytes(n) → bytes of length n."""
        for n in [8, 16, 32, 64]:
            result = fips_token_bytes(n)
            assert isinstance(result, bytes)
            assert len(result) == n

    def test_fips_choice_contract(self):
        """fips_choice(seq) → element from seq; raises IndexError if empty."""
        # Non-empty sequences
        assert fips_choice([1]) == 1
        assert fips_choice("X") == "X"
        assert fips_choice(list(range(100))) in range(100)
        assert fips_choice(tuple("abc")) in ("a", "b", "c")

        # Empty sequence raises IndexError
        with pytest.raises(IndexError, match="empty"):
            fips_choice([])
        with pytest.raises(IndexError, match="empty"):
            fips_choice("")

    def test_is_fips_mode_contract(self):
        """is_fips_mode() → bool, consistent with internal flags."""
        result = is_fips_mode()
        assert isinstance(result, bool)
        assert result == (_FIPS_MODE and _HAS_CRYPTO)

    def test_all_fips_functions_are_importable_from_package(self):
        """All 8 FIPS functions + is_fips_mode are in security.__all__."""
        from greymatter_core.security import __all__ as sec_all
        expected = [
            "fips_random_bytes", "fips_sha256", "fips_hmac_sha256",
            "fips_pbkdf2", "fips_token_urlsafe", "fips_token_bytes",
            "fips_choice", "is_fips_mode",
        ]
        for name in expected:
            assert name in sec_all, f"{name} missing from security.__all__"

    def test_fips_functions_are_referentially_identical(self):
        """Direct import and package import reference the same function objects."""
        import greymatter_core.security as sec
        import greymatter_core.security.fips as fips_mod

        assert sec.fips_random_bytes is fips_mod.fips_random_bytes
        assert sec.fips_sha256 is fips_mod.fips_sha256
        assert sec.fips_hmac_sha256 is fips_mod.fips_hmac_sha256
        assert sec.fips_pbkdf2 is fips_mod.fips_pbkdf2
        assert sec.fips_token_urlsafe is fips_mod.fips_token_urlsafe
        assert sec.fips_token_bytes is fips_mod.fips_token_bytes
        assert sec.fips_choice is fips_mod.fips_choice
        assert sec.is_fips_mode is fips_mod.is_fips_mode
