//! `CipherAlg` — `EVP_CIPHER` algorithm descriptor.
//!
//! Phase 3.2 delivers `CipherAlg`; Phase 4.2 extends this module with
//! `CipherCtx<Dir>`, `AeadEncryptCtx`, and `AeadDecryptCtx`.

use crate::error::ErrorStack;
use native_ossl_sys as sys;
use std::ffi::CStr;
use std::sync::Arc;

// ── CipherAlg — algorithm descriptor ─────────────────────────────────────────

/// An OpenSSL symmetric cipher algorithm descriptor (`EVP_CIPHER*`).
///
/// Fetched once and reused.  Implements `Clone` via `EVP_CIPHER_up_ref`.
pub struct CipherAlg {
    ptr: *mut sys::EVP_CIPHER,
    /// Keeps the library context alive while this descriptor is in use.
    lib_ctx: Option<Arc<crate::lib_ctx::LibCtx>>,
}

impl CipherAlg {
    /// Fetch a cipher algorithm from the global default library context.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the algorithm is not available.
    pub fn fetch(name: &CStr, props: Option<&CStr>) -> Result<Self, ErrorStack> {
        let props_ptr = props.map_or(std::ptr::null(), CStr::as_ptr);
        let ptr = unsafe { sys::EVP_CIPHER_fetch(std::ptr::null_mut(), name.as_ptr(), props_ptr) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(CipherAlg { ptr, lib_ctx: None })
    }

    /// Fetch a cipher algorithm from an explicit library context.
    ///
    /// # Errors
    pub fn fetch_in(
        ctx: &Arc<crate::lib_ctx::LibCtx>,
        name: &CStr,
        props: Option<&CStr>,
    ) -> Result<Self, ErrorStack> {
        let props_ptr = props.map_or(std::ptr::null(), CStr::as_ptr);
        let ptr = unsafe { sys::EVP_CIPHER_fetch(ctx.as_ptr(), name.as_ptr(), props_ptr) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(CipherAlg {
            ptr,
            lib_ctx: Some(Arc::clone(ctx)),
        })
    }

    /// Expected key length in bytes (e.g. 32 for AES-256).
    #[must_use]
    pub fn key_len(&self) -> usize {
        usize::try_from(unsafe { sys::EVP_CIPHER_get_key_length(self.ptr) }).unwrap_or(0)
    }

    /// Expected IV length in bytes (0 for ECB mode).
    #[must_use]
    pub fn iv_len(&self) -> usize {
        usize::try_from(unsafe { sys::EVP_CIPHER_get_iv_length(self.ptr) }).unwrap_or(0)
    }

    /// Block size in bytes (1 for stream ciphers; 16 for AES).
    #[must_use]
    pub fn block_size(&self) -> usize {
        usize::try_from(unsafe { sys::EVP_CIPHER_get_block_size(self.ptr) }).unwrap_or(0)
    }

    /// OpenSSL flags for this cipher (e.g. `EVP_CIPH_FLAG_AEAD_CIPHER`).
    #[must_use]
    pub fn flags(&self) -> u64 {
        unsafe { sys::EVP_CIPHER_get_flags(self.ptr) }
    }

    /// Return `true` if this is an AEAD cipher (GCM, CCM, ChaCha20-Poly1305).
    #[must_use]
    pub fn is_aead(&self) -> bool {
        // EVP_CIPH_FLAG_AEAD_CIPHER = 0x0020_0000
        (self.flags() & 0x0020_0000) != 0
    }

    /// Return the raw `EVP_CIPHER*` pointer.  Valid for the lifetime of `self`.
    #[must_use]
    pub fn as_ptr(&self) -> *const sys::EVP_CIPHER {
        self.ptr
    }
}

impl Clone for CipherAlg {
    fn clone(&self) -> Self {
        unsafe { sys::EVP_CIPHER_up_ref(self.ptr) };
        CipherAlg {
            ptr: self.ptr,
            lib_ctx: self.lib_ctx.clone(),
        }
    }
}

impl Drop for CipherAlg {
    fn drop(&mut self) {
        unsafe { sys::EVP_CIPHER_free(self.ptr) };
    }
}

// SAFETY: `EVP_CIPHER*` is reference-counted and immutable after fetch.
unsafe impl Send for CipherAlg {}
unsafe impl Sync for CipherAlg {}

// ── Direction markers (Phase 4.2) ─────────────────────────────────────────────

/// Marker type for encrypt direction.
pub struct Encrypt;
/// Marker type for decrypt direction.
pub struct Decrypt;

mod sealed {
    pub trait Direction {}
    impl Direction for super::Encrypt {}
    impl Direction for super::Decrypt {}
}

// ── CipherCtx<Dir> — stateful context ────────────────────────────────────────

/// Stateful symmetric cipher context.
///
/// `Dir` is either [`Encrypt`] or [`Decrypt`]; the type parameter prevents
/// accidentally calling encrypt operations on a decrypt context and vice versa.
///
/// `!Clone` — cipher contexts have no `up_ref`; use a fresh context per message.
pub struct CipherCtx<Dir> {
    ptr: *mut sys::EVP_CIPHER_CTX,
    _dir: std::marker::PhantomData<Dir>,
}

impl<Dir: sealed::Direction> CipherCtx<Dir> {
    /// Feed `input` into the cipher; write plaintext/ciphertext to `output`.
    ///
    /// `output` must be at least `input.len() + block_size - 1` bytes.
    /// Returns the number of bytes written.
    ///
    /// # Errors
    pub fn update(&mut self, input: &[u8], output: &mut [u8]) -> Result<usize, ErrorStack>
    where
        Dir: IsEncrypt,
    {
        // SAFETY: self.ptr is a valid EVP_CIPHER_CTX initialised in CipherAlg::encrypt/decrypt.
        unsafe { Dir::do_update(self.ptr, input, output) }
    }

    /// Feed `input` through the cipher and return the output as a `Vec<u8>`.
    ///
    /// Allocates `input.len() + block_size` bytes, calls [`Self::update`], then
    /// truncates to the actual number of bytes written.  Use this when the caller
    /// cannot easily compute the output size in advance.
    ///
    /// # Errors
    pub fn update_to_vec(&mut self, input: &[u8]) -> Result<Vec<u8>, ErrorStack>
    where
        Dir: IsEncrypt,
    {
        let block_size =
            usize::try_from(unsafe { sys::EVP_CIPHER_CTX_get_block_size(self.ptr) }).unwrap_or(0);
        let max = input.len() + block_size;
        let mut out = vec![0u8; max];
        let n = self.update(input, &mut out)?;
        out.truncate(n);
        Ok(out)
    }

    /// Finalise the operation (flush padding / verify auth tag).
    ///
    /// # Errors
    pub fn finalize(&mut self, output: &mut [u8]) -> Result<usize, ErrorStack>
    where
        Dir: IsEncrypt,
    {
        // SAFETY: same as update.
        unsafe { Dir::do_finalize(self.ptr, output) }
    }

    /// Set dynamic parameters on the context (e.g. GCM tag length).
    ///
    /// # Errors
    pub fn set_params(&mut self, params: &crate::params::Params<'_>) -> Result<(), ErrorStack> {
        crate::ossl_call!(sys::EVP_CIPHER_CTX_set_params(self.ptr, params.as_ptr()))
    }

    /// Query arbitrary parameters from the context via a pre-built getter array.
    ///
    /// Build a `Params` array with placeholder values using [`ParamBuilder`], then
    /// call this method to have OpenSSL fill in the real values.  Use the typed
    /// getters on [`Params`] (e.g. `get_size_t`) to read the results.
    ///
    /// This is the low-level interface.  For common queries prefer the typed
    /// helpers [`aead_tag_len`](Self::aead_tag_len), [`key_len`](Self::key_len),
    /// and [`iv_len`](Self::iv_len).
    ///
    /// [`ParamBuilder`]: crate::params::ParamBuilder
    /// [`Params`]: crate::params::Params
    ///
    /// # Errors
    ///
    /// Returns `Err` if `EVP_CIPHER_CTX_get_params` fails.
    pub fn get_params(&self, params: &mut crate::params::Params<'_>) -> Result<(), ErrorStack> {
        // SAFETY:
        // - self.ptr is non-null (constructor invariant)
        // - params.as_mut_ptr() is valid for the duration of this call
        // - &self ensures no concurrent mutable access to self.ptr
        crate::ossl_call!(sys::EVP_CIPHER_CTX_get_params(
            self.ptr,
            params.as_mut_ptr()
        ))
    }

    /// Return the authentication tag length for this AEAD cipher context.
    ///
    /// Available after `EVP_EncryptInit_ex2` / `EVP_DecryptInit_ex2` with an AEAD
    /// algorithm (AES-GCM, AES-CCM, ChaCha20-Poly1305, etc.).
    ///
    /// The getter pattern used here builds a `Params` array with a placeholder
    /// value (`0`) for `OSSL_CIPHER_PARAM_AEAD_TAG_LEN` (`"taglen"`); OpenSSL
    /// overwrites it with the actual tag length during `EVP_CIPHER_CTX_get_params`.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the context is not initialised with an AEAD algorithm,
    /// or if `EVP_CIPHER_CTX_get_params` fails.
    pub fn aead_tag_len(&self) -> Result<usize, ErrorStack> {
        let mut params = crate::params::ParamBuilder::new()?
            .push_size(c"taglen", 0)?
            .build()?;
        // SAFETY:
        // - self.ptr is non-null (constructor invariant)
        // - params.as_mut_ptr() is valid for the duration of this call
        // - &self ensures no concurrent mutable access to self.ptr
        crate::ossl_call!(sys::EVP_CIPHER_CTX_get_params(
            self.ptr,
            params.as_mut_ptr()
        ))?;
        params
            .get_size_t(c"taglen")
            .map_err(|_| crate::error::ErrorStack::drain())
    }

    /// Return the key length in bytes for this cipher context.
    ///
    /// Reads `OSSL_CIPHER_PARAM_KEYLEN` (`"keylen"`) from the context.
    /// Useful when the key length is variable (e.g. for some stream ciphers)
    /// or to confirm the value that was set during initialisation.
    ///
    /// # Errors
    ///
    /// Returns `Err` if `EVP_CIPHER_CTX_get_params` fails.
    pub fn key_len(&self) -> Result<usize, ErrorStack> {
        let mut params = crate::params::ParamBuilder::new()?
            .push_size(c"keylen", 0)?
            .build()?;
        // SAFETY: same as aead_tag_len.
        crate::ossl_call!(sys::EVP_CIPHER_CTX_get_params(
            self.ptr,
            params.as_mut_ptr()
        ))?;
        params
            .get_size_t(c"keylen")
            .map_err(|_| crate::error::ErrorStack::drain())
    }

    /// Return the IV length in bytes for this cipher context.
    ///
    /// Reads `OSSL_CIPHER_PARAM_IVLEN` (`"ivlen"`) from the context.
    /// Useful for allocating IV buffers when the algorithm is determined
    /// at runtime.
    ///
    /// # Errors
    ///
    /// Returns `Err` if `EVP_CIPHER_CTX_get_params` fails.
    pub fn iv_len(&self) -> Result<usize, ErrorStack> {
        let mut params = crate::params::ParamBuilder::new()?
            .push_size(c"ivlen", 0)?
            .build()?;
        // SAFETY: same as aead_tag_len.
        crate::ossl_call!(sys::EVP_CIPHER_CTX_get_params(
            self.ptr,
            params.as_mut_ptr()
        ))?;
        params
            .get_size_t(c"ivlen")
            .map_err(|_| crate::error::ErrorStack::drain())
    }

    /// Return the raw `EVP_CIPHER_CTX*` pointer.  Returns a mutable pointer
    /// because most OpenSSL EVP functions require `EVP_CIPHER_CTX*` even for
    /// logically read-only operations.
    #[must_use]
    pub fn as_ptr(&self) -> *mut sys::EVP_CIPHER_CTX {
        self.ptr
    }
}

impl<Dir> Drop for CipherCtx<Dir> {
    fn drop(&mut self) {
        unsafe { sys::EVP_CIPHER_CTX_free(self.ptr) };
    }
}

unsafe impl<Dir: sealed::Direction> Send for CipherCtx<Dir> {}

/// Helper trait that routes to the correct `EVP_Encrypt*` or `EVP_Decrypt*` functions.
///
/// Sealed: only `Encrypt` and `Decrypt` implement this.
pub trait IsEncrypt: sealed::Direction {
    /// Feed data into the cipher and write output.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the underlying EVP update call fails.
    ///
    /// # Safety
    ///
    /// `ctx` must be a valid, initialised `EVP_CIPHER_CTX*`.
    unsafe fn do_update(
        ctx: *mut sys::EVP_CIPHER_CTX,
        input: &[u8],
        output: &mut [u8],
    ) -> Result<usize, ErrorStack>;

    /// Flush final block and write output.
    ///
    /// # Errors
    ///
    /// Returns `Err` if finalisation fails (e.g. auth tag mismatch for AEAD).
    ///
    /// # Safety
    ///
    /// `ctx` must be a valid, initialised `EVP_CIPHER_CTX*`.
    unsafe fn do_finalize(
        ctx: *mut sys::EVP_CIPHER_CTX,
        output: &mut [u8],
    ) -> Result<usize, ErrorStack>;
}

impl IsEncrypt for Encrypt {
    unsafe fn do_update(
        ctx: *mut sys::EVP_CIPHER_CTX,
        input: &[u8],
        output: &mut [u8],
    ) -> Result<usize, ErrorStack> {
        let inl = i32::try_from(input.len()).map_err(|_| ErrorStack::drain())?;
        let mut outl: i32 = 0;
        crate::ossl_call!(sys::EVP_EncryptUpdate(
            ctx,
            output.as_mut_ptr(),
            std::ptr::addr_of_mut!(outl),
            input.as_ptr(),
            inl
        ))?;
        Ok(usize::try_from(outl).unwrap_or(0))
    }

    unsafe fn do_finalize(
        ctx: *mut sys::EVP_CIPHER_CTX,
        output: &mut [u8],
    ) -> Result<usize, ErrorStack> {
        let mut outl: i32 = 0;
        crate::ossl_call!(sys::EVP_EncryptFinal_ex(
            ctx,
            output.as_mut_ptr(),
            std::ptr::addr_of_mut!(outl)
        ))?;
        Ok(usize::try_from(outl).unwrap_or(0))
    }
}

impl IsEncrypt for Decrypt {
    unsafe fn do_update(
        ctx: *mut sys::EVP_CIPHER_CTX,
        input: &[u8],
        output: &mut [u8],
    ) -> Result<usize, ErrorStack> {
        let inl = i32::try_from(input.len()).map_err(|_| ErrorStack::drain())?;
        let mut outl: i32 = 0;
        crate::ossl_call!(sys::EVP_DecryptUpdate(
            ctx,
            output.as_mut_ptr(),
            std::ptr::addr_of_mut!(outl),
            input.as_ptr(),
            inl
        ))?;
        Ok(usize::try_from(outl).unwrap_or(0))
    }

    unsafe fn do_finalize(
        ctx: *mut sys::EVP_CIPHER_CTX,
        output: &mut [u8],
    ) -> Result<usize, ErrorStack> {
        let mut outl: i32 = 0;
        crate::ossl_call!(sys::EVP_DecryptFinal_ex(
            ctx,
            output.as_mut_ptr(),
            std::ptr::addr_of_mut!(outl)
        ))?;
        Ok(usize::try_from(outl).unwrap_or(0))
    }
}

impl CipherAlg {
    /// Create an encryption context.
    ///
    /// **`key` and `iv` are copied** into OpenSSL's internal state by
    /// `EVP_EncryptInit_ex2` (key scheduling + zeroization on free).
    ///
    /// # Errors
    pub fn encrypt(
        &self,
        key: &[u8],
        iv: &[u8],
        params: Option<&crate::params::Params<'_>>,
    ) -> Result<CipherCtx<Encrypt>, ErrorStack> {
        let ctx_ptr = unsafe { sys::EVP_CIPHER_CTX_new() };
        if ctx_ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        let params_ptr = params.map_or(crate::params::null_params(), crate::params::Params::as_ptr);
        crate::ossl_call!(sys::EVP_EncryptInit_ex2(
            ctx_ptr,
            self.ptr,
            key.as_ptr(),
            iv.as_ptr(),
            params_ptr
        ))
        .map_err(|e| {
            unsafe { sys::EVP_CIPHER_CTX_free(ctx_ptr) };
            e
        })?;
        Ok(CipherCtx {
            ptr: ctx_ptr,
            _dir: std::marker::PhantomData,
        })
    }

    /// Create a decryption context.
    ///
    /// Same key/IV copy semantics as `encrypt`.
    ///
    /// # Errors
    pub fn decrypt(
        &self,
        key: &[u8],
        iv: &[u8],
        params: Option<&crate::params::Params<'_>>,
    ) -> Result<CipherCtx<Decrypt>, ErrorStack> {
        let ctx_ptr = unsafe { sys::EVP_CIPHER_CTX_new() };
        if ctx_ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        let params_ptr = params.map_or(crate::params::null_params(), crate::params::Params::as_ptr);
        crate::ossl_call!(sys::EVP_DecryptInit_ex2(
            ctx_ptr,
            self.ptr,
            key.as_ptr(),
            iv.as_ptr(),
            params_ptr
        ))
        .map_err(|e| {
            unsafe { sys::EVP_CIPHER_CTX_free(ctx_ptr) };
            e
        })?;
        Ok(CipherCtx {
            ptr: ctx_ptr,
            _dir: std::marker::PhantomData,
        })
    }
}

// ── AEAD types (Phase 4.2) ────────────────────────────────────────────────────

/// AEAD encryption context (GCM, CCM, ChaCha20-Poly1305).
pub struct AeadEncryptCtx(CipherCtx<Encrypt>);

impl AeadEncryptCtx {
    /// Create an AEAD encryption context.
    ///
    /// # Panics
    ///
    /// Panics if `alg` is not an AEAD cipher (`EVP_CIPH_FLAG_AEAD_CIPHER` not set).
    ///
    /// # Errors
    pub fn new(
        alg: &CipherAlg,
        key: &[u8],
        iv: &[u8],
        params: Option<&crate::params::Params<'_>>,
    ) -> Result<Self, ErrorStack> {
        assert!(alg.is_aead(), "CipherAlg is not an AEAD algorithm");
        Ok(AeadEncryptCtx(alg.encrypt(key, iv, params)?))
    }

    /// Set additional authenticated data (AAD).  Call before first `update`.
    ///
    /// # Panics
    ///
    /// Panics if `aad.len() > i32::MAX` (effectively impossible in practice).
    ///
    /// # Errors
    pub fn set_aad(&mut self, aad: &[u8]) -> Result<(), ErrorStack> {
        // AAD is fed via EVP_EncryptUpdate with output = NULL.
        let alen = i32::try_from(aad.len()).expect("AAD too large for EVP_EncryptUpdate");
        let mut outl: i32 = 0;
        crate::ossl_call!(sys::EVP_EncryptUpdate(
            self.0.ptr,
            std::ptr::null_mut(),
            std::ptr::addr_of_mut!(outl),
            aad.as_ptr(),
            alen
        ))
    }

    /// Feed `input` into the AEAD cipher; write to `output`.
    ///
    /// # Errors
    pub fn update(&mut self, input: &[u8], output: &mut [u8]) -> Result<usize, ErrorStack> {
        self.0.update(input, output)
    }

    /// Flush any remaining bytes.  Must be called before `tag`.
    ///
    /// # Errors
    pub fn finalize(&mut self, output: &mut [u8]) -> Result<usize, ErrorStack> {
        self.0.finalize(output)
    }

    /// Retrieve the authentication tag after `finalize`.
    ///
    /// `tag` must be 16 bytes for GCM (`EVP_CTRL_GCM_GET_TAG = 16`).
    ///
    /// # Panics
    ///
    /// Panics if `tag.len() > i32::MAX`.
    ///
    /// # Errors
    pub fn tag(&self, tag: &mut [u8]) -> Result<(), ErrorStack> {
        // EVP_CTRL_GCM_GET_TAG = 16
        let tlen = i32::try_from(tag.len()).expect("tag slice too large");
        let rc = unsafe {
            sys::EVP_CIPHER_CTX_ctrl(
                self.0.ptr,
                16, // EVP_CTRL_GCM_GET_TAG
                tlen,
                tag.as_mut_ptr().cast(),
            )
        };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(())
    }
}

/// AEAD decryption context.
pub struct AeadDecryptCtx(CipherCtx<Decrypt>);

impl AeadDecryptCtx {
    /// Create an AEAD decryption context.
    ///
    /// # Panics
    ///
    /// Panics if `alg` is not an AEAD cipher.
    ///
    /// # Errors
    pub fn new(
        alg: &CipherAlg,
        key: &[u8],
        iv: &[u8],
        params: Option<&crate::params::Params<'_>>,
    ) -> Result<Self, ErrorStack> {
        assert!(alg.is_aead(), "CipherAlg is not an AEAD algorithm");
        Ok(AeadDecryptCtx(alg.decrypt(key, iv, params)?))
    }

    /// Set AAD before first `update`.
    ///
    /// # Panics
    ///
    /// Panics if `aad.len() > i32::MAX`.
    ///
    /// # Errors
    pub fn set_aad(&mut self, aad: &[u8]) -> Result<(), ErrorStack> {
        let alen = i32::try_from(aad.len()).expect("AAD too large for EVP_DecryptUpdate");
        let mut outl: i32 = 0;
        crate::ossl_call!(sys::EVP_DecryptUpdate(
            self.0.ptr,
            std::ptr::null_mut(),
            std::ptr::addr_of_mut!(outl),
            aad.as_ptr(),
            alen
        ))
    }

    /// Feed `input` into the cipher; write to `output`.
    ///
    /// # Errors
    pub fn update(&mut self, input: &[u8], output: &mut [u8]) -> Result<usize, ErrorStack> {
        self.0.update(input, output)
    }

    /// Set the expected authentication tag before `finalize`.
    ///
    /// # Panics
    ///
    /// Panics if `tag.len() > i32::MAX`.
    ///
    /// # Errors
    pub fn set_tag(&mut self, tag: &[u8]) -> Result<(), ErrorStack> {
        // EVP_CTRL_GCM_SET_TAG = 17
        let tlen = i32::try_from(tag.len()).expect("tag slice too large");
        let rc = unsafe {
            sys::EVP_CIPHER_CTX_ctrl(
                self.0.ptr,
                17, // EVP_CTRL_GCM_SET_TAG
                tlen,
                // OpenSSL API is not const-correct here; cast away const.
                tag.as_ptr().cast_mut().cast(),
            )
        };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(())
    }

    /// Finalise — returns `Err` if authentication tag verification fails.
    ///
    /// # Errors
    pub fn finalize(&mut self, output: &mut [u8]) -> Result<usize, ErrorStack> {
        self.0.finalize(output)
    }
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn fetch_aes_256_gcm_properties() {
        let alg = CipherAlg::fetch(c"AES-256-GCM", None).unwrap();
        assert_eq!(alg.key_len(), 32);
        assert_eq!(alg.iv_len(), 12);
        assert_eq!(alg.block_size(), 1);
        assert!(alg.is_aead());
    }

    #[test]
    fn fetch_aes_256_cbc_properties() {
        let alg = CipherAlg::fetch(c"AES-256-CBC", None).unwrap();
        assert_eq!(alg.key_len(), 32);
        assert_eq!(alg.iv_len(), 16);
        assert_eq!(alg.block_size(), 16);
        assert!(!alg.is_aead());
    }

    #[test]
    fn fetch_nonexistent_fails() {
        assert!(CipherAlg::fetch(c"NONEXISTENT_CIPHER_XYZ", None).is_err());
    }

    #[test]
    fn clone_then_drop_both() {
        let alg = CipherAlg::fetch(c"AES-256-GCM", None).unwrap();
        let alg2 = alg.clone();
        drop(alg);
        drop(alg2);
    }

    /// AES-256-CBC round-trip encrypt + decrypt.
    #[test]
    fn aes_256_cbc_round_trip() {
        let alg = CipherAlg::fetch(c"AES-256-CBC", None).unwrap();
        let key = [0x42u8; 32];
        let iv = [0x24u8; 16];
        let plaintext = b"Hello, cipher world!";

        // Encrypt.
        let mut enc = alg.encrypt(&key, &iv, None).unwrap();
        let mut ciphertext = vec![0u8; plaintext.len() + alg.block_size()];
        let n = enc.update(plaintext, &mut ciphertext).unwrap();
        let m = enc.finalize(&mut ciphertext[n..]).unwrap();
        ciphertext.truncate(n + m);

        // Decrypt.
        let mut dec = alg.decrypt(&key, &iv, None).unwrap();
        let mut recovered = vec![0u8; ciphertext.len() + alg.block_size()];
        let n2 = dec.update(&ciphertext, &mut recovered).unwrap();
        let m2 = dec.finalize(&mut recovered[n2..]).unwrap();
        recovered.truncate(n2 + m2);

        assert_eq!(recovered, plaintext);
    }

    /// AES-256-GCM AEAD: encrypt → tag → decrypt → verify; tag corruption → Err.
    #[test]
    fn aes_256_gcm_round_trip_and_tag_failure() {
        let alg = CipherAlg::fetch(c"AES-256-GCM", None).unwrap();
        let key = [0x11u8; 32];
        let iv = [0x22u8; 12];
        let aad = b"additional data";
        let plaintext = b"secret message!";

        // Encrypt.
        let mut enc = AeadEncryptCtx::new(&alg, &key, &iv, None).unwrap();
        enc.set_aad(aad).unwrap();
        let mut ciphertext = vec![0u8; plaintext.len()];
        let n = enc.update(plaintext, &mut ciphertext).unwrap();
        enc.finalize(&mut ciphertext[n..]).unwrap();
        let mut tag = [0u8; 16];
        enc.tag(&mut tag).unwrap();

        // Decrypt with correct tag — must succeed.
        let mut dec = AeadDecryptCtx::new(&alg, &key, &iv, None).unwrap();
        dec.set_aad(aad).unwrap();
        let mut recovered = vec![0u8; ciphertext.len()];
        let n2 = dec.update(&ciphertext, &mut recovered).unwrap();
        dec.set_tag(&tag).unwrap();
        dec.finalize(&mut recovered[n2..]).unwrap();
        assert_eq!(&recovered[..n2], plaintext);

        // Decrypt with corrupted tag — must fail.
        let mut bad_tag = tag;
        bad_tag[0] ^= 0xff;
        let mut dec2 = AeadDecryptCtx::new(&alg, &key, &iv, None).unwrap();
        dec2.set_aad(aad).unwrap();
        let mut dummy = vec![0u8; ciphertext.len()];
        dec2.update(&ciphertext, &mut dummy).unwrap();
        dec2.set_tag(&bad_tag).unwrap();
        assert!(dec2.finalize(&mut dummy).is_err());
    }

    /// AES-256-GCM: `aead_tag_len()` returns 16 (default GCM tag length).
    #[test]
    fn cipher_ctx_aead_tag_len_aes_gcm() {
        let alg = CipherAlg::fetch(c"AES-256-GCM", None).unwrap();
        let key = [0x11u8; 32];
        let iv = [0x22u8; 12];
        let ctx = alg.encrypt(&key, &iv, None).unwrap();
        assert_eq!(ctx.aead_tag_len().unwrap(), 16);
    }

    /// AES-256-GCM: `key_len()` on a `CipherCtx` returns 32.
    #[test]
    fn cipher_ctx_key_len_aes256() {
        let alg = CipherAlg::fetch(c"AES-256-GCM", None).unwrap();
        let key = [0x11u8; 32];
        let iv = [0x22u8; 12];
        let ctx = alg.encrypt(&key, &iv, None).unwrap();
        assert_eq!(ctx.key_len().unwrap(), 32);
    }

    /// AES-256-GCM: `iv_len()` on a `CipherCtx` returns 12.
    #[test]
    fn cipher_ctx_iv_len_aes256_gcm() {
        let alg = CipherAlg::fetch(c"AES-256-GCM", None).unwrap();
        let key = [0x11u8; 32];
        let iv = [0x22u8; 12];
        let ctx = alg.encrypt(&key, &iv, None).unwrap();
        assert_eq!(ctx.iv_len().unwrap(), 12);
    }

    /// `get_params` with a manually-built getter array works correctly.
    #[test]
    fn cipher_ctx_get_params_generic() {
        let alg = CipherAlg::fetch(c"AES-256-GCM", None).unwrap();
        let key = [0x11u8; 32];
        let iv = [0x22u8; 12];
        let ctx = alg.encrypt(&key, &iv, None).unwrap();

        let mut params = crate::params::ParamBuilder::new()
            .unwrap()
            .push_size(c"keylen", 0)
            .unwrap()
            .push_size(c"ivlen", 0)
            .unwrap()
            .build()
            .unwrap();
        ctx.get_params(&mut params).unwrap();
        assert_eq!(params.get_size_t(c"keylen").unwrap(), 32);
        assert_eq!(params.get_size_t(c"ivlen").unwrap(), 12);
    }
}
