//! X.509 certificate — reading, inspecting, and building.
//!
//! # Types
//!
//! | Type              | Owned / Borrowed | Description                          |
//! |-------------------|-----------------|--------------------------------------|
//! | [`X509`]          | Owned (Arc-like) | Certificate, Clone via `up_ref`      |
//! | [`X509Name`]      | Borrowed `'cert` | Subject or issuer distinguished name |
//! | [`X509NameEntry`] | Borrowed `'name` | One RDN entry (e.g. CN, O, C)       |
//! | [`X509Extension`] | Borrowed `'cert` | One certificate extension            |
//! | [`X509NameOwned`] | Owned            | Mutable name for certificate builder |
//! | [`X509Builder`]   | Owned builder    | Constructs a new X.509 certificate   |

use crate::bio::{MemBio, MemBioBuf};
use crate::error::ErrorStack;
use crate::obj::Oid;
use native_ossl_sys as sys;
use std::ffi::CStr;
use std::marker::PhantomData;
use std::sync::Arc;

// ── BrokenDownTime — structured calendar time ────────────────────────────────

/// A calendar date and time in UTC, as returned by [`X509::not_before_tm`] and
/// [`X509::not_after_tm`].
///
/// All fields are in natural units (year is the full Gregorian year; month is
/// 1–12; all time fields are 0-based).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrokenDownTime {
    /// Full Gregorian year (e.g. 2026).
    pub year: i32,
    /// Month, 1–12.
    pub month: u8,
    /// Day of the month, 1–31.
    pub day: u8,
    /// Hour, 0–23.
    pub hour: u8,
    /// Minute, 0–59.
    pub minute: u8,
    /// Second, 0–60 (60 is possible for leap seconds).
    pub second: u8,
}

// ── SignatureInfo — certificate signature algorithm metadata ──────────────────

/// Decoded signature algorithm metadata from an [`X509`] certificate.
///
/// Returned by [`X509::signature_info`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignatureInfo {
    /// NID of the digest algorithm used in the signature.
    ///
    /// `0` (`NID_undef`) for algorithms that do not use a separate
    /// pre-hash step, such as Ed25519 and ML-DSA.
    pub md_nid: i32,
    /// NID of the public-key algorithm used in the signature.
    pub pk_nid: i32,
    /// Estimated security strength in bits (e.g. 128 for AES-128-equivalent).
    pub security_bits: i32,
}

// ── KeyUsage ──────────────────────────────────────────────────────────────────

/// Bitmask of key usage bits from an X.509 [`keyUsage`][rfc5280] extension.
///
/// Returned by [`X509::key_usage`].  Combine constants with `|` or test
/// individual bits with [`Self::contains`].
///
/// [rfc5280]: https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyUsage(pub u32);

impl KeyUsage {
    /// `digitalSignature` — signing data and authentication.
    pub const DIGITAL_SIGNATURE: Self = Self(0x0080);
    /// `nonRepudiation` (`contentCommitment` in RFC 5280).
    pub const NON_REPUDIATION: Self = Self(0x0040);
    /// `keyEncipherment` — encrypting symmetric keys.
    pub const KEY_ENCIPHERMENT: Self = Self(0x0020);
    /// `dataEncipherment` — directly encrypting data.
    pub const DATA_ENCIPHERMENT: Self = Self(0x0010);
    /// `keyAgreement` — Diffie-Hellman key exchange.
    pub const KEY_AGREEMENT: Self = Self(0x0008);
    /// `keyCertSign` — signing certificates.
    pub const KEY_CERT_SIGN: Self = Self(0x0004);
    /// `cRLSign` — signing CRLs.
    pub const CRL_SIGN: Self = Self(0x0002);
    /// `encipherOnly` — only encrypt during key agreement.
    pub const ENCIPHER_ONLY: Self = Self(0x0001);
    /// `decipherOnly` — only decrypt during key agreement.
    pub const DECIPHER_ONLY: Self = Self(0x8000);

    /// Return `true` if all bits in `flag` are set in `self`.
    #[must_use]
    pub fn contains(self, flag: Self) -> bool {
        self.0 & flag.0 == flag.0
    }

    /// Return the raw bitmask value.
    #[must_use]
    pub fn bits(self) -> u32 {
        self.0
    }
}

impl std::ops::BitOr for KeyUsage {
    type Output = Self;
    fn bitor(self, rhs: Self) -> Self {
        Self(self.0 | rhs.0)
    }
}

impl std::ops::BitAnd for KeyUsage {
    type Output = Self;
    fn bitand(self, rhs: Self) -> Self {
        Self(self.0 & rhs.0)
    }
}

// ── GeneralName ───────────────────────────────────────────────────────────────

/// A decoded entry from an X.509 Subject Alternative Name (`subjectAltName`)
/// extension.
///
/// Returned by [`X509::subject_alt_names`].  Variants correspond to the
/// `GeneralName` choice types defined in RFC 5280 § 4.2.1.6.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GeneralName {
    /// `dNSName` — a DNS hostname (e.g. `"example.com"`).
    Dns(String),
    /// `rfc822Name` — an email address.
    Email(String),
    /// `uniformResourceIdentifier` — a URI.
    Uri(String),
    /// `iPAddress` — 4 bytes for IPv4, 16 bytes for IPv6.
    IpAddress(Vec<u8>),
    /// `registeredID` — an OID (used in PKIX for specific identifier types).
    RegisteredId(Oid),
    /// `otherName` — application-specific name with a type OID and DER-encoded value.
    ///
    /// `value` contains the DER encoding of the `ASN1_TYPE` value as returned
    /// by OpenSSL.  Callers that need to decode the value must parse it
    /// according to the schema identified by `type_id`.
    OtherName {
        /// The type OID identifying the value schema.
        type_id: Oid,
        /// DER-encoded value bytes.
        value: Vec<u8>,
    },
}

// ── X509 — owned certificate ──────────────────────────────────────────────────

/// An X.509 certificate (`X509*`).
///
/// Cloneable via `EVP_X509_up_ref`; wrapping in `Arc<X509>` is safe.
pub struct X509 {
    ptr: *mut sys::X509,
}

// SAFETY: `X509*` is reference-counted.
unsafe impl Send for X509 {}
unsafe impl Sync for X509 {}

impl Clone for X509 {
    /// # Panics
    ///
    /// Panics if `X509_up_ref` fails (OpenSSL internal error; practically impossible).
    fn clone(&self) -> Self {
        // SAFETY: self.ptr is a valid, non-null X509*.
        let rc = unsafe { sys::X509_up_ref(self.ptr) };
        assert_eq!(rc, 1, "X509_up_ref failed");
        X509 { ptr: self.ptr }
    }
}

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

impl X509 {
    /// Construct from a raw, owned `X509*`.
    ///
    /// # Safety
    ///
    /// `ptr` must be a valid, non-null `X509*` whose ownership is being transferred.
    pub(crate) unsafe fn from_ptr(ptr: *mut sys::X509) -> Self {
        X509 { ptr }
    }

    /// Load a certificate from PEM bytes.
    ///
    /// # Errors
    pub fn from_pem(pem: &[u8]) -> Result<Self, ErrorStack> {
        let bio = MemBioBuf::new(pem)?;
        let ptr = unsafe {
            sys::PEM_read_bio_X509(
                bio.as_ptr(),
                std::ptr::null_mut(),
                None,
                std::ptr::null_mut(),
            )
        };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(unsafe { Self::from_ptr(ptr) })
    }

    /// Load a certificate from PEM bytes, accepting a library context for API
    /// symmetry with other `from_pem_in` methods.
    ///
    /// OpenSSL 3.5 does not expose a libctx-aware `PEM_read_bio_X509_ex`
    /// variant, so this calls the standard `PEM_read_bio_X509`.  The `ctx`
    /// parameter is accepted but unused.  Certificate parsing itself does not
    /// require provider dispatch; provider-bound operations use the context
    /// stored in the key extracted from the certificate.
    ///
    /// # Errors
    pub fn from_pem_in(_ctx: &Arc<crate::lib_ctx::LibCtx>, pem: &[u8]) -> Result<Self, ErrorStack> {
        Self::from_pem(pem)
    }

    /// Create a new, empty `X509` object bound to the given library context.
    ///
    /// Use this instead of the implicit-context `X509Builder::new` when you need
    /// the certificate to be associated with a FIPS-isolated or otherwise
    /// explicit `LibCtx`.  The `propq` (property query) argument is `NULL`,
    /// meaning default provider properties are used.
    ///
    /// # Errors
    ///
    /// Returns `Err` if OpenSSL cannot allocate the certificate structure.
    pub fn new_in(ctx: &Arc<crate::lib_ctx::LibCtx>) -> Result<Self, ErrorStack> {
        // SAFETY:
        // - ctx.as_ptr() is non-null (LibCtx constructor invariant)
        // - propq is null → use default algorithm properties
        let ptr = unsafe { sys::X509_new_ex(ctx.as_ptr(), std::ptr::null()) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(unsafe { Self::from_ptr(ptr) })
    }

    /// Load a certificate from DER bytes.
    ///
    /// Zero-copy: parses from the caller's slice without an intermediate buffer.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the DER is malformed, or if `der.len()` exceeds `i64::MAX`.
    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack> {
        let mut der_ptr = der.as_ptr();
        let len = i64::try_from(der.len()).map_err(|_| ErrorStack::drain())?;
        let ptr =
            unsafe { sys::d2i_X509(std::ptr::null_mut(), std::ptr::addr_of_mut!(der_ptr), len) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(unsafe { Self::from_ptr(ptr) })
    }

    /// Serialise to PEM.
    ///
    /// # Errors
    pub fn to_pem(&self) -> Result<Vec<u8>, ErrorStack> {
        let mut bio = MemBio::new()?;
        crate::ossl_call!(sys::PEM_write_bio_X509(bio.as_ptr(), self.ptr))?;
        Ok(bio.into_vec())
    }

    /// Serialise to DER.
    ///
    /// Zero-copy: writes into a freshly allocated `Vec<u8>` without going
    /// through an OpenSSL-owned buffer.
    ///
    /// # Errors
    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack> {
        let len = unsafe { sys::i2d_X509(self.ptr, std::ptr::null_mut()) };
        if len < 0 {
            return Err(ErrorStack::drain());
        }
        let mut buf = vec![0u8; usize::try_from(len).unwrap_or(0)];
        let mut out_ptr = buf.as_mut_ptr();
        let written = unsafe { sys::i2d_X509(self.ptr, std::ptr::addr_of_mut!(out_ptr)) };
        if written < 0 {
            return Err(ErrorStack::drain());
        }
        buf.truncate(usize::try_from(written).unwrap_or(0));
        Ok(buf)
    }

    /// Subject distinguished name (borrowed).
    #[must_use]
    pub fn subject_name(&self) -> X509Name<'_> {
        // get0: does not increment ref count; valid while self is alive.
        // OpenSSL 4.x made this return `*const`; cast is safe — we never
        // mutate through the borrowed pointer.
        let ptr = unsafe { sys::X509_get_subject_name(self.ptr) };
        X509Name {
            ptr,
            _owner: PhantomData,
        }
    }

    /// Issuer distinguished name (borrowed).
    #[must_use]
    pub fn issuer_name(&self) -> X509Name<'_> {
        let ptr = unsafe { sys::X509_get_issuer_name(self.ptr) };
        X509Name {
            ptr,
            _owner: PhantomData,
        }
    }

    /// Serial number as a signed 64-bit integer.
    ///
    /// Returns `None` if the serial number is too large to fit in `i64`.
    #[must_use]
    pub fn serial_number(&self) -> Option<i64> {
        let ai = unsafe { sys::X509_get0_serialNumber(self.ptr) };
        if ai.is_null() {
            return None;
        }
        let mut n: i64 = 0;
        let rc = unsafe { sys::ASN1_INTEGER_get_int64(std::ptr::addr_of_mut!(n), ai) };
        if rc == 1 {
            Some(n)
        } else {
            None
        }
    }

    /// Serial number as a big-endian byte slice.
    ///
    /// Complementary to [`Self::serial_number`] for serials that exceed `i64::MAX`.
    /// The bytes are the raw content octets of the DER `INTEGER`, not including
    /// the tag or length.
    ///
    /// Returns `None` if the serial field is absent.
    #[must_use]
    pub fn serial_number_bytes(&self) -> Option<Vec<u8>> {
        // SAFETY:
        // - self.ptr is non-null (constructor invariant)
        // - X509_get_serialNumber returns a pointer embedded in the X509 object;
        //   it is valid for as long as self is alive
        // - &self ensures the certificate is not mutated while we read the field
        let ai = unsafe { sys::X509_get_serialNumber(self.ptr) };
        if ai.is_null() {
            return None;
        }
        // SAFETY: ai is non-null (checked above) and lives for the duration of
        // &self; ASN1_INTEGER is typedef'd to ASN1_STRING in OpenSSL, so the
        // cast to *const ASN1_STRING is safe.
        Some(unsafe { asn1_string_data(ai.cast()) }.to_vec())
    }

    /// Validity `notBefore` as a human-readable UTC string.
    ///
    /// The format is `"Mmm DD HH:MM:SS YYYY GMT"` (OpenSSL default).
    /// Returns `None` if the field is absent or cannot be printed.
    #[must_use]
    pub fn not_before_str(&self) -> Option<String> {
        let t = unsafe { sys::X509_get0_notBefore(self.ptr) };
        asn1_time_to_str(t)
    }

    /// Validity `notAfter` as a human-readable UTC string.
    ///
    /// Returns `None` if the field is absent or cannot be printed.
    #[must_use]
    pub fn not_after_str(&self) -> Option<String> {
        let t = unsafe { sys::X509_get0_notAfter(self.ptr) };
        asn1_time_to_str(t)
    }

    /// Validity `notBefore` as a structured [`BrokenDownTime`] in UTC.
    ///
    /// Returns `None` if the field is absent or cannot be parsed.
    #[must_use]
    pub fn not_before_tm(&self) -> Option<BrokenDownTime> {
        // SAFETY:
        // - self.ptr is non-null (constructor invariant)
        // - X509_get0_notBefore returns a pointer embedded in the X509; valid while &self lives
        // - &self ensures no concurrent mutation
        let t = unsafe { sys::X509_get0_notBefore(self.ptr) };
        asn1_time_to_broken_down(t)
    }

    /// Validity `notAfter` as a structured [`BrokenDownTime`] in UTC.
    ///
    /// Returns `None` if the field is absent or cannot be parsed.
    #[must_use]
    pub fn not_after_tm(&self) -> Option<BrokenDownTime> {
        // SAFETY: same as not_before_tm
        let t = unsafe { sys::X509_get0_notAfter(self.ptr) };
        asn1_time_to_broken_down(t)
    }

    /// Returns `true` if the current time is within `[notBefore, notAfter]`.
    #[must_use]
    pub fn is_valid_now(&self) -> bool {
        let nb = unsafe { sys::X509_get0_notBefore(self.ptr) };
        let na = unsafe { sys::X509_get0_notAfter(self.ptr) };
        // X509_cmp_time(t, NULL) < 0 → t < now; > 0 → t > now.
        unsafe {
            sys::X509_cmp_time(nb, std::ptr::null_mut()) <= 0
                && sys::X509_cmp_time(na, std::ptr::null_mut()) >= 0
        }
    }

    /// Extract the public key (owned `Pkey<Public>`).
    ///
    /// Calls `X509_get_pubkey` — the returned key is independently reference-counted.
    ///
    /// # Errors
    pub fn public_key(&self) -> Result<crate::pkey::Pkey<crate::pkey::Public>, ErrorStack> {
        let ptr = unsafe { sys::X509_get_pubkey(self.ptr) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(unsafe { crate::pkey::Pkey::from_ptr(ptr) })
    }

    /// Check whether the certificate's public key uses the named algorithm.
    ///
    /// Uses `X509_get0_pubkey` — no reference-count increment.  Call
    /// [`Self::public_key`] if you need an owned [`crate::pkey::Pkey`] handle.
    ///
    /// Returns `false` if the certificate has no public key or if the algorithm
    /// name does not match.
    #[must_use]
    pub fn public_key_is_a(&self, alg: &CStr) -> bool {
        // SAFETY:
        // - self.ptr is non-null (constructor invariant)
        // - X509_get0_pubkey returns a borrowed pointer valid while &self holds;
        //   we do not store it and do not outlive this call
        // - &self ensures no concurrent mutation of the certificate
        let pkey = unsafe { sys::X509_get0_pubkey(self.ptr) };
        if pkey.is_null() {
            return false;
        }
        // SAFETY: pkey is non-null (checked above); alg.as_ptr() is valid for
        // the duration of this call (CStr invariant)
        unsafe { sys::EVP_PKEY_is_a(pkey, alg.as_ptr()) == 1 }
    }

    /// Bit size of the certificate's public key.
    ///
    /// Uses `X509_get0_pubkey` — no reference-count increment.
    ///
    /// Returns `None` if the certificate has no public key.
    #[must_use]
    pub fn public_key_bits(&self) -> Option<u32> {
        // SAFETY:
        // - self.ptr is non-null (constructor invariant)
        // - X509_get0_pubkey returns a borrowed pointer valid while &self holds
        // - &self ensures no concurrent mutation of the certificate
        let pkey = unsafe { sys::X509_get0_pubkey(self.ptr) };
        if pkey.is_null() {
            return None;
        }
        // SAFETY: pkey is non-null (checked above)
        u32::try_from(unsafe { sys::EVP_PKEY_get_bits(pkey) }).ok()
    }

    /// Inspect the signature algorithm used in this certificate.
    ///
    /// Calls `X509_get_signature_info` to decode the signature algorithm fields
    /// embedded in the certificate's `signatureAlgorithm` and `signature`
    /// structures.
    ///
    /// `md_nid` is `0` (`NID_undef`) for algorithms that have no separate
    /// pre-hash step, such as Ed25519 and ML-DSA (post-quantum lattice signatures
    /// defined in FIPS 204).  Always check for `0` before using `md_nid` as a
    /// digest identifier.
    ///
    /// # Errors
    ///
    /// Returns `Err` if OpenSSL cannot decode the signature algorithm (e.g. the
    /// certificate's signature field is absent or uses an unrecognised OID).
    pub fn signature_info(&self) -> Result<SignatureInfo, ErrorStack> {
        let mut md_nid: std::ffi::c_int = 0;
        let mut pk_nid: std::ffi::c_int = 0;
        let mut security_bits: std::ffi::c_int = 0;
        // SAFETY:
        // - self.ptr is non-null (constructor invariant)
        // - md_nid, pk_nid, security_bits are local stack variables; we pass
        //   their addresses which are valid for the duration of this call
        // - flags is null_mut(): we do not need the X509_SIG_INFO_TLS /
        //   X509_SIG_INFO_VALID flags; OpenSSL accepts NULL for any out-param
        // - &self ensures the certificate is not mutated concurrently
        let rc = unsafe {
            sys::X509_get_signature_info(
                self.ptr,
                &raw mut md_nid,
                &raw mut pk_nid,
                &raw mut security_bits,
                std::ptr::null_mut(),
            )
        };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(SignatureInfo {
            md_nid,
            pk_nid,
            security_bits,
        })
    }

    /// Verify this certificate was signed by `key`.
    ///
    /// Returns `Ok(true)` if the signature is valid, `Ok(false)` if not, or
    /// `Err` on a fatal error.
    ///
    /// # Errors
    pub fn verify(&self, key: &crate::pkey::Pkey<crate::pkey::Public>) -> Result<bool, ErrorStack> {
        match unsafe { sys::X509_verify(self.ptr, key.as_ptr()) } {
            1 => Ok(true),
            0 => Ok(false),
            _ => Err(ErrorStack::drain()),
        }
    }

    /// Returns `true` if the certificate is self-signed.
    #[must_use]
    pub fn is_self_signed(&self) -> bool {
        // verify_signature=0 → only check name match, not signature itself.
        unsafe { sys::X509_self_signed(self.ptr, 0) == 1 }
    }

    /// Number of extensions in this certificate.
    #[must_use]
    pub fn extension_count(&self) -> usize {
        let n = unsafe { sys::X509_get_ext_count(self.ptr) };
        usize::try_from(n).unwrap_or(0)
    }

    /// Access extension by index (0-based).
    ///
    /// Returns `None` if `idx` is out of range.
    #[must_use]
    pub fn extension(&self, idx: usize) -> Option<X509Extension<'_>> {
        let idx_i32 = i32::try_from(idx).ok()?;
        // OpenSSL 4.x returns *const; cast is safe — borrowed, never mutated.
        let ptr = unsafe { sys::X509_get_ext(self.ptr, idx_i32) };
        if ptr.is_null() {
            None
        } else {
            Some(X509Extension {
                ptr,
                _owner: PhantomData,
            })
        }
    }

    /// Find the first extension with the given NID.
    ///
    /// Returns `None` if no such extension exists.
    #[must_use]
    pub fn extension_by_nid(&self, nid: i32) -> Option<X509Extension<'_>> {
        let idx = unsafe { sys::X509_get_ext_by_NID(self.ptr, nid, -1) };
        if idx < 0 {
            return None;
        }
        let ptr = unsafe { sys::X509_get_ext(self.ptr, idx) };
        if ptr.is_null() {
            None
        } else {
            Some(X509Extension {
                ptr,
                _owner: PhantomData,
            })
        }
    }

    /// Return the DER-encoded value of the first extension with the given NID.
    ///
    /// Returns `None` if the extension is not present.  The returned byte slice
    /// is borrowed from the certificate's internal storage — zero-copy, no
    /// allocation — and is valid for `'self`'s lifetime.
    ///
    /// To iterate all extensions or access criticality flags, use
    /// [`Self::extension_by_nid`] or [`Self::extension`] instead.
    #[must_use]
    pub fn extension_der(&self, nid: i32) -> Option<&[u8]> {
        // SAFETY: self.ptr is non-null (constructor invariant)
        let idx = unsafe { sys::X509_get_ext_by_NID(self.ptr, nid, -1) };
        if idx < 0 {
            return None;
        }
        // SAFETY: idx is a valid extension index returned by X509_get_ext_by_NID
        let ext = unsafe { sys::X509_get_ext(self.ptr, idx) };
        if ext.is_null() {
            return None;
        }
        // SAFETY: ext is non-null; X509_EXTENSION_get_data returns data borrowed
        // from ext, which itself is borrowed from self
        let data = unsafe { sys::X509_EXTENSION_get_data(ext) };
        if data.is_null() {
            return Some(&[]);
        }
        // SAFETY: data is a valid ASN1_OCTET_STRING; cast to ASN1_STRING is safe
        // because ASN1_OCTET_STRING is typedef'd to ASN1_STRING in OpenSSL
        Some(unsafe { asn1_string_data(data.cast()) })
    }

    /// Key usage bits from the certificate's `keyUsage` extension.
    ///
    /// Returns `None` if the extension is absent.  When present, returns a
    /// [`KeyUsage`] bitmask; test individual bits with [`KeyUsage::contains`].
    ///
    /// OpenSSL internally caches the parsed extension; this call is cheap.
    #[must_use]
    pub fn key_usage(&self) -> Option<KeyUsage> {
        // SAFETY: self.ptr is non-null (constructor invariant).
        // X509_get_key_usage returns UINT32_MAX when the keyUsage extension
        // is absent, and the decoded bitmask otherwise.
        let bits = unsafe { sys::X509_get_key_usage(self.ptr) };
        if bits == u32::MAX {
            None
        } else {
            Some(KeyUsage(bits))
        }
    }

    /// OIDs from the certificate's `extendedKeyUsage` extension.
    ///
    /// Returns an empty `Vec` if the extension is absent.  Each [`Oid`] in the
    /// returned list identifies one extended key purpose (e.g. id-kp-serverAuth,
    /// id-kp-clientAuth, or a vendor-specific OID not in the standard NID table).
    ///
    /// # Errors
    ///
    /// Returns an [`ErrorStack`] if the extension is present but malformed, or
    /// if `OBJ_dup` fails to allocate a copy of an OID.
    #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
    pub fn extended_key_usage_oids(&self) -> Result<Vec<Oid>, ErrorStack> {
        let nid = unsafe { sys::OBJ_sn2nid(c"extendedKeyUsage".as_ptr()) };
        // SAFETY: X509_get_ext_d2i decodes and returns an owned STACK_OF(ASN1_OBJECT)*.
        // NULL means the extension is absent.
        let stack_ptr = unsafe {
            sys::X509_get_ext_d2i(self.ptr, nid, std::ptr::null_mut(), std::ptr::null_mut())
        };
        if stack_ptr.is_null() {
            return Ok(Vec::new());
        }
        let stack = stack_ptr.cast::<sys::OPENSSL_STACK>();
        let n = usize::try_from(unsafe { sys::OPENSSL_sk_num(stack) }).unwrap_or(0);
        let mut oids = Vec::with_capacity(n);
        for i in 0..n {
            let raw = unsafe { sys::OPENSSL_sk_value(stack, i as i32) };
            if raw.is_null() {
                continue;
            }
            // Duplicate so the Oid has its own allocation independent of the stack.
            let dup = unsafe { sys::OBJ_dup(raw.cast::<sys::ASN1_OBJECT>()) };
            if dup.is_null() {
                // Free the stack (including originals) before propagating the error.
                // SAFETY: stack was returned by X509_get_ext_d2i (owned); elements
                // are ASN1_OBJECT* freed by ASN1_OBJECT_free.
                unsafe {
                    sys::OPENSSL_sk_pop_free(stack, Some(free_asn1_object));
                }
                return Err(ErrorStack::drain());
            }
            // SAFETY: dup is non-null and heap-allocated; Oid::from_ptr takes ownership.
            oids.push(unsafe { Oid::from_ptr(dup) });
        }
        // Free the stack and its original elements (we hold dup'd copies).
        unsafe { sys::OPENSSL_sk_pop_free(stack, Some(free_asn1_object)) };
        Ok(oids)
    }

    /// Decoded Subject Alternative Name entries from the certificate.
    ///
    /// Returns an empty `Vec` if the `subjectAltName` extension is absent.
    /// Variants [`GeneralName::Dns`], [`GeneralName::Email`],
    /// [`GeneralName::Uri`], [`GeneralName::IpAddress`],
    /// [`GeneralName::RegisteredId`], and [`GeneralName::OtherName`] are
    /// decoded; `DirectoryName`, `X400Address`, and `EdiPartyName` entries are
    /// silently skipped (uncommon in practice).
    ///
    /// # Errors
    ///
    /// Returns an [`ErrorStack`] if the extension is present but malformed.
    #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
    pub fn subject_alt_names(&self) -> Result<Vec<GeneralName>, ErrorStack> {
        let nid = unsafe { sys::OBJ_sn2nid(c"subjectAltName".as_ptr()) };
        // SAFETY: X509_get_ext_d2i decodes and returns an owned STACK_OF(GENERAL_NAME)*.
        let stack_ptr = unsafe {
            sys::X509_get_ext_d2i(self.ptr, nid, std::ptr::null_mut(), std::ptr::null_mut())
        };
        if stack_ptr.is_null() {
            return Ok(Vec::new());
        }
        let stack = stack_ptr.cast::<sys::OPENSSL_STACK>();
        let n = usize::try_from(unsafe { sys::OPENSSL_sk_num(stack) }).unwrap_or(0);
        let mut names = Vec::with_capacity(n);
        for i in 0..n {
            let raw = unsafe { sys::OPENSSL_sk_value(stack, i as i32) };
            if raw.is_null() {
                continue;
            }
            let gn = raw.cast::<sys::GENERAL_NAME>();
            // SAFETY: gn is a valid, non-null GENERAL_NAME* in the owned stack.
            let gn_ref = unsafe { &*gn };
            // GEN_* discriminants from <openssl/x509v3.h>
            match gn_ref.type_ {
                // GEN_EMAIL = 1
                1 => {
                    let s = unsafe { asn1_string_data(gn_ref.d.rfc822Name.cast()) };
                    names.push(GeneralName::Email(String::from_utf8_lossy(s).into_owned()));
                }
                // GEN_DNS = 2
                2 => {
                    let s = unsafe { asn1_string_data(gn_ref.d.dNSName.cast()) };
                    names.push(GeneralName::Dns(String::from_utf8_lossy(s).into_owned()));
                }
                // GEN_URI = 6
                6 => {
                    let s = unsafe { asn1_string_data(gn_ref.d.uniformResourceIdentifier.cast()) };
                    names.push(GeneralName::Uri(String::from_utf8_lossy(s).into_owned()));
                }
                // GEN_IPADD = 7
                7 => {
                    let b = unsafe { asn1_string_data(gn_ref.d.iPAddress.cast()) };
                    names.push(GeneralName::IpAddress(b.to_vec()));
                }
                // GEN_RID = 8
                8 => {
                    let rid = unsafe { gn_ref.d.registeredID };
                    if !rid.is_null() {
                        let dup = unsafe { sys::OBJ_dup(rid) };
                        if dup.is_null() {
                            log::warn!("subject_alt_names: OBJ_dup failed for RegisteredId SAN; entry skipped");
                            let _ = ErrorStack::drain();
                        } else {
                            names.push(GeneralName::RegisteredId(unsafe { Oid::from_ptr(dup) }));
                        }
                    }
                }
                // GEN_OTHERNAME = 0
                0 => {
                    let other = unsafe { gn_ref.d.otherName };
                    if !other.is_null() {
                        let other_ref = unsafe { &*other };
                        if !other_ref.type_id.is_null() {
                            let oid_dup = unsafe { sys::OBJ_dup(other_ref.type_id) };
                            if oid_dup.is_null() {
                                log::warn!("subject_alt_names: OBJ_dup failed for OtherName type_id SAN; entry skipped");
                                let _ = ErrorStack::drain();
                            } else {
                                let value = unsafe { i2d_asn1_type_to_vec(other_ref.value) };
                                names.push(GeneralName::OtherName {
                                    type_id: unsafe { Oid::from_ptr(oid_dup) },
                                    value,
                                });
                            }
                        }
                    }
                }
                // GEN_DIRNAME, GEN_X400, GEN_EDIPARTY — uncommon, skip
                _ => {}
            }
        }
        // Free the stack and all GENERAL_NAME elements.
        unsafe { sys::OPENSSL_sk_pop_free(stack, Some(free_general_name)) };
        Ok(names)
    }

    /// Raw `X509*` pointer valid for the lifetime of `self`.
    #[must_use]
    #[allow(dead_code)] // used by ssl module added in the next phase
    pub(crate) fn as_ptr(&self) -> *mut sys::X509 {
        self.ptr
    }
}

// ── X509Name — borrowed distinguished name ────────────────────────────────────

/// A borrowed distinguished name (`X509_NAME*`) tied to its owning `X509`.
pub struct X509Name<'cert> {
    ptr: *const sys::X509_NAME,
    _owner: PhantomData<&'cert X509>,
}

impl std::fmt::Debug for X509Name<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.to_oneline() {
            Some(s) => f.debug_tuple("X509Name").field(&s).finish(),
            None => f.debug_struct("X509Name").finish_non_exhaustive(),
        }
    }
}

impl X509Name<'_> {
    /// Number of entries (RDN components) in the name.
    #[must_use]
    pub fn entry_count(&self) -> usize {
        usize::try_from(unsafe { sys::X509_NAME_entry_count(self.ptr) }).unwrap_or(0)
    }

    /// Access an entry by index (0-based).
    #[must_use]
    pub fn entry(&self, idx: usize) -> Option<X509NameEntry<'_>> {
        let idx_i32 = i32::try_from(idx).ok()?;
        // OpenSSL 4.x returns *const; cast is safe — borrowed, never mutated.
        let ptr = unsafe { sys::X509_NAME_get_entry(self.ptr, idx_i32) };
        if ptr.is_null() {
            None
        } else {
            Some(X509NameEntry {
                ptr,
                _owner: PhantomData,
            })
        }
    }

    /// Return a one-line string representation of this distinguished name.
    ///
    /// Produces the legacy `/CN=.../O=.../C=...` slash-separated format via
    /// `X509_NAME_oneline`.  Useful for logging and debugging, but **not
    /// recommended** for structured access — use [`Self::entry`] or
    /// [`Self::to_string`] (which honours RFC 2253 / RFC 4514 flags) instead.
    ///
    /// Returns `None` if OpenSSL fails to allocate the string.
    #[must_use]
    pub fn to_oneline(&self) -> Option<String> {
        // SAFETY:
        // - self.ptr is non-null (constructor invariant)
        // - passing NULL + 0 causes OpenSSL to heap-allocate the result; we
        //   take ownership and must free it with OPENSSL_free
        let ptr = unsafe { sys::X509_NAME_oneline(self.ptr, std::ptr::null_mut(), 0) };
        if ptr.is_null() {
            return None;
        }
        // SAFETY: ptr is a valid NUL-terminated C string allocated by OpenSSL
        let s = unsafe { std::ffi::CStr::from_ptr(ptr) }
            .to_string_lossy()
            .into_owned();
        // SAFETY: ptr was allocated by OpenSSL; CRYPTO_free is the underlying
        // implementation of the OPENSSL_free macro (OPENSSL_free is a C macro
        // and cannot be bound directly — CRYPTO_free is the actual function)
        unsafe { sys::CRYPTO_free(ptr.cast(), c"x509.rs".as_ptr(), 0) };
        Some(s)
    }

    /// Format the entire name as a single-line string.
    ///
    /// Uses `X509_NAME_print_ex` with `XN_FLAG_COMPAT` (traditional
    /// `/CN=.../O=.../` format).  Returns `None` on error.
    #[must_use]
    pub fn to_string(&self) -> Option<String> {
        let mut bio = MemBio::new().ok()?;
        // flags = 0 → XN_FLAG_COMPAT (old /CN=…/O=… format).
        let n = unsafe { sys::X509_NAME_print_ex(bio.as_ptr(), self.ptr, 0, 0) };
        if n < 0 {
            return None;
        }
        String::from_utf8(bio.into_vec()).ok()
    }

    /// Encode this distinguished name as DER.
    ///
    /// Returns the full `SEQUENCE { … }` encoding (tag + length + contents).
    ///
    /// # Errors
    ///
    /// Returns an [`ErrorStack`] if OpenSSL fails to encode the name (e.g.,
    /// the name contains an unencodable string type or OpenSSL reports an
    /// internal error).
    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack> {
        // SAFETY: self.ptr is a valid X509_NAME*; null_mut() instructs
        // i2d_X509_NAME to return the required length without writing bytes.
        let len = unsafe { sys::i2d_X509_NAME(self.ptr, std::ptr::null_mut()) };
        if len < 0 {
            return Err(ErrorStack::drain());
        }
        let mut buf = vec![0u8; len as usize];
        let mut ptr = buf.as_mut_ptr();
        // SAFETY: self.ptr is valid; ptr points into buf which is `len` bytes.
        // i2d_X509_NAME advances *ptr past the written bytes.
        let written = unsafe { sys::i2d_X509_NAME(self.ptr, std::ptr::addr_of_mut!(ptr)) };
        if written < 0 || written as usize > buf.len() {
            // Negative: OpenSSL error. Greater than buf.len(): the second call
            // wrote more bytes than the first declared (shouldn't happen with a
            // single-threaded X509_NAME, but must be checked before truncate).
            return Err(ErrorStack::drain());
        }
        buf.truncate(written as usize);
        Ok(buf)
    }
}

// ── X509NameEntry — one RDN component ────────────────────────────────────────

/// A borrowed entry within an [`X509Name`].
pub struct X509NameEntry<'name> {
    ptr: *const sys::X509_NAME_ENTRY,
    _owner: PhantomData<&'name ()>,
}

impl std::fmt::Debug for X509NameEntry<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("X509NameEntry")
            .field("nid", &self.nid())
            .finish_non_exhaustive()
    }
}

impl X509NameEntry<'_> {
    /// NID of this field (e.g. `NID_commonName = 13`).
    #[must_use]
    pub fn nid(&self) -> i32 {
        let obj = unsafe { sys::X509_NAME_ENTRY_get_object(self.ptr) };
        unsafe { sys::OBJ_obj2nid(obj) }
    }

    /// Raw DER-encoded value bytes of this entry.
    ///
    /// The slice is valid as long as the owning certificate is alive.
    #[must_use]
    pub fn data(&self) -> &[u8] {
        let asn1 = unsafe { sys::X509_NAME_ENTRY_get_data(self.ptr) };
        if asn1.is_null() {
            return &[];
        }
        // SAFETY: asn1 is valid for 'name (guaranteed by self._owner PhantomData).
        unsafe { asn1_string_data(asn1) }
    }
}

// ── X509Extension — borrowed extension ───────────────────────────────────────

/// A borrowed extension within an [`X509`] certificate.
pub struct X509Extension<'cert> {
    ptr: *const sys::X509_EXTENSION,
    _owner: PhantomData<&'cert X509>,
}

impl std::fmt::Debug for X509Extension<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("X509Extension")
            .field("nid", &self.nid())
            .field("critical", &self.is_critical())
            .finish_non_exhaustive()
    }
}

impl X509Extension<'_> {
    /// NID of this extension (e.g. `NID_subject_key_identifier`).
    #[must_use]
    pub fn nid(&self) -> i32 {
        let obj = unsafe { sys::X509_EXTENSION_get_object(self.ptr.cast_mut()) };
        unsafe { sys::OBJ_obj2nid(obj) }
    }

    /// Returns `true` if this extension is marked critical.
    #[must_use]
    pub fn is_critical(&self) -> bool {
        unsafe { sys::X509_EXTENSION_get_critical(self.ptr) == 1 }
    }

    /// Raw DER-encoded value bytes.
    ///
    /// The slice is valid as long as the owning certificate is alive.
    #[must_use]
    pub fn data(&self) -> &[u8] {
        let asn1 = unsafe { sys::X509_EXTENSION_get_data(self.ptr.cast_mut()) };
        if asn1.is_null() {
            return &[];
        }
        // SAFETY: asn1 is valid for 'cert (guaranteed by self._owner PhantomData).
        // ASN1_OCTET_STRING is typedef'd to ASN1_STRING — cast is safe.
        unsafe { asn1_string_data(asn1.cast()) }
    }
}

// ── X509NameOwned — mutable name for the builder ─────────────────────────────

/// An owned, mutable distinguished name (`X509_NAME*`).
///
/// Pass to [`X509Builder::set_subject_name`] / [`X509Builder::set_issuer_name`].
pub struct X509NameOwned {
    ptr: *mut sys::X509_NAME,
}

impl std::fmt::Debug for X509NameOwned {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("X509NameOwned").finish_non_exhaustive()
    }
}

impl X509NameOwned {
    /// Create an empty distinguished name.
    ///
    /// # Errors
    pub fn new() -> Result<Self, ErrorStack> {
        let ptr = unsafe { sys::X509_NAME_new() };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(X509NameOwned { ptr })
    }

    /// Append a field entry by short name (e.g. `c"CN"`, `c"O"`, `c"C"`).
    ///
    /// `value` is the UTF-8 field value.
    ///
    /// # Panics
    ///
    /// # Errors
    ///
    /// Returns `Err` if the field cannot be added, or if `value.len()` exceeds `i32::MAX`.
    pub fn add_entry_by_txt(&mut self, field: &CStr, value: &[u8]) -> Result<(), ErrorStack> {
        let len = i32::try_from(value.len()).map_err(|_| ErrorStack::drain())?;
        // MBSTRING_UTF8 = 0x1000 → encode value as UTF-8.
        let rc = unsafe {
            sys::X509_NAME_add_entry_by_txt(
                self.ptr,
                field.as_ptr(),
                0x1000, // MBSTRING_UTF8
                value.as_ptr(),
                len,
                -1, // append
                0,
            )
        };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(())
    }
}

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

// ── X509Builder — certificate builder ────────────────────────────────────────

/// Builder for a new X.509 certificate.
///
/// ```ignore
/// let mut name = X509NameOwned::new()?;
/// name.add_entry_by_txt(c"CN", b"example.com")?;
///
/// let cert = X509Builder::new()?
///     .set_version(2)?                    // X.509v3
///     .set_serial_number(1)?
///     .set_not_before_offset(0)?          // valid from now
///     .set_not_after_offset(365 * 86400)? // valid for 1 year
///     .set_subject_name(&name)?
///     .set_issuer_name(&name)?            // self-signed
///     .set_public_key(&pub_key)?
///     .sign(&priv_key, None)?             // None → no digest (Ed25519)
///     .build();
/// ```
pub struct X509Builder {
    ptr: *mut sys::X509,
}

impl std::fmt::Debug for X509Builder {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("X509Builder").finish_non_exhaustive()
    }
}

impl X509Builder {
    /// Allocate a new, empty `X509` structure.
    ///
    /// # Errors
    pub fn new() -> Result<Self, ErrorStack> {
        let ptr = unsafe { sys::X509_new() };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(X509Builder { ptr })
    }

    /// Set the X.509 version (0 = v1, 1 = v2, 2 = v3).
    ///
    /// # Errors
    pub fn set_version(self, version: i64) -> Result<Self, ErrorStack> {
        crate::ossl_call!(sys::X509_set_version(self.ptr, version))?;
        Ok(self)
    }

    /// Set the serial number.
    ///
    /// # Errors
    pub fn set_serial_number(self, n: i64) -> Result<Self, ErrorStack> {
        let ai = unsafe { sys::ASN1_INTEGER_new() };
        if ai.is_null() {
            return Err(ErrorStack::drain());
        }
        crate::ossl_call!(sys::ASN1_INTEGER_set_int64(ai, n)).map_err(|e| {
            unsafe { sys::ASN1_INTEGER_free(ai) };
            e
        })?;
        let rc = unsafe { sys::X509_set_serialNumber(self.ptr, ai) };
        unsafe { sys::ASN1_INTEGER_free(ai) };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(self)
    }

    /// Set `notBefore` to `now + offset_secs`.
    ///
    /// # Errors
    pub fn set_not_before_offset(self, offset_secs: i64) -> Result<Self, ErrorStack> {
        let t = unsafe { sys::X509_getm_notBefore(self.ptr) };
        if unsafe { sys::X509_gmtime_adj(t, offset_secs) }.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(self)
    }

    /// Set `notAfter` to `now + offset_secs`.
    ///
    /// # Errors
    pub fn set_not_after_offset(self, offset_secs: i64) -> Result<Self, ErrorStack> {
        let t = unsafe { sys::X509_getm_notAfter(self.ptr) };
        if unsafe { sys::X509_gmtime_adj(t, offset_secs) }.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(self)
    }

    /// Set the subject distinguished name.
    ///
    /// # Errors
    pub fn set_subject_name(self, name: &X509NameOwned) -> Result<Self, ErrorStack> {
        crate::ossl_call!(sys::X509_set_subject_name(self.ptr, name.ptr))?;
        Ok(self)
    }

    /// Set the issuer distinguished name.
    ///
    /// # Errors
    pub fn set_issuer_name(self, name: &X509NameOwned) -> Result<Self, ErrorStack> {
        crate::ossl_call!(sys::X509_set_issuer_name(self.ptr, name.ptr))?;
        Ok(self)
    }

    /// Set the public key.
    ///
    /// # Errors
    pub fn set_public_key<T: crate::pkey::HasPublic>(
        self,
        key: &crate::pkey::Pkey<T>,
    ) -> Result<Self, ErrorStack> {
        crate::ossl_call!(sys::X509_set_pubkey(self.ptr, key.as_ptr()))?;
        Ok(self)
    }

    /// Sign the certificate.
    ///
    /// Pass `digest = None` for one-shot algorithms such as Ed25519.
    /// For ECDSA or RSA, pass the appropriate digest (e.g. SHA-256).
    ///
    /// # Errors
    pub fn sign(
        self,
        key: &crate::pkey::Pkey<crate::pkey::Private>,
        digest: Option<&crate::digest::DigestAlg>,
    ) -> Result<Self, ErrorStack> {
        let md_ptr = digest.map_or(std::ptr::null(), crate::digest::DigestAlg::as_ptr);
        // X509_sign returns the signature length (> 0) on success.
        let rc = unsafe { sys::X509_sign(self.ptr, key.as_ptr(), md_ptr) };
        if rc <= 0 {
            return Err(ErrorStack::drain());
        }
        Ok(self)
    }

    /// Finalise and return the certificate.
    #[must_use]
    pub fn build(self) -> X509 {
        let ptr = self.ptr;
        std::mem::forget(self);
        X509 { ptr }
    }
}

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

// ── X509Store — trust store ────────────────────────────────────────────────────

/// An OpenSSL certificate trust store (`X509_STORE*`).
///
/// Cloneable via `X509_STORE_up_ref`; wrapping in `Arc<X509Store>` is safe.
pub struct X509Store {
    ptr: *mut sys::X509_STORE,
}

// SAFETY: OpenSSL 3.0.7+ manages X509_STORE with an internal reference count
// protected by its own locks.  After construction all mutation methods take
// &mut self, so the borrow checker prevents concurrent mutation.  Concurrent
// reads via &X509Store (enabled by Sync) are safe because any internal cache
// updates performed by X509_verify_cert are serialised by OpenSSL's internal
// CRYPTO_RWLOCK; no unsynchronised data races can occur.
unsafe impl Send for X509Store {}
unsafe impl Sync for X509Store {}

impl Clone for X509Store {
    /// # Panics
    ///
    /// Panics if `X509_STORE_up_ref` fails (OpenSSL internal error; practically impossible).
    fn clone(&self) -> Self {
        // SAFETY: self.ptr is a valid, non-null X509_STORE*.
        let rc = unsafe { sys::X509_STORE_up_ref(self.ptr) };
        assert_eq!(rc, 1, "X509_STORE_up_ref failed");
        X509Store { ptr: self.ptr }
    }
}

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

impl X509Store {
    /// Create an empty trust store.
    ///
    /// # Errors
    pub fn new() -> Result<Self, ErrorStack> {
        let ptr = unsafe { sys::X509_STORE_new() };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(X509Store { ptr })
    }

    /// Add a trusted certificate to the store.
    ///
    /// The certificate's reference count is incremented internally.
    ///
    /// # Errors
    pub fn add_cert(&mut self, cert: &X509) -> Result<(), ErrorStack> {
        let rc = unsafe { sys::X509_STORE_add_cert(self.ptr, cert.ptr) };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(())
    }

    /// Add a CRL to the store.
    ///
    /// # Errors
    pub fn add_crl(&mut self, crl: &X509Crl) -> Result<(), ErrorStack> {
        let rc = unsafe { sys::X509_STORE_add_crl(self.ptr, crl.ptr) };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(())
    }

    /// Set verification flags (e.g. `X509_V_FLAG_CRL_CHECK`).
    ///
    /// # Errors
    pub fn set_flags(&mut self, flags: u64) -> Result<(), ErrorStack> {
        let rc = unsafe { sys::X509_STORE_set_flags(self.ptr, flags) };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(())
    }

    /// Return the raw `X509_STORE*` pointer.
    #[must_use]
    pub(crate) fn as_ptr(&self) -> *mut sys::X509_STORE {
        self.ptr
    }
}

// ── X509StoreCtx — verification context ──────────────────────────────────────

/// A chain-verification context (`X509_STORE_CTX*`).
///
/// Create with [`X509StoreCtx::new`], initialise with [`X509StoreCtx::init`] or
/// [`X509StoreCtx::init_with_chain`], then call [`X509StoreCtx::verify`].
///
// FIXME: `X509StoreCtx` stores raw pointers to the `X509Store` and `X509`
// passed to `init`/`init_with_chain` without binding their lifetimes through
// the type system.  This means the borrow checker cannot prevent use-after-free
// if those values are dropped while the context is still live.  The fix is to
// add a lifetime parameter (`X509StoreCtx<'a>`) so the struct cannot outlive
// its borrowed arguments — but that is a semver-breaking API change.
// Scheduled for the next semver-bump release.
pub struct X509StoreCtx {
    ptr: *mut sys::X509_STORE_CTX,
    /// Untrusted intermediate chain built by `init_with_chain`; freed on drop.
    untrusted: Option<*mut sys::stack_st_X509>,
}

impl Drop for X509StoreCtx {
    fn drop(&mut self) {
        // SAFETY: self.ptr is a valid X509_STORE_CTX*. X509_STORE_CTX_free
        // calls X509_STORE_CTX_cleanup internally (frees the verified chain and
        // param) but does NOT free the borrowed untrusted pointer — that is the
        // Rust side's responsibility and is freed below.
        unsafe { sys::X509_STORE_CTX_free(self.ptr) };
        if let Some(stack) = self.untrusted.take() {
            // SAFETY: stack was built by build_x509_stack_for_ctx and is
            // exclusively owned by self.  It is safe to free after
            // X509_STORE_CTX_free because OpenSSL only borrows the pointer.
            unsafe {
                sys::OPENSSL_sk_pop_free(stack.cast::<sys::OPENSSL_STACK>(), Some(free_x509_cb));
            }
        }
    }
}

// SAFETY: X509StoreCtx owns its X509_STORE_CTX* exclusively (the pointer is
// not reference-counted) and the optional untrusted STACK_OF(X509) is also
// exclusively owned.  All methods take &mut self, preventing shared concurrent
// access.  Sync is intentionally NOT implemented; Send-only permits transferring
// exclusive ownership to another thread without allowing simultaneous sharing.
unsafe impl Send for X509StoreCtx {}

impl X509StoreCtx {
    /// Free any previously-built untrusted chain and clear the ctx pointer.
    ///
    /// Called at the top of `init` and `init_with_chain` when re-initialising
    /// an already-used context so the old stack is not leaked.
    fn free_old_untrusted(&mut self) {
        if let Some(old_stack) = self.untrusted.take() {
            // SAFETY: old_stack was built by build_x509_stack_for_ctx and is
            // exclusively owned by self.  free_x509_cb decrements the ref-count
            // taken by X509_up_ref for each certificate in the stack.
            unsafe {
                sys::OPENSSL_sk_pop_free(
                    old_stack.cast::<sys::OPENSSL_STACK>(),
                    Some(free_x509_cb),
                );
            }
            // Clear ctx->untrusted immediately so the X509_STORE_CTX never
            // holds a dangling pointer, even if the caller returns before the
            // next X509_STORE_CTX_init call.
            // SAFETY: self.ptr is a valid X509_STORE_CTX*.
            unsafe { sys::X509_STORE_CTX_set0_untrusted(self.ptr, std::ptr::null_mut()) };
        }
    }

    /// Allocate a new, uninitialised verification context.
    ///
    /// # Errors
    pub fn new() -> Result<Self, ErrorStack> {
        let ptr = unsafe { sys::X509_STORE_CTX_new() };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(X509StoreCtx {
            ptr,
            untrusted: None,
        })
    }

    /// Initialise the context for verifying `cert` against `store`.
    ///
    /// Call this before [`Self::verify`].
    ///
    /// **Lifetime requirement:** `store` and `cert` must remain alive until this
    /// context is dropped or re-initialised.  OpenSSL stores their raw pointers
    /// internally and accesses them during [`Self::verify`].  The borrow checker
    /// cannot enforce this invariant; it is the caller's responsibility.
    ///
    /// # Errors
    pub fn init(&mut self, store: &X509Store, cert: &X509) -> Result<(), ErrorStack> {
        // Free any previously built untrusted chain so that re-initialising
        // without a chain does not hold stale intermediate references.
        self.free_old_untrusted();
        // SAFETY: self.ptr is a valid X509_STORE_CTX*, store.ptr is a valid
        // X509_STORE*, cert.ptr is a valid X509*.  X509_STORE_CTX_init does not
        // take ownership of any argument.
        let rc = unsafe {
            sys::X509_STORE_CTX_init(self.ptr, store.ptr, cert.ptr, std::ptr::null_mut())
        };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(())
    }

    /// Initialise the context for verifying `cert` against `store`, with
    /// `chain` supplied as untrusted intermediate certificates.
    ///
    /// OpenSSL keeps a reference to the untrusted chain until the context is
    /// freed; this method stores the chain internally and releases it on drop.
    ///
    /// **Lifetime requirement:** `store` and `cert` must remain alive until this
    /// context is dropped or re-initialised.  OpenSSL stores their raw pointers
    /// internally.  The borrow checker cannot enforce this invariant; it is the
    /// caller's responsibility.  The individual certificates in `chain` are
    /// ref-counted via `X509_up_ref` and do not need to outlive this call.
    ///
    /// # Errors
    pub fn init_with_chain(
        &mut self,
        store: &X509Store,
        cert: &X509,
        chain: &[X509],
    ) -> Result<(), ErrorStack> {
        // Free any previously built untrusted chain before re-initializing so
        // that double-calls do not leak the old STACK_OF(X509).
        self.free_old_untrusted();

        // When there are no intermediate certificates, pass NULL directly to
        // X509_STORE_CTX_init — X509_verify_cert accepts NULL for the untrusted
        // chain — avoiding an unnecessary OPENSSL_sk_new_null() allocation.
        if chain.is_empty() {
            // SAFETY: self.ptr, store.ptr, cert.ptr are valid non-null pointers.
            // NULL is a valid value for the untrusted chain argument.
            let rc = unsafe {
                sys::X509_STORE_CTX_init(self.ptr, store.ptr, cert.ptr, std::ptr::null_mut())
            };
            if rc != 1 {
                return Err(ErrorStack::drain());
            }
            // self.untrusted remains None — nothing to free in drop.
            return Ok(());
        }

        let stack = build_x509_stack_for_ctx(chain)?;
        // SAFETY: self.ptr is a valid X509_STORE_CTX*, store.ptr is a valid
        // X509_STORE*, cert.ptr is a valid X509*, stack is a valid
        // STACK_OF(X509)*.  X509_STORE_CTX_init does not take ownership of
        // any argument; the stack is kept alive in self.untrusted and freed in
        // Drop.
        let rc = unsafe { sys::X509_STORE_CTX_init(self.ptr, store.ptr, cert.ptr, stack) };
        if rc != 1 {
            // Free the stack we just built before returning the error.
            // SAFETY: same as above; stack was built moments ago and nothing
            // else holds a reference to it.
            unsafe {
                sys::OPENSSL_sk_pop_free(stack.cast::<sys::OPENSSL_STACK>(), Some(free_x509_cb));
            }
            return Err(ErrorStack::drain());
        }
        self.untrusted = Some(stack);
        Ok(())
    }

    /// Set the reference time for certificate validity checks.
    ///
    /// Pinning the verification time to a known value makes validation
    /// deterministic and independent of the system clock.  Must be called
    /// after [`Self::init`] or [`Self::init_with_chain`].
    ///
    /// Returns `Err` if called before the context is initialised.
    ///
    /// # Errors
    pub fn set_time(&mut self, time_secs: u64) -> Result<(), ErrorStack> {
        // SAFETY: X509_STORE_CTX_get0_param returns a borrowed pointer valid
        // for the lifetime of self — do not free.
        let param = unsafe { sys::X509_STORE_CTX_get0_param(self.ptr) };
        if param.is_null() {
            return Err(ErrorStack::drain());
        }
        // Convert u64 → time_t safely.  X.509 certificates never carry
        // pre-1970 dates, so the unsigned input is intentional (same trade-off
        // Kerberos makes with its 32-bit timestamps).  On 64-bit targets
        // (time_t = i64) all practical timestamps fit; on 32-bit targets
        // (time_t = i32) values beyond 2038 are rejected rather than silently
        // wrapped to negative.
        let t: sys::time_t = time_secs.try_into().map_err(|_| ErrorStack::drain())?;
        // SAFETY: param is non-null and valid for the lifetime of self.
        // X509_VERIFY_PARAM_set_time is a void function.
        unsafe { sys::X509_VERIFY_PARAM_set_time(param, t) };
        Ok(())
    }

    /// Set the expected DNS hostname for OpenSSL's built-in SAN/CN hostname check.
    ///
    /// Must be called after [`Self::init`] or [`Self::init_with_chain`].
    ///
    /// Returns `Err` if called before the context is initialised.
    ///
    /// # Errors
    pub fn set_host(&mut self, hostname: &str) -> Result<(), ErrorStack> {
        let cstr = std::ffi::CString::new(hostname).map_err(|_| ErrorStack::drain())?;
        // SAFETY: X509_STORE_CTX_get0_param returns a borrowed pointer — do not free.
        let param = unsafe { sys::X509_STORE_CTX_get0_param(self.ptr) };
        if param.is_null() {
            return Err(ErrorStack::drain());
        }
        // SAFETY: param is non-null (checked above); cstr is a valid
        // NUL-terminated string for the duration of this call.
        // X509_VERIFY_PARAM_set1_host copies the hostname, so cstr need not
        // outlive the call.
        let rc = unsafe { sys::X509_VERIFY_PARAM_set1_host(param, cstr.as_ptr(), 0) };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(())
    }

    /// Set the expected IP address for OpenSSL's built-in `SubjectAltName` IP check.
    ///
    /// `ip` must be a printable IPv4 or IPv6 address string (e.g. `"192.0.2.1"`
    /// or `"2001:db8::1"`).  Must be called after [`Self::init`] or
    /// [`Self::init_with_chain`].
    ///
    /// Returns `Err` if called before the context is initialised.
    ///
    /// # Errors
    pub fn set_ip(&mut self, ip: &str) -> Result<(), ErrorStack> {
        let cstr = std::ffi::CString::new(ip).map_err(|_| ErrorStack::drain())?;
        // SAFETY: X509_STORE_CTX_get0_param returns a borrowed pointer — do not free.
        let param = unsafe { sys::X509_STORE_CTX_get0_param(self.ptr) };
        if param.is_null() {
            return Err(ErrorStack::drain());
        }
        // SAFETY: param is non-null (checked above); cstr is a valid
        // NUL-terminated string for the duration of this call.
        // X509_VERIFY_PARAM_set1_ip_asc copies the IP string, so cstr need not
        // outlive the call.
        let rc = unsafe { sys::X509_VERIFY_PARAM_set1_ip_asc(param, cstr.as_ptr()) };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(())
    }

    /// Verify the certificate chain.
    ///
    /// Must be called after [`Self::init`] or [`Self::init_with_chain`].
    ///
    /// Returns `Ok(true)` if the chain is valid, `Ok(false)` if not (call
    /// [`Self::error`] to retrieve the error code), or `Err` on a fatal error.
    ///
    /// If called before `init` / `init_with_chain`, OpenSSL sets
    /// `X509_V_ERR_INVALID_CALL` and this method returns `Err`.
    ///
    /// # Errors
    ///
    /// Returns an [`ErrorStack`] on a fatal OpenSSL error (distinct from
    /// a verification failure, which is indicated by `Ok(false)`).
    /// Also returns `Err` when the context has not been initialised.
    pub fn verify(&mut self) -> Result<bool, ErrorStack> {
        match unsafe { sys::X509_verify_cert(self.ptr) } {
            1 => Ok(true),
            0 => Ok(false),
            _ => Err(ErrorStack::drain()),
        }
    }

    /// OpenSSL verification error code after a failed [`Self::verify`].
    ///
    /// Returns 0 (`X509_V_OK`) if no error occurred.  See `<openssl/x509_vfy.h>`
    /// for the full list of `X509_V_ERR_*` constants.
    #[must_use]
    pub fn error(&self) -> i32 {
        unsafe { sys::X509_STORE_CTX_get_error(self.ptr) }
    }

    /// Collect the verified chain into a `Vec<X509>`.
    ///
    /// Only meaningful after a successful [`Self::verify`].  Returns an empty
    /// `Vec` if the chain is not available.
    ///
    /// # Panics
    ///
    /// Panics if `X509_up_ref` fails (OpenSSL internal error; practically
    /// impossible in normal operation).
    #[must_use]
    // i < n where n came from OPENSSL_sk_num (i32), so the i as i32 cast is safe.
    #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
    pub fn chain(&self) -> Vec<X509> {
        let stack = unsafe { sys::X509_STORE_CTX_get0_chain(self.ptr) };
        if stack.is_null() {
            return Vec::new();
        }
        let n = unsafe { sys::OPENSSL_sk_num(stack.cast::<sys::OPENSSL_STACK>()) };
        let n = usize::try_from(n).unwrap_or(0);
        let mut out = Vec::with_capacity(n);
        for i in 0..n {
            let raw =
                unsafe { sys::OPENSSL_sk_value(stack.cast::<sys::OPENSSL_STACK>(), i as i32) };
            if raw.is_null() {
                continue;
            }
            // up_ref so each X509 in the Vec has its own reference.
            let cert_ptr = raw.cast::<sys::X509>();
            // SAFETY: cert_ptr is a valid non-null X509* from the verified chain.
            let up_rc = unsafe { sys::X509_up_ref(cert_ptr) };
            assert_eq!(up_rc, 1, "X509_up_ref failed: OpenSSL internal error");
            out.push(X509 { ptr: cert_ptr });
        }
        out
    }
}

// ── X509Crl — certificate revocation list ─────────────────────────────────────

/// An X.509 certificate revocation list (`X509_CRL*`).
///
/// Cloneable via `X509_CRL_up_ref`.
pub struct X509Crl {
    ptr: *mut sys::X509_CRL,
}

// SAFETY: `X509_CRL*` is reference-counted by OpenSSL; all methods take `&self`
// or `&mut self`, providing the necessary mutual exclusion.
unsafe impl Send for X509Crl {}
unsafe impl Sync for X509Crl {}

impl Clone for X509Crl {
    /// # Panics
    ///
    /// Panics if `X509_CRL_up_ref` fails (OpenSSL internal error; practically impossible).
    fn clone(&self) -> Self {
        // SAFETY: self.ptr is a valid, non-null X509_CRL*.
        let rc = unsafe { sys::X509_CRL_up_ref(self.ptr) };
        assert_eq!(rc, 1, "X509_CRL_up_ref failed");
        X509Crl { ptr: self.ptr }
    }
}

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

impl X509Crl {
    /// Construct from a raw, owned `X509_CRL*`.
    ///
    /// # Safety
    ///
    /// `ptr` must be a valid, non-null `X509_CRL*` whose ownership is transferred.
    pub(crate) unsafe fn from_ptr(ptr: *mut sys::X509_CRL) -> Self {
        X509Crl { ptr }
    }

    /// Allocate a new, empty `X509_CRL` structure.
    ///
    /// The returned CRL has no fields set. Use this when constructing a CRL
    /// programmatically before populating its fields via the OpenSSL API.
    ///
    /// # Errors
    ///
    /// Returns `Err` if OpenSSL cannot allocate the structure.
    pub fn new() -> Result<Self, ErrorStack> {
        // SAFETY:
        // - X509_CRL_new() takes no arguments and returns a new heap allocation
        // - the returned pointer is non-null on success; null on allocation failure
        // - ownership is fully transferred to this X509Crl value
        let ptr = unsafe { sys::X509_CRL_new() };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(unsafe { Self::from_ptr(ptr) })
    }

    /// Allocate a new, empty `X509_CRL` structure bound to a library context.
    ///
    /// Equivalent to `X509_CRL_new_ex(ctx, NULL)`. The CRL will use providers
    /// from `ctx` for any cryptographic operations performed on it.
    ///
    /// # Errors
    ///
    /// Returns `Err` if OpenSSL cannot allocate the structure.
    pub fn new_in(ctx: &std::sync::Arc<crate::lib_ctx::LibCtx>) -> Result<Self, ErrorStack> {
        // SAFETY:
        // - ctx.as_ptr() is non-null (LibCtx invariant)
        // - propq is NULL, which is valid (no property query)
        // - the returned pointer is non-null on success; null on allocation failure
        // - ownership is fully transferred to this X509Crl value
        // - ctx borrow covers the duration of this call; no aliasing of the OSSL_LIB_CTX*
        let ptr = unsafe { sys::X509_CRL_new_ex(ctx.as_ptr(), std::ptr::null()) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(unsafe { Self::from_ptr(ptr) })
    }

    /// Load a CRL from PEM bytes.
    ///
    /// # Errors
    pub fn from_pem(pem: &[u8]) -> Result<Self, ErrorStack> {
        let bio = MemBioBuf::new(pem)?;
        let ptr = unsafe {
            sys::PEM_read_bio_X509_CRL(
                bio.as_ptr(),
                std::ptr::null_mut(),
                None,
                std::ptr::null_mut(),
            )
        };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(unsafe { Self::from_ptr(ptr) })
    }

    /// Load a CRL from DER bytes.
    ///
    /// # Errors
    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack> {
        let bio = MemBioBuf::new(der)?;
        let ptr = unsafe { sys::d2i_X509_CRL_bio(bio.as_ptr(), std::ptr::null_mut()) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(unsafe { Self::from_ptr(ptr) })
    }

    /// Serialise the CRL to PEM.
    ///
    /// # Errors
    pub fn to_pem(&self) -> Result<Vec<u8>, ErrorStack> {
        let mut bio = MemBio::new()?;
        let rc = unsafe { sys::PEM_write_bio_X509_CRL(bio.as_ptr(), self.ptr) };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(bio.into_vec())
    }

    /// Serialise the CRL to DER.
    ///
    /// # Errors
    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack> {
        let mut bio = MemBio::new()?;
        let rc = unsafe { sys::i2d_X509_CRL_bio(bio.as_ptr(), self.ptr) };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        Ok(bio.into_vec())
    }

    /// Issuer distinguished name (borrowed).
    #[must_use]
    pub fn issuer_name(&self) -> X509Name<'_> {
        let ptr = unsafe { sys::X509_CRL_get_issuer(self.ptr) };
        X509Name {
            ptr,
            _owner: PhantomData,
        }
    }

    /// `thisUpdate` field as a human-readable string.
    #[must_use]
    pub fn last_update_str(&self) -> Option<String> {
        let t = unsafe { sys::X509_CRL_get0_lastUpdate(self.ptr) };
        asn1_time_to_str(t)
    }

    /// `nextUpdate` field as a human-readable string.
    #[must_use]
    pub fn next_update_str(&self) -> Option<String> {
        let t = unsafe { sys::X509_CRL_get0_nextUpdate(self.ptr) };
        asn1_time_to_str(t)
    }

    /// Verify this CRL was signed by `key`.
    ///
    /// Returns `Ok(true)` if valid, `Ok(false)` if not.
    ///
    /// # Errors
    pub fn verify(&self, key: &crate::pkey::Pkey<crate::pkey::Public>) -> Result<bool, ErrorStack> {
        match unsafe { sys::X509_CRL_verify(self.ptr, key.as_ptr()) } {
            1 => Ok(true),
            0 => Ok(false),
            _ => Err(ErrorStack::drain()),
        }
    }

    /// Raw `X509_CRL*` pointer for use by internal APIs.
    #[must_use]
    #[allow(dead_code)]
    pub(crate) fn as_ptr(&self) -> *mut sys::X509_CRL {
        self.ptr
    }
}

// ── NID ↔ name free functions ────────────────────────────────────────────────

/// Look up a NID by its short name (e.g. `c"sha256"`, `c"rsaEncryption"`).
///
/// Returns `None` if the name is not in OpenSSL's object table.
#[must_use]
pub fn nid_from_short_name(sn: &CStr) -> Option<i32> {
    // SAFETY:
    // - sn.as_ptr() is valid, non-null, and NUL-terminated for the duration of
    //   this call (CStr invariant)
    // - OBJ_sn2nid performs a read-only table lookup; it does not retain the pointer
    // - no mutable global state is accessed
    let nid = unsafe { sys::OBJ_sn2nid(sn.as_ptr()) };
    if nid == 0 {
        None
    } else {
        Some(nid)
    }
}

/// Look up a NID by OID text or short name.
///
/// Accepts dotted decimal (e.g. `c"2.16.840.1.101.3.4.2.1"`) or a short name
/// (e.g. `c"sha256"`).
///
/// Returns `None` if the string is not recognised by OpenSSL.
#[must_use]
pub fn nid_from_text(s: &CStr) -> Option<i32> {
    // SAFETY:
    // - s.as_ptr() is valid, non-null, and NUL-terminated for the duration of
    //   this call (CStr invariant)
    // - OBJ_txt2nid performs a read-only table lookup; it does not retain the pointer
    // - no mutable global state is accessed
    let nid = unsafe { sys::OBJ_txt2nid(s.as_ptr()) };
    if nid == 0 {
        None
    } else {
        Some(nid)
    }
}

/// Look up the short name for a NID (e.g. `13` → `"CN"`, `672` → `"SHA256"`).
///
/// Returns `None` if the NID is not in OpenSSL's object table.
///
/// The returned string points to OpenSSL's static object table; its lifetime
/// is `'static` and no allocation is performed.
///
/// Use this together with [`crate::pkey::Pkey::is_a`] to perform provider-aware key-type
/// comparison: `pkey.is_a(nid_to_short_name(pknid)?)`.
#[must_use]
pub fn nid_to_short_name(nid: i32) -> Option<&'static CStr> {
    // SAFETY:
    // - nid is a plain integer; no pointer arguments
    // - OBJ_nid2sn returns a pointer into OpenSSL's static OBJ table, valid
    //   for the lifetime of the process ('static); it never allocates
    // - no mutable state is accessed; this is a read-only table lookup
    let ptr = unsafe { sys::OBJ_nid2sn(nid) };
    if ptr.is_null() {
        return None;
    }
    // SAFETY: ptr is non-null (checked above) and points to a NUL-terminated
    // string in OpenSSL's static object table
    Some(unsafe { CStr::from_ptr(ptr) })
}

/// Look up the long name for a NID (e.g. `13` → `"commonName"`).
///
/// Returns `None` if the NID is not in OpenSSL's object table.
///
/// The returned string points to OpenSSL's static object table; its lifetime
/// is `'static` and no allocation is performed.
#[must_use]
pub fn nid_to_long_name(nid: i32) -> Option<&'static CStr> {
    // SAFETY: same as nid_to_short_name — plain integer argument, result
    // points to static storage in OpenSSL's OBJ table
    let ptr = unsafe { sys::OBJ_nid2ln(nid) };
    if ptr.is_null() {
        return None;
    }
    // SAFETY: ptr is non-null (checked above) and points to a NUL-terminated
    // string in OpenSSL's static object table
    Some(unsafe { CStr::from_ptr(ptr) })
}

// ── Private helpers ───────────────────────────────────────────────────────────

/// Convert an `ASN1_TIME*` to a [`BrokenDownTime`] via `ASN1_TIME_to_tm`.
fn asn1_time_to_broken_down(t: *const sys::ASN1_TIME) -> Option<BrokenDownTime> {
    if t.is_null() {
        return None;
    }
    // SAFETY:
    // - t is non-null (checked above) and valid for the duration of this call
    // - tm is zero-initialised; ASN1_TIME_to_tm fills all relevant fields
    // - no mutable aliasing: t comes from a &self borrow at call sites
    let mut tm = unsafe { std::mem::zeroed::<sys::tm>() };
    let rc = unsafe { sys::ASN1_TIME_to_tm(t, &raw mut tm) };
    if rc != 1 {
        return None;
    }
    Some(BrokenDownTime {
        year: tm.tm_year + 1900,
        month: u8::try_from(tm.tm_mon + 1).unwrap_or(0),
        day: u8::try_from(tm.tm_mday).unwrap_or(0),
        hour: u8::try_from(tm.tm_hour).unwrap_or(0),
        minute: u8::try_from(tm.tm_min).unwrap_or(0),
        second: u8::try_from(tm.tm_sec).unwrap_or(0),
    })
}

/// Convert an `ASN1_TIME*` to a human-readable string via `ASN1_TIME_print`.
fn asn1_time_to_str(t: *const sys::ASN1_TIME) -> Option<String> {
    if t.is_null() {
        return None;
    }
    let mut bio = MemBio::new().ok()?;
    let rc = unsafe { sys::ASN1_TIME_print(bio.as_ptr(), t) };
    if rc != 1 {
        return None;
    }
    String::from_utf8(bio.into_vec()).ok()
}

/// Free callback for `OPENSSL_sk_pop_free` when elements are `ASN1_OBJECT*`.
///
/// # Safety
///
/// `p` must be a valid, non-null `ASN1_OBJECT*`.  Intended for use only as
/// a callback to `OPENSSL_sk_pop_free`; do not call directly.
unsafe extern "C" fn free_asn1_object(p: *mut std::os::raw::c_void) {
    // SAFETY: p is a valid ASN1_OBJECT* per the OPENSSL_sk_pop_free contract.
    unsafe { sys::ASN1_OBJECT_free(p.cast()) }
}

/// Free callback for `OPENSSL_sk_pop_free` when elements are `GENERAL_NAME*`.
///
/// # Safety
///
/// `p` must be a valid, non-null `GENERAL_NAME*`.  Intended for use only as
/// a callback to `OPENSSL_sk_pop_free`; do not call directly.
unsafe extern "C" fn free_general_name(p: *mut std::os::raw::c_void) {
    // SAFETY: p is a valid GENERAL_NAME* per the OPENSSL_sk_pop_free contract.
    unsafe { sys::GENERAL_NAME_free(p.cast()) }
}

/// Free callback for `OPENSSL_sk_pop_free` when elements are `X509*`.
///
/// # Safety
///
/// `p` must be a valid, non-null `X509*` whose reference count was incremented
/// by `X509_up_ref` before being pushed to the stack.  Intended for use only
/// as a callback to `OPENSSL_sk_pop_free`; do not call directly.
unsafe extern "C" fn free_x509_cb(p: *mut std::os::raw::c_void) {
    // SAFETY: p is a valid X509* per the OPENSSL_sk_pop_free contract.
    unsafe { sys::X509_free(p.cast()) }
}

/// Build a `STACK_OF(X509)` from a slice of [`X509`] for use as an untrusted
/// intermediate chain in [`X509StoreCtx::init_with_chain`].
///
/// Each certificate's reference count is incremented so the stack holds an
/// independent reference.  The caller must free the stack with
/// `OPENSSL_sk_pop_free(..., free_x509_cb)` when done.
fn build_x509_stack_for_ctx(certs: &[X509]) -> Result<*mut sys::stack_st_X509, ErrorStack> {
    // SAFETY: OPENSSL_sk_new_null allocates an empty stack; the returned
    // pointer is null only on allocation failure.
    let raw = unsafe { sys::OPENSSL_sk_new_null() };
    if raw.is_null() {
        return Err(ErrorStack::drain());
    }
    for cert in certs {
        // SAFETY: cert.ptr is a valid X509*; X509_up_ref increments the
        // reference count so the stack holds an independent reference.
        let up_rc = unsafe { sys::X509_up_ref(cert.ptr) };
        if up_rc != 1 {
            // Refcount overflow — practically impossible, but must not be
            // silently ignored in unsafe code.
            // SAFETY: raw is non-null; free_x509_cb calls X509_free on each
            // element, undoing the X509_up_ref taken for each successfully
            // pushed cert.  OPENSSL_sk_pop_free frees the stack itself.
            unsafe { sys::OPENSSL_sk_pop_free(raw, Some(free_x509_cb)) };
            return Err(ErrorStack::drain());
        }
        // SAFETY: raw is a valid OPENSSL_STACK*, cert.ptr is a valid X509*.
        let rc = unsafe { sys::OPENSSL_sk_push(raw, cert.ptr.cast::<std::ffi::c_void>()) };
        if rc == 0 {
            // Push failed. OPENSSL_sk_push is atomic — either the pointer is
            // added or the stack is left unchanged — so cert.ptr was NEVER
            // inserted.  X509_free undoes only the up-ref taken immediately
            // above; we must NOT call X509_free again for entries already in
            // the stack (OPENSSL_sk_pop_free handles those).
            // SAFETY: cert.ptr is valid; the up-ref taken above is the only
            // extra reference, so X509_free decrements it back to baseline.
            unsafe { sys::X509_free(cert.ptr) };
            // SAFETY: raw is non-null; free_x509_cb calls X509_free on each
            // element, undoing the X509_up_ref taken for each successfully
            // pushed cert.  OPENSSL_sk_pop_free frees the stack itself.
            unsafe { sys::OPENSSL_sk_pop_free(raw, Some(free_x509_cb)) };
            return Err(ErrorStack::drain());
        }
    }
    Ok(raw.cast::<sys::stack_st_X509>())
}

/// Serialize an `ASN1_TYPE*` to a DER byte vector.
/// Returns an empty `Vec` if `typ` is null or serialization fails.
unsafe fn i2d_asn1_type_to_vec(typ: *mut sys::ASN1_TYPE) -> Vec<u8> {
    if typ.is_null() {
        return Vec::new();
    }
    let len = unsafe { sys::i2d_ASN1_TYPE(typ, std::ptr::null_mut()) };
    let Ok(len_usize) = usize::try_from(len) else {
        log::warn!(
            "i2d_asn1_type_to_vec: i2d_ASN1_TYPE length query failed (rc={len}); \
             OtherName value will be empty",
        );
        let _ = ErrorStack::drain();
        return Vec::new();
    };
    let mut buf = vec![0u8; len_usize];
    let mut ptr = buf.as_mut_ptr();
    let written = unsafe { sys::i2d_ASN1_TYPE(typ, std::ptr::addr_of_mut!(ptr)) };
    let Ok(written_usize) = usize::try_from(written) else {
        log::warn!(
            "i2d_asn1_type_to_vec: i2d_ASN1_TYPE encode failed (rc={written}); \
             OtherName value will be empty",
        );
        let _ = ErrorStack::drain();
        return Vec::new();
    };
    buf.truncate(written_usize);
    buf
}

/// Extract the raw data bytes from an `ASN1_STRING*`.
///
/// # Safety
///
/// `asn1` must be a valid, non-null pointer for at least the duration of
/// lifetime `'a`.  The caller is responsible for ensuring the returned slice
/// does not outlive the owning ASN1 object.  Call sites bind the true lifetime
/// through their own `&self` borrow and `PhantomData` fields.
unsafe fn asn1_string_data<'a>(asn1: *const sys::ASN1_STRING) -> &'a [u8] {
    let raw_len = sys::ASN1_STRING_length(asn1);
    // A negative length indicates a malformed or uninitialised ASN1_STRING.
    // Log at warn level so this is visible in release builds too, not only
    // when debug_assert fires.
    if raw_len < 0 {
        log::warn!(
            "asn1_string_data: ASN1_STRING_length returned {raw_len}; returning empty slice"
        );
    }
    let len = usize::try_from(raw_len).unwrap_or(0);
    let ptr = sys::ASN1_STRING_get0_data(asn1);
    if ptr.is_null() || len == 0 {
        return &[];
    }
    // SAFETY: ptr is valid for `len` bytes; lifetime 'a is upheld by the caller.
    std::slice::from_raw_parts(ptr, len)
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::pkey::{KeygenCtx, Pkey, Private, Public};

    /// Build a self-signed Ed25519 certificate and run all read operations.
    fn make_self_signed() -> (X509, Pkey<Private>, Pkey<Public>) {
        let mut kgen = KeygenCtx::new(c"ED25519").unwrap();
        let priv_key = kgen.generate().unwrap();
        let pub_key = Pkey::<Public>::from(priv_key.clone());

        let mut name = X509NameOwned::new().unwrap();
        name.add_entry_by_txt(c"CN", b"Test Cert").unwrap();
        name.add_entry_by_txt(c"O", b"Example Org").unwrap();

        let cert = X509Builder::new()
            .unwrap()
            .set_version(2)
            .unwrap()
            .set_serial_number(1)
            .unwrap()
            .set_not_before_offset(0)
            .unwrap()
            .set_not_after_offset(365 * 86400)
            .unwrap()
            .set_subject_name(&name)
            .unwrap()
            .set_issuer_name(&name)
            .unwrap()
            .set_public_key(&pub_key)
            .unwrap()
            .sign(&priv_key, None)
            .unwrap()
            .build();

        (cert, priv_key, pub_key)
    }

    #[test]
    fn build_and_verify_self_signed() {
        let (cert, _, pub_key) = make_self_signed();
        assert!(cert.verify(&pub_key).unwrap());
        assert!(cert.is_self_signed());
    }

    #[test]
    fn pem_round_trip() {
        let (cert, _, _) = make_self_signed();
        let pem = cert.to_pem().unwrap();
        assert!(pem.starts_with(b"-----BEGIN CERTIFICATE-----"));

        let cert2 = X509::from_pem(&pem).unwrap();
        // Both should verify with the same key.
        assert_eq!(cert.to_der().unwrap(), cert2.to_der().unwrap());
    }

    #[test]
    fn der_round_trip() {
        let (cert, _, _) = make_self_signed();
        let der = cert.to_der().unwrap();
        assert!(!der.is_empty());

        let cert2 = X509::from_der(&der).unwrap();
        assert_eq!(cert2.to_der().unwrap(), der);
    }

    #[test]
    fn subject_name_entries() {
        let (cert, _, _) = make_self_signed();
        let name = cert.subject_name();

        assert_eq!(name.entry_count(), 2);

        // First entry: CN (NID 13)
        let e0 = name.entry(0).unwrap();
        assert_eq!(e0.nid(), 13); // NID_commonName
        assert!(!e0.data().is_empty());

        // to_string should include both components.
        let s = name.to_string().unwrap();
        assert!(s.contains("Test Cert") || s.contains("CN=Test Cert"));
    }

    #[test]
    fn serial_number() {
        let (cert, _, _) = make_self_signed();
        assert_eq!(cert.serial_number(), Some(1));
    }

    #[test]
    fn validity_strings_present() {
        let (cert, _, _) = make_self_signed();
        let nb = cert.not_before_str().unwrap();
        let na = cert.not_after_str().unwrap();
        // Both should contain "GMT" as OpenSSL uses UTC.
        assert!(nb.contains("GMT"), "not_before_str = {nb:?}");
        assert!(na.contains("GMT"), "not_after_str  = {na:?}");
    }

    #[test]
    fn is_valid_now() {
        let (cert, _, _) = make_self_signed();
        assert!(cert.is_valid_now());
    }

    #[test]
    fn public_key_extraction() {
        let (cert, _, pub_key) = make_self_signed();
        let extracted = cert.public_key().unwrap();
        // Both keys should verify the same signature.
        assert!(extracted.is_a(c"ED25519"));
        assert_eq!(pub_key.bits(), extracted.bits());
    }

    #[test]
    fn clone_cert() {
        let (cert, _, pub_key) = make_self_signed();
        let cert2 = cert.clone();
        // Both references should share the same content.
        assert_eq!(cert.to_der().unwrap(), cert2.to_der().unwrap());
        assert!(cert2.verify(&pub_key).unwrap());
    }

    #[test]
    fn verify_fails_with_wrong_key() {
        let (cert, _, _) = make_self_signed();
        // Generate a fresh, unrelated key pair.
        let mut kgen = KeygenCtx::new(c"ED25519").unwrap();
        let other_priv = kgen.generate().unwrap();
        let other_pub = Pkey::<Public>::from(other_priv);

        // cert was not signed by other_pub → should return Ok(false).
        assert!(!cert.verify(&other_pub).unwrap());
    }

    // ── X509Store / X509StoreCtx tests ──────────────────────────────────────

    #[test]
    fn x509_store_add_cert_and_verify() {
        let (cert, _, _) = make_self_signed();

        let mut store = X509Store::new().unwrap();
        store.add_cert(&cert).unwrap();

        let mut ctx = X509StoreCtx::new().unwrap();
        ctx.init(&store, &cert).unwrap();
        // Self-signed cert trusted by its own store → should verify.
        assert!(ctx.verify().unwrap());
    }

    #[test]
    fn x509_store_verify_untrusted_fails() {
        let (cert, _, _) = make_self_signed();
        // Empty store (nothing trusted).
        let store = X509Store::new().unwrap();

        let mut ctx = X509StoreCtx::new().unwrap();
        ctx.init(&store, &cert).unwrap();
        assert!(!ctx.verify().unwrap());
        // Error code must be non-zero.
        assert_ne!(ctx.error(), 0);
    }

    #[test]
    fn x509_store_ctx_chain_populated_after_verify() {
        let (cert, _, _) = make_self_signed();
        let mut store = X509Store::new().unwrap();
        store.add_cert(&cert).unwrap();

        let mut ctx = X509StoreCtx::new().unwrap();
        ctx.init(&store, &cert).unwrap();
        assert!(ctx.verify().unwrap());

        let chain = ctx.chain();
        assert!(
            !chain.is_empty(),
            "verified chain should contain at least the leaf"
        );
    }

    // ── X509Crl tests ────────────────────────────────────────────────────────

    #[test]
    fn x509crl_new_roundtrip() {
        // Allocate an empty CRL and verify it can be DER-serialised without panic.
        // (The DER of an empty/incomplete CRL may fail to encode a valid structure,
        // so we only assert that the call itself does not panic or crash.)
        let crl = X509Crl::new().expect("X509_CRL_new should succeed");
        // The CRL has no fields set; to_der may return Err (invalid ASN.1 state),
        // but must not panic or abort.
        let _result = crl.to_der();
        drop(crl);
    }

    #[test]
    fn x509crl_new_in_roundtrip() {
        use std::sync::Arc;
        let ctx = Arc::new(crate::lib_ctx::LibCtx::new().expect("LibCtx::new should succeed"));
        let crl = X509Crl::new_in(&ctx).expect("X509_CRL_new_ex should succeed");
        let _result = crl.to_der();
        drop(crl);
    }

    // A minimal CRL signed by an RSA/SHA-256 CA (generated via the openssl CLI).
    const TEST_CRL_PEM: &[u8] = b"\
-----BEGIN X509 CRL-----\n\
MIIBVjBAMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMMBlJTQSBDQRcNMjYwNDE1\n\
MTUwNDEzWhcNMjYwNTE1MTUwNDEzWjANBgkqhkiG9w0BAQsFAAOCAQEAi209u0hh\n\
Vz42YaqLplQwBoYCjtjETenl4xXRNcFOYU6Y+FmR66XNGkl9HbPClrz3hRMnbBYr\n\
OQJfWQOKS9lS0zpEI4qtlH/H1JBNGwiY32HMqf5HULn0w0ARvmoXR4NzsCecK22G\n\
gN61k5FCCpPY8HztsuoHMHAQ65W1WfBiTWu8ZH0nCCU0CA4MSaPZUiNt8/mJZzTG\n\
UwTGcZ/hcHQMpocBX40nE7ta5opcIpjG+q2uiCWhXwoqmYsLvdJ+Obw20bLirMHt\n\
UsmESTw5G+vcRCudoiSw89Z/jzsYq8yuFhRzF9kA/RtqCoQ+ylQSSH5hxzW2+bPd\n\
QPHivSGDiUhH6Q==\n\
-----END X509 CRL-----\n";

    #[test]
    fn crl_pem_round_trip() {
        let crl = X509Crl::from_pem(TEST_CRL_PEM).unwrap();
        // issuer_name should be non-empty (RSA CA)
        let issuer = crl.issuer_name();
        assert!(issuer.entry_count() > 0);
        // last_update and next_update are present.
        assert!(crl.last_update_str().is_some());
        assert!(crl.next_update_str().is_some());
        // to_pem produces a valid CRL PEM.
        let pem = crl.to_pem().unwrap();
        assert!(pem.starts_with(b"-----BEGIN X509 CRL-----"));
    }

    #[test]
    fn crl_der_round_trip() {
        let crl = X509Crl::from_pem(TEST_CRL_PEM).unwrap();
        let der = crl.to_der().unwrap();
        assert!(!der.is_empty());
        let crl2 = X509Crl::from_der(&der).unwrap();
        assert_eq!(crl2.to_der().unwrap(), der);
    }

    #[test]
    fn crl_clone() {
        let crl = X509Crl::from_pem(TEST_CRL_PEM).unwrap();
        let crl2 = crl.clone();
        assert_eq!(crl.to_der().unwrap(), crl2.to_der().unwrap());
    }

    // ── nid_from_short_name / nid_from_text tests ────────────────────────────

    #[test]
    fn x509_nid_from_short_name_known() {
        // OBJ_sn2nid is case-sensitive; "SHA256" is the registered short name,
        // not "sha256" (lowercase).  CN has stable NID 13 across all versions.
        let nid = nid_from_short_name(c"SHA256");
        assert!(nid.is_some(), "SHA256 should be a known short name");
        assert_eq!(nid_from_short_name(c"CN"), Some(13));
    }

    #[test]
    fn x509_nid_from_short_name_unknown() {
        assert_eq!(nid_from_short_name(c"not-a-real-algorithm-xyz"), None);
    }

    #[test]
    fn x509_nid_from_text_dotted_oid() {
        // 2.5.4.3 is commonName — NID 13.
        assert_eq!(nid_from_text(c"2.5.4.3"), Some(13));
    }

    #[test]
    fn x509_nid_from_text_short_name() {
        // OBJ_txt2nid accepts short names; use uppercase which OBJ_sn2nid also
        // recognises (OBJ_sn2nid is case-sensitive, lowercase "sha256" has no entry).
        let via_sn = nid_from_short_name(c"SHA256");
        let via_txt = nid_from_text(c"SHA256");
        assert_eq!(
            via_sn, via_txt,
            "short-name lookup must agree between OBJ_sn2nid and OBJ_txt2nid"
        );
    }

    #[test]
    fn x509_nid_from_text_unknown() {
        assert_eq!(nid_from_text(c"9.9.9.9.9.9.9.9"), None);
    }

    // ── serial_number_bytes tests ────────────────────────────────────────────

    #[test]
    fn x509_serial_number_bytes_small_serial() {
        let (cert, _, _) = make_self_signed();
        // set_serial_number(1) → bytes should be [0x01].
        let bytes = cert.serial_number_bytes().unwrap();
        assert!(!bytes.is_empty());
        assert_eq!(*bytes.last().unwrap(), 1u8);
    }

    #[test]
    fn x509_serial_number_bytes_consistent_with_serial_number() {
        let (cert, _, _) = make_self_signed();
        let bytes = cert.serial_number_bytes().unwrap();
        let n = cert.serial_number().unwrap();
        // For small positives: the big-endian bytes equal the i64 value.
        let be = n.to_be_bytes();
        // strip leading zeros to match OpenSSL's minimal encoding
        let start = be.iter().position(|&b| b != 0).unwrap_or(7);
        assert_eq!(bytes, &be[start..]);
    }

    // ── not_before_tm / not_after_tm tests ──────────────────────────────────

    #[test]
    fn x509_not_before_tm_is_some() {
        let (cert, _, _) = make_self_signed();
        let tm = cert.not_before_tm().expect("notBefore must be parseable");
        // The self-signed cert was built with set_not_before_offset(0) (≈ now).
        assert!(tm.year >= 2026, "year must be 2026 or later");
        assert!((1..=12).contains(&tm.month));
        assert!((1..=31).contains(&tm.day));
    }

    #[test]
    fn x509_not_after_tm_one_year_after_not_before() {
        let (cert, _, _) = make_self_signed();
        let nb = cert.not_before_tm().unwrap();
        let na = cert.not_after_tm().unwrap();
        // The cert has a 365-day validity window.  After wrapping at year boundary
        // the notAfter year is either the same or one more than notBefore.
        let year_diff = na.year - nb.year;
        assert!(year_diff == 0 || year_diff == 1, "year diff must be 0 or 1");
    }

    #[test]
    fn x509_not_before_tm_consistent_with_not_before_str() {
        let (cert, _, _) = make_self_signed();
        // Both methods must succeed on the same certificate.
        assert!(cert.not_before_tm().is_some());
        assert!(cert.not_before_str().is_some());
    }

    // ── public_key_is_a / public_key_bits tests ──────────────────────────────

    #[test]
    fn x509_public_key_is_a_ed25519() {
        let (cert, _, _) = make_self_signed();
        assert!(cert.public_key_is_a(c"ED25519"));
        assert!(!cert.public_key_is_a(c"RSA"));
    }

    #[test]
    fn x509_public_key_bits_ed25519() {
        let (cert, _, _) = make_self_signed();
        // Ed25519 keys are 255 bits / 32 bytes; OpenSSL reports 253 or 256 bits
        // depending on version — just check it's non-zero.
        let bits = cert.public_key_bits().unwrap();
        assert!(bits > 0, "Ed25519 key must report non-zero bit size");
    }

    #[test]
    fn x509_public_key_bits_agrees_with_public_key_method() {
        let (cert, _, _) = make_self_signed();
        let owned_bits = cert.public_key().unwrap().bits();
        let borrow_bits = cert.public_key_bits().unwrap();
        assert_eq!(owned_bits, borrow_bits);
    }

    // ── nid_to_short_name / nid_to_long_name tests ───────────────────────────

    #[test]
    fn nid_to_short_name_known_nid() {
        // NID 13 is "CN" (commonName) in all OpenSSL versions.
        let sn = nid_to_short_name(13).expect("NID 13 must be known");
        assert_eq!(sn.to_bytes(), b"CN");
    }

    #[test]
    fn nid_to_long_name_known_nid() {
        let ln = nid_to_long_name(13).expect("NID 13 must be known");
        assert_eq!(ln.to_bytes(), b"commonName");
    }

    #[test]
    fn nid_to_short_name_unknown_nid() {
        // A very large NID that is guaranteed not to be in the table.
        assert!(nid_to_short_name(i32::MAX).is_none());
    }

    #[test]
    fn nid_to_long_name_unknown_nid() {
        assert!(nid_to_long_name(i32::MAX).is_none());
    }

    #[test]
    fn nid_to_short_name_sha256() {
        // SHA-256 has NID 672 in OpenSSL; short name is "SHA256".
        let sn = nid_to_short_name(672).expect("NID 672 (SHA256) must be known");
        assert_eq!(sn.to_bytes(), b"SHA256");
    }

    // ── X509Name::to_oneline tests ───────────────────────────────────────────

    #[test]
    fn x509_name_oneline_returns_string() {
        let (cert, _, _) = make_self_signed();
        let name = cert.subject_name();
        let s = name
            .to_oneline()
            .expect("to_oneline must return Some for a non-empty name");
        // The legacy format includes "CN=" for the commonName entry.
        assert!(
            s.contains("CN="),
            "to_oneline output should contain CN=: {s:?}"
        );
    }

    // ── X509::new_in tests ───────────────────────────────────────────────────

    #[test]
    fn x509_new_in_lib_ctx() {
        use crate::lib_ctx::LibCtx;
        let ctx = Arc::new(LibCtx::new().expect("LibCtx::new must succeed"));
        let cert = X509::new_in(&ctx);
        assert!(
            cert.is_ok(),
            "X509::new_in must succeed with a valid LibCtx"
        );
    }

    // ── X509::extension_der tests ────────────────────────────────────────────

    #[test]
    fn x509_extension_der_absent_nid_returns_none() {
        let (cert, _, _) = make_self_signed();
        // NID 85 = subjectAltName; our simple self-signed cert has no SAN.
        let result = cert.extension_der(85);
        assert!(
            result.is_none(),
            "extension_der must return None for absent extension"
        );
    }

    #[test]
    fn x509_extension_der_present_returns_some() {
        // Build a cert that has at least one extension: subjectKeyIdentifier (NID 82).
        // We rely on extension_count() to confirm at least one extension was found.
        let (cert, _, _) = make_self_signed();
        // The basic self-signed cert may or may not carry extensions depending on the
        // builder; use extension_count to find a valid NID and then call extension_der.
        let count = cert.extension_count();
        if count > 0 {
            let ext = cert
                .extension(0)
                .expect("extension(0) must be Some when count > 0");
            let nid = ext.nid();
            let der = cert
                .extension_der(nid)
                .expect("extension_der must return Some for a NID that exists in the cert");
            // The DER bytes for the value must match X509Extension::data().
            assert_eq!(
                der,
                ext.data(),
                "extension_der bytes must match X509Extension::data"
            );
        }
        // If count == 0, the test trivially passes: no extensions to check.
    }
}

#[cfg(test)]
mod signature_info_tests {
    use super::*;
    use crate::pkey::{KeygenCtx, Pkey, Private, Public};

    fn make_ed25519_cert() -> (X509, Pkey<Private>, Pkey<Public>) {
        let mut kgen = KeygenCtx::new(c"ED25519").unwrap();
        let priv_key = kgen.generate().unwrap();
        let pub_key = Pkey::<Public>::from(priv_key.clone());

        let mut name = X509NameOwned::new().unwrap();
        name.add_entry_by_txt(c"CN", b"Ed25519 Sig Info Test")
            .unwrap();

        let cert = X509Builder::new()
            .unwrap()
            .set_version(2)
            .unwrap()
            .set_serial_number(42)
            .unwrap()
            .set_not_before_offset(0)
            .unwrap()
            .set_not_after_offset(365 * 86400)
            .unwrap()
            .set_subject_name(&name)
            .unwrap()
            .set_issuer_name(&name)
            .unwrap()
            .set_public_key(&pub_key)
            .unwrap()
            .sign(&priv_key, None)
            .unwrap()
            .build();

        (cert, priv_key, pub_key)
    }

    /// Ed25519 is a "pre-hash-free" (one-shot) algorithm: the digest NID is
    /// `NID_undef` (0) because there is no separate hashing step.
    /// The public-key NID must correspond to "ED25519" in OpenSSL's OBJ table.
    #[test]
    fn x509_signature_info_ed25519() {
        let (cert, _, _) = make_ed25519_cert();
        let info = cert
            .signature_info()
            .expect("signature_info must succeed for a signed cert");

        // Ed25519 uses no separate digest — md_nid must be NID_undef (0).
        assert_eq!(
            info.md_nid, 0,
            "Ed25519 md_nid must be 0 (NID_undef); got {}",
            info.md_nid
        );

        // pk_nid must be non-zero and must map to the "ED25519" short name.
        assert_ne!(info.pk_nid, 0, "Ed25519 pk_nid must not be NID_undef");
        let sn = nid_to_short_name(info.pk_nid)
            .expect("Ed25519 pk_nid must have a short name in OpenSSL's OBJ table");
        assert_eq!(
            sn.to_bytes(),
            b"ED25519",
            "pk_nid short name must be ED25519; got {sn:?}"
        );

        // Security bits for Ed25519 are 128.
        assert_eq!(
            info.security_bits, 128,
            "Ed25519 security bits must be 128; got {}",
            info.security_bits
        );
    }
}

// ── key_usage / extended_key_usage / subject_alt_names tests ─────────────────

#[cfg(test)]
mod x509_extensions_tests {
    use super::*;
    use crate::obj::Oid;
    use crate::pkey::{KeygenCtx, Pkey, Public};

    /// A P-256 cert with KU, EKU, and SAN extensions set.
    fn make_ku_eku_san_cert() -> X509 {
        // Cert generated with:
        //   openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc
        //     -days 3650 -subj "/CN=test"
        //     -addext "keyUsage=digitalSignature,keyEncipherment"
        //     -addext "extendedKeyUsage=serverAuth,clientAuth"
        //     -addext "subjectAltName=DNS:example.com,DNS:*.example.org,IP:127.0.0.1,email:user@example.com"
        let pem = b"-----BEGIN CERTIFICATE-----\n\
MIIB3zCCAYWgAwIBAgITO3vrfoe0FOHYovyQw92y4L6ttTAKBggqhkjOPQQDAjAP\n\
MQ0wCwYDVQQDDAR0ZXN0MB4XDTI2MDQyNTE1MjUzMFoXDTM2MDQyMjE1MjUzMFow\n\
DzENMAsGA1UEAwwEdGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDE//RDS\n\
5hyz9K7M62qDPsbwg/drLem4n5W0r8e6D6QjQ6WNBca1BABVxXmCKDvSIpKSRa+p\n\
Zhv5FbwiO/L2v+Ojgb8wgbwwHQYDVR0OBBYEFJxnt8JgIqZwtrg63T0lgeJBElCk\n\
MB8GA1UdIwQYMBaAFJxnt8JgIqZwtrg63T0lgeJBElCkMA8GA1UdEwEB/wQFMAMB\n\
Af8wCwYDVR0PBAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjA9\n\
BgNVHREENjA0ggtleGFtcGxlLmNvbYINKi5leGFtcGxlLm9yZ4cEfwAAAYEQdXNl\n\
ckBleGFtcGxlLmNvbTAKBggqhkjOPQQDAgNIADBFAiANTbn0J+j4dpkFxb0JKRoF\n\
2Dh22JaVPgM+d/t+tt+GfQIhAJlQHlMSntnwVUsE0w1H/aMLoo94HStc1dGGi8/a\n\
8Hbj\n\
-----END CERTIFICATE-----\n";
        X509::from_pem(pem).expect("test cert must parse")
    }

    #[test]
    fn key_usage_present_and_correct() {
        let cert = make_ku_eku_san_cert();
        let ku = cert.key_usage().expect("keyUsage must be present");
        assert!(
            ku.contains(KeyUsage::DIGITAL_SIGNATURE),
            "digitalSignature must be set"
        );
        assert!(
            ku.contains(KeyUsage::KEY_ENCIPHERMENT),
            "keyEncipherment must be set"
        );
        assert!(!ku.contains(KeyUsage::CRL_SIGN), "cRLSign must not be set");
        assert!(
            !ku.contains(KeyUsage::KEY_CERT_SIGN),
            "keyCertSign must not be set"
        );
    }

    fn make_plain_cert() -> X509 {
        let mut kgen = KeygenCtx::new(c"ED25519").unwrap();
        let priv_key = kgen.generate().unwrap();
        let pub_key = Pkey::<Public>::from(priv_key.clone());
        let mut name = X509NameOwned::new().unwrap();
        name.add_entry_by_txt(c"CN", b"Plain").unwrap();
        (|| -> Result<X509, ErrorStack> {
            Ok(X509Builder::new()?
                .set_version(2)?
                .set_serial_number(1)?
                .set_not_before_offset(0)?
                .set_not_after_offset(365 * 86400)?
                .set_subject_name(&name)?
                .set_issuer_name(&name)?
                .set_public_key(&pub_key)?
                .sign(&priv_key, None)?
                .build())
        })()
        .unwrap()
    }

    #[test]
    fn key_usage_absent_returns_none() {
        let cert = make_plain_cert();
        assert!(cert.key_usage().is_none(), "plain cert has no keyUsage");
    }

    #[test]
    fn extended_key_usage_oids_present() {
        let cert = make_ku_eku_san_cert();
        let oids = cert.extended_key_usage_oids().unwrap();
        assert_eq!(
            oids.len(),
            2,
            "expected 2 EKU OIDs: serverAuth + clientAuth"
        );

        let server_auth = Oid::from_text("1.3.6.1.5.5.7.3.1").unwrap();
        let client_auth = Oid::from_text("1.3.6.1.5.5.7.3.2").unwrap();
        assert!(
            oids.contains(&server_auth),
            "serverAuth OID must be present"
        );
        assert!(
            oids.contains(&client_auth),
            "clientAuth OID must be present"
        );
    }

    #[test]
    fn extended_key_usage_absent_returns_empty() {
        let cert = make_plain_cert();
        let oids = cert.extended_key_usage_oids().unwrap();
        assert!(oids.is_empty(), "plain cert has no EKU extension");
    }

    #[test]
    fn subject_alt_names_dns_ip_email() {
        let cert = make_ku_eku_san_cert();
        let sans = cert.subject_alt_names().unwrap();

        let dns_names: Vec<_> = sans
            .iter()
            .filter_map(|n| {
                if let GeneralName::Dns(s) = n {
                    Some(s.as_str())
                } else {
                    None
                }
            })
            .collect();
        assert!(
            dns_names.contains(&"example.com"),
            "example.com DNS SAN must be present"
        );
        assert!(
            dns_names.contains(&"*.example.org"),
            "*.example.org DNS SAN must be present"
        );

        let ips: Vec<_> = sans
            .iter()
            .filter_map(|n| {
                if let GeneralName::IpAddress(b) = n {
                    Some(b.clone())
                } else {
                    None
                }
            })
            .collect();
        assert!(
            ips.contains(&vec![127, 0, 0, 1]),
            "127.0.0.1 IP SAN must be present"
        );

        let emails: Vec<_> = sans
            .iter()
            .filter_map(|n| {
                if let GeneralName::Email(s) = n {
                    Some(s.as_str())
                } else {
                    None
                }
            })
            .collect();
        assert!(
            emails.contains(&"user@example.com"),
            "email SAN must be present"
        );
    }

    #[test]
    fn subject_alt_names_absent_returns_empty() {
        let cert = make_plain_cert();
        let sans = cert.subject_alt_names().unwrap();
        assert!(sans.is_empty(), "plain cert has no subjectAltName");
    }

    // ── X509StoreCtx::set_time / set_host / set_ip tests ─────────────────────
    //
    // make_ku_eku_san_cert() has fixed validity [2026-04-25, 2036-04-22]:
    //   - timestamp 1_800_000_000 ≈ 2027-01-15 — within validity
    //   - timestamp 1_735_689_600 = 2025-01-01 — before notBefore
    //   - timestamp 2_200_000_000 ≈ 2039-09-18 — after notAfter
    // SANs: DNS:example.com, DNS:*.example.org, IP:127.0.0.1, email:user@example.com

    #[test]
    fn set_time_within_validity_passes() {
        let cert = make_ku_eku_san_cert();
        let mut store = X509Store::new().unwrap();
        store.add_cert(&cert).unwrap();
        let mut ctx = X509StoreCtx::new().unwrap();
        ctx.init(&store, &cert).unwrap();
        ctx.set_time(1_800_000_000).unwrap();
        assert!(
            ctx.verify().unwrap(),
            "cert should be valid within its validity window"
        );
    }

    #[test]
    fn set_time_before_not_before_fails() {
        let cert = make_ku_eku_san_cert();
        let mut store = X509Store::new().unwrap();
        store.add_cert(&cert).unwrap();
        let mut ctx = X509StoreCtx::new().unwrap();
        ctx.init(&store, &cert).unwrap();
        ctx.set_time(1_735_689_600).unwrap(); // 2025-01-01, before notBefore 2026-04-25
        assert!(
            !ctx.verify().unwrap(),
            "cert should fail when verifying before notBefore"
        );
        assert_ne!(ctx.error(), 0, "error code must be set");
    }

    #[test]
    fn set_time_after_not_after_fails() {
        let cert = make_ku_eku_san_cert();
        let mut store = X509Store::new().unwrap();
        store.add_cert(&cert).unwrap();
        let mut ctx = X509StoreCtx::new().unwrap();
        ctx.init(&store, &cert).unwrap();
        ctx.set_time(2_200_000_000).unwrap(); // 2039-09-18, after notAfter 2036-04-22
        assert!(
            !ctx.verify().unwrap(),
            "cert should fail when verifying after notAfter"
        );
        assert_ne!(ctx.error(), 0, "error code must be set");
    }

    #[test]
    fn set_host_matching_dns_passes() {
        let cert = make_ku_eku_san_cert();
        let mut store = X509Store::new().unwrap();
        store.add_cert(&cert).unwrap();
        let mut ctx = X509StoreCtx::new().unwrap();
        ctx.init(&store, &cert).unwrap();
        ctx.set_time(1_800_000_000).unwrap();
        ctx.set_host("example.com").unwrap();
        assert!(ctx.verify().unwrap(), "example.com DNS SAN should match");
    }

    #[test]
    fn set_host_matching_wildcard_passes() {
        let cert = make_ku_eku_san_cert();
        let mut store = X509Store::new().unwrap();
        store.add_cert(&cert).unwrap();
        let mut ctx = X509StoreCtx::new().unwrap();
        ctx.init(&store, &cert).unwrap();
        ctx.set_time(1_800_000_000).unwrap();
        ctx.set_host("foo.example.org").unwrap();
        assert!(
            ctx.verify().unwrap(),
            "foo.example.org should match *.example.org wildcard"
        );
    }

    #[test]
    fn set_host_non_matching_fails() {
        let cert = make_ku_eku_san_cert();
        let mut store = X509Store::new().unwrap();
        store.add_cert(&cert).unwrap();
        let mut ctx = X509StoreCtx::new().unwrap();
        ctx.init(&store, &cert).unwrap();
        ctx.set_time(1_800_000_000).unwrap();
        ctx.set_host("notexample.com").unwrap();
        assert!(
            !ctx.verify().unwrap(),
            "notexample.com should not match any SAN"
        );
    }

    #[test]
    fn set_ip_matching_passes() {
        let cert = make_ku_eku_san_cert();
        let mut store = X509Store::new().unwrap();
        store.add_cert(&cert).unwrap();
        let mut ctx = X509StoreCtx::new().unwrap();
        ctx.init(&store, &cert).unwrap();
        ctx.set_time(1_800_000_000).unwrap();
        ctx.set_ip("127.0.0.1").unwrap();
        assert!(ctx.verify().unwrap(), "127.0.0.1 IP SAN should match");
    }

    #[test]
    fn set_ip_non_matching_fails() {
        let cert = make_ku_eku_san_cert();
        let mut store = X509Store::new().unwrap();
        store.add_cert(&cert).unwrap();
        let mut ctx = X509StoreCtx::new().unwrap();
        ctx.init(&store, &cert).unwrap();
        ctx.set_time(1_800_000_000).unwrap();
        ctx.set_ip("10.0.0.1").unwrap();
        assert!(
            !ctx.verify().unwrap(),
            "10.0.0.1 is not listed in the cert's IP SANs"
        );
    }

    // A self-signed P-256 cert with SANs: IP:::1 (IPv6) and IP:127.0.0.1.
    // Valid 2026-05-24 → 2036-05-21. Use timestamp 1_800_000_000 (≈2027-01-15).
    // If tests start failing with Expired, regenerate (cert expires 2036-05-21).
    const IPV6_SAN_CERT_PEM: &[u8] = b"-----BEGIN CERTIFICATE-----\n\
MIIBoDCCAUagAwIBAgIULmBzwGVGt772/aVlAKiXf1rmh0UwCgYIKoZIzj0EAwIw\n\
FDESMBAGA1UEAwwJdGVzdC1pcHY2MB4XDTI2MDUyNDA3MjA1M1oXDTM2MDUyMTA3\n\
MjA1M1owFDESMBAGA1UEAwwJdGVzdC1pcHY2MFkwEwYHKoZIzj0CAQYIKoZIzj0D\n\
AQcDQgAEGmmLFrYHX6pidi0ocopv291+VSehfBm59MI5DKLc99Uv4MrgwADIWE2y\n\
SbHgLCILEi6sbtfp2pWRnKTrRg8Li6N2MHQwHQYDVR0OBBYEFNBi2gYoXTXcN+YA\n\
MOdF5enn7KAmMB8GA1UdIwQYMBaAFNBi2gYoXTXcN+YAMOdF5enn7KAmMA8GA1Ud\n\
EwEB/wQFMAMBAf8wIQYDVR0RBBowGIcQAAAAAAAAAAAAAAAAAAAAAYcEfwAAATAK\n\
BggqhkjOPQQDAgNIADBFAiACwgtWTk9XRkfn6rJXNz9JrNrF0NHA1gReGKgK7NA/\n\
kQIhAJMEiXauuyLtybgoIcdgIT7Fuo9qLBax1oF1aZFgwJY5\n\
-----END CERTIFICATE-----\n";

    // ── IPv6 IP-SAN tests ──────────────────────────────────────────────────────

    #[test]
    fn set_ip_ipv6_matching_passes() {
        let cert = X509::from_pem(IPV6_SAN_CERT_PEM).unwrap();
        let mut store = X509Store::new().unwrap();
        store.add_cert(&cert).unwrap();
        let mut ctx = X509StoreCtx::new().unwrap();
        ctx.init(&store, &cert).unwrap();
        ctx.set_time(1_800_000_000).unwrap();
        ctx.set_ip("::1").unwrap();
        assert!(ctx.verify().unwrap(), "::1 IPv6 IP SAN should match");
    }

    #[test]
    fn set_ip_ipv6_non_matching_fails() {
        let cert = X509::from_pem(IPV6_SAN_CERT_PEM).unwrap();
        let mut store = X509Store::new().unwrap();
        store.add_cert(&cert).unwrap();
        let mut ctx = X509StoreCtx::new().unwrap();
        ctx.init(&store, &cert).unwrap();
        ctx.set_time(1_800_000_000).unwrap();
        ctx.set_ip("::2").unwrap();
        assert!(
            !ctx.verify().unwrap(),
            "::2 is not listed in the cert's IPv6 SANs"
        );
    }

    // ── NUL-byte rejection in set_host / set_ip ───────────────────────────────

    #[test]
    fn set_host_with_null_byte_returns_err() {
        let cert = make_ku_eku_san_cert();
        let mut store = X509Store::new().unwrap();
        store.add_cert(&cert).unwrap();
        let mut ctx = X509StoreCtx::new().unwrap();
        ctx.init(&store, &cert).unwrap();
        // CString::new rejects embedded NUL bytes — set_host must propagate the error.
        assert!(
            ctx.set_host("example.com\x00evil.com").is_err(),
            "embedded NUL byte in hostname must return Err",
        );
    }

    #[test]
    fn set_ip_with_null_byte_returns_err() {
        let cert = make_ku_eku_san_cert();
        let mut store = X509Store::new().unwrap();
        store.add_cert(&cert).unwrap();
        let mut ctx = X509StoreCtx::new().unwrap();
        ctx.init(&store, &cert).unwrap();
        // CString::new rejects embedded NUL bytes — set_ip must propagate the error.
        assert!(
            ctx.set_ip("127.0.0.1\x00evil.com").is_err(),
            "embedded NUL byte in IP string must return Err",
        );
    }

    // ── X509Name::to_der direct unit test ────────────────────────────────────

    #[test]
    fn subject_name_to_der_produces_sequence_der() {
        let cert = make_ku_eku_san_cert();
        let name = cert.subject_name();
        let der = name.to_der().unwrap();
        assert!(der.len() >= 2, "DER must have at least tag + length byte");
        // A DER-encoded distinguished name is always wrapped in a SEQUENCE (tag 0x30).
        assert_eq!(
            der[0], 0x30,
            "DER-encoded DN must start with SEQUENCE tag 0x30"
        );
        // For short-form lengths (≤ 127 bytes), der[1] is the content length;
        // it must equal der.len() - 2 (tag + length byte already consumed).
        if der[1] <= 0x7f {
            assert_eq!(
                der[1] as usize,
                der.len() - 2,
                "short-form length byte must equal content length",
            );
        }
    }

    // ── X509StoreCtx uninitialised-state behaviour ────────────────────────────

    #[test]
    fn store_ctx_verify_before_init_returns_err() {
        let mut ctx = X509StoreCtx::new().unwrap();
        // X509_verify_cert on an uninitialised context sets X509_V_ERR_INVALID_CALL
        // and returns -1, which our verify() maps to Err.
        assert!(
            ctx.verify().is_err(),
            "verify() before init must return Err (OpenSSL X509_V_ERR_INVALID_CALL)",
        );
    }

    #[test]
    fn store_ctx_set_time_before_init_returns_err() {
        let mut ctx = X509StoreCtx::new().unwrap();
        // X509_STORE_CTX_get0_param returns NULL before init, so set_time returns Err.
        assert!(
            ctx.set_time(0).is_err(),
            "set_time() before init must return Err (null verify param)",
        );
    }

    #[test]
    fn store_ctx_set_host_before_init_returns_err() {
        let mut ctx = X509StoreCtx::new().unwrap();
        assert!(
            ctx.set_host("example.com").is_err(),
            "set_host() before init must return Err (null verify param)",
        );
    }

    #[test]
    fn store_ctx_set_ip_before_init_returns_err() {
        let mut ctx = X509StoreCtx::new().unwrap();
        assert!(
            ctx.set_ip("127.0.0.1").is_err(),
            "set_ip() before init must return Err (null verify param)",
        );
    }

    #[test]
    fn init_with_chain_empty_intermediates_no_stack_allocation() {
        use crate::pkey::{KeygenCtx, Pkey, Public};
        let mut kgen = KeygenCtx::new(c"ED25519").unwrap();
        let priv_key = kgen.generate().unwrap();
        let pub_key = Pkey::<Public>::from(priv_key.clone());
        let mut name = X509NameOwned::new().unwrap();
        name.add_entry_by_txt(c"CN", b"test").unwrap();
        let cert = X509Builder::new()
            .unwrap()
            .set_version(2)
            .unwrap()
            .set_serial_number(1)
            .unwrap()
            .set_not_before_offset(0)
            .unwrap()
            .set_not_after_offset(365 * 86400)
            .unwrap()
            .set_subject_name(&name)
            .unwrap()
            .set_issuer_name(&name)
            .unwrap()
            .set_public_key(&pub_key)
            .unwrap()
            .sign(&priv_key, None)
            .unwrap()
            .build();
        let mut store = X509Store::new().unwrap();
        store.add_cert(&cert).unwrap();
        let mut ctx = X509StoreCtx::new().unwrap();
        // Empty chain slice → fast path (no OPENSSL_sk_new_null); must succeed.
        ctx.init_with_chain(&store, &cert, &[]).unwrap();
        // untrusted is None after empty-chain init, but verify still succeeds.
        assert!(
            ctx.verify().unwrap(),
            "self-signed cert must verify with empty intermediates"
        );
    }
}
