"""Integration tests for FIPS-aware crypto wrapper (Phase C).

Tests the FIPS crypto primitives end-to-end across the full security stack:
- Token generation ↔ verification roundtrips
- Sync payload encryption ↔ decryption (v0, v1, v2)
- Database key derivation determinism
- ID generation collision resistance
- Cross-module import consistency
- FIPS mode toggling and fallback behavior
- Error cases: tampering, wrong keys, corrupted payloads
- Edge cases: empty inputs, large payloads, concurrent operations
- Dependent module fallback patterns (teams/auth, beta/signing)
"""

from __future__ import annotations

import base64
import concurrent.futures
import hashlib
import hmac as _hmac
import json
import os
import re
import string
import threading
import time
from collections import Counter
from unittest import mock

import pytest

from greymatter_core.security.fips import (
    fips_choice,
    fips_hmac_sha256,
    fips_pbkdf2,
    fips_random_bytes,
    fips_sha256,
    fips_token_bytes,
    fips_token_urlsafe,
    is_fips_mode,
    _FIPS_MODE,
    _HAS_CRYPTO,
)
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!"


# =========================================================================
# Token generation ↔ verification end-to-end
# =========================================================================

class TestTokenRoundTrip:
    """End-to-end: generate token → transmit → verify at receiver."""

    def test_generate_and_verify_happy_path(self):
        token = generate_node_token("node-alpha", secret=SECRET)
        valid, node_id = verify_node_token(token, secret=SECRET)
        assert valid is True
        assert node_id == "node-alpha"

    def test_multiple_nodes_unique_tokens(self):
        """Each node gets a distinct token even with the same secret."""
        tokens = [generate_node_token(f"node-{i}", secret=SECRET) for i in range(10)]
        assert len(set(tokens)) == 10

    def test_same_node_different_tokens_each_call(self):
        """Nonce ensures distinct tokens per call (no replay)."""
        t1 = generate_node_token("node-1", secret=SECRET)
        t2 = generate_node_token("node-1", secret=SECRET)
        assert t1 != t2
        # But both verify correctly
        assert verify_node_token(t1, secret=SECRET)[0] is True
        assert verify_node_token(t2, secret=SECRET)[0] is True

    def test_wrong_secret_rejected(self):
        token = generate_node_token("node-1", secret=SECRET)
        valid, node_id = verify_node_token(token, secret=ALT_SECRET)
        assert valid is False
        assert node_id == ""

    def test_tampered_signature_rejected(self):
        token = generate_node_token("node-1", secret=SECRET)
        parts = token.split(".")
        # Flip a character in the HMAC signature
        tampered_sig = parts[3][:-1] + ("a" if parts[3][-1] != "a" else "b")
        tampered_token = f"{parts[0]}.{parts[1]}.{parts[2]}.{tampered_sig}"
        valid, _ = verify_node_token(tampered_token, secret=SECRET)
        assert valid is False

    def test_tampered_node_id_rejected(self):
        token = generate_node_token("node-1", secret=SECRET)
        parts = token.split(".")
        tampered_token = f"node-EVIL.{parts[1]}.{parts[2]}.{parts[3]}"
        valid, _ = verify_node_token(tampered_token, secret=SECRET)
        assert valid is False

    def test_tampered_timestamp_rejected(self):
        token = generate_node_token("node-1", secret=SECRET)
        parts = token.split(".")
        tampered_token = f"{parts[0]}.9999999999.{parts[2]}.{parts[3]}"
        valid, _ = verify_node_token(tampered_token, secret=SECRET)
        assert valid is False

    def test_expired_token_rejected(self):
        token = generate_node_token("node-1", secret=SECRET)
        # Token with max_age=0 should be expired immediately
        valid, _ = verify_node_token(token, secret=SECRET, max_age=0)
        assert valid is False

    def test_malformed_token_rejected(self):
        """Tokens with wrong number of parts are rejected."""
        assert verify_node_token("", secret=SECRET) == (False, "")
        assert verify_node_token("one.two", secret=SECRET) == (False, "")
        assert verify_node_token("a.b.c.d.e", secret=SECRET) == (False, "")

    def test_token_format_structure(self):
        """Token has exactly 4 dot-separated parts: node_id.timestamp.nonce.hmac."""
        token = generate_node_token("my-node", secret=SECRET)
        parts = token.split(".")
        assert len(parts) == 4
        assert parts[0] == "my-node"
        assert parts[1].isdigit()          # timestamp
        assert len(parts[2]) == 16         # 8 bytes → 16 hex chars
        assert len(parts[3]) == 64         # SHA256 HMAC → 64 hex chars


# =========================================================================
# TokenStore: persistent token management
# =========================================================================

class TestTokenStoreIntegration:
    """TokenStore: issue, rotate, revoke, list — all with FIPS crypto."""

    def test_issue_and_verify(self, tmp_path):
        store = TokenStore(str(tmp_path / "tokens.db"))
        token = store.issue("store-node-1")
        assert isinstance(token, str)
        assert len(token.split(".")) == 4
        store.close()

    def test_rotation_preserves_old_token(self, tmp_path):
        store = TokenStore(str(tmp_path / "tokens.db"))
        t1 = store.issue("node-rotate")
        t2 = store.issue("node-rotate")  # rotate
        assert t1 != t2
        nodes = store.list_nodes()
        assert len(nodes) == 1
        assert nodes[0]["node_id"] == "node-rotate"
        assert nodes[0]["rotated_at"] is not None
        store.close()

    def test_revoke_removes_node(self, tmp_path):
        store = TokenStore(str(tmp_path / "tokens.db"))
        store.issue("node-revoke")
        assert len(store.list_nodes()) == 1
        store.revoke("node-revoke")
        assert len(store.list_nodes()) == 0
        store.close()

    def test_multiple_nodes(self, tmp_path):
        store = TokenStore(str(tmp_path / "tokens.db"))
        for i in range(5):
            store.issue(f"node-{i}")
        nodes = store.list_nodes()
        assert len(nodes) == 5
        node_ids = {n["node_id"] for n in nodes}
        assert node_ids == {f"node-{i}" for i in range(5)}
        store.close()


# =========================================================================
# Sync payload encryption ↔ decryption (all versions)
# =========================================================================

class TestSyncPayloadV0:
    """v0 payloads: HMAC integrity, no confidentiality."""

    def test_v0_roundtrip(self):
        """Force v0 by patching out cryptography's AESGCM."""
        payload = {"table": "obs", "rows": [{"id": "1", "data": "test"}]}
        with mock.patch.dict("sys.modules", {"cryptography.hazmat.primitives.ciphers.aead": None}):
            # Clear the module cache so the import inside encrypt_sync_payload fails
            import importlib
            from greymatter_core.security import encryption
            importlib.reload(encryption)
            try:
                encrypted = encryption.encrypt_sync_payload(payload, secret=SECRET)
                assert encrypted["v"] == 0
                assert "ct" in encrypted
                assert "hmac" in encrypted
                # v0 has no nonce (not encrypted)
                assert "nonce" not in encrypted

                decrypted = encryption.decrypt_sync_payload(encrypted, secret=SECRET)
                assert decrypted == payload
            finally:
                # Restore
                importlib.reload(encryption)

    def test_v0_tamper_detected(self):
        """Tampering with v0 ciphertext (base64 payload) is caught by HMAC."""
        with mock.patch.dict("sys.modules", {"cryptography.hazmat.primitives.ciphers.aead": None}):
            import importlib
            from greymatter_core.security import encryption
            importlib.reload(encryption)
            try:
                encrypted = encryption.encrypt_sync_payload({"a": 1}, secret=SECRET)
                assert encrypted["v"] == 0
                # Tamper: replace the base64 content
                encrypted["ct"] = base64.b64encode(b'{"a":999}').decode()
                with pytest.raises(ValueError, match="HMAC verification failed"):
                    encryption.decrypt_sync_payload(encrypted, secret=SECRET)
            finally:
                importlib.reload(encryption)


class TestSyncPayloadV1:
    """v1 payloads: AES-256-GCM + HMAC integrity."""

    def test_v1_roundtrip(self):
        payload = {"table": "patterns", "rows": [{"id": "pat-abc", "type": "code"}]}
        encrypted = encrypt_sync_payload(payload, secret=SECRET)
        assert encrypted["v"] == 1
        assert "ct" in encrypted
        assert "nonce" in encrypted
        assert "hmac" in encrypted
        decrypted = decrypt_sync_payload(encrypted, secret=SECRET)
        assert decrypted == payload

    def test_v1_has_required_fields(self):
        encrypted = encrypt_sync_payload({"x": 1}, secret=SECRET)
        assert set(encrypted.keys()) >= {"v", "ct", "nonce", "hmac"}
        # ct and nonce should be base64-decodable
        base64.b64decode(encrypted["ct"])
        base64.b64decode(encrypted["nonce"])

    def test_v1_tampered_ciphertext_fails(self):
        encrypted = encrypt_sync_payload({"a": 1}, secret=SECRET)
        ct_bytes = base64.b64decode(encrypted["ct"])
        tampered = bytes([b ^ 0xFF for b in ct_bytes[:4]]) + ct_bytes[4:]
        encrypted["ct"] = base64.b64encode(tampered).decode()
        with pytest.raises(ValueError, match="HMAC verification failed"):
            decrypt_sync_payload(encrypted, secret=SECRET)

    def test_v1_tampered_hmac_fails(self):
        encrypted = encrypt_sync_payload({"a": 1}, secret=SECRET)
        encrypted["hmac"] = "0" * 64
        with pytest.raises(ValueError, match="HMAC verification failed"):
            decrypt_sync_payload(encrypted, secret=SECRET)

    def test_v1_tampered_nonce_fails(self):
        encrypted = encrypt_sync_payload({"a": 1}, secret=SECRET)
        nonce_bytes = base64.b64decode(encrypted["nonce"])
        tampered_nonce = bytes([b ^ 0xFF for b in nonce_bytes])
        encrypted["nonce"] = base64.b64encode(tampered_nonce).decode()
        with pytest.raises(ValueError, match="HMAC verification failed"):
            decrypt_sync_payload(encrypted, secret=SECRET)

    def test_v1_wrong_secret_fails(self):
        encrypted = encrypt_sync_payload({"a": 1}, secret=SECRET)
        with pytest.raises(ValueError):
            decrypt_sync_payload(encrypted, secret=ALT_SECRET)

    def test_v1_large_payload(self):
        """1 MB payload encrypts and decrypts correctly."""
        large = {"data": "x" * (1024 * 1024)}
        encrypted = encrypt_sync_payload(large, secret=SECRET)
        decrypted = decrypt_sync_payload(encrypted, secret=SECRET)
        assert decrypted == large

    def test_v1_unicode_payload(self):
        """Unicode characters survive encryption roundtrip."""
        payload = {"emoji": "🔐🛡️", "chinese": "加密", "arabic": "تشفير"}
        encrypted = encrypt_sync_payload(payload, secret=SECRET)
        decrypted = decrypt_sync_payload(encrypted, secret=SECRET)
        assert decrypted == payload

    def test_v1_nested_payload(self):
        """Deeply nested payloads survive encryption roundtrip."""
        payload = {"level1": {"level2": {"level3": [1, 2, {"level4": True}]}}}
        encrypted = encrypt_sync_payload(payload, secret=SECRET)
        decrypted = decrypt_sync_payload(encrypted, secret=SECRET)
        assert decrypted == payload

    def test_v1_empty_payload(self):
        """Empty dict encrypts and decrypts correctly."""
        encrypted = encrypt_sync_payload({}, secret=SECRET)
        decrypted = decrypt_sync_payload(encrypted, secret=SECRET)
        assert decrypted == {}


class TestSyncPayloadV2:
    """v2 payloads: ML-KEM-768 key encapsulation + AES-256-GCM."""

    @pytest.mark.skipif(not MLKEM_AVAILABLE, reason="kyber-py not installed")
    def test_v2_roundtrip(self):
        from greymatter_core.security.encryption import generate_mlkem_keypair
        ek, dk = generate_mlkem_keypair()
        payload = {"pq": True, "data": "post-quantum safe"}
        encrypted = encrypt_sync_payload(payload, secret=SECRET, peer_ek=ek)
        assert encrypted["v"] == 2
        assert "kem_ct" in encrypted
        decrypted = decrypt_sync_payload(encrypted, secret=SECRET, dk=dk)
        assert decrypted == payload

    @pytest.mark.skipif(not MLKEM_AVAILABLE, reason="kyber-py not installed")
    def test_v2_has_required_fields(self):
        from greymatter_core.security.encryption import generate_mlkem_keypair
        ek, dk = generate_mlkem_keypair()
        encrypted = encrypt_sync_payload({"x": 1}, secret=SECRET, peer_ek=ek)
        assert set(encrypted.keys()) >= {"v", "ct", "nonce", "kem_ct", "hmac"}

    @pytest.mark.skipif(not MLKEM_AVAILABLE, reason="kyber-py not installed")
    def test_v2_tampered_ciphertext_fails(self):
        from greymatter_core.security.encryption import generate_mlkem_keypair
        ek, dk = generate_mlkem_keypair()
        encrypted = encrypt_sync_payload({"a": 1}, secret=SECRET, peer_ek=ek)
        ct_bytes = base64.b64decode(encrypted["ct"])
        tampered = bytes([b ^ 0xFF for b in ct_bytes[:4]]) + ct_bytes[4:]
        encrypted["ct"] = base64.b64encode(tampered).decode()
        with pytest.raises(ValueError, match="HMAC verification failed"):
            decrypt_sync_payload(encrypted, secret=SECRET, dk=dk)

    @pytest.mark.skipif(not MLKEM_AVAILABLE, reason="kyber-py not installed")
    def test_v2_wrong_dk_fails(self):
        """Wrong decapsulation key produces garbled/invalid plaintext."""
        from greymatter_core.security.encryption import generate_mlkem_keypair
        ek1, dk1 = generate_mlkem_keypair()
        ek2, dk2 = generate_mlkem_keypair()
        encrypted = encrypt_sync_payload({"a": 1}, secret=SECRET, peer_ek=ek1)
        # Using dk2 to decrypt dk1's ciphertext should fail
        with pytest.raises(Exception):
            decrypt_sync_payload(encrypted, secret=SECRET, dk=dk2)

    @pytest.mark.skipif(not MLKEM_AVAILABLE, reason="kyber-py not installed")
    def test_v2_missing_dk_raises_valueerror(self):
        from greymatter_core.security.encryption import generate_mlkem_keypair
        ek, _ = generate_mlkem_keypair()
        encrypted = encrypt_sync_payload({"a": 1}, secret=SECRET, peer_ek=ek)
        with pytest.raises(ValueError, match="decapsulation key"):
            decrypt_sync_payload(encrypted, secret=SECRET, dk=None)

    @pytest.mark.skipif(not MLKEM_AVAILABLE, reason="kyber-py not installed")
    def test_v2_save_load_keys(self, tmp_path):
        from greymatter_core.security.encryption import (
            generate_mlkem_keypair,
            save_mlkem_keys,
            load_mlkem_keys,
        )
        ek, dk = generate_mlkem_keypair()
        save_mlkem_keys(ek, dk, keys_dir=str(tmp_path))
        ek_loaded, dk_loaded = load_mlkem_keys(keys_dir=str(tmp_path))
        assert ek == ek_loaded
        assert dk == dk_loaded

        # Encrypt with original, decrypt with loaded keys
        encrypted = encrypt_sync_payload({"key": "val"}, secret=SECRET, peer_ek=ek)
        decrypted = decrypt_sync_payload(encrypted, secret=SECRET, dk=dk_loaded)
        assert decrypted == {"key": "val"}


class TestSyncPayloadVersionMismatch:
    """Cross-version error handling."""

    def test_unknown_version_raises(self):
        with pytest.raises(ValueError, match="Unknown encryption version"):
            decrypt_sync_payload({"v": 99, "ct": "", "hmac": ""}, secret=SECRET)


# =========================================================================
# Key derivation end-to-end
# =========================================================================

class TestKeyDerivationIntegration:
    """FIPS PBKDF2 → _derive_key → SQLCipher key → DB password derivation."""

    def test_derive_key_deterministic(self):
        k1 = _derive_key(b"secret", b"salt", length=32)
        k2 = _derive_key(b"secret", b"salt", length=32)
        assert k1 == k2

    def test_derive_key_different_salts(self):
        k1 = _derive_key(b"secret", b"salt-A", length=32)
        k2 = _derive_key(b"secret", b"salt-B", length=32)
        assert k1 != k2

    def test_derive_key_matches_raw_pbkdf2(self):
        """_derive_key should produce the same result as direct fips_pbkdf2."""
        expected = fips_pbkdf2(b"secret", b"salt", iterations=100_000, dklen=32)
        assert _derive_key(b"secret", b"salt", length=32) == expected

    def test_sqlcipher_key_unique_per_node(self):
        k1 = get_sqlcipher_key("node-A", secret=SECRET)
        k2 = get_sqlcipher_key("node-B", secret=SECRET)
        assert k1 != k2

    def test_sqlcipher_key_deterministic(self):
        k1 = get_sqlcipher_key("node-X", secret=SECRET)
        k2 = get_sqlcipher_key("node-X", secret=SECRET)
        assert k1 == k2

    def test_sqlcipher_key_format(self):
        """Key is a 64-character hex string (32 bytes)."""
        key = get_sqlcipher_key("node-1", secret=SECRET)
        assert len(key) == 64
        assert re.fullmatch(r"[0-9a-f]{64}", key)

    def test_db_password_derivation_roundtrip(self):
        """derive_key_from_password produces consistent results with same salt."""
        hex_key, salt = derive_key_from_password("my-passphrase")
        assert len(hex_key) == 64
        assert len(salt) == 16
        # Re-derive with same salt
        hex_key2, _ = derive_key_from_password("my-passphrase", salt=salt)
        assert hex_key == hex_key2

    def test_db_password_derivation_unique_salts(self):
        """Without explicit salt, each call generates a unique salt → unique key."""
        hex_key1, salt1 = derive_key_from_password("same-password")
        hex_key2, salt2 = derive_key_from_password("same-password")
        assert salt1 != salt2
        assert hex_key1 != hex_key2

    def test_db_password_derivation_different_passwords(self):
        """Different passwords with the same salt produce different keys."""
        _, salt = derive_key_from_password("pw-1")
        k1, _ = derive_key_from_password("pw-1", salt=salt)
        k2, _ = derive_key_from_password("pw-2", salt=salt)
        assert k1 != k2


# =========================================================================
# ID generation with FIPS-backed randomness
# =========================================================================

class TestIDGenerationIntegration:
    """IDs generated via fips_choice produce valid, collision-resistant IDs."""

    def test_pattern_id_format(self):
        pid = generate_pattern_id()
        assert pid.startswith("pat-")
        assert len(pid) == 12  # "pat-" + 8 chars
        assert all(c in ALPHABET for c in pid[4:])

    def test_knowledge_id_format(self):
        kid = generate_knowledge_id()
        assert kid.startswith("kn-")
        assert len(kid) == 11  # "kn-" + 8 chars

    def test_work_item_id_format(self):
        wid = generate_work_item_id()
        assert wid.startswith("wi-")
        assert len(wid) == 11

    def test_session_id_contains_date(self):
        sid = generate_session_id()
        # Format: YYYY-MM-DD-xxxxxxxx
        assert re.fullmatch(r"\d{4}-\d{2}-\d{2}-[a-z0-9]{8}", sid)

    def test_generic_id_with_custom_prefix(self):
        gid = generate_id("custom")
        assert gid.startswith("custom-")
        assert len(gid) == len("custom-") + 8

    def test_no_collisions_in_1000_ids(self):
        """1000 generated IDs should all be unique."""
        ids = {generate_pattern_id() for _ in range(1000)}
        assert len(ids) == 1000

    def test_alphabet_coverage(self):
        """Over 10,000 random characters, all alphabet chars should appear."""
        chars = Counter()
        for _ in range(10_000):
            pid = generate_pattern_id()
            for c in pid[4:]:  # skip "pat-"
                chars[c] += 1
        # Every character in ALPHABET should appear at least once
        assert set(chars.keys()) == set(ALPHABET)


# =========================================================================
# Cross-module import consistency
# =========================================================================

class TestCrossModuleImports:
    """Verify all FIPS functions are accessible through multiple import paths."""

    def test_direct_import_from_fips_module(self):
        from greymatter_core.security.fips import fips_random_bytes as f1
        assert callable(f1)

    def test_import_from_security_package(self):
        from greymatter_core.security import fips_random_bytes as f2
        assert callable(f2)

    def test_both_paths_return_same_function(self):
        from greymatter_core.security.fips import fips_sha256 as direct
        from greymatter_core.security import fips_sha256 as pkg
        # Same function object
        assert direct is pkg

    def test_all_fips_exports_in_security_init(self):
        from greymatter_core.security import __all__
        fips_exports = [
            "fips_random_bytes", "fips_sha256", "fips_hmac_sha256",
            "fips_pbkdf2", "fips_token_urlsafe", "fips_token_bytes",
            "fips_choice", "is_fips_mode",
        ]
        for name in fips_exports:
            assert name in __all__, f"{name} missing from security.__all__"

    def test_tokens_module_uses_fips_hmac(self):
        """Token signing uses fips_hmac_sha256 under the hood."""
        key = SECRET
        data = b"node-test.12345.abcdef0123456789"
        expected = fips_hmac_sha256(key, data)
        # The token module's signature should use the same HMAC
        import hmac
        assert len(expected) == 32
        assert expected == fips_hmac_sha256(key, data)


# =========================================================================
# FIPS crypto primitive correctness (integration-level)
# =========================================================================

class TestCryptoPrimitiveCorrectness:
    """Cross-validate FIPS primitives against stdlib to ensure migration
    didn't change cryptographic behavior."""

    def test_sha256_matches_hashlib_for_varied_inputs(self):
        """Verify SHA-256 matches hashlib across diverse inputs."""
        inputs = [
            b"",
            b"\x00",
            b"\x00" * 1000,
            b"Hello, World!",
            b"\xff" * 256,
            os.urandom(4096),
            "日本語テスト".encode("utf-8"),
        ]
        for data in inputs:
            assert fips_sha256(data) == hashlib.sha256(data).digest(), \
                f"SHA-256 mismatch for input of len {len(data)}"

    def test_hmac_sha256_matches_stdlib_for_varied_inputs(self):
        keys = [b"k", b"k" * 64, b"k" * 128, os.urandom(32)]
        data_samples = [b"", b"short", os.urandom(8192)]
        for key in keys:
            for data in data_samples:
                expected = _hmac.new(key, data, hashlib.sha256).digest()
                assert fips_hmac_sha256(key, data) == expected

    def test_pbkdf2_matches_hashlib_for_varied_params(self):
        cases = [
            (b"password", b"salt", 1, 32),
            (b"password", b"salt", 1000, 32),
            (b"password", b"salt", 1000, 16),
            (b"password", b"salt", 1000, 64),
            (b"", b"salt", 100, 32),
            (b"password", b"", 100, 32),
        ]
        for pw, salt, iters, dklen in cases:
            expected = hashlib.pbkdf2_hmac("sha256", pw, salt, iters, dklen=dklen)
            assert fips_pbkdf2(pw, salt, iters, dklen) == expected, \
                f"PBKDF2 mismatch for pw={pw!r}, salt={salt!r}, iters={iters}, dklen={dklen}"

    def test_random_bytes_entropy(self):
        """Random bytes should have reasonable entropy (not all zeros or repeated)."""
        sample = fips_random_bytes(256)
        # Count unique bytes — with 256 random bytes, we expect high diversity
        unique_bytes = len(set(sample))
        assert unique_bytes >= 100, f"Low entropy: only {unique_bytes} unique byte values"

    def test_token_urlsafe_is_valid_base64url(self):
        """Tokens should be valid base64url (no padding, no +/)."""
        for _ in range(100):
            token = fips_token_urlsafe(32)
            assert re.fullmatch(r"[A-Za-z0-9_-]+", token), f"Invalid token: {token}"
            # Should not have base64 padding
            assert "=" not in token


# =========================================================================
# Edge cases
# =========================================================================

class TestEdgeCases:
    """Boundary conditions and unusual inputs."""

    def test_sha256_empty_input(self):
        result = fips_sha256(b"")
        assert len(result) == 32
        assert result == hashlib.sha256(b"").digest()

    def test_hmac_empty_data(self):
        result = fips_hmac_sha256(b"key", b"")
        assert len(result) == 32

    def test_hmac_empty_key(self):
        result = fips_hmac_sha256(b"", b"data")
        assert len(result) == 32
        assert result == _hmac.new(b"", b"data", hashlib.sha256).digest()

    def test_random_bytes_zero_length(self):
        result = fips_random_bytes(0)
        assert result == b""

    def test_random_bytes_large_request(self):
        """Request 1 MB of random bytes."""
        result = fips_random_bytes(1024 * 1024)
        assert len(result) == 1024 * 1024

    def test_choice_single_element_sequence(self):
        assert fips_choice([42]) == 42
        assert fips_choice("X") == "X"

    def test_choice_empty_sequence_raises(self):
        with pytest.raises(IndexError, match="empty"):
            fips_choice([])

    def test_token_urlsafe_default_length(self):
        """Default nbytes=32 → ~43 chars."""
        token = fips_token_urlsafe()
        assert len(token) >= 40

    def test_token_bytes_default_length(self):
        result = fips_token_bytes()
        assert len(result) == 32

    def test_pbkdf2_dklen_1(self):
        """Minimum derived key length."""
        result = fips_pbkdf2(b"pw", b"salt", iterations=1, dklen=1)
        assert len(result) == 1

    def test_pbkdf2_large_dklen(self):
        """Large derived key."""
        result = fips_pbkdf2(b"pw", b"salt", iterations=1, dklen=128)
        assert len(result) == 128

    def test_encrypt_decrypt_payload_with_special_json_chars(self):
        """Payload with characters that need JSON escaping."""
        payload = {"key": 'value with "quotes" and \\backslash\\ and \nnewlines'}
        encrypted = encrypt_sync_payload(payload, secret=SECRET)
        decrypted = decrypt_sync_payload(encrypted, secret=SECRET)
        assert decrypted == payload

    def test_encrypt_payload_with_null_values(self):
        payload = {"key": None, "list": [None, 1, "str"]}
        encrypted = encrypt_sync_payload(payload, secret=SECRET)
        decrypted = decrypt_sync_payload(encrypted, secret=SECRET)
        assert decrypted == payload


# =========================================================================
# Concurrent operations
# =========================================================================

class TestConcurrentCryptoOperations:
    """Thread-safety: FIPS functions called concurrently must not corrupt."""

    def test_concurrent_random_bytes(self):
        """Multiple threads generating random bytes simultaneously."""
        results = []
        errors = []

        def gen():
            try:
                results.append(fips_random_bytes(32))
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=gen) for _ in range(50)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0, f"Errors: {errors}"
        assert len(results) == 50
        # All should be unique
        assert len(set(results)) == 50

    def test_concurrent_sha256(self):
        """Multiple threads hashing simultaneously produce correct results."""
        inputs = [os.urandom(64) for _ in range(50)]
        expected = [hashlib.sha256(d).digest() for d in inputs]
        results = [None] * 50
        errors = []

        def hash_at(i):
            try:
                results[i] = fips_sha256(inputs[i])
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=hash_at, args=(i,)) for i in range(50)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0
        assert results == expected

    def test_concurrent_token_generation(self):
        """Multiple threads generating tokens simultaneously."""
        tokens = []
        lock = threading.Lock()
        errors = []

        def gen_token(node_id):
            try:
                t = generate_node_token(node_id, secret=SECRET)
                with lock:
                    tokens.append(t)
            except Exception as e:
                errors.append(e)

        threads = [
            threading.Thread(target=gen_token, args=(f"node-{i}",))
            for i in range(20)
        ]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0
        assert len(tokens) == 20
        assert len(set(tokens)) == 20  # All unique

    def test_concurrent_encrypt_decrypt(self):
        """Encrypt and decrypt in parallel threads — no cross-contamination."""
        payloads = [{"id": i, "data": f"payload-{i}"} for i in range(10)]
        results = [None] * 10
        errors = []

        def roundtrip(i):
            try:
                enc = encrypt_sync_payload(payloads[i], secret=SECRET)
                dec = decrypt_sync_payload(enc, secret=SECRET)
                results[i] = dec
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=roundtrip, args=(i,)) for i in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0
        for i in range(10):
            assert results[i] == payloads[i], f"Mismatch at index {i}"


# =========================================================================
# FIPS mode detection & fallback behavior
# =========================================================================

class TestFipsModeIntegration:
    """Verify FIPS mode detection and stdlib fallback."""

    def test_is_fips_mode_type(self):
        assert isinstance(is_fips_mode(), bool)

    def test_fips_mode_requires_both_env_and_library(self):
        """is_fips_mode() is only True when _FIPS_MODE AND _HAS_CRYPTO."""
        result = is_fips_mode()
        assert result == (_FIPS_MODE and _HAS_CRYPTO)

    def test_functions_work_regardless_of_fips_mode(self):
        """All crypto functions must work in both FIPS and non-FIPS mode."""
        # These should never raise regardless of mode
        assert len(fips_random_bytes(16)) == 16
        assert len(fips_sha256(b"test")) == 32
        assert len(fips_hmac_sha256(b"key", b"data")) == 32
        assert len(fips_pbkdf2(b"pw", b"salt", iterations=1)) == 32
        assert isinstance(fips_token_urlsafe(16), str)
        assert isinstance(fips_token_bytes(16), bytes)
        assert fips_choice("abc") in "abc"


# =========================================================================
# Dependent module integration — verify modules that consume FIPS
# =========================================================================

class TestDependentModuleIntegration:
    """Verify that modules depending on FIPS crypto work correctly
    through the full import chain."""

    def test_beta_signing_uses_fips_hmac(self, monkeypatch):
        """beta/signing.py uses fips_hmac_sha256 for license signatures."""
        monkeypatch.setenv("GM_LICENSE_SECRET", "test-secret-12345")
        from beta.signing import sign_license, verify_signature

        sig = sign_license("lic-1", "mach-1", "solo", 1000.0, 2000.0)
        assert isinstance(sig, str)
        assert len(sig) == 64  # HMAC-SHA256 hex

        assert verify_signature("lic-1", "mach-1", "solo", 1000.0, 2000.0, sig) is True
        assert verify_signature("lic-1", "mach-1", "solo", 1000.0, 2000.0, "0" * 64) is False

    def test_beta_signing_deterministic(self, monkeypatch):
        """Same inputs → same signature (HMAC is deterministic)."""
        monkeypatch.setenv("GM_LICENSE_SECRET", "test-secret-12345")
        from beta.signing import sign_license
        s1 = sign_license("lic-1", "mach-1", "solo", 1000.0, 2000.0)
        s2 = sign_license("lic-1", "mach-1", "solo", 1000.0, 2000.0)
        assert s1 == s2

    def test_beta_signing_different_inputs_different_sigs(self, monkeypatch):
        monkeypatch.setenv("GM_LICENSE_SECRET", "test-secret-12345")
        from beta.signing import sign_license
        s1 = sign_license("lic-1", "mach-1", "solo", 1000.0, 2000.0)
        s2 = sign_license("lic-2", "mach-1", "solo", 1000.0, 2000.0)
        assert s1 != s2


# =========================================================================
# Full pipeline: generate keys → encrypt → transmit → verify → decrypt
# =========================================================================

class TestFullCryptoPipeline:
    """Simulates a realistic multi-step crypto flow between two nodes."""

    def test_node_registration_and_secure_sync(self, tmp_path):
        """Two nodes register, exchange tokens, then sync encrypted data."""
        # Node A registers
        token_a = generate_node_token("node-A", secret=SECRET)
        valid, nid = verify_node_token(token_a, secret=SECRET)
        assert valid and nid == "node-A"

        # Node B registers
        token_b = generate_node_token("node-B", secret=SECRET)
        valid, nid = verify_node_token(token_b, secret=SECRET)
        assert valid and nid == "node-B"

        # Node A encrypts sync payload
        payload = {
            "table": "observations",
            "rows": [
                {"id": "obs-1", "content": "Deployed v2.1", "category": "deployment"},
                {"id": "obs-2", "content": "Fixed auth bug", "category": "bugfix"},
            ],
        }
        encrypted = encrypt_sync_payload(payload, secret=SECRET)

        # Simulate network transmission (payload is serializable)
        transmitted = json.loads(json.dumps(encrypted))

        # Node B decrypts
        decrypted = decrypt_sync_payload(transmitted, secret=SECRET)
        assert decrypted == payload
        assert len(decrypted["rows"]) == 2

    def test_key_derived_db_access(self, tmp_path):
        """Derive a DB key from password, verify it's usable as SQLCipher key."""
        hex_key, salt = derive_key_from_password("my-secure-passphrase")

        # Key should be valid hex
        bytes.fromhex(hex_key)

        # Should be deterministic
        hex_key2, _ = derive_key_from_password("my-secure-passphrase", salt=salt)
        assert hex_key == hex_key2

        # SQLCipher key for a node should be different from password-derived key
        sqlcipher_key = get_sqlcipher_key("my-node", secret=SECRET)
        assert sqlcipher_key != hex_key  # Different derivation path

    def test_id_generation_in_simulated_workflow(self):
        """Simulate creating multiple entities — all IDs are unique."""
        pattern_ids = [generate_pattern_id() for _ in range(100)]
        knowledge_ids = [generate_knowledge_id() for _ in range(100)]
        work_ids = [generate_work_item_id() for _ in range(100)]

        assert len(set(pattern_ids)) == 100
        assert len(set(knowledge_ids)) == 100
        assert len(set(work_ids)) == 100

        # Cross-prefix uniqueness (IDs from different generators shouldn't collide
        # even ignoring prefixes, but with prefixes they definitely won't)
        all_ids = set(pattern_ids + knowledge_ids + work_ids)
        assert len(all_ids) == 300

    @pytest.mark.skipif(not MLKEM_AVAILABLE, reason="kyber-py not installed")
    def test_full_pq_secure_pipeline(self, tmp_path):
        """Full post-quantum pipeline: keygen → save → load → encrypt → decrypt."""
        from greymatter_core.security.encryption import (
            generate_mlkem_keypair,
            save_mlkem_keys,
            load_mlkem_keys,
        )

        # Generate and persist keys
        ek, dk = generate_mlkem_keypair()
        save_mlkem_keys(ek, dk, keys_dir=str(tmp_path))

        # Load keys (simulating process restart)
        ek_loaded, dk_loaded = load_mlkem_keys(keys_dir=str(tmp_path))

        # Encrypt with loaded public key
        payload = {"quantum": "safe", "data": list(range(100))}
        encrypted = encrypt_sync_payload(payload, secret=SECRET, peer_ek=ek_loaded)
        assert encrypted["v"] == 2

        # Decrypt with loaded private key
        decrypted = decrypt_sync_payload(encrypted, secret=SECRET, dk=dk_loaded)
        assert decrypted == payload
