//! High-level encryption, decryption, signing, and verification API.
//!
//! Provides a unified API for cryptographic operations with automatic algorithm
//! selection based on use case or security level.
//!
//! ## Unified API
//!
//! All operations use `CryptoConfig` for configuration:
//!
//! ```no_run
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use latticearc::unified_api::{encrypt, decrypt, CryptoConfig, UseCase};
//! use latticearc::unified_api::crypto_types::{EncryptKey, DecryptKey};
//!
//! // Symmetric encryption with use case. Generate a fresh AEAD key —
//! // `[0u8; 32]` is rejected by the AEAD constructor as a weak key.
//! let key = latticearc::primitives::rand::random_bytes(32);
//! let encrypted = encrypt(b"secret", EncryptKey::Symmetric(&key),
//!     CryptoConfig::new().force_scheme(latticearc::CryptoScheme::Symmetric))?;
//! let plaintext = decrypt(&encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new())?;
//!
//! // Hybrid encryption (ML-KEM-768 + X25519 + AES-256-GCM)
//! let (pk, sk) = latticearc::generate_hybrid_keypair()?;
//! let encrypted = encrypt(b"secret", EncryptKey::Hybrid(&pk),
//!     CryptoConfig::new().use_case(UseCase::FileStorage))?;
//! let plaintext = decrypt(&encrypted, DecryptKey::Hybrid(&sk), CryptoConfig::new())?;
//! # Ok(())
//! # }
//! ```

use chrono::Utc;

/// Validate a 256-bit AEAD key length (32 bytes).
///
/// Centralises the `if k.len() != 32 { return Err(InvalidKeyLength { 32... }) }`
/// check that previously appeared at four call sites in this module
/// (AES-256-GCM and ChaCha20-Poly1305, encrypt + decrypt). Returns
/// `Ok(())` on the right length and the structured `InvalidKeyLength`
/// error otherwise.
#[inline]
fn validate_aes256_key_length(k: &[u8]) -> Result<()> {
    if k.len() != 32 {
        return Err(CoreError::InvalidKeyLength { expected: 32, actual: k.len() });
    }
    Ok(())
}

/// Current Unix timestamp clamped to non-negative `u64`.
///
/// Centralises the timestamp idiom that previously appeared at four call
/// sites in this module. `chrono` returns `i64` seconds-since-epoch;
/// `max(0)` clamps any pre-1970 system clock to 0, and `unsigned_abs`
/// then widens the proven-non-negative value to `u64` — a total
/// conversion with no fallible `try_from` and no `as`-cast. A
/// wrong-but-bounded timestamp is preferable to a panic in audit-record
/// construction.
#[inline]
fn current_timestamp() -> u64 {
    Utc::now().timestamp().max(0).unsigned_abs()
}
use zeroize::Zeroizing;

use crate::primitives::sig::{
    fndsa::FnDsaSecurityLevel, ml_dsa::MlDsaParameterSet, slh_dsa::SlhDsaSecurityLevel,
};

use crate::unified_api::crypto_types::{
    DecryptKey, EncryptKey, EncryptedOutput, EncryptionScheme, HybridComponents,
};
use crate::unified_api::{
    CoreConfig,
    error::{CoreError, Result},
    selector::CryptoPolicyEngine,
    types::{AlgorithmSelection, CryptoConfig, SignedData, SignedMetadata},
};

use super::aes_gcm::{decrypt_aes_gcm_with_aad_internal, encrypt_aes_gcm_with_aad_internal};
use super::ed25519::{sign_ed25519_internal, verify_ed25519_internal};
use super::keygen::{
    generate_fn_dsa_keypair_with_level, generate_keypair, generate_ml_dsa_keypair,
    generate_slh_dsa_keypair,
};
use super::pq_sig::{
    sign_pq_fn_dsa_unverified, sign_pq_ml_dsa_unverified, sign_pq_slh_dsa_unverified,
    verify_pq_fn_dsa_unverified, verify_pq_ml_dsa_unverified, verify_pq_slh_dsa_unverified,
};
#[cfg(not(feature = "fips"))]
use crate::primitives::aead::{AeadCipher, chacha20poly1305::ChaCha20Poly1305Cipher};
use crate::unified_api::logging::op;
use crate::{
    log_crypto_operation_complete, log_crypto_operation_error, log_crypto_operation_start,
};

use crate::hybrid::encrypt_hybrid::{HybridCiphertext, decrypt_hybrid, encrypt_hybrid};
use crate::hybrid::pq_only;

/// Generate a hybrid ML-DSA + Ed25519 keypair for the given parameter set.
fn generate_hybrid_signing_keypair_for(
    params: MlDsaParameterSet,
) -> Result<(crate::types::PublicKey, crate::types::PrivateKey)> {
    let (pq_pk, pq_sk) = generate_ml_dsa_keypair(params)?;
    let (ed_pk, ed_sk) = generate_keypair()?;
    let combined_pk = [pq_pk.into_bytes(), ed_pk.into_bytes()].concat();
    let combined_sk = [pq_sk.expose_secret(), ed_sk.expose_secret()].concat();
    Ok((crate::types::PublicKey::new(combined_pk), crate::types::PrivateKey::new(combined_sk)))
}

/// Sign a message with a hybrid ML-DSA + Ed25519 key.
fn sign_hybrid_ml_dsa_ed25519(
    message: &[u8],
    secret_key: &[u8],
    public_key: &[u8],
    params: MlDsaParameterSet,
) -> Result<(Vec<u8>, Vec<u8>)> {
    use crate::primitives::ec::ed25519::ED25519_SECRET_KEY_LEN;
    let pq_sk_len = params.secret_key_size();
    let pq_pk_len = params.public_key_size();

    let expected_sk_len = pq_sk_len
        .checked_add(ED25519_SECRET_KEY_LEN)
        .ok_or_else(|| CoreError::InvalidKey("Secret key length overflow".to_string()))?;
    if secret_key.len() != expected_sk_len {
        return Err(CoreError::InvalidKey(format!(
            "Hybrid secret key length mismatch: expected {expected_sk_len}, got {}",
            secret_key.len()
        )));
    }
    use crate::primitives::ec::ed25519::ED25519_PUBLIC_KEY_LEN;
    let expected_pk_len = pq_pk_len
        .checked_add(ED25519_PUBLIC_KEY_LEN)
        .ok_or_else(|| CoreError::InvalidKey("Public key length overflow".to_string()))?;
    if public_key.len() != expected_pk_len {
        return Err(CoreError::InvalidKey(format!(
            "Hybrid public key length mismatch: expected {expected_pk_len}, got {}",
            public_key.len()
        )));
    }

    let pq_sk = secret_key
        .get(..pq_sk_len)
        .ok_or_else(|| CoreError::InvalidKey("Failed to split hybrid secret key".to_string()))?;
    let ed_sk = secret_key
        .get(pq_sk_len..)
        .ok_or_else(|| CoreError::InvalidKey("Failed to split hybrid secret key".to_string()))?;

    // H1 / M1 fix: bind both legs of the hybrid signature to the
    // hybrid-scheme context, NOT the per-leg pure-PQ context. Without this,
    // the ML-DSA leg of a hybrid signature is byte-identical to a standalone
    // ML-DSA signature, and an attacker can re-label the envelope's `scheme`
    // to "ml-dsa-65" and truncate the components to forge a "valid"
    // standalone signature. The pure-PQ default ctx used by
    // `sign_pq_ml_dsa_unverified` produces exactly that vulnerability —
    // call the explicit-ctx internal helper with the hybrid scheme label
    // so the ML-DSA leg cannot be reinterpreted under a different scheme.
    // Ed25519 has no native context (RFC 8032) so a SHA-512 domain digest
    // is signed instead of the raw message; the digest is fixed-size so it
    // stays under the signature-size cap regardless of message length.
    use crate::types::domains::{hash_with_context, sig_context};
    use crate::unified_api::convenience::pq_sig::{
        hybrid_scheme_label_for_param_set, sign_pq_ml_dsa_internal_with_ctx,
    };
    let hybrid_ctx = sig_context(hybrid_scheme_label_for_param_set(params));
    let pq_sig = sign_pq_ml_dsa_internal_with_ctx(message, pq_sk, params, hybrid_ctx)?;
    let ed_digest = hash_with_context(hybrid_ctx, message);
    let ed_sig = sign_ed25519_internal(&ed_digest, ed_sk)?;
    let combined_sig = [pq_sig, ed_sig].concat();
    Ok((public_key.to_vec(), combined_sig))
}

/// Check FIPS module is operational before any crypto operation.
/// On first call, runs power-up self-tests (lazy initialization).
/// No-op when `fips-self-test` feature is not enabled.
#[cfg(feature = "fips-self-test")]
pub(super) fn fips_verify_operational() -> Result<()> {
    use std::sync::Once;
    static INIT: Once = Once::new();
    INIT.call_once(|| {
        // initialize_and_test() calls std::process::abort() on failure (FIPS 140-3 §9.1).
        // If execution continues past this call, self-tests passed.
        let _passed = crate::primitives::self_test::initialize_and_test();
    });
    crate::primitives::self_test::verify_operational().map_err(|e| CoreError::SelfTestFailed {
        component: "FIPS module".to_string(),
        status: e.to_string(),
    })
}

#[cfg(not(feature = "fips-self-test"))]
#[expect(
    clippy::unnecessary_wraps,
    reason = "Result returned for API parity with the fips-self-test-enabled path; this no-op variant must keep the same signature"
)]
pub(super) fn fips_verify_operational() -> Result<()> {
    Ok(())
}

use crate::primitives::resource_limits::{
    validate_decryption_size, validate_encryption_size, validate_signature_size,
};

// ============================================================================
// Internal Helpers
// ============================================================================

/// Extract nonce and tag from `EncryptedOutput` as fixed-size arrays.
///
/// Runs on adversary-reachable input (a caller can hand `decrypt` a
/// crafted `EncryptedOutput`), so a length mismatch returns the opaque
/// `DecryptionFailed("decryption failed")` used everywhere on the
/// decrypt path (Pattern 6) — the returned error must not reveal which
/// field was malformed. The specific reason is kept in a
/// `tracing::debug!` line for operator-side diagnosis.
fn extract_nonce_tag(encrypted: &EncryptedOutput) -> Result<([u8; 12], [u8; 16])> {
    let opaque = || CoreError::DecryptionFailed("decryption failed".to_string());
    let nonce: [u8; 12] = encrypted.nonce().try_into().map_err(|_slice_err| {
        tracing::debug!(
            nonce_len = encrypted.nonce().len(),
            "decrypt rejected: nonce length != 12"
        );
        opaque()
    })?;
    let tag: [u8; 16] = encrypted.tag().try_into().map_err(|_slice_err| {
        tracing::debug!(tag_len = encrypted.tag().len(), "decrypt rejected: tag length != 16");
        opaque()
    })?;
    Ok((nonce, tag))
}

/// Select encryption scheme based on CryptoConfig, returning a type-safe enum.
///
/// This is the type-safe replacement for `select_encryption_scheme`.
/// Respects `CryptoConfig.crypto_mode` to select hybrid vs PQ-only schemes.
fn select_encryption_scheme_typed(options: &CryptoConfig) -> Result<EncryptionScheme> {
    let mode = options.get_crypto_mode();
    match options.get_selection() {
        AlgorithmSelection::UseCase(use_case) => {
            let config = CoreConfig::default();
            let scheme = CryptoPolicyEngine::recommend_encryption_scheme(use_case, &config)?;
            // PQ-only mode: convert hybrid scheme to its PQ-only equivalent
            if matches!(mode, crate::types::types::CryptoMode::PqOnly)
                && let Some(pq_scheme) = scheme.to_pq_equivalent()
            {
                return Ok(pq_scheme);
            }
            Ok(scheme)
        }
        AlgorithmSelection::SecurityLevel(level) => {
            let config = CoreConfig::default().with_security_level(*level);
            Ok(CryptoPolicyEngine::select_encryption_scheme_typed_with_mode(&config, mode))
        }
        AlgorithmSelection::ForcedScheme(scheme) => {
            let scheme_str = CryptoPolicyEngine::force_scheme(scheme);
            EncryptionScheme::parse_str(&scheme_str).ok_or_else(|| {
                CoreError::InvalidInput(format!(
                    "Forced scheme '{}' is not an encryption scheme (may be a signature scheme)",
                    scheme_str
                ))
            })
        }
    }
}

/// Select signature scheme based on CryptoConfig.
///
/// Use cases are routed through `UseCaseConfig`, which carries an explicit
/// signature `CoreConfig` (with the right `SecurityLevel`) for every use case.
/// We must NOT use `recommend_scheme(use_case...)` here — that helper returns
/// the *encryption* scheme for many use cases (e.g. `IoTDevice` →
/// `hybrid-ml-kem-512-aes-256-gcm`), which would be a fatal type error in a
/// signature dispatcher.
fn select_signature_scheme(options: &CryptoConfig) -> Result<String> {
    let pq_only = options.get_crypto_mode() == crate::types::types::CryptoMode::PqOnly;
    match options.get_selection() {
        AlgorithmSelection::UseCase(use_case) => {
            let uc_config = crate::types::config::UseCaseConfig::new(*use_case);
            if pq_only {
                Ok(CryptoPolicyEngine::select_pq_signature_scheme(&uc_config.signature)?)
            } else {
                Ok(CryptoPolicyEngine::select_signature_scheme(&uc_config.signature)?)
            }
        }
        AlgorithmSelection::SecurityLevel(level) => {
            let config = CoreConfig::default().with_security_level(*level);
            if pq_only {
                Ok(CryptoPolicyEngine::select_pq_signature_scheme(&config)?)
            } else {
                Ok(CryptoPolicyEngine::select_signature_scheme(&config)?)
            }
        }
        AlgorithmSelection::ForcedScheme(scheme) => Ok(CryptoPolicyEngine::force_scheme(scheme)),
    }
}

/// ChaCha20-Poly1305 encrypt (format: nonce || ciphertext || tag).
///
/// `aad` is bound into the AEAD tag — the same value must be supplied
/// at decrypt time. Pass `&[]` if the caller has no associated data.
#[cfg(not(feature = "fips"))]
fn encrypt_chacha20_internal(data: &[u8], key: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
    use crate::primitives::aead::AeadError;
    let cipher = ChaCha20Poly1305Cipher::new(key).map_err(|e| match e {
        // Distinguish all-zero (and other future weak-key) rejection from
        // length mismatch — collapsing both into `InvalidKeyLength {32, 32}`
        // produced the absurd message "Invalid key length: expected 32,
        // got 32".
        AeadError::WeakKey => CoreError::InvalidKey(
            "All-zero key rejected by ChaCha20-Poly1305 — generate a fresh key via \
             `latticearc::primitives::security::generate_secure_random_bytes(32)`."
                .to_string(),
        ),
        // `AeadError::InvalidKeyLength` is a unit variant — actual
        // length came from the caller's `key.len()`, expected is
        // the algorithm's KEY_LEN (32 for ChaCha20-Poly1305).
        AeadError::InvalidKeyLength => {
            CoreError::InvalidKeyLength { expected: 32, actual: key.len() }
        }
        // Replaced an earlier wildcard `_ => InvalidKeyLength { 32, 32 }`
        // arm. With `AeadError` marked `#[non_exhaustive]`, any future
        // variant routed through that arm produced the contradictory
        // diagnostic "wrong key length: expected 32, got 32". Forward
        // unknown variants as `EncryptionFailed(e.to_string())`,
        // matching the AES-GCM sibling at `aes_gcm.rs:97`.
        other => CoreError::EncryptionFailed(other.to_string()),
    })?;
    let nonce = ChaCha20Poly1305Cipher::generate_nonce();
    // Empty AAD is the no-AAD case; pass through directly. The
    // underlying `AeadCipher::encrypt` accepts `Option<&[u8]>` for
    // historical reasons, but `Some(&[])` and `None` produce
    // byte-identical output.
    let (ciphertext, tag) = cipher
        .encrypt(&nonce, data, Some(aad))
        .map_err(|e| CoreError::EncryptionFailed(e.to_string()))?;
    // Format: nonce (12) || ciphertext || tag (16)
    let mut result =
        Vec::with_capacity(12_usize.saturating_add(ciphertext.len()).saturating_add(tag.len()));
    result.extend_from_slice(&nonce);
    result.extend_from_slice(&ciphertext);
    result.extend_from_slice(&tag);
    Ok(result)
}

/// ChaCha20-Poly1305 decrypt (expects: nonce || ciphertext || tag).
///
/// `aad` must match the value supplied at encrypt time (or be empty if
/// the encrypt path used no AAD).
#[cfg(not(feature = "fips"))]
fn decrypt_chacha20_internal(
    encrypted: &[u8],
    key: &[u8],
    aad: &[u8],
) -> Result<Zeroizing<Vec<u8>>> {
    // All adversary-reachable parse/auth failures collapse to one opaque
    // returned error (HPKE RFC 9180 §5.2 / SP 800-38D §5.2.2). Internal
    // `tracing` logs keep the specific reason so operators can debug via
    // correlation IDs. See DESIGN_PATTERNS.md Pattern 6.
    let opaque = || CoreError::DecryptionFailed("decryption failed".to_string());

    if encrypted.len() < 28 {
        log_crypto_operation_error!(op::CHACHA20_DECRYPT, "input too short");
        return Err(opaque());
    }
    let nonce_slice = encrypted.get(..12).ok_or_else(|| {
        log_crypto_operation_error!(op::CHACHA20_DECRYPT, "nonce slice extraction failed");
        opaque()
    })?;
    let mut nonce = [0u8; 12];
    nonce.copy_from_slice(nonce_slice);

    let ciphertext_and_tag = encrypted.get(12..).ok_or_else(|| {
        log_crypto_operation_error!(op::CHACHA20_DECRYPT, "ct+tag slice extraction failed");
        opaque()
    })?;
    let tag_start = ciphertext_and_tag.len().saturating_sub(16);
    let ciphertext = ciphertext_and_tag.get(..tag_start).ok_or_else(|| {
        log_crypto_operation_error!(op::CHACHA20_DECRYPT, "ciphertext slice extraction failed");
        opaque()
    })?;
    let tag_slice = ciphertext_and_tag.get(tag_start..).ok_or_else(|| {
        log_crypto_operation_error!(op::CHACHA20_DECRYPT, "tag slice extraction failed");
        opaque()
    })?;
    let mut tag = [0u8; 16];
    tag.copy_from_slice(tag_slice);

    // Key init is caller-side; distinguishable in the returned error.
    use crate::primitives::aead::AeadError;
    let cipher = ChaCha20Poly1305Cipher::new(key).map_err(|e| match e {
        AeadError::WeakKey => CoreError::InvalidKey(
            "All-zero key rejected by ChaCha20-Poly1305 — generate a fresh key via \
             `latticearc::primitives::security::generate_secure_random_bytes(32)`."
                .to_string(),
        ),
        // `AeadError::InvalidKeyLength` is a unit variant — actual
        // length came from the caller's `key.len()`, expected is the
        // algorithm's KEY_LEN (32 for ChaCha20-Poly1305). Reported as a
        // structured error because key-init is caller-side, not
        // adversary-reachable (see the opacity note above).
        AeadError::InvalidKeyLength => {
            CoreError::InvalidKeyLength { expected: 32, actual: key.len() }
        }
        // `AeadError` is `#[non_exhaustive]`; a wildcard mapping to
        // `InvalidKeyLength` would mislabel any future construction-time
        // variant as the contradictory "expected 32, got 32". Forward
        // unknown variants as `DecryptionFailed`, mirroring the
        // encrypt-side catch-all at `encrypt_chacha20_internal`.
        other => CoreError::DecryptionFailed(other.to_string()),
    })?;
    // `Some(&[])` produces a zero-byte GHASH input, byte-identical to
    // `None` per RFC 8439 §2.8. Pass through unconditionally to match
    // the encrypt-side path.
    cipher.decrypt(&nonce, ciphertext, &tag, Some(aad)).map_err(|_aead_err| {
        log_crypto_operation_error!(op::CHACHA20_DECRYPT, "AEAD authentication failed");
        opaque()
    })
}

// ============================================================================
// Unified Public API
// ============================================================================

/// Encrypt data with automatic algorithm selection.
///
/// This is the **single entry point** for all encryption. The key type
/// (`EncryptKey::Symmetric` or `EncryptKey::Hybrid`) determines the cipher.
/// If the selected scheme requires a hybrid key but a symmetric key is provided
/// (or vice versa), this function returns `Err` — it **never silently degrades**.
///
/// # Examples
///
/// ```no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use latticearc::unified_api::{encrypt, CryptoConfig, UseCase, SecurityLevel};
/// use latticearc::unified_api::crypto_types::EncryptKey;
///
/// // Symmetric encryption (AES-256-GCM). Generate a fresh key — `[0u8; 32]`
/// // is rejected by the AEAD constructor as a weak key.
/// let key = latticearc::primitives::rand::random_bytes(32);
/// let encrypted = encrypt(b"secret", EncryptKey::Symmetric(&key),
///     CryptoConfig::new().force_scheme(latticearc::CryptoScheme::Symmetric))?;
///
/// // Hybrid encryption (ML-KEM-768 + X25519 + HKDF + AES-256-GCM)
/// let (pk, sk) = latticearc::generate_hybrid_keypair()?;
/// let encrypted = encrypt(b"secret", EncryptKey::Hybrid(&pk),
///     CryptoConfig::new().use_case(UseCase::FileStorage))?;
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// Returns an error if:
/// - Session is set and has expired (`CoreError::SessionExpired`)
/// - Key type doesn't match the selected scheme (no silent fallback)
/// - Data size exceeds resource limits
/// - Key length is invalid for the selected scheme
/// - Encryption operation fails
#[must_use = "encryption result must be used or errors will be silently dropped"]
pub fn encrypt(data: &[u8], key: EncryptKey<'_>, config: CryptoConfig) -> Result<EncryptedOutput> {
    encrypt_with_aad(data, key, config, &[])
}

/// Encrypt with associated data (AAD) bound into the AEAD tag.
///
/// `aad` is authenticated but not encrypted — any modification to the
/// ciphertext or to the AAD on the wire is detected at decrypt time.
/// Must be supplied byte-identical to [`decrypt_with_aad`]; pass `&[]`
/// if no AAD is needed (equivalent to [`encrypt`]).
///
/// # Errors
///
/// Same as [`encrypt`].
#[must_use = "encryption result must be used or errors will be silently dropped"]
pub fn encrypt_with_aad(
    data: &[u8],
    key: EncryptKey<'_>,
    config: CryptoConfig,
    aad: &[u8],
) -> Result<EncryptedOutput> {
    fips_verify_operational()?;
    config.validate()?;

    let scheme = select_encryption_scheme_typed(&config)?;
    config.validate_scheme_compliance(scheme.as_str())?;

    // Type-safe key-scheme validation: FAIL on mismatch, never degrade
    CryptoPolicyEngine::validate_key_matches_scheme(&key, &scheme)
        .map_err(|e| CoreError::ConfigurationError(e.to_string()))?;

    // opaque error string. Discarding the
    // upstream `ResourceError` on the adversary-reachable path is a
    // Pattern-6 defense — `ResourceError::Display` interpolates
    // `requested=N, limit=M`, leaking the configured cap.
    if let Err(e) = validate_encryption_size(data.len()) {
        tracing::debug!(error = %e, data_len = data.len(), "encrypt rejected: plaintext exceeds resource limit");
        return Err(CoreError::ResourceExceeded("plaintext exceeds resource limit".to_string()));
    }

    log_crypto_operation_start!(op::ENCRYPT, scheme = %scheme, data_size = data.len());

    let output = match (&key, &scheme) {
        // Symmetric AES-256-GCM. `_with_aad_internal` accepts empty
        // AAD (treats it as the no-AAD case) — no need to dispatch.
        (EncryptKey::Symmetric(k), EncryptionScheme::Aes256Gcm) => {
            validate_aes256_key_length(k)?;
            let encrypted = encrypt_aes_gcm_with_aad_internal(data, k, aad)?;
            symmetric_bytes_to_output(scheme, &encrypted)?
        }
        // Symmetric ChaCha20-Poly1305 (non-FIPS only)
        #[cfg(not(feature = "fips"))]
        (EncryptKey::Symmetric(k), EncryptionScheme::ChaCha20Poly1305) => {
            validate_aes256_key_length(k)?;
            let encrypted = encrypt_chacha20_internal(data, k, aad)?;
            symmetric_bytes_to_output(scheme, &encrypted)?
        }
        #[cfg(feature = "fips")]
        (EncryptKey::Symmetric(_), EncryptionScheme::ChaCha20Poly1305) => {
            return Err(CoreError::ComplianceViolation(
                "ChaCha20-Poly1305 is not FIPS 140-3 approved. Use AES-256-GCM.".to_string(),
            ));
        }
        // Hybrid ML-KEM + X25519 + HKDF + AES-256-GCM
        (EncryptKey::Hybrid(pk), _) if scheme.requires_hybrid_key() => {
            let ctx = if aad.is_empty() {
                None
            } else {
                Some(crate::hybrid::encrypt_hybrid::HybridEncryptionContext::with_aad(aad.to_vec()))
            };
            let ct = encrypt_hybrid(pk, data, ctx.as_ref()).map_err(|e| {
                CoreError::EncryptionFailed(format!("Hybrid encryption failed: {e}"))
            })?;
            let timestamp = current_timestamp();
            EncryptedOutput::new(
                scheme,
                ct.symmetric_ciphertext().to_vec(),
                ct.nonce().to_vec(),
                ct.tag().to_vec(),
                Some(HybridComponents::new(
                    ct.kem_ciphertext().to_vec(),
                    ct.ecdh_ephemeral_pk().to_vec(),
                )),
                timestamp,
                None,
            )
            // `EncryptedOutput::new` fails on a scheme/component-shape
            // mismatch — a construction-invariant violation, not an
            // encryption failure (the cipher op above already succeeded).
            .map_err(|e| CoreError::ConfigurationError(e.to_string()))?
        }
        // PQ-only ML-KEM + HKDF + AES-256-GCM (no X25519)
        (EncryptKey::PqOnly(pk), _) if scheme.requires_pq_key() => {
            let ct = pq_only::encrypt_pq_only_with_aad(pk, data, aad).map_err(|e| {
                CoreError::EncryptionFailed(format!("PQ-only encryption failed: {e}"))
            })?;
            let timestamp = current_timestamp();
            let (kem_ct, sym_ct, nonce, tag) = ct.into_parts();
            EncryptedOutput::new(
                scheme,
                sym_ct,
                nonce.to_vec(),
                tag.to_vec(),
                Some(HybridComponents::new(kem_ct, vec![])),
                timestamp,
                None,
            )
            // Construction-invariant violation, not an encryption
            // failure — see the hybrid arm above.
            .map_err(|e| CoreError::ConfigurationError(e.to_string()))?
        }
        // This arm should be unreachable due to validate_key_matches_scheme above
        _ => {
            return Err(CoreError::InvalidInput(format!(
                "Key type does not match scheme '{}'",
                scheme
            )));
        }
    };

    log_crypto_operation_complete!(op::ENCRYPT, result_size = output.ciphertext().len(), scheme = %output.scheme());

    Ok(output)
}

/// Convert raw symmetric ciphertext bytes (nonce || ciphertext || tag) into EncryptedOutput.
fn symmetric_bytes_to_output(
    scheme: EncryptionScheme,
    encrypted: &[u8],
) -> Result<EncryptedOutput> {
    if encrypted.len() < 28 {
        return Err(CoreError::EncryptionFailed(
            "Encrypted data too short (need nonce + tag)".to_string(),
        ));
    }
    let nonce = encrypted
        .get(..12)
        .ok_or_else(|| CoreError::EncryptionFailed("Failed to extract nonce".to_string()))?
        .to_vec();

    let tag_start = encrypted.len().saturating_sub(16);
    let tag = encrypted
        .get(tag_start..)
        .ok_or_else(|| CoreError::EncryptionFailed("Failed to extract tag".to_string()))?
        .to_vec();

    let timestamp = current_timestamp();

    // A scheme/component-shape mismatch is a construction-invariant
    // violation, not an encryption failure — see the hybrid/PQ-only arms.
    EncryptedOutput::new(scheme, encrypted.to_vec(), nonce, tag, None, timestamp, None)
        .map_err(|e| CoreError::ConfigurationError(e.to_string()))
}

/// Decrypt data encrypted by `encrypt()`.
///
/// The decryption algorithm is determined by `encrypted.scheme` (an enum, not a string).
/// The key type must match the scheme — mismatches return `Err`.
///
/// # Examples
///
/// ```no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use latticearc::unified_api::{encrypt, decrypt, CryptoConfig};
/// use latticearc::unified_api::crypto_types::{EncryptKey, DecryptKey};
///
/// // Fresh AEAD key — `[0u8; 32]` is rejected as a weak key.
/// let key = latticearc::primitives::rand::random_bytes(32);
/// let encrypted = encrypt(b"secret", EncryptKey::Symmetric(&key),
///     CryptoConfig::new().force_scheme(latticearc::CryptoScheme::Symmetric))?;
/// let plaintext = decrypt(&encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new())?;
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// Returns an error if:
/// - Session is set and has expired (`CoreError::SessionExpired`)
/// - Key type doesn't match the encryption scheme
/// - Encrypted data size exceeds resource limits
/// - Decryption or authentication fails
#[must_use = "decryption result must be used or errors will be silently dropped"]
pub fn decrypt(
    encrypted: &EncryptedOutput,
    key: DecryptKey<'_>,
    config: CryptoConfig,
) -> Result<Zeroizing<Vec<u8>>> {
    decrypt_with_aad(encrypted, key, config, &[])
}

/// Decrypt with associated data — must match the value supplied to
/// [`encrypt_with_aad`].
///
/// Pass `&[]` if the encrypt path used no AAD (equivalent to
/// [`decrypt`]).
///
/// # Errors
///
/// Same as [`decrypt`]. AEAD authentication failure (including AAD
/// mismatch) collapses to an opaque `DecryptionFailed`.
#[must_use = "decryption result must be used or errors will be silently dropped"]
pub fn decrypt_with_aad(
    encrypted: &EncryptedOutput,
    key: DecryptKey<'_>,
    config: CryptoConfig,
    aad: &[u8],
) -> Result<Zeroizing<Vec<u8>>> {
    fips_verify_operational()?;
    config.validate()?;
    config.validate_scheme_compliance(encrypted.scheme().as_str())?;

    // Opt-in replay protection. `max_age` is `None` by default to
    // preserve backward compatibility with callers that use the
    // timestamp purely for audit / display; when a value is set, we
    // reject ciphertexts older than the configured window. See the
    // `CryptoConfig::max_age` docstring for caveats (wall-clock
    // dependence; not a substitute for nonce-cache replay defence).
    if let Some(max_age) = config.max_age_seconds() {
        let now = current_timestamp();
        let stamped = encrypted.timestamp();
        // saturating_sub handles future-stamped ciphertexts (clock skew
        // on the sender side) by clamping at 0 — which always passes
        // the freshness check, on the assumption that "future" stamps
        // are skew, not adversarial.
        let age = now.saturating_sub(stamped);
        if age > max_age {
            // Dedicated `CoreError::Replay { .. }` variant — distinct from
            // `ResourceExceeded` so callers can pattern-match the replay
            // case and react appropriately (suspected replay alert vs.
            // genuine size-quota issue).
            return Err(CoreError::Replay { age_seconds: age, max_age_seconds: max_age });
        }
    }

    // Type-safe key-scheme validation
    CryptoPolicyEngine::validate_decrypt_key_matches_scheme(&key, encrypted.scheme())
        .map_err(|e| CoreError::ConfigurationError(e.to_string()))?;

    log_crypto_operation_start!(op::DECRYPT, scheme = %encrypted.scheme(), data_size = encrypted.ciphertext().len());

    // Opaque: don't leak configured resource-limit values (requested/limit
    // bytes) via e.to_string(). Matches the encrypt-path pre-flight in
    // primitives/aead/aes_gcm.rs:74.
    validate_decryption_size(encrypted.ciphertext().len()).map_err(|_e| {
        CoreError::ResourceExceeded("ciphertext exceeds resource limit".to_string())
    })?;

    let result: Result<Zeroizing<Vec<u8>>> = match (&key, encrypted.scheme()) {
        // Symmetric AES-256-GCM. `_with_aad_internal` accepts empty
        // AAD (treats it as the no-AAD case) — no need to dispatch.
        (DecryptKey::Symmetric(k), EncryptionScheme::Aes256Gcm) => {
            validate_aes256_key_length(k)?;
            decrypt_aes_gcm_with_aad_internal(encrypted.ciphertext(), k, aad)
        }
        // Symmetric ChaCha20-Poly1305 (non-FIPS only)
        #[cfg(not(feature = "fips"))]
        (DecryptKey::Symmetric(k), EncryptionScheme::ChaCha20Poly1305) => {
            validate_aes256_key_length(k)?;
            decrypt_chacha20_internal(encrypted.ciphertext(), k, aad)
        }
        #[cfg(feature = "fips")]
        (DecryptKey::Symmetric(_), EncryptionScheme::ChaCha20Poly1305) => {
            return Err(CoreError::ComplianceViolation(
                "ChaCha20-Poly1305 is not FIPS 140-3 approved. Use AES-256-GCM.".to_string(),
            ));
        }
        // Hybrid ML-KEM + X25519 + HKDF + AES-256-GCM
        (DecryptKey::Hybrid(sk), scheme) if scheme.requires_hybrid_key() => {
            // Pattern-6: `encrypted.scheme` is wire-format adversary
            // input. A distinct "scheme says hybrid but no hybrid_data"
            // string vs the canonical "decryption failed" below would
            // give an attacker a wrong-tag-vs-AEAD-reject oracle.
            let hybrid_data = encrypted.hybrid_data().ok_or_else(|| {
                tracing::debug!(
                    scheme = ?scheme,
                    "decrypt rejected: hybrid scheme tag but EncryptedOutput.hybrid_data is None"
                );
                CoreError::DecryptionFailed("decryption failed".to_string())
            })?;

            let ct = HybridCiphertext::new(
                hybrid_data.ml_kem_ciphertext().to_vec(),
                hybrid_data.ecdh_ephemeral_pk().to_vec(),
                encrypted.ciphertext().to_vec(),
                encrypted.nonce().to_vec(),
                encrypted.tag().to_vec(),
            );

            // `decrypt_hybrid` returns `Zeroizing<Vec<u8>>` — propagate directly,
            // do NOT `.to_vec()` which would strip the zeroizing wrapper.
            // Error collapsed to opaque: underlying HybridEncryptionError is
            // already opaque per encrypt_hybrid.rs; don't re-wrap with detail.
            let ctx = if aad.is_empty() {
                None
            } else {
                Some(crate::hybrid::encrypt_hybrid::HybridEncryptionContext::with_aad(aad.to_vec()))
            };
            decrypt_hybrid(sk, &ct, ctx.as_ref())
                .map_err(|_e| CoreError::DecryptionFailed("decryption failed".to_string()))
        }
        // PQ-only ML-KEM + HKDF + AES-256-GCM (no X25519)
        (DecryptKey::PqOnly(sk), scheme) if scheme.requires_pq_key() => {
            // Pattern-6: see hybrid arm above for rationale.
            let hybrid_data = encrypted.hybrid_data().ok_or_else(|| {
                tracing::debug!(
                    scheme = ?scheme,
                    "decrypt rejected: PQ-only scheme tag but EncryptedOutput.hybrid_data is None"
                );
                CoreError::DecryptionFailed("decryption failed".to_string())
            })?;

            let (nonce, tag) = extract_nonce_tag(encrypted)?;

            pq_only::decrypt_pq_only_with_aad(
                sk,
                hybrid_data.ml_kem_ciphertext(),
                encrypted.ciphertext(),
                &nonce,
                &tag,
                aad,
            )
            .map_err(|_e| CoreError::DecryptionFailed("decryption failed".to_string()))
        }
        // Unreachable due to validate_decrypt_key_matches_scheme above
        _ => {
            return Err(CoreError::InvalidInput(format!(
                "Key type does not match scheme '{}'",
                encrypted.scheme()
            )));
        }
    };

    match result {
        Ok(plaintext) => {
            // do NOT log `plaintext.len()` on
            // successful decrypt. Per-call plaintext length is a
            // partial-size oracle for variable-length content (e.g. a
            // request handler that decrypts a per-user blob then
            // routes — the trace target sees the size profile). The
            // encrypt-side `result_size` is the ciphertext length,
            // already public over the wire, so logging it there is
            // fine; symmetry on decrypt would leak.
            log_crypto_operation_complete!(op::DECRYPT, scheme = %encrypted.scheme());
            Ok(plaintext)
        }
        Err(e) => {
            log_crypto_operation_error!(op::DECRYPT, e, scheme = %encrypted.scheme());
            Err(e)
        }
    }
}

/// The result of [`generate_signing_keypair`]: public key, secret key, and
/// the scheme tag bound together.
///
/// Named accessors prevent losing track of which element is which (especially
/// the scheme tag, which [`sign_with_key`] needs to dispatch correctly), and
/// the manual redacting `Debug` keeps the secret key out of logs — neither
/// of which a bare `(Vec<u8>, Zeroizing<Vec<u8>>, String)` tuple guarantees.
///
/// Fields are private (Pattern 5 property 5); read them with
/// [`public_key`](Self::public_key), [`expose_secret_key`](Self::expose_secret_key),
/// and [`scheme`](Self::scheme), or consume the bundle with
/// [`into_parts`](Self::into_parts). The secret is held in `Zeroizing`, so it
/// is wiped when the keypair (or the tuple from `into_parts`) drops
/// (Pattern 5 property 1).
pub struct SigningKeypair {
    public_key: Vec<u8>,
    secret_key: Zeroizing<Vec<u8>>,
    scheme: String,
}

impl SigningKeypair {
    /// Public-key bytes for the selected scheme.
    #[must_use]
    pub fn public_key(&self) -> &[u8] {
        &self.public_key
    }

    /// Secret-key bytes. Named `expose_secret_key` — not `as_bytes` — so every
    /// read of secret material is a grep-able audit point (Anti-Pattern 2).
    #[must_use]
    pub fn expose_secret_key(&self) -> &[u8] {
        &self.secret_key
    }

    /// Scheme tag (e.g. `"hybrid-ml-dsa-65-ed25519"`). Pass this back to
    /// [`sign_with_key`] via `CryptoConfig::force_scheme(...)` so the
    /// dispatcher routes signing through the same algorithm that generated
    /// the keypair.
    #[must_use]
    pub fn scheme(&self) -> &str {
        &self.scheme
    }

    /// Consume the bundle into its raw `(public_key, secret_key, scheme)`
    /// parts. The secret stays wrapped in `Zeroizing`, so it is still wiped
    /// on drop (Pattern 5 property 1; the Anti-Pattern 5 `into_parts` escape
    /// hatch for destructuring without public fields).
    #[must_use]
    pub fn into_parts(self) -> (Vec<u8>, Zeroizing<Vec<u8>>, String) {
        (self.public_key, self.secret_key, self.scheme)
    }
}

// Manual `Debug` is required: `Zeroizing<Vec<u8>>` forwards `Debug` to the
// inner `Vec<u8>` (see zeroize crate docs), so deriving Debug here would
// print the raw secret-key bytes whenever a caller `dbg!`s or formats the
// keypair. Pattern 5 Property 2 mandates `[REDACTED]` for secret material.
impl core::fmt::Debug for SigningKeypair {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("SigningKeypair")
            .field("public_key", &format_args!("<{} bytes>", self.public_key.len()))
            .field("secret_key", &"[REDACTED]")
            .field("scheme", &self.scheme)
            .finish()
    }
}

impl From<(Vec<u8>, Zeroizing<Vec<u8>>, String)> for SigningKeypair {
    fn from(tuple: (Vec<u8>, Zeroizing<Vec<u8>>, String)) -> Self {
        Self { public_key: tuple.0, secret_key: tuple.1, scheme: tuple.2 }
    }
}

/// Generate a signing keypair for the scheme selected by config.
///
/// The scheme selector auto-picks hybrid (ML-DSA-65 + Ed25519) by default.
/// Returns a [`SigningKeypair`] bundling the public key, secret key, and
/// the scheme tag — the tag is what [`sign_with_key`] needs to dispatch
/// correctly, and the type's redacting `Debug` keeps the secret out of logs.
///
/// # Unified API
///
/// This is the keypair generation companion to [`sign_with_key`]. Together they
/// form the unified signing API — the scheme selector handles PQ-only, hybrid,
/// and classical transparently based on `CryptoConfig`.
///
/// # Errors
///
/// Returns an error if:
/// - Session is set and has expired (`CoreError::SessionExpired`)
/// - Key generation fails for the selected scheme
#[must_use = "keypair result must be used or errors will be silently dropped"]
pub fn generate_signing_keypair(config: CryptoConfig) -> Result<SigningKeypair> {
    fips_verify_operational()?;
    config.validate()?;

    let scheme = select_signature_scheme(&config)?;

    let (public_key, secret_key) = match scheme.as_str() {
        "ml-dsa-44" | "pq-ml-dsa-44" => {
            let (pk, sk) = generate_ml_dsa_keypair(MlDsaParameterSet::MlDsa44)?;
            (pk, sk)
        }
        "ml-dsa-65" | "pq-ml-dsa-65" => {
            let (pk, sk) = generate_ml_dsa_keypair(MlDsaParameterSet::MlDsa65)?;
            (pk, sk)
        }
        "ml-dsa-87" | "pq-ml-dsa-87" => {
            let (pk, sk) = generate_ml_dsa_keypair(MlDsaParameterSet::MlDsa87)?;
            (pk, sk)
        }
        "slh-dsa-shake-128s" => {
            let (pk, sk) = generate_slh_dsa_keypair(SlhDsaSecurityLevel::Shake128s)?;
            (pk, sk)
        }
        "slh-dsa-shake-192s" => {
            let (pk, sk) = generate_slh_dsa_keypair(SlhDsaSecurityLevel::Shake192s)?;
            (pk, sk)
        }
        "slh-dsa-shake-256s" => {
            let (pk, sk) = generate_slh_dsa_keypair(SlhDsaSecurityLevel::Shake256s)?;
            (pk, sk)
        }
        // Accept both the canonical "fn-dsa-512" / "fn-dsa-1024" tags
        // and the legacy bare "fn-dsa" alias (treated as Level512 for
        // backward compatibility with keys produced before the
        // explicit-suffix tags were introduced).
        "fn-dsa-512" | "fn-dsa" => {
            let (pk, sk) = generate_fn_dsa_keypair_with_level(FnDsaSecurityLevel::Level512)?;
            (pk, sk)
        }
        "fn-dsa-1024" => {
            let (pk, sk) = generate_fn_dsa_keypair_with_level(FnDsaSecurityLevel::Level1024)?;
            (pk, sk)
        }
        // Both `hybrid-ml-dsa-{44,65,87}-ed25519` (canonical) and the
        // legacy `ml-dsa-{44,65,87}-hybrid-ed25519` aliases are accepted
        // for symmetric tag handling. No code emits the alias form
        // today, but accepting it keeps wire-format parsing identical
        // across all three parameter sets.
        "hybrid-ml-dsa-44-ed25519" | "ml-dsa-44-hybrid-ed25519" => {
            generate_hybrid_signing_keypair_for(MlDsaParameterSet::MlDsa44)?
        }
        "hybrid-ml-dsa-65-ed25519" | "ml-dsa-65-hybrid-ed25519" => {
            generate_hybrid_signing_keypair_for(MlDsaParameterSet::MlDsa65)?
        }
        "hybrid-ml-dsa-87-ed25519" | "ml-dsa-87-hybrid-ed25519" => {
            generate_hybrid_signing_keypair_for(MlDsaParameterSet::MlDsa87)?
        }
        // the dispatch
        // tables for sign_with_key and verify both have an `"ed25519"`
        // arm (sign cfg-gated to non-FIPS, verify ungated), but
        // `generate_signing_keypair` had none. A caller that resolves
        // to scheme="ed25519" via select_signature_scheme could not
        // produce keys for an algorithm the library will sign with.
        // The arm is cfg-gated to match sign_with_key.
        #[cfg(not(feature = "fips"))]
        "ed25519" => {
            use crate::primitives::ec::ed25519::Ed25519KeyPair;
            use crate::primitives::ec::traits::EcKeyPair;
            let kp = Ed25519KeyPair::generate()
                .map_err(|e| CoreError::InvalidInput(format!("Ed25519 keygen failed: {e}")))?;
            let pk_bytes = kp.public_key_bytes();
            let sk_bytes = kp.secret_key_bytes();
            return Ok((pk_bytes, sk_bytes, scheme).into());
        }
        _ => {
            return Err(CoreError::InvalidInput(format!("Unsupported signing scheme: {scheme}")));
        }
    };

    Ok((public_key.into_bytes(), Zeroizing::new(secret_key.expose_secret().to_vec()), scheme)
        .into())
}

/// Sign a message using caller-provided keys (unified API).
///
/// Use [`generate_signing_keypair`] to create matching keys. The scheme is
/// determined by `CryptoConfig` — hybrid by default.
///
/// # Errors
///
/// Returns an error if:
/// - Session is set and has expired (`CoreError::SessionExpired`)
/// - Message size exceeds resource limits
/// - Secret/public key bytes don't match the expected sizes for the scheme
/// - Signing operation fails
#[must_use = "signing result must be used or errors will be silently dropped"]
pub fn sign_with_key(
    message: &[u8],
    secret_key: &[u8],
    public_key: &[u8],
    config: CryptoConfig,
) -> Result<SignedData> {
    fips_verify_operational()?;
    config.validate()?;

    let scheme = select_signature_scheme(&config)?;
    config.validate_scheme_compliance(&scheme)?;

    // opaque ResourceExceeded error.
    if let Err(e) = validate_signature_size(message.len()) {
        tracing::debug!(error = %e, msg_len = message.len(), "sign rejected: message exceeds resource limit");
        return Err(CoreError::ResourceExceeded("message exceeds resource limit".to_string()));
    }

    log_crypto_operation_start!(op::SIGN_WITH_KEY, scheme = %scheme, message_size = message.len());

    // Uses string-based dispatch. The SignatureScheme enum exists in
    // types::crypto but is not yet wired into this path for compile-time exhaustiveness.
    let (result_pk, signature) = match scheme.as_str() {
        "ml-dsa-44" | "pq-ml-dsa-44" => {
            let sig = sign_pq_ml_dsa_unverified(message, secret_key, MlDsaParameterSet::MlDsa44)?;
            (public_key.to_vec(), sig)
        }
        "ml-dsa-65" | "pq-ml-dsa-65" => {
            let sig = sign_pq_ml_dsa_unverified(message, secret_key, MlDsaParameterSet::MlDsa65)?;
            (public_key.to_vec(), sig)
        }
        "ml-dsa-87" | "pq-ml-dsa-87" => {
            let sig = sign_pq_ml_dsa_unverified(message, secret_key, MlDsaParameterSet::MlDsa87)?;
            (public_key.to_vec(), sig)
        }
        "slh-dsa-shake-128s" => {
            let sig =
                sign_pq_slh_dsa_unverified(message, secret_key, SlhDsaSecurityLevel::Shake128s)?;
            (public_key.to_vec(), sig)
        }
        "slh-dsa-shake-192s" => {
            let sig =
                sign_pq_slh_dsa_unverified(message, secret_key, SlhDsaSecurityLevel::Shake192s)?;
            (public_key.to_vec(), sig)
        }
        "slh-dsa-shake-256s" => {
            let sig =
                sign_pq_slh_dsa_unverified(message, secret_key, SlhDsaSecurityLevel::Shake256s)?;
            (public_key.to_vec(), sig)
        }
        "fn-dsa-512" | "fn-dsa" => {
            let sig = sign_pq_fn_dsa_unverified(message, secret_key, FnDsaSecurityLevel::Level512)?;
            (public_key.to_vec(), sig)
        }
        "fn-dsa-1024" => {
            let sig =
                sign_pq_fn_dsa_unverified(message, secret_key, FnDsaSecurityLevel::Level1024)?;
            (public_key.to_vec(), sig)
        }
        #[cfg(not(feature = "fips"))]
        "ed25519" => {
            // M-A: bind the pure-Ed25519 path to its own per-scheme context
            // so a captured "ed25519" envelope cannot be re-labelled (M5
            // strips the label but the signed bytes here re-include it via
            // the SHA-512 digest). Mirrors the hybrid leg construction at
            // line 165: `SHA-512(scheme_ctx || 0x00 || message)` is signed
            // instead of the raw message. Ed25519 / RFC 8032 expose no
            // native context parameter, so prefix-padding via a fixed-size
            // digest is the canonical workaround used here and in the
            // hybrid leg.
            use crate::types::domains::{SigSchemeLabel, hash_with_context, sig_context};
            let ed_ctx = sig_context(SigSchemeLabel::Ed25519);
            let ed_digest = hash_with_context(ed_ctx, message);
            let sig = sign_ed25519_internal(&ed_digest, secret_key)?;
            (public_key.to_vec(), sig)
        }
        "hybrid-ml-dsa-44-ed25519" | "ml-dsa-44-hybrid-ed25519" => {
            sign_hybrid_ml_dsa_ed25519(message, secret_key, public_key, MlDsaParameterSet::MlDsa44)?
        }
        "hybrid-ml-dsa-65-ed25519" | "ml-dsa-65-hybrid-ed25519" => {
            sign_hybrid_ml_dsa_ed25519(message, secret_key, public_key, MlDsaParameterSet::MlDsa65)?
        }
        "hybrid-ml-dsa-87-ed25519" | "ml-dsa-87-hybrid-ed25519" => {
            sign_hybrid_ml_dsa_ed25519(message, secret_key, public_key, MlDsaParameterSet::MlDsa87)?
        }
        _ => {
            return Err(CoreError::InvalidInput(format!("Unsupported signing scheme: {scheme}")));
        }
    };

    let timestamp = current_timestamp();

    log_crypto_operation_complete!(op::SIGN_WITH_KEY, signature_size = signature.len(), scheme = %scheme);

    Ok(SignedData::new(
        message.to_vec(),
        SignedMetadata::new(signature, scheme.clone(), result_pk, None),
        scheme,
        timestamp,
    ))
}

/// Verify a signed message using the public key embedded in the envelope.
///
/// The verification algorithm is determined by the `signed.scheme` field.
///
/// > **⚠️ Security: embedded-key trust shape (H2)**
/// >
/// > `verify` checks the signature against `signed.metadata.public_key` —
/// > the public key that rides inside the envelope itself. For an envelope
/// > delivered over an untrusted channel, this is **not** the same as "the
/// > signature was made by the key I trust": an attacker who fabricates an
/// > entire `(pk, sk, signature)` triple under their own key will see this
/// > function return `Ok(true)`. Use [`verify_with_anchor`] when the caller
/// > has an external trust anchor (CA pin, configured operator key, prior
/// > TOFU record); the helper pins the expected pubkey AND scheme before
/// > dispatching to this function.
///
/// `verify` is appropriate when the envelope's public key is itself the
/// trust root (e.g. content-addressed key distribution) or when the
/// surrounding protocol already pins the key in a different way.
///
/// # Examples
///
/// ```no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use latticearc::unified_api::{verify, CryptoConfig, VerifiedSession, SignedData, SignedMetadata};
/// # let signed = SignedData::new(
/// #     vec![],
/// #     SignedMetadata::new(vec![], "ed25519".to_string(), vec![], None),
/// #     "ed25519".to_string(),
/// #     0,
/// # );
/// # let pk = [0u8; 32];
/// # let sk = [0u8; 32];
///
/// // Simple: No session
/// let is_valid = verify(&signed, CryptoConfig::new())?;
///
/// // With Zero Trust session
/// let session = VerifiedSession::establish(&pk, &sk)?;
/// let is_valid = verify(&signed, CryptoConfig::new()
///     .session(&session))?;
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// Returns an error if:
/// - Session is set and has expired (`CoreError::SessionExpired`)
/// - Message size exceeds resource limits
/// - Public key is invalid
/// - Signature is malformed or invalid
#[must_use = "verification result must be used or errors will be silently dropped"]
pub fn verify(signed: &SignedData, config: CryptoConfig) -> Result<bool> {
    fips_verify_operational()?;
    config.validate()?;
    config.validate_scheme_compliance(&signed.scheme)?;

    log_crypto_operation_start!(op::VERIFY, scheme = ?signed.scheme, message_size = signed.data.len());

    // collapse to `Ok(false)` on the
    // verify path so an adversary cannot binary-search the configured
    // cap from the Result variant.
    if let Err(e) = validate_signature_size(signed.data.len()) {
        tracing::debug!(error = %e, msg_len = signed.data.len(), "verify rejected: message exceeds resource limit");
        return Ok(false);
    }

    let result = match signed.scheme.as_str() {
        "ml-dsa-44" | "pq-ml-dsa-44" => verify_pq_ml_dsa_unverified(
            &signed.data,
            &signed.metadata.signature,
            &signed.metadata.public_key,
            MlDsaParameterSet::MlDsa44,
        ),
        "ml-dsa-65" | "pq-ml-dsa-65" => verify_pq_ml_dsa_unverified(
            &signed.data,
            &signed.metadata.signature,
            &signed.metadata.public_key,
            MlDsaParameterSet::MlDsa65,
        ),
        "ml-dsa-87" | "pq-ml-dsa-87" => verify_pq_ml_dsa_unverified(
            &signed.data,
            &signed.metadata.signature,
            &signed.metadata.public_key,
            MlDsaParameterSet::MlDsa87,
        ),
        "slh-dsa-shake-128s" => verify_pq_slh_dsa_unverified(
            &signed.data,
            &signed.metadata.signature,
            &signed.metadata.public_key,
            SlhDsaSecurityLevel::Shake128s,
        ),
        "slh-dsa-shake-192s" => verify_pq_slh_dsa_unverified(
            &signed.data,
            &signed.metadata.signature,
            &signed.metadata.public_key,
            SlhDsaSecurityLevel::Shake192s,
        ),
        "slh-dsa-shake-256s" => verify_pq_slh_dsa_unverified(
            &signed.data,
            &signed.metadata.signature,
            &signed.metadata.public_key,
            SlhDsaSecurityLevel::Shake256s,
        ),
        "fn-dsa-512" | "fn-dsa" => verify_pq_fn_dsa_unverified(
            &signed.data,
            &signed.metadata.signature,
            &signed.metadata.public_key,
            FnDsaSecurityLevel::Level512,
        ),
        "fn-dsa-1024" => verify_pq_fn_dsa_unverified(
            &signed.data,
            &signed.metadata.signature,
            &signed.metadata.public_key,
            FnDsaSecurityLevel::Level1024,
        ),
        // replace magic literals
        // with `MlDsaParameterSet::public_key_size()` / `signature_size()`
        // so a future drift in the FIPS 204 parameter table propagates
        // automatically. The sister `sign_*` path at line ~1118 already
        // derives sizes via the same methods.
        "hybrid-ml-dsa-44-ed25519" | "ml-dsa-44-hybrid-ed25519" => verify_hybrid_ml_dsa_ed25519(
            &signed.data,
            &signed.metadata.signature,
            &signed.metadata.public_key,
            MlDsaParameterSet::MlDsa44.public_key_size(),
            MlDsaParameterSet::MlDsa44.signature_size(),
            MlDsaParameterSet::MlDsa44,
        ),
        "hybrid-ml-dsa-65-ed25519" | "ml-dsa-65-hybrid-ed25519" => verify_hybrid_ml_dsa_ed25519(
            &signed.data,
            &signed.metadata.signature,
            &signed.metadata.public_key,
            MlDsaParameterSet::MlDsa65.public_key_size(),
            MlDsaParameterSet::MlDsa65.signature_size(),
            MlDsaParameterSet::MlDsa65,
        ),
        "hybrid-ml-dsa-87-ed25519" | "ml-dsa-87-hybrid-ed25519" => verify_hybrid_ml_dsa_ed25519(
            &signed.data,
            &signed.metadata.signature,
            &signed.metadata.public_key,
            MlDsaParameterSet::MlDsa87.public_key_size(),
            MlDsaParameterSet::MlDsa87.signature_size(),
            MlDsaParameterSet::MlDsa87,
        ),
        // cfg-gate the
        // `"ed25519"` arm under `not(feature = "fips")` for symmetry
        // with the sign_with_key dispatch and the keygen arm. FIPS
        // builds now reject Ed25519 verification at the dispatch
        // layer rather than producing an Ed25519-only path that
        // can't be reached via FIPS sign + can be reached via FIPS
        // verify (the prior asymmetry).
        #[cfg(not(feature = "fips"))]
        "ed25519" => {
            // M-A: match the sign-side construction. The signed bytes are
            // the SHA-512 digest of `scheme_ctx || 0x00 || message`, not
            // the raw message. A legitimate envelope produced by the M-A
            // sign path round-trips; a pre-M-A or external raw-message
            // signature will fail and return `Ok(false)`.
            use crate::types::domains::{SigSchemeLabel, hash_with_context, sig_context};
            let ed_ctx = sig_context(SigSchemeLabel::Ed25519);
            let ed_digest = hash_with_context(ed_ctx, &signed.data);
            verify_ed25519_internal(
                &ed_digest,
                &signed.metadata.signature,
                &signed.metadata.public_key,
            )
        }
        // L-A: under `--features fips`, "ed25519" envelopes are rejected
        // at deserialization (M5 allowlist no longer maps the string), so
        // this arm is only reachable for in-memory `SignedData` values
        // constructed without going through the deserializer. Collapse to
        // `Ok(false)` to preserve the Pattern-6 contract documented on
        // `verify_with_anchor` — the prior `Err(InvalidInput(...))` echoed
        // the scheme string and was asymmetric with the verify_with_anchor
        // path that ALSO sees this dispatch.
        #[cfg(feature = "fips")]
        "ed25519" => Ok(false),
        _ => {
            return Err(CoreError::InvalidInput(format!(
                "Unsupported verification scheme: {}",
                signed.scheme
            )));
        }
    };

    match &result {
        Ok(valid) => {
            log_crypto_operation_complete!(op::VERIFY, valid = *valid, scheme = ?signed.scheme);
        }
        Err(e) => {
            log_crypto_operation_error!(op::VERIFY, e, scheme = ?signed.scheme);
        }
    }
    result
}

/// Verify a `SignedData` envelope against an operator-pinned trust anchor.
///
/// H2 fix: [`verify`] uses the public key embedded in the envelope itself, so
/// an envelope crafted with an attacker-controlled `(pk, sk, sig)` triple
/// verifies as VALID under that path. Callers that need the contract "this
/// signature was made by the key I trust" must compare against an external
/// trust anchor before dispatching the cryptographic verify. This helper
/// does that comparison and the scheme assertion atomically:
///
/// 1. `expected_pk` is compared in constant time against
///    `signed.metadata.public_key`. Mismatch returns `Ok(false)` —
///    indistinguishable from a crypto-reject (Pattern 6).
/// 2. `expected_scheme` is canonicalised through the M5 [`SigSchemeLabel`]
///    allowlist and compared against the envelope's `scheme` field.
///    Mismatch returns `Ok(false)`. Unknown schemes also return `Ok(false)`
///    rather than `Err`, again for Pattern-6 indistinguishability.
/// 3. Otherwise the call delegates to [`verify`] which runs the scheme-bound
///    cryptographic verification.
///
/// The pubkey comparison runs in constant time even though public keys are
/// not secret: the discipline keeps verify-path timing decoupled from
/// trust-anchor bytes, which simplifies operator threat modelling.
///
/// # Errors
///
/// Returns an error if:
/// - The session is set and has expired (`CoreError::SessionExpired`)
/// - Internal verify hits a configuration / FIPS-self-test failure
///
/// Mismatched trust anchor, mismatched scheme, unknown scheme, or
/// cryptographic verify-false all return `Ok(false)`.
///
/// # Examples
///
/// ```no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use latticearc::unified_api::{verify_with_anchor, CryptoConfig, SignedData, SignedMetadata};
/// # let signed = SignedData::new(
/// #     vec![],
/// #     SignedMetadata::new(vec![], "ml-dsa-65".to_string(), vec![], None),
/// #     "ml-dsa-65".to_string(),
/// #     0,
/// # );
/// # let trust_anchor_pk: &[u8] = &[];
/// let is_valid = verify_with_anchor(
///     &signed,
///     trust_anchor_pk,
///     "ml-dsa-65",
///     CryptoConfig::new(),
/// )?;
/// # Ok(())
/// # }
/// ```
#[must_use = "verification result must be used or errors will be silently dropped"]
pub fn verify_with_anchor(
    signed: &SignedData,
    expected_pk: &[u8],
    expected_scheme: &str,
    config: CryptoConfig,
) -> Result<bool> {
    use crate::types::domains::SigSchemeLabel;
    use subtle::ConstantTimeEq;

    // Canonicalise both schemes through the M5 allowlist so aliases
    // (`pq-ml-dsa-65` ⇔ `ml-dsa-65`) compare equal and unknown strings
    // collapse to `Ok(false)`.
    let Some(envelope_label) = SigSchemeLabel::from_scheme_str(&signed.scheme) else {
        tracing::debug!(
            envelope_scheme = %signed.scheme,
            "verify_with_anchor rejected: envelope scheme not in M5 allowlist"
        );
        return Ok(false);
    };
    let Some(expected_label) = SigSchemeLabel::from_scheme_str(expected_scheme) else {
        tracing::debug!(
            expected_scheme = %expected_scheme,
            "verify_with_anchor rejected: expected scheme not in M5 allowlist"
        );
        return Ok(false);
    };
    if envelope_label != expected_label {
        tracing::debug!(
            envelope_scheme = %signed.scheme,
            expected_scheme = %expected_scheme,
            "verify_with_anchor rejected: envelope scheme does not match expected scheme"
        );
        return Ok(false);
    }

    // Length-check then constant-time byte comparison. `subtle::ct_eq`
    // requires equal-length inputs and short-circuits inside the helper
    // on length mismatch — that short-circuit is observable via wall
    // clock, so we surface it as an explicit early return rather than
    // relying on `ct_eq`'s internal length check. The length check
    // itself is NOT constant time, but the only information it leaks is
    // whether the envelope's embedded pk has the same byte length as
    // the operator's pinned pk. Public-key lengths in this library are
    // fixed per algorithm (X25519 PK = 32 B, ML-KEM-768 PK = 1184 B,
    // ML-DSA-65 PK = 1952 B, hybrid composites are deterministic sums),
    // so a length mismatch reveals at most the envelope's algorithm
    // identity — not secret material. The `ct_eq` below then runs over
    // equal-length slices and reveals nothing beyond the equal/not-
    // equal verdict.
    if expected_pk.len() != signed.metadata.public_key.len() {
        tracing::debug!(
            expected_pk_len = expected_pk.len(),
            envelope_pk_len = signed.metadata.public_key.len(),
            "verify_with_anchor rejected: trust-anchor pk length differs from envelope pk length"
        );
        return Ok(false);
    }
    if expected_pk.ct_eq(&signed.metadata.public_key).unwrap_u8() != 1u8 {
        tracing::debug!("verify_with_anchor rejected: trust-anchor pk does not match envelope pk");
        return Ok(false);
    }

    // Trust anchor pinned and scheme asserted; delegate the cryptographic
    // verification to the existing dispatcher. `verify` will recompute the
    // H1 scheme context and reject if the signature was produced under a
    // different scheme even though the envelope claims this one.
    verify(signed, config)
}

/// Hybrid ML-DSA + Ed25519 verification with Pattern 6 timing-oracle safety.
///
/// Splits `full_pk` and `full_sig` at the post-quantum boundary, then runs
/// both component verifies unconditionally and combines them bitwise so a
/// malformed PQ component cannot timing-leak the fact that Ed25519 was
/// never invoked. Mirrors the canonical shape in `sig_hybrid::verify`. Per-
/// component errors collapse to `false`, preserving opacity.
fn verify_hybrid_ml_dsa_ed25519(
    data: &[u8],
    full_sig: &[u8],
    full_pk: &[u8],
    pq_pk_len: usize,
    min_total_sig_len: usize,
    param_set: MlDsaParameterSet,
) -> Result<bool> {
    use crate::primitives::ec::ed25519::ED25519_PUBLIC_KEY_LEN as ED25519_PK_LEN;
    const ED25519_SIG_LEN: usize = 64;

    // Pattern 6 (strict, with timing-equalizer): on any shape-rejection
    // path we DON'T early-return — we substitute shape-correct dummy
    // material drawn from a process-cached real keypair and run the full
    // ML-DSA + Ed25519 verify pipeline against it. The verify result is
    // discarded; the `Ok(false)` returned to the caller is the original
    // shape decision. Without the equalizer, attackers varying wire-
    // supplied PK / sig lengths could distinguish "shape-failure (bailed
    // in nanoseconds)" from "verify-failure (full ML-DSA + Ed25519, ~ms)"
    // by elapsed wall-clock time alone. The TOFU / server-supplied-key
    // exposure called out in the second-round audit is closed by this
    // path.
    //
    // DoS trade-off: on shape failure we now spend ~1 ML-DSA verify worth
    // of CPU instead of bailing immediately. Rate-limiting / WAF layers
    // upstream are the right place to bound that — cryptographic
    // primitives are responsible for being timing-safe, not for absorbing
    // arbitrary inbound load. The cached keypair amortises keygen cost
    // (paid at most once per parameter set per process).
    //
    // The hard `Err` path is reserved for invariant violations the caller
    // can never legitimately produce (currently: arithmetic overflow on
    // `pq_pk_len + ED25519_PK_LEN`, which is impossible for any real
    // `MlDsaParameterSet`).
    let sig_len = full_sig.len();
    let pk_len = full_pk.len();
    let Some(expected_pk_len) = pq_pk_len.checked_add(ED25519_PK_LEN) else {
        return Err(CoreError::InvalidInput("Invalid hybrid signature".to_string()));
    };

    let shape_ok = sig_len >= min_total_sig_len && pk_len == expected_pk_len;

    // Slice the real or substitute material in a single branch-free path
    // so the downstream verifies always see shape-correct buffers.
    // `.get(..)` instead of `[..]` keeps `clippy::indexing_slicing` happy
    // even though the shape check above proves the indices are valid.
    let (pq_pk_bytes, ed_pk_bytes, pq_sig_bytes, ed_sig_bytes, real_inputs) = if shape_ok {
        let pq_pk = full_pk.get(..pq_pk_len).unwrap_or(&[]);
        let ed_pk = full_pk.get(pq_pk_len..).unwrap_or(&[]);
        let pq_sig_len = sig_len.saturating_sub(ED25519_SIG_LEN);
        let pq_sig = full_sig.get(..pq_sig_len).unwrap_or(&[]);
        let ed_sig = full_sig.get(pq_sig_len..).unwrap_or(&[]);
        (pq_pk, ed_pk, pq_sig, ed_sig, true)
    } else {
        // Substitute shape-correct dummies from the cached keypair so the
        // verify pipeline sees real-shape input regardless of caller
        // adversarial wire bytes.
        let dummy = crate::hybrid::verify_equalizer::hybrid_verify_dummy_material(param_set);
        (
            dummy.pq_pk.as_slice(),
            dummy.ed_pk.as_slice(),
            dummy.pq_sig.as_slice(),
            dummy.ed_sig.as_slice(),
            false,
        )
    };

    // H1 / M1 fix: verify both legs under the hybrid scheme context, NOT
    // the per-leg pure-PQ context. Reverting either leg's ctx would
    // re-open the downgrade attack (see `sign_hybrid_ml_dsa_ed25519`).
    use crate::types::domains::{hash_with_context, sig_context};
    use crate::unified_api::convenience::pq_sig::{
        hybrid_scheme_label_for_param_set, verify_pq_ml_dsa_internal_with_ctx,
    };
    let hybrid_ctx = sig_context(hybrid_scheme_label_for_param_set(param_set));
    let pq_valid =
        verify_pq_ml_dsa_internal_with_ctx(data, pq_sig_bytes, pq_pk_bytes, param_set, hybrid_ctx)
            .unwrap_or(false);
    let ed_digest = hash_with_context(hybrid_ctx, data);
    let ed_valid = verify_ed25519_internal(&ed_digest, ed_sig_bytes, ed_pk_bytes).unwrap_or(false);

    // Combine without short-circuit. If shape was invalid we hold the
    // result at false unconditionally — both verifies still ran for the
    // timing-equalization side-effect.
    let combined = pq_valid & ed_valid;
    Ok(if real_inputs { combined } else { false })
}

#[cfg(test)]
#[expect(
    clippy::panic,
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::indexing_slicing,
    clippy::panic_in_result_fn,
    clippy::redundant_clone,
    clippy::useless_vec,
    clippy::clone_on_copy,
    clippy::single_match,
    unused_qualifications,
    reason = "test/bench scaffolding: lints suppressed for this module"
)]
// `unnecessary_wraps` is `#[allow]`, not `#[expect]`: it fires only under the
// `fips` feature config and would be an unfulfilled expectation in the
// default (non-FIPS) build, which `-D warnings` rejects.
#[allow(clippy::unnecessary_wraps, reason = "feature-config-dependent; see above")]
mod tests {
    use super::*;
    use crate::types::types::CryptoScheme;
    use crate::unified_api::test_helpers::non_fips_config;
    use crate::{CryptoConfig, CryptoMode, SecurityLevel, UseCase};
    use static_assertions::assert_not_impl_any;

    // SigningKeypair holds
    // `Zeroizing<Vec<u8>>` for the secret key. A future contributor
    // deriving `PartialEq`/`Eq` would route equality through Vec's
    // short-circuiting `==`, leaking secret-prefix-agreement timing.
    // The integration-level `tests/no_partial_eq_on_secret_types.rs`
    // can't reach `SigningKeypair` (it's `pub` inside a private
    // module), so the matching guard lives here.
    assert_not_impl_any!(SigningKeypair: PartialEq, Eq);

    /// Helper: generate keypair + sign + return signed data
    fn sign_message(message: &[u8], config: CryptoConfig) -> Result<SignedData> {
        let (pk, sk, _scheme) = generate_signing_keypair(config.clone())?.into_parts();
        sign_with_key(message, &sk, &pk, config)
    }

    // Sign/Verify tests with different security levels
    #[test]
    fn test_sign_verify_with_standard_security_succeeds() -> Result<()> {
        let message = b"Test message with standard security";
        let config = CryptoConfig::new().security_level(SecurityLevel::Standard);

        let signed = sign_message(message, config)?;

        assert!(!signed.metadata.signature.is_empty());
        assert!(!signed.metadata.public_key.is_empty());

        let is_valid = verify(&signed, CryptoConfig::new())?;
        assert!(is_valid, "Signature should be valid");

        Ok(())
    }

    #[test]
    fn test_sign_verify_with_high_security_succeeds() -> Result<()> {
        let message = b"Test message with high security";
        let config = CryptoConfig::new().security_level(SecurityLevel::High);

        let signed = sign_message(message, config)?;

        let is_valid = verify(&signed, CryptoConfig::new())?;
        assert!(is_valid, "Signature should be valid");

        Ok(())
    }

    #[test]
    fn test_sign_verify_with_maximum_security_succeeds() -> Result<()> {
        let message = b"Test message with maximum security";
        let config = CryptoConfig::new().security_level(SecurityLevel::Maximum);

        let signed = sign_message(message, config)?;

        let is_valid = verify(&signed, CryptoConfig::new())?;
        assert!(is_valid, "Signature should be valid");

        Ok(())
    }

    #[test]
    fn test_sign_verify_wrong_message_fails() -> Result<()> {
        let message = b"Original message";
        let config = CryptoConfig::new();

        let signed = sign_message(message, config)?;

        // Modify the message
        let mut modified_signed = signed.clone();
        modified_signed.data = b"Modified message".to_vec();

        // verify() may return Ok(false) or Err depending on implementation
        match verify(&modified_signed, CryptoConfig::new()) {
            Ok(valid) => assert!(!valid, "Modified message should fail verification"),
            Err(_) => {} // Error is also acceptable
        }

        Ok(())
    }

    #[test]
    fn test_sign_verify_corrupted_signature_fails() -> Result<()> {
        let message = b"Test message";
        let config = CryptoConfig::new();

        let signed = sign_message(message, config)?;

        // Corrupt the signature
        let mut corrupted_signed = signed.clone();
        if let Some(byte) = corrupted_signed.metadata.signature.first_mut() {
            *byte ^= 0xFF;
        }

        // verify() may return Ok(false) or Err depending on implementation
        match verify(&corrupted_signed, CryptoConfig::new()) {
            Ok(valid) => assert!(!valid, "Corrupted signature should fail verification"),
            Err(_) => {} // Error is also acceptable
        }

        Ok(())
    }

    #[test]
    fn test_sign_empty_message_succeeds() -> Result<()> {
        let message = b"";
        let config = CryptoConfig::new();

        let signed = sign_message(message, config)?;

        let is_valid = verify(&signed, CryptoConfig::new())?;
        assert!(is_valid, "Empty message signature should be valid");

        Ok(())
    }

    #[test]
    fn test_sign_large_message_succeeds() -> Result<()> {
        let message = vec![0xABu8; 10_000]; // 10KB message
        let config = CryptoConfig::new();

        let signed = sign_message(&message, config)?;

        let is_valid = verify(&signed, CryptoConfig::new())?;
        assert!(is_valid, "Large message signature should be valid");

        Ok(())
    }

    // Use case selection tests
    #[test]
    fn test_sign_with_financial_transactions_use_case_succeeds() -> Result<()> {
        let message = b"Financial transaction data";
        // FIPS-mode behaviour for this use case is covered by the dedicated
        // `test_fips_*` tests below.
        let config = non_fips_config(UseCase::FinancialTransactions);

        let signed = sign_message(message, config)?;

        // Financial transactions should use strong signature
        assert!(
            signed.scheme.contains("ml-dsa") || signed.scheme.contains("ed25519"),
            "Financial transactions should use strong signatures"
        );

        let is_valid = verify(&signed, CryptoConfig::new())?;
        assert!(is_valid);

        Ok(())
    }

    #[test]
    fn test_sign_with_authentication_use_case_succeeds() -> Result<()> {
        let message = b"Authentication data";
        let config = CryptoConfig::new().use_case(UseCase::Authentication);

        let signed = sign_message(message, config)?;

        let is_valid = verify(&signed, CryptoConfig::new())?;
        assert!(is_valid);

        Ok(())
    }

    #[test]
    fn test_sign_with_firmware_signing_use_case_succeeds() -> Result<()> {
        let message = b"Firmware binary data";
        let config = CryptoConfig::new().use_case(UseCase::FirmwareSigning);

        let signed = sign_message(message, config)?;

        let is_valid = verify(&signed, CryptoConfig::new())?;
        assert!(is_valid);

        Ok(())
    }

    // Invalid key tests
    #[test]
    fn test_encrypt_with_invalid_key_length_returns_error() {
        let message = b"Test message";
        let short_key = vec![0x42u8; 16]; // Too short for AES-256
        let config = CryptoConfig::new().force_scheme(CryptoScheme::Symmetric);

        let result = encrypt(message, EncryptKey::Symmetric(&short_key), config);
        assert!(result.is_err(), "Encryption with short key should fail");
    }

    #[test]
    fn test_decrypt_empty_ciphertext_returns_error() {
        let key = vec![0x42u8; 32];
        let empty_encrypted = EncryptedOutput::new(
            EncryptionScheme::Aes256Gcm,
            vec![],
            vec![],
            vec![],
            None,
            0,
            None,
        )
        .expect("symmetric scheme + None hybrid_data is a valid shape");

        // Empty ciphertext should be rejected (too short for nonce)
        let result = decrypt(&empty_encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new());
        assert!(result.is_err(), "Empty ciphertext should be rejected");
    }

    // Cross-algorithm tests for signing
    #[test]
    fn test_sign_verify_multiple_security_levels_succeed_roundtrip() -> Result<()> {
        let message = b"Test cross-level signatures";

        let levels = [SecurityLevel::Standard, SecurityLevel::High, SecurityLevel::Maximum];
        for level in &levels {
            let config = CryptoConfig::new().security_level(level.clone());
            let signed = sign_message(message, config)?;
            let is_valid = verify(&signed, CryptoConfig::new())?;
            assert!(is_valid, "Failed for security level: {:?}", level);
        }

        Ok(())
    }

    // ========================================================================
    // Additional Signing Algorithm Coverage
    // ========================================================================

    // Test specific algorithm branches in sign_with_key/verify
    #[test]
    fn test_sign_verify_metadata_is_populated_roundtrip() -> Result<()> {
        let message = b"Test metadata";
        let config = CryptoConfig::new();

        let signed = sign_message(message, config)?;

        assert!(!signed.metadata.signature.is_empty(), "Signature should not be empty");
        assert!(!signed.metadata.public_key.is_empty(), "Public key should not be empty");
        assert!(!signed.metadata.signature_algorithm.is_empty(), "Algorithm should be set");
        assert!(!signed.scheme.is_empty(), "Scheme should be set");
        assert!(signed.timestamp > 0, "Timestamp should be set");

        Ok(())
    }

    #[test]
    fn test_verify_with_corrupted_public_key_fails() -> Result<()> {
        let message = b"Test message";
        let config = CryptoConfig::new();

        let signed = sign_message(message, config)?;

        // Corrupt the public key
        let mut corrupted_signed = signed.clone();
        if let Some(byte) = corrupted_signed.metadata.public_key.first_mut() {
            *byte ^= 0xFF;
        }

        // Verification should fail
        match verify(&corrupted_signed, CryptoConfig::new()) {
            Ok(valid) => assert!(!valid, "Corrupted public key should fail verification"),
            Err(_) => {} // Error is also acceptable
        }

        Ok(())
    }

    #[test]
    fn test_sign_verify_binary_message_succeeds() -> Result<()> {
        let message = vec![0x00, 0xFF, 0x7F, 0x80, 0x01, 0xFE];
        let config = CryptoConfig::new();

        let signed = sign_message(&message, config)?;
        let is_valid = verify(&signed, CryptoConfig::new())?;
        assert!(is_valid, "Binary message signature should be valid");

        Ok(())
    }

    #[test]
    fn test_sign_verify_unicode_message_succeeds() -> Result<()> {
        let message = "Test with Unicode: 你好世界 🔐".as_bytes();
        let config = CryptoConfig::new();

        let signed = sign_message(message, config)?;
        let is_valid = verify(&signed, CryptoConfig::new())?;
        assert!(is_valid, "Unicode message signature should be valid");

        Ok(())
    }

    #[test]
    fn test_sign_verify_with_blockchain_transaction_use_case_succeeds() -> Result<()> {
        let message = b"Blockchain transaction data";
        let config = CryptoConfig::new().use_case(UseCase::BlockchainTransaction);

        let signed = sign_message(message, config)?;
        let is_valid = verify(&signed, CryptoConfig::new())?;
        assert!(is_valid);

        Ok(())
    }

    #[test]
    fn test_sign_verify_with_legal_documents_use_case_succeeds() -> Result<()> {
        let message = b"Legal document hash";
        let config = CryptoConfig::new().use_case(UseCase::LegalDocuments);

        let signed = sign_message(message, config)?;
        let is_valid = verify(&signed, CryptoConfig::new())?;
        assert!(is_valid);

        Ok(())
    }

    #[test]
    fn test_sign_multiple_messages_succeeds() -> Result<()> {
        let config = CryptoConfig::new();
        let messages =
            vec![b"First message".as_ref(), b"Second message".as_ref(), b"Third message".as_ref()];

        for message in messages {
            let signed = sign_message(message, config.clone())?;
            let is_valid = verify(&signed, CryptoConfig::new())?;
            assert!(is_valid, "Message: {:?}", String::from_utf8_lossy(message));
        }

        Ok(())
    }

    #[test]
    fn test_sign_produces_unique_signatures_are_unique() -> Result<()> {
        let message = b"Same message";
        let config = CryptoConfig::new();

        let signed1 = sign_message(message, config.clone())?;
        let signed2 = sign_message(message, config)?;

        // Different key pairs should produce different signatures
        assert_ne!(signed1.metadata.signature, signed2.metadata.signature);
        assert_ne!(signed1.metadata.public_key, signed2.metadata.public_key);

        // Both should verify successfully
        let is_valid1 = verify(&signed1, CryptoConfig::new())?;
        let is_valid2 = verify(&signed2, CryptoConfig::new())?;
        assert!(is_valid1);
        assert!(is_valid2);

        Ok(())
    }

    #[test]
    fn test_verify_rejects_empty_signature_fails() {
        let signed = SignedData::new(
            b"Test message".to_vec(),
            SignedMetadata::new(vec![], "ml-dsa-44".to_string(), vec![0u8; 1312], None),
            "ml-dsa-44".to_string(),
            0,
        );

        let result = verify(&signed, CryptoConfig::new());
        assert!(result.is_err() || (result.is_ok() && !result.unwrap()));
    }

    #[test]
    fn test_verify_rejects_empty_public_key_fails() {
        let signed = SignedData::new(
            b"Test message".to_vec(),
            SignedMetadata::new(vec![0u8; 2420], "ml-dsa-44".to_string(), vec![], None),
            "ml-dsa-44".to_string(),
            0,
        );

        let result = verify(&signed, CryptoConfig::new());
        assert!(result.is_err() || (result.is_ok() && !result.unwrap()));
    }

    // Decrypt error handling (doesn't require encrypt roundtrip)
    #[test]
    fn test_decrypt_with_short_key_returns_error() {
        let encrypted = EncryptedOutput::new(
            EncryptionScheme::Aes256Gcm,
            vec![1, 2, 3, 4],
            vec![0u8; 12],
            vec![0u8; 16],
            None,
            0,
            None,
        )
        .expect("symmetric scheme + None hybrid_data is a valid shape");
        let short_key = vec![0x42u8; 16]; // Too short

        let result = decrypt(&encrypted, DecryptKey::Symmetric(&short_key), CryptoConfig::new());
        assert!(result.is_err(), "Decryption with short key should fail");
    }

    #[test]
    fn test_encrypted_output_rejects_hybrid_scheme_without_hybrid_data() {
        // Constructing a hybrid-scheme `EncryptedOutput` without the
        // ML-KEM ciphertext + ECDH ephemeral PK is structurally broken —
        // the decryption path has no way to recover. The shape check in
        // `EncryptedOutput::new` rejects it before any decrypt attempt.
        let result = EncryptedOutput::new(
            EncryptionScheme::HybridMlKem768Aes256Gcm,
            vec![0x12u8; 40],
            vec![0u8; 12],
            vec![0u8; 16],
            None, // missing hybrid_data
            0,
            None,
        );
        assert!(
            result.is_err(),
            "hybrid scheme without hybrid_data must be rejected at construction"
        );
    }

    // === SLH-DSA sign/verify roundtrip tests ===

    #[test]
    fn test_slh_dsa_shake_128s_roundtrip() {
        std::thread::Builder::new()
            .name("slh_dsa_128s".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                use crate::unified_api::convenience::keygen::generate_slh_dsa_keypair;
                use crate::unified_api::convenience::pq_sig::sign_pq_slh_dsa_unverified;

                let (pk, sk) = generate_slh_dsa_keypair(SlhDsaSecurityLevel::Shake128s).unwrap();
                let message = b"SLH-DSA-128s test message";
                let signature = sign_pq_slh_dsa_unverified(
                    message,
                    sk.expose_secret(),
                    SlhDsaSecurityLevel::Shake128s,
                )
                .unwrap();

                let signed_data = SignedData::new(
                    message.to_vec(),
                    SignedMetadata::new(
                        signature,
                        "slh-dsa-shake-128s".to_string(),
                        pk.into_bytes(),
                        None,
                    ),
                    "slh-dsa-shake-128s".to_string(),
                    0,
                );

                let verified = verify(&signed_data, CryptoConfig::new()).unwrap();
                assert!(verified, "SLH-DSA-128s signature should verify");
            })
            .unwrap()
            .join()
            .unwrap();
    }

    #[test]
    fn test_slh_dsa_shake_192s_roundtrip() {
        std::thread::Builder::new()
            .name("slh_dsa_192s".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                use crate::unified_api::convenience::keygen::generate_slh_dsa_keypair;
                use crate::unified_api::convenience::pq_sig::sign_pq_slh_dsa_unverified;

                let (pk, sk) = generate_slh_dsa_keypair(SlhDsaSecurityLevel::Shake192s).unwrap();
                let message = b"SLH-DSA-192s test message";
                let signature = sign_pq_slh_dsa_unverified(
                    message,
                    sk.expose_secret(),
                    SlhDsaSecurityLevel::Shake192s,
                )
                .unwrap();

                let signed_data = SignedData::new(
                    message.to_vec(),
                    SignedMetadata::new(
                        signature,
                        "slh-dsa-shake-192s".to_string(),
                        pk.into_bytes(),
                        None,
                    ),
                    "slh-dsa-shake-192s".to_string(),
                    0,
                );

                let verified = verify(&signed_data, CryptoConfig::new()).unwrap();
                assert!(verified, "SLH-DSA-192s signature should verify");
            })
            .unwrap()
            .join()
            .unwrap();
    }

    #[test]
    fn test_slh_dsa_shake_256s_roundtrip() {
        std::thread::Builder::new()
            .name("slh_dsa_256s".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                use crate::unified_api::convenience::keygen::generate_slh_dsa_keypair;
                use crate::unified_api::convenience::pq_sig::sign_pq_slh_dsa_unverified;

                let (pk, sk) = generate_slh_dsa_keypair(SlhDsaSecurityLevel::Shake256s).unwrap();
                let message = b"SLH-DSA-256s test message";
                let signature = sign_pq_slh_dsa_unverified(
                    message,
                    sk.expose_secret(),
                    SlhDsaSecurityLevel::Shake256s,
                )
                .unwrap();

                let signed_data = SignedData::new(
                    message.to_vec(),
                    SignedMetadata::new(
                        signature,
                        "slh-dsa-shake-256s".to_string(),
                        pk.into_bytes(),
                        None,
                    ),
                    "slh-dsa-shake-256s".to_string(),
                    0,
                );

                let verified = verify(&signed_data, CryptoConfig::new()).unwrap();
                assert!(verified, "SLH-DSA-256s signature should verify");
            })
            .unwrap()
            .join()
            .unwrap();
    }

    // === FN-DSA sign/verify roundtrip test ===

    #[test]
    fn test_fn_dsa_roundtrip() {
        std::thread::Builder::new()
            .name("fn_dsa".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                use crate::unified_api::convenience::keygen::generate_fn_dsa_keypair;
                use crate::unified_api::convenience::pq_sig::sign_pq_fn_dsa_unverified;

                let (pk, sk) = generate_fn_dsa_keypair().unwrap();
                let message = b"FN-DSA test message";
                let signature = sign_pq_fn_dsa_unverified(
                    message,
                    sk.expose_secret(),
                    FnDsaSecurityLevel::Level512,
                )
                .unwrap();

                let signed_data = SignedData::new(
                    message.to_vec(),
                    SignedMetadata::new(signature, "fn-dsa".to_string(), pk.into_bytes(), None),
                    "fn-dsa".to_string(),
                    0,
                );

                let verified = verify(&signed_data, CryptoConfig::new()).unwrap();
                assert!(verified, "FN-DSA signature should verify");
            })
            .unwrap()
            .join()
            .unwrap();
    }

    // === Hybrid ML-DSA-44 + Ed25519 sign/verify roundtrip ===

    #[test]
    fn test_hybrid_ml_dsa_44_ed25519_roundtrip() {
        std::thread::Builder::new()
            .name("hybrid_44".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                use crate::primitives::sig::ml_dsa::MlDsaParameterSet;
                use crate::unified_api::convenience::keygen::{
                    generate_keypair, generate_ml_dsa_keypair,
                };

                let (pq_pk, pq_sk) = generate_ml_dsa_keypair(MlDsaParameterSet::MlDsa44).unwrap();
                let (ed_pk, ed_sk) = generate_keypair().unwrap();
                let combined_pk = [pq_pk.into_bytes(), ed_pk.into_bytes()].concat();
                let combined_sk = [pq_sk.expose_secret(), ed_sk.expose_secret()].concat();

                let message = b"Hybrid ML-DSA-44 + Ed25519 test";
                let config = CryptoConfig::new().security_level(SecurityLevel::Standard);
                let signed_data =
                    sign_with_key(message, &combined_sk, &combined_pk, config).unwrap();
                assert!(signed_data.metadata.signature_algorithm.contains("hybrid"));

                let verified = verify(&signed_data, CryptoConfig::new()).unwrap();
                assert!(verified, "Hybrid ML-DSA-44+Ed25519 should verify");
            })
            .unwrap()
            .join()
            .unwrap();
    }

    // === Hybrid ML-DSA-87 + Ed25519 sign/verify roundtrip ===

    #[test]
    fn test_hybrid_ml_dsa_87_ed25519_roundtrip() {
        std::thread::Builder::new()
            .name("hybrid_87".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                use crate::primitives::sig::ml_dsa::MlDsaParameterSet;
                use crate::unified_api::convenience::keygen::{
                    generate_keypair, generate_ml_dsa_keypair,
                };

                let (pq_pk, pq_sk) = generate_ml_dsa_keypair(MlDsaParameterSet::MlDsa87).unwrap();
                let (ed_pk, ed_sk) = generate_keypair().unwrap();
                let combined_pk = [pq_pk.into_bytes(), ed_pk.into_bytes()].concat();
                let combined_sk = [pq_sk.expose_secret(), ed_sk.expose_secret()].concat();

                let message = b"Hybrid ML-DSA-87 + Ed25519 test";
                let config = CryptoConfig::new().security_level(SecurityLevel::Maximum);
                let signed_data =
                    sign_with_key(message, &combined_sk, &combined_pk, config).unwrap();
                assert!(signed_data.metadata.signature_algorithm.contains("hybrid"));

                let verified = verify(&signed_data, CryptoConfig::new()).unwrap();
                assert!(verified, "Hybrid ML-DSA-87+Ed25519 should verify");
            })
            .unwrap()
            .join()
            .unwrap();
    }

    // === Error handling tests for sign_with_key ===

    #[test]
    fn test_sign_with_invalid_hybrid_key_lengths_returns_error() {
        std::thread::Builder::new()
            .name("hybrid_bad_key".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                let message = b"test";
                // Try to sign with keys that are too short for any hybrid scheme
                let _short_key = vec![1u8; 10];
                let short_pk = vec![2u8; 10];
                let signed = SignedData::new(
                    message.to_vec(),
                    SignedMetadata::new(
                        vec![0u8; 10],
                        "hybrid-ml-dsa-65-ed25519".to_string(),
                        short_pk,
                        None,
                    ),
                    "hybrid-ml-dsa-65-ed25519".to_string(),
                    0,
                );

                // Verification should fail because the keys/signature are too short
                let result = verify(&signed, CryptoConfig::new());
                assert!(result.is_err() || (result.is_ok() && !result.unwrap()));
            })
            .unwrap()
            .join()
            .unwrap();
    }

    // === Ed25519 fallback test ===

    #[test]
    fn test_verify_with_unsupported_scheme_rejected_fails() {
        let signed = SignedData::new(
            b"test".to_vec(),
            SignedMetadata::new(
                vec![0u8; 64],
                "unsupported-scheme".to_string(),
                vec![0u8; 32],
                None,
            ),
            "unsupported-scheme".to_string(),
            0,
        );

        // Unsupported schemes are explicitly rejected
        let result = verify(&signed, CryptoConfig::new());
        assert!(result.is_err(), "Unsupported scheme should be rejected");
    }

    // === Encryption with different security levels ===

    #[test]
    fn test_encrypt_decrypt_with_maximum_security_succeeds() {
        std::thread::Builder::new()
            .name("max_security_enc".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                let data = b"Maximum security encryption test";
                let key = vec![0x42u8; 32];
                let config = CryptoConfig::new()
                    .security_level(SecurityLevel::Maximum)
                    .force_scheme(CryptoScheme::Symmetric);

                let encrypted = encrypt(data, EncryptKey::Symmetric(&key), config.clone()).unwrap();
                let decrypted =
                    decrypt(&encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new()).unwrap();
                assert_eq!(data.as_slice(), decrypted.as_slice());
            })
            .unwrap()
            .join()
            .unwrap();
    }

    // === select_encryption_scheme and select_signature_scheme ===

    #[test]
    fn test_select_scheme_with_use_case_succeeds() {
        let config = CryptoConfig::new().use_case(UseCase::SecureMessaging);
        let scheme = select_signature_scheme(&config);
        assert!(scheme.is_ok(), "Should select a scheme for SecureMessaging");

        let config = CryptoConfig::new().use_case(UseCase::FinancialTransactions);
        let scheme = select_signature_scheme(&config);
        assert!(scheme.is_ok(), "Should select a scheme for FinancialTransactions");
    }

    #[test]
    fn test_select_encryption_scheme_with_use_case_succeeds() {
        let config = CryptoConfig::new().use_case(UseCase::SecureMessaging);
        let scheme = select_encryption_scheme_typed(&config);
        assert!(scheme.is_ok(), "Should select encryption scheme for SecureMessaging");
    }

    // === Verify error branches for hybrid schemes ===

    #[test]
    fn test_verify_hybrid_44_signature_too_short_returns_error() {
        let signed = SignedData::new(
            b"test".to_vec(),
            SignedMetadata::new(
                vec![0u8; 100],
                "hybrid-ml-dsa-44-ed25519".to_string(),
                vec![0u8; 1344],
                None,
            ),
            "hybrid-ml-dsa-44-ed25519".to_string(),
            0,
        );
        let result = verify(&signed, CryptoConfig::new());
        // Pattern 6 (TOFU follow-up): shape failures (sig too short, PK
        // wrong length) collapse into `Ok(false)` so they are
        // indistinguishable from genuine verify failures at both the API
        // and timing layer. Test names retain the legacy `_returns_error`
        // suffix to keep CI test-filter shell scripts working.
        assert!(!result.unwrap(), "shape failure must collapse to Ok(false)");
    }

    #[test]
    fn test_verify_hybrid_44_invalid_pk_length_returns_error() {
        let signed = SignedData::new(
            b"test".to_vec(),
            SignedMetadata::new(
                vec![0u8; 3000],
                "hybrid-ml-dsa-44-ed25519".to_string(),
                vec![0u8; 100],
                None,
            ),
            "hybrid-ml-dsa-44-ed25519".to_string(),
            0,
        );
        let result = verify(&signed, CryptoConfig::new());
        assert!(!result.unwrap(), "shape failure must collapse to Ok(false)");
    }

    #[test]
    fn test_verify_hybrid_65_signature_too_short_returns_error() {
        let signed = SignedData::new(
            b"test".to_vec(),
            SignedMetadata::new(
                vec![0u8; 100],
                "hybrid-ml-dsa-65-ed25519".to_string(),
                vec![0u8; 1984],
                None,
            ),
            "hybrid-ml-dsa-65-ed25519".to_string(),
            0,
        );
        let result = verify(&signed, CryptoConfig::new());
        assert!(!result.unwrap(), "shape failure must collapse to Ok(false)");
    }

    #[test]
    fn test_verify_hybrid_65_invalid_pk_length_returns_error() {
        let signed = SignedData::new(
            b"test".to_vec(),
            SignedMetadata::new(
                vec![0u8; 4000],
                "hybrid-ml-dsa-65-ed25519".to_string(),
                vec![0u8; 100],
                None,
            ),
            "hybrid-ml-dsa-65-ed25519".to_string(),
            0,
        );
        let result = verify(&signed, CryptoConfig::new());
        assert!(!result.unwrap(), "shape failure must collapse to Ok(false)");
    }

    #[test]
    fn test_verify_hybrid_87_signature_too_short_returns_error() {
        let signed = SignedData::new(
            b"test".to_vec(),
            SignedMetadata::new(
                vec![0u8; 100],
                "hybrid-ml-dsa-87-ed25519".to_string(),
                vec![0u8; 2624],
                None,
            ),
            "hybrid-ml-dsa-87-ed25519".to_string(),
            0,
        );
        let result = verify(&signed, CryptoConfig::new());
        assert!(!result.unwrap(), "shape failure must collapse to Ok(false)");
    }

    #[test]
    fn test_verify_hybrid_87_invalid_pk_length_returns_error() {
        let signed = SignedData::new(
            b"test".to_vec(),
            SignedMetadata::new(
                vec![0u8; 5000],
                "hybrid-ml-dsa-87-ed25519".to_string(),
                vec![0u8; 100],
                None,
            ),
            "hybrid-ml-dsa-87-ed25519".to_string(),
            0,
        );
        let result = verify(&signed, CryptoConfig::new());
        assert!(!result.unwrap(), "shape failure must collapse to Ok(false)");
    }

    // === Hybrid forgery tests: confirm bitwise & combine semantics ===
    //
    // These tests pin the security property that BOTH components must verify
    // for the hybrid signature to be accepted. A regression that swapped
    // `pq_valid & ed_valid` to `pq_valid | ed_valid` (either-suffices) would
    // pass every other hybrid test in the suite, so the only way to detect
    // it is to construct a signature where exactly one component is forged
    // and assert the result is `false`.

    #[test]
    fn test_hybrid_ml_dsa_44_ed25519_forged_pq_component_fails() {
        std::thread::Builder::new()
            .name("hybrid_44_forge_pq".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                use crate::primitives::sig::ml_dsa::MlDsaParameterSet;
                use crate::unified_api::convenience::keygen::{
                    generate_keypair, generate_ml_dsa_keypair,
                };

                let (pq_pk, pq_sk) = generate_ml_dsa_keypair(MlDsaParameterSet::MlDsa44).unwrap();
                let (ed_pk, ed_sk) = generate_keypair().unwrap();
                let combined_pk = [pq_pk.into_bytes(), ed_pk.into_bytes()].concat();
                let combined_sk = [pq_sk.expose_secret(), ed_sk.expose_secret()].concat();

                let message = b"Hybrid forgery test: PQ component flipped";
                let config = CryptoConfig::new().security_level(SecurityLevel::Standard);
                let mut signed =
                    sign_with_key(message, &combined_sk, &combined_pk, config).unwrap();

                // Sanity: untampered signature verifies.
                assert!(verify(&signed, CryptoConfig::new()).unwrap());

                // Flip a byte in the PQ portion (front of the combined sig).
                // The Ed25519 portion is the last 64 bytes; everything before
                // is ML-DSA. Index 16 sits firmly in the ML-DSA payload.
                signed.metadata.signature[16] ^= 0x01;

                let result = verify(&signed, CryptoConfig::new()).unwrap();
                assert!(
                    !result,
                    "Hybrid signature with forged PQ component must NOT verify \
                     (regression to bitwise OR would pass this case)"
                );
            })
            .unwrap()
            .join()
            .unwrap();
    }

    #[test]
    fn test_hybrid_ml_dsa_44_ed25519_forged_ed25519_component_fails() {
        std::thread::Builder::new()
            .name("hybrid_44_forge_ed".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                use crate::primitives::sig::ml_dsa::MlDsaParameterSet;
                use crate::unified_api::convenience::keygen::{
                    generate_keypair, generate_ml_dsa_keypair,
                };

                let (pq_pk, pq_sk) = generate_ml_dsa_keypair(MlDsaParameterSet::MlDsa44).unwrap();
                let (ed_pk, ed_sk) = generate_keypair().unwrap();
                let combined_pk = [pq_pk.into_bytes(), ed_pk.into_bytes()].concat();
                let combined_sk = [pq_sk.expose_secret(), ed_sk.expose_secret()].concat();

                let message = b"Hybrid forgery test: Ed25519 component flipped";
                let config = CryptoConfig::new().security_level(SecurityLevel::Standard);
                let mut signed =
                    sign_with_key(message, &combined_sk, &combined_pk, config).unwrap();

                // Sanity: untampered signature verifies.
                assert!(verify(&signed, CryptoConfig::new()).unwrap());

                // Flip a byte in the Ed25519 portion (last 64 bytes of sig).
                let len = signed.metadata.signature.len();
                signed.metadata.signature[len - 32] ^= 0x01;

                let result = verify(&signed, CryptoConfig::new()).unwrap();
                assert!(
                    !result,
                    "Hybrid signature with forged Ed25519 component must NOT verify \
                     (regression to bitwise OR would pass this case)"
                );
            })
            .unwrap()
            .join()
            .unwrap();
    }

    // === sign_with_key error branches for hybrid ===

    #[test]
    fn test_sign_with_key_hybrid_44_invalid_sk_length_returns_error() {
        std::thread::Builder::new()
            .name("hybrid_44_bad_sk".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                use crate::primitives::sig::ml_dsa::MlDsaParameterSet;
                let pq_pk_len = MlDsaParameterSet::MlDsa44.public_key_size();

                let message = b"test";
                let bad_sk = vec![0u8; 10]; // Wrong length
                let pk = vec![0u8; pq_pk_len + 32]; // Correct PK length

                let signed = SignedData::new(
                    message.to_vec(),
                    SignedMetadata::new(
                        vec![],
                        "hybrid-ml-dsa-44-ed25519".to_string(),
                        pk.clone(),
                        None,
                    ),
                    "hybrid-ml-dsa-44-ed25519".to_string(),
                    0,
                );

                // Direct call to sign_with_key with wrong SK length
                let config = CryptoConfig::new().security_level(SecurityLevel::Standard);
                let result = sign_with_key(message, &bad_sk, &pk, config);
                // The scheme selector may not pick hybrid-44, so we test via verify instead
                let _ = result;

                // Test verify with correct-length PK but bad signature
                let result = verify(&signed, CryptoConfig::new());
                assert!(result.is_err() || matches!(result, Ok(false)));
            })
            .unwrap()
            .join()
            .unwrap();
    }

    // === Encrypt/Decrypt with different stored scheme names ===
    // NOTE: test_decrypt_with_ml_kem_scheme_name and test_decrypt_with_chacha_scheme_name_rejected
    // have been removed. EncryptionScheme is now an enum, so there are no arbitrary scheme
    // strings to substitute. The typed scheme field prevents these scenarios at compile time.

    #[test]
    fn test_decrypt_unknown_scheme_short_key_returns_error() {
        // EncryptionScheme is an enum — no unknown schemes exist.
        // Test: short symmetric key with AES-GCM scheme → key length error.
        let encrypted = EncryptedOutput::new(
            EncryptionScheme::Aes256Gcm,
            vec![1, 2, 3, 4],
            vec![0u8; 12],
            vec![0u8; 16],
            None,
            0,
            None,
        )
        .expect("symmetric scheme + None hybrid_data is a valid shape");
        let short_key = vec![0x42u8; 16]; // Too short for AES-256-GCM
        let result = decrypt(&encrypted, DecryptKey::Symmetric(&short_key), CryptoConfig::new());
        assert!(result.is_err());
    }

    #[test]
    fn test_decrypt_short_key_ml_kem_scheme_returns_error() {
        // Hybrid scheme with no hybrid_data is structurally invalid; the
        // shape guard in `EncryptedOutput::new` rejects it. The
        // key-scheme-mismatch case is exercised at decrypt time only when
        // the EncryptedOutput is well-formed.
        let result = EncryptedOutput::new(
            EncryptionScheme::HybridMlKem768Aes256Gcm,
            vec![1, 2, 3, 4],
            vec![0u8; 12],
            vec![0u8; 16],
            None,
            0,
            None,
        );
        assert!(result.is_err());
    }

    #[test]
    fn test_encrypt_empty_data_returns_error() {
        let key = vec![0x42u8; 32];
        let config = CryptoConfig::new().force_scheme(CryptoScheme::Symmetric);
        let encrypted = encrypt(b"", EncryptKey::Symmetric(&key), config).unwrap();
        let decrypted =
            decrypt(&encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new()).unwrap();
        assert!(decrypted.is_empty());
    }

    // === Key-scheme mismatch tests (the core anti-degradation guarantee) ===

    #[test]
    fn test_encrypt_symmetric_key_default_config_rejects_hybrid_scheme_fails() {
        // CryptoConfig::new() selects a hybrid scheme by default.
        // Passing EncryptKey::Symmetric MUST fail — never silently degrade.
        let key = vec![0x42u8; 32];
        let result = encrypt(b"hello", EncryptKey::Symmetric(&key), CryptoConfig::new());
        assert!(result.is_err(), "Symmetric key + hybrid scheme (default config) must be rejected");
        let err = format!("{}", result.unwrap_err());
        assert!(
            err.contains("requires a hybrid key")
                || err.contains("requires a hybrid or PQ-only key")
                || err.contains("mismatch"),
            "Error should mention key type mismatch, got: {}",
            err
        );
    }

    #[test]
    fn test_encrypt_hybrid_key_symmetric_scheme_rejects_fails() {
        // force_scheme(Symmetric) selects AES-256-GCM, but EncryptKey::Hybrid
        // is provided — must fail.
        let (pk, _sk) = crate::unified_api::convenience::generate_hybrid_keypair().unwrap();
        let config = CryptoConfig::new().force_scheme(CryptoScheme::Symmetric);
        let result = encrypt(b"hello", EncryptKey::Hybrid(&pk), config);
        assert!(result.is_err(), "Hybrid key + symmetric scheme must be rejected");
        let err = format!("{}", result.unwrap_err());
        assert!(
            err.contains("requires a symmetric key")
                || err.contains("does not accept a hybrid key")
                || err.contains("mismatch"),
            "Error should mention key type mismatch, got: {}",
            err
        );
    }

    #[test]
    fn test_decrypt_symmetric_key_hybrid_encrypted_data_rejects_fails() {
        // Encrypt with hybrid key, try to decrypt with symmetric key.
        let (pk, _sk) = crate::unified_api::convenience::generate_hybrid_keypair().unwrap();
        let encrypted = encrypt(b"hello", EncryptKey::Hybrid(&pk), CryptoConfig::new()).unwrap();
        let fake_key = vec![0x42u8; 32];
        let result = decrypt(&encrypted, DecryptKey::Symmetric(&fake_key), CryptoConfig::new());
        assert!(result.is_err(), "Symmetric key cannot decrypt hybrid-encrypted data");
    }

    // === Use case-based encryption ===

    #[test]
    fn test_encrypt_decrypt_with_use_case_succeeds() {
        let key = vec![0x42u8; 32];
        let data = b"UseCase-based encryption test";
        let config = CryptoConfig::new()
            .use_case(UseCase::FileStorage)
            .force_scheme(CryptoScheme::Symmetric);

        let encrypted = encrypt(data, EncryptKey::Symmetric(&key), config).unwrap();
        let decrypted =
            decrypt(&encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new()).unwrap();
        assert_eq!(decrypted.as_slice(), data.as_slice());
    }

    #[test]
    fn test_encrypt_with_iot_use_case_succeeds() {
        let key = vec![0x42u8; 32];
        let data = b"IoT device data";
        let config =
            CryptoConfig::new().use_case(UseCase::IoTDevice).force_scheme(CryptoScheme::Symmetric);

        let encrypted = encrypt(data, EncryptKey::Symmetric(&key), config).unwrap();
        assert!(!encrypted.ciphertext().is_empty());
    }

    // === Keygen through generate_signing_keypair with different use cases ===

    #[test]
    fn test_generate_signing_keypair_iot_use_case_succeeds() {
        // `IoTDevice` is fundamentally a low-resource use case, not a category
        // that bans signing. `select_signature_scheme` routes use cases through
        // `UseCaseConfig`, which assigns IoT → `SecurityLevel::Standard`, so
        // signing keygen returns `hybrid-ml-dsa-44-ed25519` (the L1 hybrid).
        //
        // Regression: prior to the use-case scheme dispatch fix, this call
        // crashed with "Unsupported signing scheme: hybrid-ml-kem-512-aes-256-gcm"
        // because the dispatcher accidentally routed through the encryption
        // scheme selector.
        let config = CryptoConfig::new().use_case(UseCase::IoTDevice);
        let (pk, sk, scheme) =
            generate_signing_keypair(config).expect("IoT signing keygen must succeed").into_parts();
        assert_eq!(scheme, "hybrid-ml-dsa-44-ed25519");
        assert!(!pk.is_empty());
        assert!(!sk.is_empty());
    }

    #[test]
    fn test_generate_signing_keypair_file_storage_use_case_succeeds() {
        // `FileStorage` is a "long-term security" use case → `SecurityLevel::Maximum`,
        // which selects the L5 hybrid signing scheme.
        let config = CryptoConfig::new().use_case(UseCase::FileStorage);
        let (_, _, scheme) = generate_signing_keypair(config)
            .expect("FileStorage signing keygen must succeed")
            .into_parts();
        assert_eq!(scheme, "hybrid-ml-dsa-87-ed25519");
    }

    #[test]
    fn test_generate_signing_keypair_secure_messaging_use_case_succeeds() {
        // SecureMessaging defaults to `SecurityLevel::High` (the `UseCaseConfig`
        // base level when no explicit level is set), which selects the L3
        // hybrid signing scheme.
        let config = CryptoConfig::new().use_case(UseCase::SecureMessaging);
        let (_, _, scheme) = generate_signing_keypair(config)
            .expect("SecureMessaging signing keygen must succeed")
            .into_parts();
        assert_eq!(scheme, "hybrid-ml-dsa-65-ed25519");
    }

    // === Additional keygen scheme branches ===

    #[test]
    fn test_generate_signing_keypair_blockchain_use_case_succeeds() {
        let config = CryptoConfig::new().use_case(UseCase::BlockchainTransaction);
        let result = generate_signing_keypair(config);
        assert!(result.is_ok());
        let (pk, sk, scheme) = result.unwrap().into_parts();
        assert!(!pk.is_empty());
        assert!(!sk.is_empty());
        assert!(!scheme.is_empty());
    }

    #[test]
    fn test_generate_signing_keypair_legal_use_case_succeeds() {
        let config = CryptoConfig::new().use_case(UseCase::LegalDocuments);
        let result = generate_signing_keypair(config);
        assert!(result.is_ok());
    }

    #[test]
    fn test_generate_signing_keypair_firmware_use_case_succeeds() {
        let config = CryptoConfig::new().use_case(UseCase::FirmwareSigning);
        let result = generate_signing_keypair(config);
        assert!(result.is_ok());
    }

    // === Verified session API tests ===

    #[test]
    fn test_encrypt_decrypt_with_verified_session_succeeds() {
        std::thread::Builder::new()
            .name("enc_verified".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                let data = b"Verified session test data";
                let key = vec![0x42u8; 32];

                let (auth_pk, auth_sk) =
                    crate::unified_api::convenience::keygen::generate_keypair().unwrap();
                let session =
                    crate::VerifiedSession::establish(auth_pk.as_slice(), auth_sk.expose_secret())
                        .unwrap();

                let config =
                    CryptoConfig::new().session(&session).force_scheme(CryptoScheme::Symmetric);
                let encrypted = encrypt(data, EncryptKey::Symmetric(&key), config.clone()).unwrap();
                let decrypted =
                    decrypt(&encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new()).unwrap();
                assert_eq!(data.as_slice(), decrypted.as_slice());
            })
            .unwrap()
            .join()
            .unwrap();
    }

    #[test]
    fn test_sign_verify_with_verified_session_succeeds() {
        std::thread::Builder::new()
            .name("sign_verified".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                let message = b"Verified session sign test";

                let (auth_pk, auth_sk) =
                    crate::unified_api::convenience::keygen::generate_keypair().unwrap();
                let session =
                    crate::VerifiedSession::establish(auth_pk.as_slice(), auth_sk.expose_secret())
                        .unwrap();

                let config = CryptoConfig::new().session(&session);
                let (pk, sk, _scheme) =
                    generate_signing_keypair(config.clone()).unwrap().into_parts();
                let signed = sign_with_key(message, &sk, &pk, config.clone()).unwrap();
                let valid = verify(&signed, config).unwrap();
                assert!(valid);
            })
            .unwrap()
            .join()
            .unwrap();
    }

    // === Expired session API tests (T-008) ===

    #[test]
    fn test_encrypt_with_expired_session_returns_session_expired_succeeds() {
        std::thread::Builder::new()
            .name("enc_expired".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                let (auth_pk, auth_sk) =
                    crate::unified_api::convenience::keygen::generate_keypair().unwrap();
                let session =
                    crate::VerifiedSession::establish(auth_pk.as_slice(), auth_sk.expose_secret())
                        .unwrap();
                let expired = session.expired_clone();

                let key = vec![0x42u8; 32];
                let config =
                    CryptoConfig::new().session(&expired).force_scheme(CryptoScheme::Symmetric);
                let result = encrypt(b"test", EncryptKey::Symmetric(&key), config);

                assert!(result.is_err(), "Encrypt with expired session should fail");
                match result.unwrap_err() {
                    CoreError::SessionExpired => {}
                    other => panic!("Expected SessionExpired, got: {other:?}"),
                }
            })
            .unwrap()
            .join()
            .unwrap();
    }

    #[test]
    fn test_decrypt_with_expired_session_returns_session_expired_succeeds() {
        std::thread::Builder::new()
            .name("dec_expired".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                let key = vec![0x42u8; 32];

                // First encrypt with no session
                let config = CryptoConfig::new().force_scheme(CryptoScheme::Symmetric);
                let encrypted = encrypt(b"test data", EncryptKey::Symmetric(&key), config).unwrap();

                // Now try to decrypt with an expired session
                let (auth_pk, auth_sk) =
                    crate::unified_api::convenience::keygen::generate_keypair().unwrap();
                let session =
                    crate::VerifiedSession::establish(auth_pk.as_slice(), auth_sk.expose_secret())
                        .unwrap();
                let expired = session.expired_clone();

                let config = CryptoConfig::new().session(&expired);
                let result = decrypt(&encrypted, DecryptKey::Symmetric(&key), config);

                assert!(result.is_err(), "Decrypt with expired session should fail");
                match result.unwrap_err() {
                    CoreError::SessionExpired => {}
                    other => panic!("Expected SessionExpired, got: {other:?}"),
                }
            })
            .unwrap()
            .join()
            .unwrap();
    }

    #[test]
    fn test_sign_with_expired_session_returns_session_expired_succeeds() {
        std::thread::Builder::new()
            .name("sign_expired".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                let (auth_pk, auth_sk) =
                    crate::unified_api::convenience::keygen::generate_keypair().unwrap();
                let session =
                    crate::VerifiedSession::establish(auth_pk.as_slice(), auth_sk.expose_secret())
                        .unwrap();

                // Generate a valid keypair first
                let (pk, sk, _scheme) =
                    generate_signing_keypair(CryptoConfig::new()).unwrap().into_parts();

                let expired = session.expired_clone();
                let config = CryptoConfig::new().session(&expired);
                let result = sign_with_key(b"test message", &sk, &pk, config);

                assert!(result.is_err(), "Sign with expired session should fail");
                match result.unwrap_err() {
                    CoreError::SessionExpired => {}
                    other => panic!("Expected SessionExpired, got: {other:?}"),
                }
            })
            .unwrap()
            .join()
            .unwrap();
    }

    #[test]
    fn test_verify_with_expired_session_returns_session_expired_succeeds() {
        std::thread::Builder::new()
            .name("verify_expired".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                // Sign with no session first
                let message = b"test message";
                let config = CryptoConfig::new();
                let (pk, sk, _scheme) =
                    generate_signing_keypair(config.clone()).unwrap().into_parts();
                let signed = sign_with_key(message, &sk, &pk, config).unwrap();

                // Now try to verify with an expired session
                let (auth_pk, auth_sk) =
                    crate::unified_api::convenience::keygen::generate_keypair().unwrap();
                let session =
                    crate::VerifiedSession::establish(auth_pk.as_slice(), auth_sk.expose_secret())
                        .unwrap();
                let expired = session.expired_clone();

                let config = CryptoConfig::new().session(&expired);
                let result = verify(&signed, config);

                assert!(result.is_err(), "Verify with expired session should fail");
                match result.unwrap_err() {
                    CoreError::SessionExpired => {}
                    other => panic!("Expected SessionExpired, got: {other:?}"),
                }
            })
            .unwrap()
            .join()
            .unwrap();
    }

    // === sign_with_key hybrid error branches ===

    #[test]
    fn test_sign_with_key_hybrid_87_wrong_sk_length_returns_error() {
        std::thread::Builder::new()
            .name("hybrid_87_bad_sk".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                use crate::primitives::sig::ml_dsa::MlDsaParameterSet;
                let pq_pk_len = MlDsaParameterSet::MlDsa87.public_key_size();

                let message = b"test";
                let bad_sk = vec![0u8; 10]; // Wrong length
                let pk = vec![0u8; pq_pk_len + 32]; // Correct PK length

                // Need to manually construct SignedData with hybrid-87 scheme
                // to test the verify error path
                let signed = SignedData::new(
                    message.to_vec(),
                    SignedMetadata::new(
                        vec![0u8; 100],
                        "hybrid-ml-dsa-87-ed25519".to_string(),
                        pk,
                        None,
                    ),
                    "hybrid-ml-dsa-87-ed25519".to_string(),
                    0,
                );
                let result = verify(&signed, CryptoConfig::new());
                // Pattern 6 (TOFU follow-up): see comment on
                // `test_verify_hybrid_44_signature_too_short_returns_error`.
                assert!(!result.unwrap(), "shape failure must collapse to Ok(false)");

                let _ = bad_sk; // suppress unused warning
            })
            .unwrap()
            .join()
            .unwrap();
    }

    #[test]
    fn test_sign_with_key_hybrid_44_wrong_pk_length_returns_error() {
        std::thread::Builder::new()
            .name("hybrid_44_bad_pk".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                use crate::primitives::sig::ml_dsa::MlDsaParameterSet;
                let pq_sk_len = MlDsaParameterSet::MlDsa44.secret_key_size();

                let message = b"test";
                let sk = vec![0u8; pq_sk_len + 32]; // Correct SK length
                let bad_pk = vec![0u8; 100]; // Wrong PK length

                // This should return InvalidKey error for wrong PK length
                let config = CryptoConfig::new().security_level(SecurityLevel::Standard);
                let _result = sign_with_key(message, &sk, &bad_pk, config);
                // The scheme selector may not pick hybrid-44, so we just check it doesn't panic
            })
            .unwrap()
            .join()
            .unwrap();
    }

    // === Encrypt with short key for hybrid/PQ scheme path ===

    #[test]
    fn test_encrypt_short_key_with_use_case_returns_error() {
        let short_key = vec![0x42u8; 16]; // Too short
        let config = CryptoConfig::new()
            .use_case(UseCase::FileStorage)
            .force_scheme(CryptoScheme::Symmetric);
        let result = encrypt(b"test", EncryptKey::Symmetric(&short_key), config);
        assert!(result.is_err(), "Short key should fail even with use case");
    }

    // === select_encryption_scheme_typed with SecurityLevel ===

    #[test]
    fn test_select_encryption_scheme_with_security_level_succeeds() {
        let levels = vec![SecurityLevel::Standard, SecurityLevel::High, SecurityLevel::Maximum];
        for level in levels {
            let config = CryptoConfig::new().security_level(level.clone());
            let scheme = select_encryption_scheme_typed(&config);
            assert!(scheme.is_ok(), "Should select scheme for security level: {:?}", level);
        }
    }

    // === select_signature_scheme with SecurityLevel ===

    #[test]
    fn test_select_signature_scheme_with_security_levels_succeeds() {
        let levels = vec![SecurityLevel::Standard, SecurityLevel::High, SecurityLevel::Maximum];
        for level in levels {
            let config = CryptoConfig::new().security_level(level.clone());
            let scheme = select_signature_scheme(&config);
            assert!(scheme.is_ok(), "Should select sig scheme for security level: {:?}", level);
        }
    }

    // === Encrypt/decrypt with different security levels ===

    #[test]
    fn test_encrypt_decrypt_standard_security_level_succeeds() {
        let key = vec![0x42u8; 32];
        let data = b"Standard level encryption";
        let config = CryptoConfig::new()
            .security_level(SecurityLevel::Standard)
            .force_scheme(CryptoScheme::Symmetric);

        let encrypted = encrypt(data, EncryptKey::Symmetric(&key), config).unwrap();
        let decrypted =
            decrypt(&encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new()).unwrap();
        assert_eq!(decrypted.as_slice(), data.as_slice());
    }

    #[test]
    fn test_encrypt_decrypt_maximum_security_level_succeeds() {
        let key = vec![0x42u8; 32];
        let data = b"Maximum level encryption";
        let config = CryptoConfig::new()
            .security_level(SecurityLevel::Maximum)
            .force_scheme(CryptoScheme::Symmetric);

        let encrypted = encrypt(data, EncryptKey::Symmetric(&key), config).unwrap();
        let decrypted =
            decrypt(&encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new()).unwrap();
        assert_eq!(decrypted.as_slice(), data.as_slice());
    }

    // === ChaCha20-Poly1305 unified API dispatch ===

    #[cfg(not(feature = "fips"))]
    #[test]
    fn test_encrypt_decrypt_chacha20poly1305_roundtrip() {
        let key = [0x55u8; 32];
        let data = b"ChaCha20-Poly1305 roundtrip test";

        // Force ChaCha20-Poly1305 via the internal scheme selection
        let encrypted = encrypt_chacha20_internal(data, &key, &[]).unwrap();
        let decrypted = decrypt_chacha20_internal(&encrypted, &key, &[]).unwrap();
        assert_eq!(decrypted.as_slice(), data.as_slice());
    }

    #[cfg(not(feature = "fips"))]
    #[test]
    fn test_encrypt_decrypt_chacha20poly1305_via_unified_api_succeeds() {
        let key = [0xAAu8; 32];
        let data = b"ChaCha20 unified dispatch test";

        // Use internal function to build EncryptedOutput with ChaCha20 scheme
        let encrypted_bytes = encrypt_chacha20_internal(data, &key, &[]).unwrap();
        let output =
            symmetric_bytes_to_output(EncryptionScheme::ChaCha20Poly1305, &encrypted_bytes)
                .unwrap();

        // Decrypt via unified decrypt
        let decrypted = decrypt(&output, DecryptKey::Symmetric(&key), CryptoConfig::new()).unwrap();
        assert_eq!(decrypted.as_slice(), data.as_slice());
    }

    #[cfg(feature = "fips")]
    #[test]
    fn test_chacha20poly1305_rejected_in_fips_mode_fails() {
        let key = [0xBBu8; 32];
        let encrypted_bytes = vec![0u8; 100]; // doesn't matter, should fail before decryption
        let output = EncryptedOutput::new(
            EncryptionScheme::ChaCha20Poly1305,
            encrypted_bytes,
            vec![0u8; 12],
            vec![0u8; 16],
            None,
            0,
            None,
        )
        .expect("symmetric scheme + None hybrid_data is a valid shape");

        let result = decrypt(&output, DecryptKey::Symmetric(&key), CryptoConfig::new());
        assert!(result.is_err(), "ChaCha20 should be rejected in FIPS mode");
    }

    // === Generate signing keypair with all use cases ===

    #[test]
    fn test_generate_signing_keypair_authentication_use_case_succeeds() {
        std::thread::Builder::new()
            .name("auth_keygen".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                let config = CryptoConfig::new().use_case(UseCase::Authentication);
                let (pk, sk, scheme) = generate_signing_keypair(config).unwrap().into_parts();
                assert!(
                    scheme.contains("hybrid-ml-dsa-87"),
                    "Auth should use hybrid-87: {}",
                    scheme
                );
                assert!(!pk.is_empty());
                assert!(!sk.is_empty());
            })
            .unwrap()
            .join()
            .unwrap();
    }

    #[test]
    fn test_generate_signing_keypair_digital_certificate_use_case_succeeds() {
        std::thread::Builder::new()
            .name("cert_keygen".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                let config = CryptoConfig::new().use_case(UseCase::DigitalCertificate);
                let (pk, sk, scheme) = generate_signing_keypair(config).unwrap().into_parts();
                assert!(
                    scheme.contains("hybrid-ml-dsa-87"),
                    "DigitalCertificate should use hybrid-87: {}",
                    scheme
                );
                assert!(!pk.is_empty());
                assert!(!sk.is_empty());
            })
            .unwrap()
            .join()
            .unwrap();
    }

    // === Sign message wrapper for various use cases ===

    #[test]
    fn test_sign_message_standard_level_succeeds() {
        std::thread::Builder::new()
            .name("sign_standard".to_string())
            .stack_size(32 * 1024 * 1024)
            .spawn(|| {
                let config = CryptoConfig::new().security_level(SecurityLevel::Standard);
                let signed = sign_message(b"Standard", config).unwrap();
                assert!(
                    signed.scheme.contains("hybrid-ml-dsa-44"),
                    "Standard should use hybrid-44: {}",
                    signed.scheme
                );
                let valid = verify(&signed, CryptoConfig::new()).unwrap();
                assert!(valid);
            })
            .unwrap()
            .join()
            .unwrap();
    }

    // ========================================================================
    // Compliance enforcement tests
    // ========================================================================

    #[test]
    fn test_cnsa_verify_rejects_ed25519_fails() -> Result<()> {
        // Sign with ed25519 (default Standard security uses ed25519-based hybrid)
        let message = b"compliance test";
        let config_default = CryptoConfig::new().security_level(SecurityLevel::Standard);
        let signed = sign_message(message, config_default)?;

        // If the scheme is "ed25519", CNSA 2.0 should reject it
        // CNSA 2.0 requires CryptoMode::PqOnly.
        if signed.scheme == "ed25519" {
            let cnsa_config = CryptoConfig::new()
                .security_level(SecurityLevel::Maximum)
                .crypto_mode(crate::types::types::CryptoMode::PqOnly)
                .compliance(crate::types::types::ComplianceMode::Cnsa2_0);
            let result = verify(&signed, cnsa_config);
            assert!(result.is_err(), "CNSA 2.0 should reject ed25519 verification");
            let err_msg = format!("{}", result.unwrap_err());
            assert!(err_msg.contains("Compliance violation"));
        }
        // For hybrid schemes, CNSA 2.0 allows them (transitional)
        Ok(())
    }

    #[cfg(feature = "fips")]
    #[test]
    fn test_cnsa_verify_rejects_standalone_ed25519_signature_fails() -> Result<()> {
        // Manually construct a SignedData with ed25519 scheme
        let signed = SignedData::new(
            b"test data".to_vec(),
            SignedMetadata::new(vec![0u8; 64], "ed25519".to_string(), vec![0u8; 32], None),
            "ed25519".to_string(),
            0,
        );

        let cnsa_config = CryptoConfig::new()
            .security_level(SecurityLevel::Maximum)
            .crypto_mode(crate::types::types::CryptoMode::PqOnly)
            .compliance(crate::types::types::ComplianceMode::Cnsa2_0);
        let result = verify(&signed, cnsa_config);
        assert!(result.is_err(), "CNSA 2.0 must reject ed25519 signatures");
        let err_msg = format!("{}", result.unwrap_err());
        assert!(err_msg.contains("CNSA 2.0"));
        Ok(())
    }

    #[cfg(feature = "fips")]
    #[test]
    fn test_fips_decrypt_allows_aes_gcm_succeeds() -> Result<()> {
        // Encrypt with symmetric force (produces AES-256-GCM)
        let key = vec![0x42u8; 32];
        let data = b"fips compliance test";
        let encrypted = encrypt(
            data,
            EncryptKey::Symmetric(&key),
            CryptoConfig::new().force_scheme(CryptoScheme::Symmetric),
        )?;

        assert_eq!(encrypted.scheme(), &EncryptionScheme::Aes256Gcm);

        // Decrypt with FIPS config should succeed (AES-256-GCM is FIPS-approved)
        let fips_config =
            CryptoConfig::new().compliance(crate::types::types::ComplianceMode::Fips140_3);
        let plaintext = decrypt(&encrypted, DecryptKey::Symmetric(&key), fips_config)?;
        assert_eq!(plaintext.as_slice(), data.as_slice());
        Ok(())
    }

    #[cfg(feature = "fips")]
    #[test]
    fn test_cnsa_decrypt_rejects_aes_gcm_fails() -> Result<()> {
        // Encrypt with symmetric force (produces AES-256-GCM)
        let key = vec![0x42u8; 32];
        let data = b"cnsa decrypt test";
        let encrypted = encrypt(
            data,
            EncryptKey::Symmetric(&key),
            CryptoConfig::new().force_scheme(CryptoScheme::Symmetric),
        )?;

        assert_eq!(encrypted.scheme(), &EncryptionScheme::Aes256Gcm);

        // Decrypt with CNSA 2.0 should reject (aes-256-gcm is classical-only)
        // CNSA 2.0 requires CryptoMode::PqOnly.
        let cnsa_config = CryptoConfig::new()
            .security_level(SecurityLevel::Maximum)
            .crypto_mode(crate::types::types::CryptoMode::PqOnly)
            .compliance(crate::types::types::ComplianceMode::Cnsa2_0);
        let result = decrypt(&encrypted, DecryptKey::Symmetric(&key), cnsa_config);
        assert!(result.is_err(), "CNSA 2.0 should reject standalone AES-256-GCM");
        let err_msg = format!("{}", result.unwrap_err());
        assert!(err_msg.contains("CNSA 2.0"));
        Ok(())
    }

    #[test]
    fn test_default_compliance_allows_all_in_verify_succeeds() -> Result<()> {
        // Sign and verify with default compliance — should always work
        let message = b"default compliance test";
        let config = CryptoConfig::new();
        let signed = sign_message(message, config)?;

        let default_config = CryptoConfig::new();
        let is_valid = verify(&signed, default_config)?;
        assert!(is_valid);
        Ok(())
    }

    #[test]
    fn test_default_compliance_allows_all_in_decrypt_succeeds() -> Result<()> {
        // Encrypt and decrypt with default compliance — should always work
        let key = vec![0x42u8; 32];
        let data = b"default compliance decrypt test";
        let encrypted = encrypt(
            data,
            EncryptKey::Symmetric(&key),
            CryptoConfig::new().force_scheme(CryptoScheme::Symmetric),
        )?;

        let default_config = CryptoConfig::new();
        let plaintext = decrypt(&encrypted, DecryptKey::Symmetric(&key), default_config)?;
        assert_eq!(plaintext.as_slice(), data.as_slice());
        Ok(())
    }

    #[cfg(feature = "fips")]
    #[test]
    fn test_fips_verify_allows_pq_signatures_succeeds() -> Result<()> {
        // Sign with PQ algorithm (ML-DSA-65)
        let message = b"pq fips compliance test";
        let config = CryptoConfig::new().security_level(SecurityLevel::High);
        let signed = sign_message(message, config)?;

        // FIPS config should accept PQ and hybrid signatures
        let fips_config =
            CryptoConfig::new().compliance(crate::types::types::ComplianceMode::Fips140_3);
        let is_valid = verify(&signed, fips_config)?;
        assert!(is_valid);
        Ok(())
    }

    #[cfg(feature = "fips")]
    #[test]
    fn test_cnsa_verify_allows_hybrid_signatures_succeeds() -> Result<()> {
        // Sign with hybrid (default High security → hybrid-ml-dsa-65-ed25519)
        let message = b"hybrid cnsa test";
        let config = CryptoConfig::new().security_level(SecurityLevel::High);
        let signed = sign_message(message, config)?;

        // Hybrid schemes are allowed under CNSA 2.0 (transitional per NIST SP 800-227)
        // Note: CNSA 2.0 config requires CryptoMode::PqOnly. The verifier
        // SecurityLevel must match (or be below) the signature's PQ
        // component — ml-dsa-65 is High, so we pin High here. Pinning
        // Maximum would be rejected by the
        // SecurityLevel-floor check (silent-downgrade prevention).
        if signed.scheme.contains("hybrid") {
            let cnsa_config = CryptoConfig::new()
                .security_level(SecurityLevel::High)
                .crypto_mode(crate::types::types::CryptoMode::PqOnly)
                .compliance(crate::types::types::ComplianceMode::Cnsa2_0);
            let is_valid = verify(&signed, cnsa_config)?;
            assert!(is_valid);
        }
        Ok(())
    }

    // =========================================================================
    // PQ-Only Unified API Tests (CryptoMode::PqOnly)
    // =========================================================================

    #[test]
    fn test_pq_only_encrypt_decrypt_roundtrip_768_succeeds() {
        use crate::hybrid::pq_only::generate_pq_keypair;
        let (pk, sk) = generate_pq_keypair().unwrap();
        let data = b"PQ-only unified API roundtrip test";
        let config =
            CryptoConfig::new().crypto_mode(CryptoMode::PqOnly).security_level(SecurityLevel::High);
        let encrypted = encrypt(data, EncryptKey::PqOnly(&pk), config.clone()).unwrap();
        assert_eq!(encrypted.scheme().as_str(), "pq-ml-kem-768-aes-256-gcm");
        let decrypted = decrypt(&encrypted, DecryptKey::PqOnly(&sk), config).unwrap();
        assert_eq!(decrypted.as_slice(), data.as_slice());
    }

    #[test]
    fn test_pq_only_encrypt_decrypt_roundtrip_512_succeeds() {
        use crate::hybrid::pq_only::generate_pq_keypair_with_level;
        use crate::primitives::kem::ml_kem::MlKemSecurityLevel;
        let (pk, sk) = generate_pq_keypair_with_level(MlKemSecurityLevel::MlKem512).unwrap();
        let data = b"PQ-only 512 roundtrip";
        let config = CryptoConfig::new()
            .crypto_mode(CryptoMode::PqOnly)
            .security_level(SecurityLevel::Standard);
        let encrypted = encrypt(data, EncryptKey::PqOnly(&pk), config.clone()).unwrap();
        assert_eq!(encrypted.scheme().as_str(), "pq-ml-kem-512-aes-256-gcm");
        let decrypted = decrypt(&encrypted, DecryptKey::PqOnly(&sk), config).unwrap();
        assert_eq!(decrypted.as_slice(), data.as_slice());
    }

    #[test]
    fn test_pq_only_encrypt_decrypt_roundtrip_1024_succeeds() {
        use crate::hybrid::pq_only::generate_pq_keypair_with_level;
        use crate::primitives::kem::ml_kem::MlKemSecurityLevel;
        let (pk, sk) = generate_pq_keypair_with_level(MlKemSecurityLevel::MlKem1024).unwrap();
        let data = b"PQ-only 1024 roundtrip";
        let config = CryptoConfig::new()
            .crypto_mode(CryptoMode::PqOnly)
            .security_level(SecurityLevel::Maximum);
        let encrypted = encrypt(data, EncryptKey::PqOnly(&pk), config.clone()).unwrap();
        assert_eq!(encrypted.scheme().as_str(), "pq-ml-kem-1024-aes-256-gcm");
        let decrypted = decrypt(&encrypted, DecryptKey::PqOnly(&sk), config).unwrap();
        assert_eq!(decrypted.as_slice(), data.as_slice());
    }

    #[test]
    fn test_pq_only_key_rejects_hybrid_scheme_fails() {
        // PQ-only key + hybrid config (default) must be rejected
        use crate::hybrid::pq_only::generate_pq_keypair;
        let (pk, _sk) = generate_pq_keypair().unwrap();
        let config = CryptoConfig::new(); // default = Hybrid mode
        let result = encrypt(b"test", EncryptKey::PqOnly(&pk), config);
        assert!(result.is_err(), "PQ-only key + hybrid scheme must fail");
    }

    #[test]
    fn test_hybrid_key_rejects_pq_only_scheme_fails() {
        // Hybrid key + PqOnly mode must be rejected
        let (pk, _sk) = crate::unified_api::convenience::generate_hybrid_keypair().unwrap();
        let config =
            CryptoConfig::new().crypto_mode(CryptoMode::PqOnly).security_level(SecurityLevel::High);
        let result = encrypt(b"test", EncryptKey::Hybrid(&pk), config);
        assert!(result.is_err(), "Hybrid key + PQ-only scheme must fail");
    }

    #[test]
    fn test_pq_only_empty_data_roundtrip_succeeds() {
        use crate::hybrid::pq_only::generate_pq_keypair;
        let (pk, sk) = generate_pq_keypair().unwrap();
        let config =
            CryptoConfig::new().crypto_mode(CryptoMode::PqOnly).security_level(SecurityLevel::High);
        let encrypted = encrypt(b"", EncryptKey::PqOnly(&pk), config.clone()).unwrap();
        let decrypted = decrypt(&encrypted, DecryptKey::PqOnly(&sk), config).unwrap();
        assert!(decrypted.is_empty());
    }
}
