//! `OSSL_PARAM_BLD` builder and `Params<'a>` wrapper.
//!
//! `ParamBuilder` wraps `OSSL_PARAM_BLD` and provides typed push methods that
//! map Rust types to OpenSSL parameter types.  `build()` finalises the builder
//! and returns an owned `Params` array.
//!
//! ## Lifetime
//!
//! `ParamBuilder<'a>` carries a lifetime `'a` that covers any borrowed byte
//! slices passed via `push_octet_ptr` or `push_utf8_ptr`.  Those calls store
//! a pointer into the caller's buffer; the buffer must outlive the `Params`
//! returned by `build()`.  Calls that copy (`push_octet_slice`, `push_utf8_string`)
//! have no such constraint.
//!
//! ## Zero-copy decision table
//!
//! | Method             | C function                      | Copies? |
//! |--------------------|---------------------------------|---------|
//! | `push_octet_slice` | `OSSL_PARAM_BLD_push_octet_string` | Yes  |
//! | `push_octet_ptr`   | `OSSL_PARAM_BLD_push_octet_ptr` | No      |
//! | `push_utf8_string` | `OSSL_PARAM_BLD_push_utf8_string`  | Yes  |
//! | `push_utf8_ptr`    | (manual OSSL_PARAM_BLD extension) | No    |

use crate::error::ErrorStack;
use native_ossl_sys as sys;
use std::ffi::CStr;
use std::marker::PhantomData;
use std::ptr;

// ── Params — the built array ──────────────────────────────────────────────────

/// An owned, finalized `OSSL_PARAM` array.
///
/// Created by `ParamBuilder::build()`.  The lifetime `'a` covers any borrowed
/// byte or string slices stored by pointer (zero-copy push calls).
pub struct Params<'a> {
    /// The heap-allocated array of `OSSL_PARAM` elements.
    ptr: *mut sys::OSSL_PARAM,
    /// Phantom to carry the borrow lifetime of any `push_octet_ptr` data.
    _lifetime: PhantomData<&'a [u8]>,
}

impl Params<'_> {
    /// Return a const pointer to the first `OSSL_PARAM` in the array.
    ///
    /// Pass this pointer to OpenSSL functions that take `const OSSL_PARAM[]`.
    /// The pointer is valid for the lifetime of `self`.
    #[must_use]
    pub fn as_ptr(&self) -> *const sys::OSSL_PARAM {
        self.ptr
    }
}

impl Drop for Params<'_> {
    fn drop(&mut self) {
        unsafe { sys::OSSL_PARAM_free(self.ptr) };
    }
}

// SAFETY: `OSSL_PARAM` arrays are read-only after construction.
// The builder ensures all owned copies are heap-allocated by OpenSSL.
unsafe impl Send for Params<'_> {}
unsafe impl Sync for Params<'_> {}

// ── ParamBuilder ──────────────────────────────────────────────────────────────

/// Builder for `OSSL_PARAM` arrays.
///
/// ```ignore
/// use native_ossl::params::ParamBuilder;
///
/// let params = ParamBuilder::new()
///     .push_int(c"key_bits", 2048)?
///     .push_utf8_ptr(c"pad-mode", c"oaep")?
///     .build()?;
/// ```
pub struct ParamBuilder<'a> {
    ptr: *mut sys::OSSL_PARAM_BLD,
    /// BIGNUMs pushed via `push_bn` that must remain alive until `build()`.
    ///
    /// `OSSL_PARAM_BLD_push_BN` stores a *pointer* to the BIGNUM internally;
    /// the actual copy into the `OSSL_PARAM` array happens only in
    /// `OSSL_PARAM_BLD_to_param`.  We must therefore keep each BIGNUM alive
    /// until after `build()` completes.
    bns: Vec<*mut sys::BIGNUM>,
    _lifetime: PhantomData<&'a [u8]>,
}

// SAFETY: BIGNUM pointers are not shared; we control their lifetime.
unsafe impl Send for ParamBuilder<'_> {}

impl<'a> ParamBuilder<'a> {
    /// Create a new empty builder.
    ///
    /// # Errors
    ///
    /// Returns `Err` if OpenSSL cannot allocate the builder.
    pub fn new() -> Result<Self, ErrorStack> {
        let ptr = unsafe { sys::OSSL_PARAM_BLD_new() };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(ParamBuilder {
            ptr,
            bns: Vec::new(),
            _lifetime: PhantomData,
        })
    }

    /// Push a signed integer parameter.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the push fails (allocation error or key too long).
    pub fn push_int(self, key: &CStr, val: i32) -> Result<Self, ErrorStack> {
        let rc = unsafe { sys::OSSL_PARAM_BLD_push_int(self.ptr, key.as_ptr(), val) };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(self)
    }

    /// Push an unsigned integer parameter.
    ///
    /// # Errors
    pub fn push_uint(self, key: &CStr, val: u32) -> Result<Self, ErrorStack> {
        let rc = unsafe { sys::OSSL_PARAM_BLD_push_uint(self.ptr, key.as_ptr(), val) };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(self)
    }

    /// Push a 64-bit unsigned integer parameter.
    ///
    /// Used for parameters such as the scrypt `N` cost factor.
    ///
    /// # Errors
    pub fn push_uint64(self, key: &CStr, val: u64) -> Result<Self, ErrorStack> {
        let rc = unsafe { sys::OSSL_PARAM_BLD_push_uint64(self.ptr, key.as_ptr(), val) };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(self)
    }

    /// Push a `size_t` parameter.
    ///
    /// # Errors
    pub fn push_size(self, key: &CStr, val: usize) -> Result<Self, ErrorStack> {
        let rc = unsafe { sys::OSSL_PARAM_BLD_push_size_t(self.ptr, key.as_ptr(), val) };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(self)
    }

    /// Push an octet-string parameter — **copies** `val` into the param array.
    ///
    /// Use this when `val` may be dropped before the resulting `Params` is consumed.
    /// If `val` is guaranteed to outlive `Params`, prefer `push_octet_ptr`.
    ///
    /// # Errors
    pub fn push_octet_slice(self, key: &CStr, val: &[u8]) -> Result<Self, ErrorStack> {
        let rc = unsafe {
            sys::OSSL_PARAM_BLD_push_octet_string(
                self.ptr,
                key.as_ptr(),
                val.as_ptr().cast(),
                val.len(),
            )
        };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(self)
    }

    /// Push an octet-string parameter — **stores a pointer** into `val`.
    ///
    /// Zero-copy: no allocation occurs.  The lifetime `'b` of `val` must be at
    /// least `'a` to prevent the reference from being invalidated before the
    /// `Params` array is consumed.
    ///
    /// # Errors
    pub fn push_octet_ptr<'b>(
        self,
        key: &CStr,
        val: &'b [u8],
    ) -> Result<ParamBuilder<'b>, ErrorStack>
    where
        'a: 'b,
    {
        let rc = unsafe {
            sys::OSSL_PARAM_BLD_push_octet_ptr(
                self.ptr,
                key.as_ptr(),
                // OpenSSL declares the parameter as *mut c_void but treats it
                // as read-only once stored (pointer storage, no write-back).
                // The cast from *const to *mut is intentional: OpenSSL's API
                // is not const-correct here.
                val.as_ptr() as *mut std::os::raw::c_void,
                val.len(),
            )
        };
        if rc != 1 {
            // `self` drops here, running Drop (frees builder + BIGNUMs).
            return Err(ErrorStack::drain());
        }
        // Transfer both `ptr` and `bns` into the new lifetime-narrowed builder.
        // Use ManuallyDrop so the Drop impl of `self` does not run a second free.
        let md = std::mem::ManuallyDrop::new(self);
        Ok(ParamBuilder {
            ptr: md.ptr,
            // SAFETY: we are the sole owner; md's Drop will not run.
            bns: unsafe { ptr::read(&raw const md.bns) },
            _lifetime: PhantomData,
        })
    }

    /// Push a UTF-8 string parameter — **copies** `val` into the param array.
    ///
    /// # Errors
    pub fn push_utf8_string(self, key: &CStr, val: &CStr) -> Result<Self, ErrorStack> {
        let rc = unsafe {
            sys::OSSL_PARAM_BLD_push_utf8_string(
                self.ptr,
                key.as_ptr(),
                val.as_ptr(),
                0, // 0 = use strlen
            )
        };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(self)
    }

    /// Push a UTF-8 string parameter — **stores a pointer** into `val`.
    ///
    /// Zero-copy: no allocation occurs.  Use only for `'static` literals such as
    /// algorithm names (`c"oaep"`, `c"SHA-256"`).
    ///
    /// # Errors
    pub fn push_utf8_ptr(self, key: &CStr, val: &'static CStr) -> Result<Self, ErrorStack> {
        // OpenSSL does not provide OSSL_PARAM_BLD_push_utf8_ptr in all builds,
        // so we use push_utf8_string which copies, but annotate that the intent
        // is pointer storage.  For 'static values the copy is negligible.
        //
        // NOTE: If OpenSSL gains OSSL_PARAM_BLD_push_utf8_ptr exposure in future
        // bindgen output, replace the body with the pointer variant.
        let rc = unsafe {
            sys::OSSL_PARAM_BLD_push_utf8_string(self.ptr, key.as_ptr(), val.as_ptr(), 0)
        };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(self)
    }

    /// Push a BIGNUM parameter from big-endian bytes.
    ///
    /// Converts `bigendian_bytes` to an OpenSSL `BIGNUM` and pushes it into the
    /// builder.  The `BIGNUM` is copied into the builder's internal storage so
    /// the caller's slice is not referenced after this call returns.
    ///
    /// # Panics
    ///
    /// Panics if `bigendian_bytes` is longer than `i32::MAX` bytes, which is
    /// not a practical concern for key material.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the allocation fails or the push fails.
    pub fn push_bn(mut self, key: &CStr, bigendian_bytes: &[u8]) -> Result<Self, ErrorStack> {
        // BN_bin2bn converts big-endian bytes into a newly allocated BIGNUM.
        let bn = unsafe {
            sys::BN_bin2bn(
                bigendian_bytes.as_ptr(),
                // len is int in C; values >2 GiB are pathological for key material.
                i32::try_from(bigendian_bytes.len()).expect("BN too large"),
                ptr::null_mut(),
            )
        };
        if bn.is_null() {
            return Err(ErrorStack::drain());
        }
        // OSSL_PARAM_BLD_push_BN stores a *pointer* to the BIGNUM; the actual
        // copy into the OSSL_PARAM array is deferred to OSSL_PARAM_BLD_to_param.
        // Keep the BIGNUM alive in `self.bns` until `build()` completes.
        let rc = unsafe { sys::OSSL_PARAM_BLD_push_BN(self.ptr, key.as_ptr(), bn) };
        if rc != 1 {
            unsafe { sys::BN_free(bn) };
            return Err(ErrorStack::drain());
        }
        self.bns.push(bn);
        Ok(self)
    }

    /// Finalise the builder and return the `Params` array.
    ///
    /// Consumes the builder.  The returned `Params` must outlive any borrowed
    /// slices stored via `push_octet_ptr`.
    ///
    /// # Errors
    ///
    /// Returns `Err` if `OSSL_PARAM_BLD_to_param` fails.
    pub fn build(self) -> Result<Params<'a>, ErrorStack> {
        // Take ownership of the raw pointers and prevent Drop from running,
        // so we control the exact point at which cleanup happens.
        let builder_ptr = self.ptr;
        let bns = unsafe { ptr::read(&raw const self.bns) };
        std::mem::forget(self);

        let param_ptr = unsafe { sys::OSSL_PARAM_BLD_to_param(builder_ptr) };
        // Free the builder — the OSSL_PARAM array is independent of it.
        unsafe { sys::OSSL_PARAM_BLD_free(builder_ptr) };
        // Now that to_param has copied all BIGNUM values, free the temporaries.
        for bn in bns {
            unsafe { sys::BN_free(bn) };
        }

        if param_ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(Params {
            ptr: param_ptr,
            _lifetime: PhantomData,
        })
    }
}

impl Drop for ParamBuilder<'_> {
    fn drop(&mut self) {
        if !self.ptr.is_null() {
            unsafe { sys::OSSL_PARAM_BLD_free(self.ptr) };
        }
        for bn in self.bns.drain(..) {
            unsafe { sys::BN_free(bn) };
        }
    }
}

// ── Private BigNum helper ─────────────────────────────────────────────────────

struct Bn(*mut sys::BIGNUM);

impl Bn {
    /// Convert this `BIGNUM` to a big-endian byte vector.
    fn to_bigendian_vec(&self) -> Vec<u8> {
        let nbits = unsafe { sys::BN_num_bits(self.0) };
        let nbytes = usize::try_from(nbits).unwrap_or(0).div_ceil(8);
        if nbytes == 0 {
            return Vec::new();
        }
        let mut out = vec![0u8; nbytes];
        // BN_bn2bin writes exactly nbytes; returns nbytes on success.
        unsafe { sys::BN_bn2bin(self.0, out.as_mut_ptr()) };
        out
    }
}

impl Drop for Bn {
    fn drop(&mut self) {
        unsafe { sys::BN_free(self.0) };
    }
}

// ── Params — getters and ownership transfer ───────────────────────────────────

impl Params<'_> {
    /// Adopt an OpenSSL-allocated `OSSL_PARAM` array, taking ownership.
    ///
    /// The array will be freed with `OSSL_PARAM_free` on drop.  Use this to
    /// wrap arrays returned by functions such as `EVP_PKEY_todata`.
    ///
    /// # Safety
    ///
    /// `ptr` must be a valid, `OSSL_PARAM_END`-terminated array allocated by
    /// OpenSSL.  After this call the caller must not use or free `ptr`.
    #[must_use]
    pub unsafe fn from_owned_ptr(ptr: *mut sys::OSSL_PARAM) -> Params<'static> {
        Params {
            ptr,
            _lifetime: PhantomData,
        }
    }

    /// Return a mutable pointer to the first `OSSL_PARAM` element.
    ///
    /// Pass to functions such as `EVP_PKEY_get_params` that fill a
    /// pre-prepared query array.
    #[must_use]
    pub fn as_mut_ptr(&mut self) -> *mut sys::OSSL_PARAM {
        self.ptr
    }

    /// Return `true` if a parameter with the given name exists in this array.
    #[must_use]
    pub fn has_param(&self, key: &CStr) -> bool {
        !unsafe { sys::OSSL_PARAM_locate(self.ptr, key.as_ptr()) }.is_null()
    }

    /// Locate `key` and read its value as an `i32`.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the key is not found or the value cannot be converted.
    pub fn get_int(&self, key: &CStr) -> Result<i32, ErrorStack> {
        let elem = unsafe { sys::OSSL_PARAM_locate(self.ptr, key.as_ptr()) };
        if elem.is_null() {
            return Err(ErrorStack::drain());
        }
        let mut val: std::os::raw::c_int = 0;
        crate::ossl_call!(sys::OSSL_PARAM_get_int(elem, std::ptr::addr_of_mut!(val)))?;
        Ok(val)
    }

    /// Locate `key` and read its value as a `u32`.
    ///
    /// # Errors
    pub fn get_uint(&self, key: &CStr) -> Result<u32, ErrorStack> {
        let elem = unsafe { sys::OSSL_PARAM_locate(self.ptr, key.as_ptr()) };
        if elem.is_null() {
            return Err(ErrorStack::drain());
        }
        let mut val: std::os::raw::c_uint = 0;
        crate::ossl_call!(sys::OSSL_PARAM_get_uint(elem, std::ptr::addr_of_mut!(val)))?;
        Ok(val)
    }

    /// Locate `key` and read its value as a `usize`.
    ///
    /// # Errors
    pub fn get_size_t(&self, key: &CStr) -> Result<usize, ErrorStack> {
        let elem = unsafe { sys::OSSL_PARAM_locate(self.ptr, key.as_ptr()) };
        if elem.is_null() {
            return Err(ErrorStack::drain());
        }
        let mut val: usize = 0;
        crate::ossl_call!(sys::OSSL_PARAM_get_size_t(
            elem,
            std::ptr::addr_of_mut!(val)
        ))?;
        Ok(val)
    }

    /// Locate `key` and read its value as an `i64`.
    ///
    /// # Errors
    pub fn get_i64(&self, key: &CStr) -> Result<i64, ErrorStack> {
        let elem = unsafe { sys::OSSL_PARAM_locate(self.ptr, key.as_ptr()) };
        if elem.is_null() {
            return Err(ErrorStack::drain());
        }
        let mut val: i64 = 0;
        crate::ossl_call!(sys::OSSL_PARAM_get_int64(elem, std::ptr::addr_of_mut!(val)))?;
        Ok(val)
    }

    /// Locate `key` and read its value as a `u64`.
    ///
    /// # Errors
    pub fn get_u64(&self, key: &CStr) -> Result<u64, ErrorStack> {
        let elem = unsafe { sys::OSSL_PARAM_locate(self.ptr, key.as_ptr()) };
        if elem.is_null() {
            return Err(ErrorStack::drain());
        }
        let mut val: u64 = 0;
        crate::ossl_call!(sys::OSSL_PARAM_get_uint64(
            elem,
            std::ptr::addr_of_mut!(val)
        ))?;
        Ok(val)
    }

    /// Locate `key` and read it as BIGNUM, returning big-endian bytes.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the key is not found or is not a BIGNUM parameter.
    pub fn get_bn(&self, key: &CStr) -> Result<Vec<u8>, ErrorStack> {
        let elem = unsafe { sys::OSSL_PARAM_locate(self.ptr, key.as_ptr()) };
        if elem.is_null() {
            return Err(ErrorStack::drain());
        }
        let mut bn_ptr: *mut sys::BIGNUM = ptr::null_mut();
        crate::ossl_call!(sys::OSSL_PARAM_get_BN(elem, std::ptr::addr_of_mut!(bn_ptr)))?;
        let bn = Bn(bn_ptr);
        Ok(bn.to_bigendian_vec())
    }

    /// Locate `key` and return a borrowed slice of its octet-string data.
    ///
    /// The returned slice is valid for the lifetime of `self`.
    ///
    /// # Errors
    pub fn get_octet_string(&self, key: &CStr) -> Result<&[u8], ErrorStack> {
        let elem = unsafe { sys::OSSL_PARAM_locate(self.ptr, key.as_ptr()) };
        if elem.is_null() {
            return Err(ErrorStack::drain());
        }
        let mut p: *const std::os::raw::c_void = ptr::null();
        let mut len: usize = 0;
        crate::ossl_call!(sys::OSSL_PARAM_get_octet_string_ptr(
            elem,
            std::ptr::addr_of_mut!(p),
            std::ptr::addr_of_mut!(len),
        ))?;
        // SAFETY: p points into the OSSL_PARAM array owned by self; valid for &self lifetime.
        Ok(unsafe { std::slice::from_raw_parts(p.cast::<u8>(), len) })
    }

    /// Locate `key` and return a borrowed `CStr` of its UTF-8 string data.
    ///
    /// The returned reference is valid for the lifetime of `self`.
    ///
    /// # Errors
    pub fn get_utf8_string(&self, key: &CStr) -> Result<&CStr, ErrorStack> {
        let elem = unsafe { sys::OSSL_PARAM_locate(self.ptr, key.as_ptr()) };
        if elem.is_null() {
            return Err(ErrorStack::drain());
        }
        let mut p: *const std::os::raw::c_char = ptr::null();
        crate::ossl_call!(sys::OSSL_PARAM_get_utf8_string_ptr(
            elem,
            std::ptr::addr_of_mut!(p),
        ))?;
        // SAFETY: p points into the OSSL_PARAM array owned by self; valid for &self lifetime.
        Ok(unsafe { CStr::from_ptr(p) })
    }

    /// Locate `key` and overwrite its value in place with a new `i32`.
    ///
    /// The `Params` array must have been built with `ParamBuilder::push_int` for
    /// this key so that the element already has storage of the right type and size.
    /// This is useful after a `get_params` call to propagate changes back, or to
    /// update a query template before re-issuing it.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the key is not found or the type is incompatible.
    pub fn set_int(&mut self, key: &CStr, val: i32) -> Result<(), ErrorStack> {
        let elem = unsafe { sys::OSSL_PARAM_locate(self.ptr, key.as_ptr()) };
        if elem.is_null() {
            return Err(ErrorStack::drain());
        }
        crate::ossl_call!(sys::OSSL_PARAM_set_int(elem, val))
    }

    /// Locate `key` and overwrite its value in place with a new `u32`.
    ///
    /// See [`set_int`](Self::set_int) for usage notes.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the key is not found or the type is incompatible.
    pub fn set_uint(&mut self, key: &CStr, val: u32) -> Result<(), ErrorStack> {
        let elem = unsafe { sys::OSSL_PARAM_locate(self.ptr, key.as_ptr()) };
        if elem.is_null() {
            return Err(ErrorStack::drain());
        }
        crate::ossl_call!(sys::OSSL_PARAM_set_uint(elem, val))
    }

    /// Locate `key` and overwrite its value in place with a new `usize`.
    ///
    /// See [`set_int`](Self::set_int) for usage notes.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the key is not found or the type is incompatible.
    pub fn set_size(&mut self, key: &CStr, val: usize) -> Result<(), ErrorStack> {
        let elem = unsafe { sys::OSSL_PARAM_locate(self.ptr, key.as_ptr()) };
        if elem.is_null() {
            return Err(ErrorStack::drain());
        }
        crate::ossl_call!(sys::OSSL_PARAM_set_size_t(elem, val))
    }

    /// Locate `key` and read its value as an `i64`.
    ///
    /// Wraps `OSSL_PARAM_get_long`.  On LP64 platforms (Linux x86-64) C `long`
    /// is 64 bits; on LLP64 platforms (Windows) it is 32 bits.  The result is
    /// always widened to `i64`.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the key is not found or the value cannot be converted.
    pub fn get_long(&self, key: &CStr) -> Result<i64, ErrorStack> {
        let elem = unsafe { sys::OSSL_PARAM_locate(self.ptr, key.as_ptr()) };
        if elem.is_null() {
            return Err(ErrorStack::drain());
        }
        let mut val: std::os::raw::c_long = 0;
        crate::ossl_call!(sys::OSSL_PARAM_get_long(elem, std::ptr::addr_of_mut!(val)))?;
        Ok(val as i64)
    }

    /// Return `true` if the parameter named `key` has been populated by a
    /// `get_params` call (i.e. `OSSL_PARAM_modified` returns 1).
    ///
    /// Returns `false` if the key does not exist in the array or has not been
    /// written by OpenSSL yet.  This is typically used after passing a query
    /// array to `EVP_PKEY_get_params` or similar to confirm that a particular
    /// field was actually filled.
    #[must_use]
    pub fn was_modified(&self, key: &CStr) -> bool {
        let elem = unsafe { sys::OSSL_PARAM_locate(self.ptr, key.as_ptr()) };
        if elem.is_null() {
            return false;
        }
        unsafe { sys::OSSL_PARAM_modified(elem) == 1 }
    }
}

// ── Null sentinel ─────────────────────────────────────────────────────────────

/// A const null `OSSL_PARAM` pointer — used to pass `NULL` to OpenSSL functions
/// that accept an optional `const OSSL_PARAM[]` argument.
#[must_use]
pub(crate) fn null_params() -> *const sys::OSSL_PARAM {
    ptr::null()
}

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

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

    #[test]
    fn int_round_trip() {
        let params = ParamBuilder::new()
            .unwrap()
            .push_int(c"mykey", 42)
            .unwrap()
            .build()
            .unwrap();

        // Locate the specific OSSL_PARAM element by name, then get its value.
        let elem = unsafe { sys::OSSL_PARAM_locate(params.as_ptr().cast_mut(), c"mykey".as_ptr()) };
        assert!(!elem.is_null(), "OSSL_PARAM_locate failed");

        let mut out: i32 = 0;
        let rc = unsafe { sys::OSSL_PARAM_get_int(elem, std::ptr::addr_of_mut!(out)) };
        assert_eq!(rc, 1, "OSSL_PARAM_get_int failed");
        assert_eq!(out, 42);
    }

    #[test]
    fn octet_slice_round_trip() {
        let data = b"hello world";
        let params = ParamBuilder::new()
            .unwrap()
            .push_octet_slice(c"blob", data)
            .unwrap()
            .build()
            .unwrap();

        let elem = unsafe { sys::OSSL_PARAM_locate(params.as_ptr().cast_mut(), c"blob".as_ptr()) };
        assert!(!elem.is_null());

        let mut p: *const std::os::raw::c_void = ptr::null();
        let mut len: usize = 0;
        let rc = unsafe {
            sys::OSSL_PARAM_get_octet_string_ptr(
                elem,
                std::ptr::addr_of_mut!(p),
                std::ptr::addr_of_mut!(len),
            )
        };
        assert_eq!(rc, 1, "OSSL_PARAM_get_octet_string_ptr failed");
        assert_eq!(len, data.len());
        let got = unsafe { std::slice::from_raw_parts(p.cast::<u8>(), len) };
        assert_eq!(got, data);
    }

    // push_octet_ptr is verified through KDF integration tests (Phase 6),
    // where it is used in a complete derive() operation.  Isolated unit tests
    // for it are unreliable because OSSL_PARAM_BLD_to_param may exhaust the
    // OpenSSL secure heap in test environments.

    #[test]
    fn params_set_int_roundtrip() {
        // Build a Params array with an initial int value.
        let mut params = ParamBuilder::new()
            .unwrap()
            .push_int(c"myint", 10)
            .unwrap()
            .build()
            .unwrap();

        // Verify the initial value via the get_int getter.
        assert_eq!(params.get_int(c"myint").unwrap(), 10);

        // Overwrite the value in place via set_int.
        params.set_int(c"myint", 99).expect("set_int failed");

        // Confirm the new value is visible.
        assert_eq!(params.get_int(c"myint").unwrap(), 99);
    }

    #[test]
    fn params_was_modified() {
        use crate::pkey::{KeygenCtx, Pkey, Private};

        // Generate a small RSA key so we have a real Pkey to query against.
        let key: Pkey<Private> = {
            let mut ctx = KeygenCtx::new(c"RSA").expect("KeygenCtx::new");
            let bits = ParamBuilder::new()
                .unwrap()
                .push_uint(c"bits", 512)
                .unwrap()
                .build()
                .unwrap();
            ctx.set_params(&bits).expect("set_params bits");
            ctx.generate().expect("generate")
        };

        // Query "bits" (key size as uint) — a uint param avoids the BN type
        // mismatch that octet_slice causes for BN params like "e" or "n".
        let mut query = ParamBuilder::new()
            .unwrap()
            .push_uint(c"bits", 0)
            .unwrap()
            .build()
            .unwrap();

        assert!(!query.was_modified(c"bits"), "not yet modified");

        // After get_params, OpenSSL fills the element: was_modified returns true.
        key.get_params(&mut query).expect("get_params");
        assert!(
            query.was_modified(c"bits"),
            "should be modified after get_params"
        );

        // A key that does not exist returns false regardless.
        assert!(!query.was_modified(c"no_such_key"));
    }

    #[test]
    fn utf8_string_round_trip() {
        let params = ParamBuilder::new()
            .unwrap()
            .push_utf8_string(c"alg", c"SHA-256")
            .unwrap()
            .build()
            .unwrap();

        let elem = unsafe { sys::OSSL_PARAM_locate(params.as_ptr().cast_mut(), c"alg".as_ptr()) };
        assert!(!elem.is_null());

        let mut out: *const std::os::raw::c_char = ptr::null();
        let rc = unsafe { sys::OSSL_PARAM_get_utf8_string_ptr(elem, std::ptr::addr_of_mut!(out)) };
        assert_eq!(rc, 1, "OSSL_PARAM_get_utf8_string_ptr failed");
        let got = unsafe { CStr::from_ptr(out) };
        assert_eq!(got.to_bytes(), b"SHA-256");
    }
}
