"""End-to-end integration tests for FIPS Phase C crypto wrapper.

Covers gaps not addressed in the existing test suite (test_fips.py,
test_fips_integration.py, test_fips_integration_core.py,
test_fips_integration_extended.py):

- All ID generators (standard, build-run, document, registry-event, etc.)
- beta/auth.py FIPS token integration
- TokenStore persistence across close/reopen cycles
- TokenStore concurrent access (thread safety)
- Special/adversarial node_id values
- Cross-version payload migration (v0 -> v1 re-encryption)
- Cluster secret auto-generation and persistence
- Full node lifecycle simulation (onboard -> communicate -> rotate -> decommission)
- Multiprocessing safety for FIPS crypto operations
- open_encrypted_sqlite fallback behavior
- Incremental hashing patterns (streaming SHA-256)
- FIPS choice modulo bias validation for small sequences
- Payload ordering stability through encrypt/decrypt
"""

from __future__ import annotations

import base64
import concurrent.futures
import hashlib
import hmac as _hmac
import json
import multiprocessing
import os
import re
import sqlite3
import string
import threading
import time
from collections import Counter
from pathlib import Path
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 (
    TokenStore,
    generate_node_token,
    verify_node_token,
)
from greymatter_core.security.encryption import (
    MLKEM_AVAILABLE,
    _derive_key,
    decrypt_sync_payload,
    encrypt_sync_payload,
    get_sqlcipher_key,
    open_encrypted_sqlite,
)
from greymatter_core.ids import (
    ALPHABET,
    generate_build_run_id,
    generate_contact_id,
    generate_conversation_session_id,
    generate_document_id,
    generate_id,
    generate_knowledge_id,
    generate_lineage_id,
    generate_mood_id,
    generate_pattern_id,
    generate_registry_event_id,
    generate_session_id,
    generate_standard_id,
    generate_work_item_id,
)
from greymatter_core.db import derive_key_from_password


# Shared test constants
SECRET = b"e2e-test-cluster-secret-32bytes!"
ALT_SECRET = b"e2e-alt-cluster-secret-32bytes!!"


# =========================================================================
# All ID generators — complete coverage
# =========================================================================


class TestAllIDGenerators:
    """Verify every ID generator produces valid, unique IDs using FIPS-backed
    randomness. The existing tests only cover pattern, knowledge, work_item,
    session, and generic IDs."""

    @pytest.mark.parametrize(
        "gen_fn, prefix, total_len",
        [
            (generate_pattern_id, "pat-", 12),
            (generate_knowledge_id, "kn-", 11),
            (generate_work_item_id, "wi-", 11),
            (generate_build_run_id, "br-", 11),
            (generate_document_id, "doc-", 12),
            (generate_registry_event_id, "sre-", 12),
            (generate_contact_id, "con-", 12),
            (generate_lineage_id, "lin-", 12),
            (generate_mood_id, "mood-", 13),
        ],
    )
    def test_id_format_and_length(self, gen_fn, prefix, total_len):
        """Each generator produces IDs with correct prefix and length."""
        gid = gen_fn()
        assert gid.startswith(prefix), f"Expected prefix '{prefix}', got '{gid}'"
        assert len(gid) == total_len, f"Expected len {total_len}, got {len(gid)}"

    @pytest.mark.parametrize(
        "gen_fn, prefix",
        [
            (generate_pattern_id, "pat-"),
            (generate_knowledge_id, "kn-"),
            (generate_work_item_id, "wi-"),
            (generate_build_run_id, "br-"),
            (generate_document_id, "doc-"),
            (generate_registry_event_id, "sre-"),
            (generate_contact_id, "con-"),
            (generate_lineage_id, "lin-"),
            (generate_mood_id, "mood-"),
        ],
    )
    def test_suffix_uses_fips_alphabet(self, gen_fn, prefix):
        """All suffix characters come from ALPHABET (lowercase + digits)."""
        for _ in range(50):
            gid = gen_fn()
            suffix = gid[len(prefix):]
            assert all(c in ALPHABET for c in suffix), (
                f"ID '{gid}' has char outside ALPHABET in suffix '{suffix}'"
            )

    @pytest.mark.parametrize(
        "gen_fn",
        [
            generate_pattern_id,
            generate_knowledge_id,
            generate_work_item_id,
            generate_build_run_id,
            generate_document_id,
            generate_registry_event_id,
            generate_contact_id,
            generate_lineage_id,
            generate_mood_id,
        ],
    )
    def test_no_collisions_across_500_ids(self, gen_fn):
        """500 IDs from each generator should all be unique."""
        ids = {gen_fn() for _ in range(500)}
        assert len(ids) == 500, f"Collision detected in {gen_fn.__name__}"

    def test_standard_id_format(self):
        """generate_standard_id includes the category in the prefix."""
        sid = generate_standard_id("lint")
        assert sid.startswith("qs-lint-")
        assert len(sid) == len("qs-lint-") + 8
        suffix = sid[len("qs-lint-"):]
        assert all(c in ALPHABET for c in suffix)

    def test_standard_id_different_categories(self):
        """Different categories produce different prefixes."""
        s1 = generate_standard_id("lint")
        s2 = generate_standard_id("test")
        assert s1.startswith("qs-lint-")
        assert s2.startswith("qs-test-")
        # Even same category should produce unique IDs
        ids = {generate_standard_id("sec") for _ in range(100)}
        assert len(ids) == 100

    def test_conversation_session_id_format(self):
        """cs-YYYY-MM-DD-xxxxxxxx format."""
        csid = generate_conversation_session_id()
        assert csid.startswith("cs-")
        # cs-YYYY-MM-DD-xxxxxxxx
        match = re.fullmatch(r"cs-\d{4}-\d{2}-\d{2}-[a-z0-9]{8}", csid)
        assert match, f"Invalid format: {csid}"

    def test_conversation_session_id_uniqueness(self):
        """Multiple calls produce unique IDs even within the same second."""
        ids = {generate_conversation_session_id() for _ in range(100)}
        assert len(ids) == 100

    def test_cross_generator_no_collisions(self):
        """IDs from all generators should never collide (prefixes guarantee this)."""
        all_ids = set()
        generators = [
            generate_pattern_id,
            generate_knowledge_id,
            generate_work_item_id,
            generate_build_run_id,
            generate_document_id,
            generate_registry_event_id,
            generate_contact_id,
            generate_lineage_id,
            generate_mood_id,
        ]
        for gen_fn in generators:
            for _ in range(50):
                all_ids.add(gen_fn())
        # 9 generators * 50 IDs = 450
        assert len(all_ids) == 450


# =========================================================================
# beta/auth.py integration
# =========================================================================


class TestBetaAuthIntegration:
    """beta/auth.py uses fips_token_urlsafe for admin token generation."""

    def test_get_admin_token_generates_fips_token(self, monkeypatch):
        """When GM_LICENSE_ADMIN_TOKEN is not set, a FIPS-backed token is generated."""
        monkeypatch.delenv("GM_LICENSE_ADMIN_TOKEN", raising=False)
        from beta.auth import _get_admin_token

        # Force re-generation by clearing the env var
        token = _get_admin_token()
        assert isinstance(token, str)
        assert len(token) > 40  # urlsafe token from 32 bytes
        # Should be URL-safe characters only
        assert re.fullmatch(r"[A-Za-z0-9_-]+", token)

    def test_get_admin_token_caches_in_env(self, monkeypatch):
        """Generated token is stored in env so subsequent calls return the same token."""
        monkeypatch.delenv("GM_LICENSE_ADMIN_TOKEN", raising=False)
        from beta.auth import _get_admin_token

        t1 = _get_admin_token()
        t2 = _get_admin_token()
        assert t1 == t2

    def test_get_admin_token_uses_env_when_set(self, monkeypatch):
        """When GM_LICENSE_ADMIN_TOKEN is set, it's used directly."""
        monkeypatch.setenv("GM_LICENSE_ADMIN_TOKEN", "my-preset-token")
        from beta.auth import _get_admin_token

        assert _get_admin_token() == "my-preset-token"

    def test_beta_auth_import_fallback_shim(self):
        """beta/auth.py has a fallback shim for fips_token_urlsafe."""
        # Verify the import structure is correct
        import beta.auth as mod
        assert hasattr(mod, "fips_token_urlsafe")
        assert callable(mod.fips_token_urlsafe)

    def test_beta_auth_token_is_cryptographic_quality(self, monkeypatch):
        """Generated admin tokens have sufficient entropy."""
        monkeypatch.delenv("GM_LICENSE_ADMIN_TOKEN", raising=False)
        from beta.auth import _get_admin_token

        tokens = set()
        for _ in range(20):
            # Force new token each time
            monkeypatch.delenv("GM_LICENSE_ADMIN_TOKEN", raising=False)
            tokens.add(_get_admin_token())
        assert len(tokens) == 20, "Admin tokens should be unique each generation"


# =========================================================================
# TokenStore persistence across close/reopen
# =========================================================================


class TestTokenStorePersistence:
    """Verify TokenStore data persists across close and reopen cycles."""

    def test_data_persists_after_close_and_reopen(self, tmp_path):
        """Tokens issued before close are visible after reopen."""
        db_path = str(tmp_path / "persist.db")

        # Phase 1: Issue tokens and close
        store1 = TokenStore(db_path)
        t1 = store1.issue("persist-node-a")
        t2 = store1.issue("persist-node-b")
        store1.close()

        # Phase 2: Reopen and verify
        store2 = TokenStore(db_path)
        nodes = store2.list_nodes()
        node_ids = {n["node_id"] for n in nodes}
        assert "persist-node-a" in node_ids
        assert "persist-node-b" in node_ids
        assert len(nodes) == 2
        store2.close()

    def test_rotation_state_persists(self, tmp_path):
        """Rotation metadata (rotated_at) survives close/reopen."""
        db_path = str(tmp_path / "rotate-persist.db")

        store1 = TokenStore(db_path)
        store1.issue("rotate-node")
        store1.issue("rotate-node")  # rotate
        nodes1 = store1.list_nodes()
        assert nodes1[0]["rotated_at"] is not None
        rotated_at = nodes1[0]["rotated_at"]
        store1.close()

        store2 = TokenStore(db_path)
        nodes2 = store2.list_nodes()
        assert nodes2[0]["rotated_at"] == rotated_at
        store2.close()

    def test_revocation_persists(self, tmp_path):
        """Revoked nodes stay revoked after reopen."""
        db_path = str(tmp_path / "revoke-persist.db")

        store1 = TokenStore(db_path)
        store1.issue("revoke-me")
        store1.issue("keep-me")
        store1.revoke("revoke-me")
        assert len(store1.list_nodes()) == 1
        store1.close()

        store2 = TokenStore(db_path)
        nodes = store2.list_nodes()
        assert len(nodes) == 1
        assert nodes[0]["node_id"] == "keep-me"
        store2.close()

    def test_multiple_close_reopen_cycles(self, tmp_path):
        """Data survives multiple open/close/reopen cycles."""
        db_path = str(tmp_path / "multi-cycle.db")

        for i in range(5):
            store = TokenStore(db_path)
            store.issue(f"cycle-node-{i}")
            store.close()

        store_final = TokenStore(db_path)
        nodes = store_final.list_nodes()
        assert len(nodes) == 5
        node_ids = {n["node_id"] for n in nodes}
        assert node_ids == {f"cycle-node-{i}" for i in range(5)}
        store_final.close()


# =========================================================================
# TokenStore concurrent access
# =========================================================================


class TestTokenStoreConcurrency:
    """Thread safety of TokenStore operations.

    SQLite's default mode binds connections to the creating thread.
    The correct multi-threaded pattern is one TokenStore per thread,
    all pointing to the same DB file. SQLite handles file-level locking.
    """

    def test_concurrent_issue_different_nodes(self, tmp_path):
        """Multiple threads issuing tokens via separate TokenStore instances."""
        db_path = str(tmp_path / "concurrent.db")
        errors = []

        def issue_node(node_id):
            try:
                store = TokenStore(db_path)
                store.issue(node_id)
                store.close()
            except Exception as e:
                errors.append(e)

        threads = [
            threading.Thread(target=issue_node, 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, f"Errors during concurrent issue: {errors}"
        store = TokenStore(db_path)
        nodes = store.list_nodes()
        assert len(nodes) == 20
        store.close()

    def test_concurrent_issue_and_list(self, tmp_path):
        """Issuing tokens while listing nodes from separate stores."""
        db_path = str(tmp_path / "issue-list.db")
        errors = []

        def issue_nodes():
            try:
                store = TokenStore(db_path)
                for i in range(10):
                    store.issue(f"issuer-{i}")
                store.close()
            except Exception as e:
                errors.append(e)

        def list_nodes():
            try:
                store = TokenStore(db_path)
                for _ in range(10):
                    store.list_nodes()
                store.close()
            except Exception as e:
                errors.append(e)

        t1 = threading.Thread(target=issue_nodes)
        t2 = threading.Thread(target=list_nodes)
        t1.start()
        t2.start()
        t1.join()
        t2.join()

        assert len(errors) == 0, f"Errors: {errors}"

    def test_concurrent_issue_and_revoke(self, tmp_path):
        """Issuing and revoking different nodes from separate stores."""
        db_path = str(tmp_path / "issue-revoke.db")

        # Pre-populate nodes to revoke
        store = TokenStore(db_path)
        for i in range(10):
            store.issue(f"revoke-target-{i}")
        store.close()

        errors = []

        def issue_new():
            try:
                s = TokenStore(db_path)
                for i in range(10):
                    s.issue(f"new-node-{i}")
                s.close()
            except Exception as e:
                errors.append(e)

        def revoke_old():
            try:
                s = TokenStore(db_path)
                for i in range(10):
                    s.revoke(f"revoke-target-{i}")
                s.close()
            except Exception as e:
                errors.append(e)

        t1 = threading.Thread(target=issue_new)
        t2 = threading.Thread(target=revoke_old)
        t1.start()
        t2.start()
        t1.join()
        t2.join()

        assert len(errors) == 0, f"Errors: {errors}"
        final_store = TokenStore(db_path)
        nodes = final_store.list_nodes()
        node_ids = {n["node_id"] for n in nodes}
        # All revoke targets should be gone
        for i in range(10):
            assert f"revoke-target-{i}" not in node_ids
        # All new nodes should exist
        for i in range(10):
            assert f"new-node-{i}" in node_ids
        final_store.close()

    def test_shared_store_requires_same_thread(self, tmp_path):
        """Documents that sharing a single TokenStore across threads
        raises ProgrammingError (known SQLite limitation)."""
        db_path = str(tmp_path / "shared.db")
        store = TokenStore(db_path)
        error_type = None

        def use_from_other_thread():
            nonlocal error_type
            try:
                store.issue("cross-thread-node")
            except Exception as e:
                error_type = type(e)

        t = threading.Thread(target=use_from_other_thread)
        t.start()
        t.join()
        store.close()

        # SQLite raises ProgrammingError when connection is used from wrong thread
        import sqlite3
        assert error_type is sqlite3.ProgrammingError


# =========================================================================
# Special / adversarial node_id values
# =========================================================================


class TestSpecialNodeIDs:
    """Edge cases for node_id values in token generation."""

    def test_empty_node_id(self):
        """Empty node_id produces a valid token (behavior is caller's responsibility)."""
        token = generate_node_token("", secret=SECRET)
        valid, nid = verify_node_token(token, secret=SECRET)
        assert valid is True
        assert nid == ""

    def test_very_long_node_id(self):
        """Long node_id (1000 chars) works correctly."""
        long_id = "a" * 1000
        token = generate_node_token(long_id, secret=SECRET)
        valid, nid = verify_node_token(token, secret=SECRET)
        assert valid is True
        assert nid == long_id

    def test_unicode_node_id(self):
        """Unicode node_id works correctly through FIPS HMAC."""
        token = generate_node_token("node-\u00e9\u00e8\u00ea", secret=SECRET)
        valid, nid = verify_node_token(token, secret=SECRET)
        assert valid is True
        assert nid == "node-\u00e9\u00e8\u00ea"

    def test_numeric_node_id(self):
        """Numeric-looking node_id doesn't confuse timestamp parsing."""
        token = generate_node_token("12345678", secret=SECRET)
        valid, nid = verify_node_token(token, secret=SECRET)
        assert valid is True
        assert nid == "12345678"

    def test_node_id_with_special_chars(self):
        """Special characters (except dots) in node_id."""
        special_ids = ["node@host", "node:8000", "node/path", "node#1"]
        for sid in special_ids:
            token = generate_node_token(sid, secret=SECRET)
            valid, nid = verify_node_token(token, secret=SECRET)
            assert valid is True, f"Failed for node_id='{sid}'"
            assert nid == sid

    def test_node_id_with_dots_breaks_format(self):
        """Node IDs containing dots break the 4-part token format.

        This documents a known constraint: dots in node_id produce
        tokens with more than 4 parts, which fails verification.
        """
        token = generate_node_token("node.with.dots", secret=SECRET)
        # Token will have >4 parts when split on '.'
        parts = token.split(".")
        assert len(parts) > 4  # Broken format
        # Verification should fail due to wrong part count
        valid, _ = verify_node_token(token, secret=SECRET)
        assert valid is False

    def test_node_id_whitespace(self):
        """Whitespace in node_id is preserved through HMAC signing."""
        token = generate_node_token("node with spaces", secret=SECRET)
        valid, nid = verify_node_token(token, secret=SECRET)
        assert valid is True
        assert nid == "node with spaces"


# =========================================================================
# Cross-version payload migration
# =========================================================================


class TestCrossVersionMigration:
    """Migrate encrypted payloads between encryption versions."""

    def test_v0_to_v1_migration(self):
        """Decrypt a v0 payload, re-encrypt as v1."""
        import importlib
        from greymatter_core.security import encryption

        payload = {"table": "observations", "rows": [{"id": "obs-1"}]}

        # Create v0 payload by mocking out AESGCM
        with mock.patch.dict(
            "sys.modules", {"cryptography.hazmat.primitives.ciphers.aead": None}
        ):
            importlib.reload(encryption)
            try:
                enc_v0 = encryption.encrypt_sync_payload(payload, secret=SECRET)
                assert enc_v0["v"] == 0

                # Decrypt v0
                decrypted = encryption.decrypt_sync_payload(enc_v0, secret=SECRET)
                assert decrypted == payload
            finally:
                importlib.reload(encryption)

        # Re-encrypt as v1 (after restoring AESGCM)
        enc_v1 = encrypt_sync_payload(decrypted, secret=SECRET)
        assert enc_v1["v"] == 1

        # Verify v1 decrypts correctly
        result = decrypt_sync_payload(enc_v1, secret=SECRET)
        assert result == payload

    def test_v1_re_encrypt_idempotency(self):
        """Decrypt v1 and re-encrypt produces a valid (but different) v1 payload."""
        payload = {"data": "idempotency test"}
        enc1 = encrypt_sync_payload(payload, secret=SECRET)
        assert enc1["v"] == 1

        # Decrypt and re-encrypt
        decrypted = decrypt_sync_payload(enc1, secret=SECRET)
        enc2 = encrypt_sync_payload(decrypted, secret=SECRET)
        assert enc2["v"] == 1

        # Different ciphertext (random nonce)
        assert enc1["ct"] != enc2["ct"]
        assert enc1["nonce"] != enc2["nonce"]

        # But same plaintext
        result = decrypt_sync_payload(enc2, secret=SECRET)
        assert result == payload

    def test_batch_v0_to_v1_migration(self):
        """Migrate a batch of v0 payloads to v1."""
        import importlib
        from greymatter_core.security import encryption

        payloads = [{"id": i, "data": f"record-{i}"} for i in range(10)]

        # Create v0 payloads
        v0_encrypted = []
        with mock.patch.dict(
            "sys.modules", {"cryptography.hazmat.primitives.ciphers.aead": None}
        ):
            importlib.reload(encryption)
            try:
                for p in payloads:
                    v0_encrypted.append(
                        encryption.encrypt_sync_payload(p, secret=SECRET)
                    )
                # Decrypt all v0
                decrypted = [
                    encryption.decrypt_sync_payload(e, secret=SECRET)
                    for e in v0_encrypted
                ]
            finally:
                importlib.reload(encryption)

        # Re-encrypt all as v1
        v1_encrypted = [encrypt_sync_payload(d, secret=SECRET) for d in decrypted]

        # Verify all v1 payloads
        for i, enc in enumerate(v1_encrypted):
            assert enc["v"] == 1
            result = decrypt_sync_payload(enc, secret=SECRET)
            assert result == payloads[i]


# =========================================================================
# Cluster secret auto-generation
# =========================================================================


class TestClusterSecretAutoGeneration:
    """Verify _get_cluster_secret auto-generates and persists a secret."""

    def test_generates_secret_when_missing(self, tmp_path, monkeypatch):
        """Auto-generates a cluster secret file when it doesn't exist."""
        secret_path = tmp_path / "cluster.key"
        monkeypatch.delenv("GM_CLUSTER_SECRET", raising=False)
        monkeypatch.setenv("GM_CLUSTER_SECRET_PATH", str(secret_path))

        from greymatter_core.security.tokens import _get_cluster_secret

        secret = _get_cluster_secret()
        assert isinstance(secret, bytes)
        assert len(secret) == 64  # fips_random_bytes(64)
        assert secret_path.exists()
        assert secret_path.read_bytes() == secret

    def test_persists_across_calls(self, tmp_path, monkeypatch):
        """Same secret is returned on subsequent calls."""
        secret_path = tmp_path / "cluster.key"
        monkeypatch.delenv("GM_CLUSTER_SECRET", raising=False)
        monkeypatch.setenv("GM_CLUSTER_SECRET_PATH", str(secret_path))

        from greymatter_core.security.tokens import _get_cluster_secret

        s1 = _get_cluster_secret()
        s2 = _get_cluster_secret()
        assert s1 == s2

    def test_file_permissions(self, tmp_path, monkeypatch):
        """Generated secret file should have restrictive permissions."""
        secret_path = tmp_path / "cluster.key"
        monkeypatch.delenv("GM_CLUSTER_SECRET", raising=False)
        monkeypatch.setenv("GM_CLUSTER_SECRET_PATH", str(secret_path))

        from greymatter_core.security.tokens import _get_cluster_secret

        _get_cluster_secret()
        mode = oct(secret_path.stat().st_mode & 0o777)
        assert mode == "0o600", f"Expected 0o600, got {mode}"

    def test_env_var_takes_precedence_over_file(self, tmp_path, monkeypatch):
        """GM_CLUSTER_SECRET env var takes precedence over file."""
        secret_path = tmp_path / "cluster.key"
        secret_path.write_bytes(b"file-secret")
        monkeypatch.setenv("GM_CLUSTER_SECRET", "env-secret")
        monkeypatch.setenv("GM_CLUSTER_SECRET_PATH", str(secret_path))

        from greymatter_core.security.tokens import _get_cluster_secret

        secret = _get_cluster_secret()
        assert secret == b"env-secret"


# =========================================================================
# Full node lifecycle simulation
# =========================================================================


class TestFullNodeLifecycle:
    """End-to-end simulation: node onboarding, secure communication,
    key rotation, and decommission — all through FIPS crypto."""

    CLUSTER_SECRET = b"lifecycle-cluster-secret-32byte!"

    def test_node_onboarding_to_decommission(self, tmp_path):
        """Complete lifecycle:
        1. Node registers and gets auth token
        2. Node gets unique DB encryption key
        3. Node generates IDs for entities
        4. Node sends encrypted sync payloads
        5. Peer node receives and decrypts
        6. Token rotation
        7. Node decommission (revoke token)
        """
        store = TokenStore(str(tmp_path / "lifecycle.db"))

        # --- Step 1: Registration ---
        token = store.issue("lifecycle-node")
        valid, nid = verify_node_token(token, secret=None)
        assert isinstance(token, str)
        assert len(token.split(".")) == 4

        # --- Step 2: DB encryption key ---
        db_key = get_sqlcipher_key("lifecycle-node", secret=self.CLUSTER_SECRET)
        assert len(db_key) == 64
        assert re.fullmatch(r"[0-9a-f]{64}", db_key)

        # --- Step 3: Generate entity IDs ---
        pattern_id = generate_pattern_id()
        knowledge_id = generate_knowledge_id()
        work_id = generate_work_item_id()
        assert pattern_id.startswith("pat-")
        assert knowledge_id.startswith("kn-")
        assert work_id.startswith("wi-")

        # --- Step 4: Encrypt sync payload ---
        payload = {
            "table": "observations",
            "rows": [
                {"id": pattern_id, "type": "code", "content": "Deployed v2.3"},
                {"id": knowledge_id, "type": "insight", "content": "FIPS works"},
            ],
        }
        encrypted = encrypt_sync_payload(payload, secret=self.CLUSTER_SECRET)
        assert encrypted["v"] == 1

        # --- Step 5: Peer decrypts ---
        transmitted = json.loads(json.dumps(encrypted))
        decrypted = decrypt_sync_payload(transmitted, secret=self.CLUSTER_SECRET)
        assert decrypted == payload
        assert len(decrypted["rows"]) == 2

        # --- Step 6: Token rotation ---
        new_token = store.issue("lifecycle-node")
        assert new_token != token
        nodes = store.list_nodes()
        assert len(nodes) == 1
        assert nodes[0]["rotated_at"] is not None

        # --- Step 7: Decommission ---
        store.revoke("lifecycle-node")
        assert len(store.list_nodes()) == 0

        store.close()

    def test_multi_node_fleet_lifecycle(self, tmp_path):
        """Multiple nodes in a fleet: all get unique tokens, keys, and IDs."""
        store = TokenStore(str(tmp_path / "fleet.db"))
        node_names = [f"fleet-node-{i}" for i in range(5)]

        tokens = {}
        db_keys = {}
        entity_ids = {}

        # Register all nodes
        for name in node_names:
            tokens[name] = store.issue(name)
            db_keys[name] = get_sqlcipher_key(name, secret=self.CLUSTER_SECRET)
            entity_ids[name] = generate_pattern_id()

        # All tokens unique
        assert len(set(tokens.values())) == 5
        # All DB keys unique
        assert len(set(db_keys.values())) == 5
        # All entity IDs unique
        assert len(set(entity_ids.values())) == 5

        # Each node can encrypt and any node can decrypt
        for sender in node_names:
            payload = {"from": sender, "data": f"hello from {sender}"}
            enc = encrypt_sync_payload(payload, secret=self.CLUSTER_SECRET)
            for receiver in node_names:
                dec = decrypt_sync_payload(enc, secret=self.CLUSTER_SECRET)
                assert dec == payload

        store.close()


# =========================================================================
# open_encrypted_sqlite fallback
# =========================================================================


class TestOpenEncryptedSqliteFallback:
    """Verify open_encrypted_sqlite falls back to standard sqlite3
    when pysqlcipher3 is not available."""

    def test_fallback_to_standard_sqlite(self, tmp_path):
        """Without pysqlcipher3, should fall back to unencrypted sqlite3."""
        db_path = str(tmp_path / "fallback.db")
        key = get_sqlcipher_key("fallback-node", secret=SECRET)

        # Mock pysqlcipher3 as unavailable
        with mock.patch.dict("sys.modules", {"pysqlcipher3": None, "pysqlcipher3.dbapi2": None}):
            conn = open_encrypted_sqlite(db_path, key)

        # Should be a standard sqlite3 connection
        assert isinstance(conn, sqlite3.Connection)

        # Should be functional
        conn.execute("CREATE TABLE test (id TEXT)")
        conn.execute("INSERT INTO test VALUES ('hello')")
        conn.commit()
        row = conn.execute("SELECT id FROM test").fetchone()
        assert row[0] == "hello"
        conn.close()

    def test_fallback_db_is_readable_by_standard_sqlite(self, tmp_path):
        """Fallback DB should be readable by standard sqlite3."""
        db_path = str(tmp_path / "readable.db")
        key = get_sqlcipher_key("node-1", secret=SECRET)

        with mock.patch.dict("sys.modules", {"pysqlcipher3": None, "pysqlcipher3.dbapi2": None}):
            conn1 = open_encrypted_sqlite(db_path, key)
            conn1.execute("CREATE TABLE test (value INTEGER)")
            conn1.execute("INSERT INTO test VALUES (42)")
            conn1.commit()
            conn1.close()

        # Read with standard sqlite3
        conn2 = sqlite3.connect(db_path)
        row = conn2.execute("SELECT value FROM test").fetchone()
        assert row[0] == 42
        conn2.close()


# =========================================================================
# Multiprocessing safety
# =========================================================================


def _mp_random_bytes_worker(_):
    """Worker function for multiprocessing test."""
    return fips_random_bytes(32)


def _mp_sha256_worker(data):
    """Worker function for multiprocessing SHA-256 test."""
    return fips_sha256(data)


def _mp_hmac_worker(args):
    """Worker function for multiprocessing HMAC test."""
    key, data = args
    return fips_hmac_sha256(key, data)


class TestMultiprocessingSafety:
    """FIPS crypto functions must work correctly across process boundaries.

    Unlike threads, processes don't share memory, so this tests that
    the FIPS module initializes correctly in forked/spawned workers.
    """

    def test_random_bytes_in_multiple_processes(self):
        """Random bytes generated in parallel processes should all be unique."""
        with multiprocessing.Pool(4) as pool:
            results = pool.map(_mp_random_bytes_worker, range(20))

        assert len(results) == 20
        assert all(isinstance(r, bytes) and len(r) == 32 for r in results)
        # All should be unique
        assert len(set(results)) == 20

    def test_sha256_in_multiple_processes(self):
        """SHA-256 across processes produces correct, matching results."""
        inputs = [os.urandom(64) for _ in range(20)]
        expected = [hashlib.sha256(d).digest() for d in inputs]

        with multiprocessing.Pool(4) as pool:
            results = pool.map(_mp_sha256_worker, inputs)

        assert results == expected

    def test_hmac_in_multiple_processes(self):
        """HMAC-SHA256 across processes produces correct results."""
        key = b"multiprocessing-test-key"
        inputs = [(key, os.urandom(64)) for _ in range(20)]
        expected = [
            _hmac.new(key, data, hashlib.sha256).digest() for _, data in inputs
        ]

        with multiprocessing.Pool(4) as pool:
            results = pool.map(_mp_hmac_worker, inputs)

        assert results == expected


# =========================================================================
# FIPS choice modulo bias validation
# =========================================================================


class TestFipsChoiceModuloBias:
    """Validate that fips_choice doesn't exhibit significant modulo bias
    for various sequence lengths, especially powers-of-2 boundaries."""

    @pytest.mark.parametrize("seq_len", [2, 3, 5, 7, 10, 16, 26, 36, 50])
    def test_choice_distribution_for_sequence_length(self, seq_len):
        """fips_choice should produce a roughly uniform distribution
        across various sequence lengths."""
        seq = list(range(seq_len))
        n_samples = seq_len * 500  # 500 samples per element for statistical power
        counts = Counter()
        for _ in range(n_samples):
            counts[fips_choice(seq)] += 1

        expected_per_elem = n_samples / seq_len
        # Chi-squared test: allow up to 3x the critical value for robustness
        chi_squared = sum(
            (count - expected_per_elem) ** 2 / expected_per_elem
            for count in counts.values()
        )
        # Critical chi-squared for p=0.001 at df = seq_len - 1
        # Use generous bound: 4 * df as threshold
        threshold = 4 * (seq_len - 1)
        assert chi_squared < threshold, (
            f"Non-uniform distribution for seq_len={seq_len}: "
            f"chi^2={chi_squared:.1f} > {threshold}"
        )

    def test_choice_with_tuple(self):
        """fips_choice works with tuple sequences."""
        seq = (10, 20, 30, 40, 50)
        results = {fips_choice(seq) for _ in range(100)}
        assert results == set(seq)

    def test_choice_with_range(self):
        """fips_choice works with range objects."""
        seq = range(5)
        results = {fips_choice(seq) for _ in range(100)}
        assert results == {0, 1, 2, 3, 4}


# =========================================================================
# Payload ordering stability
# =========================================================================


class TestPayloadOrderingStability:
    """Verify that dict key ordering is preserved through encrypt/decrypt."""

    def test_key_ordering_preserved(self):
        """Dict keys maintain their order through JSON serialization in the
        encrypt/decrypt pipeline."""
        payload = {"zebra": 1, "apple": 2, "mango": 3}
        encrypted = encrypt_sync_payload(payload, secret=SECRET)
        decrypted = decrypt_sync_payload(encrypted, secret=SECRET)
        assert list(decrypted.keys()) == list(payload.keys())

    def test_nested_structure_preserved(self):
        """Deeply nested structures survive encryption roundtrip."""
        payload = {
            "level1": {
                "level2": {
                    "level3": [
                        {"a": 1, "b": [2, 3]},
                        {"c": None, "d": True, "e": False},
                    ]
                }
            },
            "empty": {},
            "list": [],
        }
        encrypted = encrypt_sync_payload(payload, secret=SECRET)
        decrypted = decrypt_sync_payload(encrypted, secret=SECRET)
        assert decrypted == payload

    def test_numeric_precision_preserved(self):
        """Floating-point numbers survive the roundtrip."""
        payload = {
            "int": 42,
            "float": 3.14159265358979,
            "negative": -1.5,
            "zero": 0,
            "large": 10**15,
        }
        encrypted = encrypt_sync_payload(payload, secret=SECRET)
        decrypted = decrypt_sync_payload(encrypted, secret=SECRET)
        assert decrypted == payload


# =========================================================================
# Streaming / incremental hashing patterns
# =========================================================================


class TestIncrementalCryptoPatterns:
    """Verify FIPS primitives can be composed for streaming operations."""

    def test_chunked_hmac_integrity(self):
        """HMAC of concatenated chunks matches HMAC of full data.

        This validates the pattern used in sync payload construction
        where multiple fields are concatenated before HMAC.
        """
        chunks = [os.urandom(256) for _ in range(10)]
        full_data = b"".join(chunks)

        key = fips_random_bytes(32)
        hmac_full = fips_hmac_sha256(key, full_data)

        # Stdlib hmac allows incremental update
        h = _hmac.new(key, digestmod=hashlib.sha256)
        for chunk in chunks:
            h.update(chunk)
        hmac_incremental = h.digest()

        assert hmac_full == hmac_incremental

    def test_sha256_hash_chaining(self):
        """Hash chaining: H(H(H(data))) is deterministic."""
        data = b"initial data"
        h1 = fips_sha256(data)
        h2 = fips_sha256(h1)
        h3 = fips_sha256(h2)

        # Repeat and verify determinism
        assert fips_sha256(fips_sha256(fips_sha256(data))) == h3

    def test_merkle_tree_leaf_hashing(self):
        """FIPS SHA-256 can build Merkle tree hashes correctly."""
        leaves = [f"leaf-{i}".encode() for i in range(4)]
        leaf_hashes = [fips_sha256(leaf) for leaf in leaves]

        # Combine pairwise
        node01 = fips_sha256(leaf_hashes[0] + leaf_hashes[1])
        node23 = fips_sha256(leaf_hashes[2] + leaf_hashes[3])
        root = fips_sha256(node01 + node23)

        # Root should be deterministic
        expected_root = hashlib.sha256(
            hashlib.sha256(
                hashlib.sha256(leaves[0]).digest()
                + hashlib.sha256(leaves[1]).digest()
            ).digest()
            + hashlib.sha256(
                hashlib.sha256(leaves[2]).digest()
                + hashlib.sha256(leaves[3]).digest()
            ).digest()
        ).digest()
        assert root == expected_root


# =========================================================================
# Large batch operations — data integrity under load
# =========================================================================


class TestLargeBatchIntegrity:
    """Verify FIPS crypto operations maintain data integrity
    under high-volume operations."""

    def test_1000_encrypt_decrypt_roundtrips(self):
        """1000 unique payloads all roundtrip correctly."""
        payloads = [{"id": i, "data": f"payload-{i}", "val": i * 1.5} for i in range(1000)]
        for payload in payloads:
            enc = encrypt_sync_payload(payload, secret=SECRET)
            dec = decrypt_sync_payload(enc, secret=SECRET)
            assert dec == payload, f"Mismatch at id={payload['id']}"

    def test_1000_token_roundtrips(self):
        """1000 tokens all generate and verify correctly."""
        for i in range(1000):
            node_id = f"batch-node-{i}"
            token = generate_node_token(node_id, secret=SECRET)
            valid, nid = verify_node_token(token, secret=SECRET)
            assert valid is True, f"Token {i} failed verification"
            assert nid == node_id

    def test_10000_id_uniqueness(self):
        """10,000 IDs across all generators are unique."""
        all_ids = set()
        generators = [
            generate_pattern_id,
            generate_knowledge_id,
            generate_work_item_id,
            generate_build_run_id,
            generate_document_id,
            generate_contact_id,
            generate_lineage_id,
            generate_mood_id,
        ]
        for gen_fn in generators:
            for _ in range(1250):
                all_ids.add(gen_fn())
        # 8 generators * 1250 = 10,000
        assert len(all_ids) == 10_000


# =========================================================================
# FIPS wrapper with concurrent encrypt + verify (realistic workload)
# =========================================================================


class TestRealisticConcurrentWorkload:
    """Simulates a realistic concurrent workload where multiple
    operations happen simultaneously."""

    def test_concurrent_auth_and_sync(self):
        """Parallel token verification and payload encryption."""
        errors = []
        results = {"tokens": [], "payloads": []}
        lock = threading.Lock()

        def auth_work():
            try:
                for i in range(50):
                    token = generate_node_token(f"auth-node-{i}", secret=SECRET)
                    valid, _ = verify_node_token(token, secret=SECRET)
                    assert valid
                    with lock:
                        results["tokens"].append(token)
            except Exception as e:
                errors.append(("auth", e))

        def sync_work():
            try:
                for i in range(50):
                    payload = {"id": i, "data": f"sync-{i}"}
                    enc = encrypt_sync_payload(payload, secret=SECRET)
                    dec = decrypt_sync_payload(enc, secret=SECRET)
                    assert dec == payload
                    with lock:
                        results["payloads"].append(enc)
            except Exception as e:
                errors.append(("sync", e))

        def id_work():
            try:
                for _ in range(200):
                    generate_pattern_id()
            except Exception as e:
                errors.append(("id", e))

        threads = [
            threading.Thread(target=auth_work),
            threading.Thread(target=sync_work),
            threading.Thread(target=id_work),
            threading.Thread(target=auth_work),
            threading.Thread(target=sync_work),
        ]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0, f"Concurrent workload errors: {errors}"
        assert len(results["tokens"]) == 100
        assert len(results["payloads"]) == 100
        # All tokens unique
        assert len(set(results["tokens"])) == 100


# =========================================================================
# FIPS random quality: byte value distribution
# =========================================================================


class TestRandomQuality:
    """Statistical tests for the quality of FIPS random output."""

    def test_byte_value_distribution(self):
        """All 256 byte values should appear in a large random sample."""
        sample = fips_random_bytes(8192)
        unique_bytes = set(sample)
        # With 8192 random bytes, all 256 values should appear
        # (probability of missing any value is astronomically low)
        assert len(unique_bytes) == 256, (
            f"Expected all 256 byte values, got {len(unique_bytes)}"
        )

    def test_sequential_calls_independent(self):
        """Sequential random calls should not produce correlated output."""
        samples = [fips_random_bytes(32) for _ in range(100)]
        # No two consecutive samples should be identical
        for i in range(len(samples) - 1):
            assert samples[i] != samples[i + 1]

    def test_random_bytes_bit_balance(self):
        """Roughly 50% of bits should be set in a large random sample."""
        sample = fips_random_bytes(10000)
        total_bits = len(sample) * 8
        set_bits = sum(bin(b).count("1") for b in sample)
        ratio = set_bits / total_bits
        # Should be within 2% of 0.5
        assert 0.48 < ratio < 0.52, (
            f"Bit balance ratio {ratio:.4f} is outside [0.48, 0.52]"
        )


# =========================================================================
# FIPS primitive type safety
# =========================================================================


class TestTypeSafety:
    """Verify FIPS functions handle type errors gracefully."""

    def test_sha256_rejects_string_input(self):
        """fips_sha256 expects bytes, not str."""
        with pytest.raises(TypeError):
            fips_sha256("not bytes")  # type: ignore

    def test_hmac_rejects_string_key(self):
        """fips_hmac_sha256 expects bytes key."""
        with pytest.raises(TypeError):
            fips_hmac_sha256("string-key", b"data")  # type: ignore

    def test_hmac_rejects_string_data(self):
        """fips_hmac_sha256 expects bytes data."""
        with pytest.raises(TypeError):
            fips_hmac_sha256(b"key", "string-data")  # type: ignore

    def test_pbkdf2_rejects_string_password(self):
        """fips_pbkdf2 expects bytes password."""
        with pytest.raises(TypeError):
            fips_pbkdf2("string-pw", b"salt", iterations=1)  # type: ignore

    def test_pbkdf2_rejects_string_salt(self):
        """fips_pbkdf2 expects bytes salt."""
        with pytest.raises(TypeError):
            fips_pbkdf2(b"pw", "string-salt", iterations=1)  # type: ignore

    def test_random_bytes_rejects_negative(self):
        """fips_random_bytes should reject negative length."""
        with pytest.raises((ValueError, OverflowError, OSError)):
            fips_random_bytes(-1)

    def test_token_bytes_returns_correct_type(self):
        """fips_token_bytes always returns bytes."""
        for n in [1, 8, 16, 32, 64, 128]:
            result = fips_token_bytes(n)
            assert type(result) is bytes
            assert len(result) == n

    def test_token_urlsafe_returns_correct_type(self):
        """fips_token_urlsafe always returns str."""
        for n in [1, 8, 16, 32, 64]:
            result = fips_token_urlsafe(n)
            assert type(result) is str
