//! OCSP — Online Certificate Status Protocol (`RFC 2560` / `RFC 6960`).
//!
//! Provides the full client-side OCSP stack:
//!
//! - [`OcspCertId`] — identifies a certificate to query
//! - [`OcspRequest`] — encodes the DER request to send to a responder
//! - [`OcspResponse`] — decodes and validates a DER response
//! - [`OcspBasicResp`] — the signed inner response; drives per-cert status lookup
//! - [`OcspSingleStatus`] — per-certificate status result from [`OcspBasicResp::find_status`]
//!
//! # Typical flow
//!
//! ```ignore
//! // Build a request for a specific certificate.
//! let id = OcspCertId::from_cert(None, &end_entity_cert, &issuer_cert)?;
//! let mut req = OcspRequest::new()?;
//! req.add_cert_id(id)?;
//! let req_der = req.to_der()?;
//!
//! // ... send req_der over HTTP, receive resp_der ...
//!
//! let resp = OcspResponse::from_der(&resp_der)?;
//! assert_eq!(resp.status(), OcspResponseStatus::Successful);
//!
//! let basic = resp.basic()?;
//! basic.verify(&trust_store, 0)?;
//!
//! let id2 = OcspCertId::from_cert(None, &end_entity_cert, &issuer_cert)?;
//! match basic.find_status(&id2)? {
//!     Some(s) if s.cert_status == OcspCertStatus::Good => println!("certificate is good"),
//!     Some(s) => println!("certificate status: {:?}", s.cert_status),
//!     None => println!("certificate not found in response"),
//! }
//! ```
//!
//! HTTP transport is **out of scope** — the caller is responsible for fetching
//! the OCSP response from the responder URL and passing the raw DER bytes.

use crate::bio::MemBio;
use crate::error::ErrorStack;
use native_ossl_sys as sys;

// ── OcspResponseStatus ────────────────────────────────────────────────────────

/// OCSP response status (RFC 6960 §4.2.1).
///
/// This is the *top-level* status of the response packet itself, not the status
/// of any individual certificate.  A `Successful` response still requires
/// per-certificate inspection via [`OcspBasicResp::find_status`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OcspResponseStatus {
    /// `successful` (0) — Response packet is valid.
    Successful,
    /// `malformedRequest` (1) — Server could not parse the request.
    MalformedRequest,
    /// `internalError` (2) — Server internal error.
    InternalError,
    /// `tryLater` (3) — Server is busy; retry later.
    TryLater,
    /// `sigRequired` (5) — Signed request required by policy.
    SigRequired,
    /// `unauthorized` (6) — Unauthorized request.
    Unauthorized,
    /// Unknown status code (forward-compatibility guard).
    Unknown(i32),
}

impl From<i32> for OcspResponseStatus {
    fn from(v: i32) -> Self {
        match v {
            0 => Self::Successful,
            1 => Self::MalformedRequest,
            2 => Self::InternalError,
            3 => Self::TryLater,
            5 => Self::SigRequired,
            6 => Self::Unauthorized,
            n => Self::Unknown(n),
        }
    }
}

// ── OcspCertStatus ────────────────────────────────────────────────────────────

/// Per-certificate revocation status from an `OCSP_SINGLERESP`.
///
/// Returned inside [`OcspSingleStatus`] by [`OcspBasicResp::find_status`], and
/// also by [`BorrowedOcspSingleResp::status`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OcspCertStatus {
    /// Certificate is currently valid (`V_OCSP_CERTSTATUS_GOOD = 0`).
    Good,
    /// Certificate has been revoked (`V_OCSP_CERTSTATUS_REVOKED = 1`).
    Revoked,
    /// Responder does not know this certificate (`V_OCSP_CERTSTATUS_UNKNOWN = 2`).
    Unknown,
}

impl OcspCertStatus {
    fn from_raw(status: i32) -> Self {
        match status {
            0 => Self::Good,
            1 => Self::Revoked,
            _ => Self::Unknown,
        }
    }
}

// ── OcspRevokeReason ──────────────────────────────────────────────────────────

/// CRL revocation reason codes (RFC 5280 §5.3.1).
///
/// Carried inside [`OcspSingleStatus`] and [`SingleRespStatus`] when the
/// certificate status is [`OcspCertStatus::Revoked`].  A value of `None` means
/// no reason extension was present in the response (OpenSSL returns
/// `OCSP_REVOKED_STATUS_NOSTATUS`, i.e. `-1`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OcspRevokeReason {
    /// `unspecified` (0).
    Unspecified,
    /// `keyCompromise` (1).
    KeyCompromise,
    /// `cACompromise` (2).
    CaCompromise,
    /// `affiliationChanged` (3).
    AffiliationChanged,
    /// `superseded` (4).
    Superseded,
    /// `cessationOfOperation` (5).
    CessationOfOperation,
    /// `certificateHold` (6).
    CertificateHold,
    /// `removeFromCRL` (8).
    RemoveFromCrl,
    /// `privilegeWithdrawn` (9).
    PrivilegeWithdrawn,
    /// `aACompromise` (10).
    AaCompromise,
    /// Any other numeric reason code (forward-compatibility guard).
    Other(i32),
}

impl OcspRevokeReason {
    fn from_raw(reason: i32) -> Option<Self> {
        // -1 means "no reason given" (OCSP_REVOKED_STATUS_NOSTATUS).
        if reason < 0 {
            return None;
        }
        Some(match reason {
            0 => Self::Unspecified,
            1 => Self::KeyCompromise,
            2 => Self::CaCompromise,
            3 => Self::AffiliationChanged,
            4 => Self::Superseded,
            5 => Self::CessationOfOperation,
            6 => Self::CertificateHold,
            8 => Self::RemoveFromCrl,
            9 => Self::PrivilegeWithdrawn,
            10 => Self::AaCompromise,
            n => Self::Other(n),
        })
    }
}

// ── OcspSingleStatus ──────────────────────────────────────────────────────────

/// Status of a single certificate, returned by [`OcspBasicResp::find_status`].
#[derive(Debug, Clone)]
pub struct OcspSingleStatus {
    /// Per-certificate status.
    pub cert_status: OcspCertStatus,
    /// Revocation reason code (RFC 5280 §5.3.1).
    ///
    /// Set only when `cert_status == Revoked` and the responder included a
    /// reason code; `None` when the reason was absent or the cert is not revoked.
    pub reason: Option<OcspRevokeReason>,
    /// `thisUpdate` time as a human-readable UTC string, if present.
    pub this_update: Option<String>,
    /// `nextUpdate` time as a human-readable UTC string, if present.
    pub next_update: Option<String>,
    /// Revocation time as a human-readable UTC string (`cert_status == Revoked` only).
    pub revocation_time: Option<String>,
}

// ── OcspCertId ───────────────────────────────────────────────────────────────

/// Certificate identifier for OCSP (`OCSP_CERTID*`).
///
/// Created from a subject certificate and its issuer with [`OcspCertId::from_cert`].
/// Add to a request with [`OcspRequest::add_cert_id`], or use to look up a status
/// in a response with [`OcspBasicResp::find_status`].
///
/// `Clone` is implemented via `OCSP_CERTID_dup`.
pub struct OcspCertId {
    ptr: *mut sys::OCSP_CERTID,
}

unsafe impl Send for OcspCertId {}

impl Clone for OcspCertId {
    fn clone(&self) -> Self {
        let ptr = unsafe { sys::OCSP_CERTID_dup(self.ptr) };
        // OCSP_CERTID_dup returns null only on allocation failure; treat as abort.
        assert!(!ptr.is_null(), "OCSP_CERTID_dup: allocation failure");
        OcspCertId { ptr }
    }
}

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

impl OcspCertId {
    /// Allocate a new, empty `OCSP_CERTID`.
    ///
    /// The returned object has all fields zeroed.  Use [`OcspCertId::from_cert`]
    /// to build a fully populated cert ID from a subject certificate and its
    /// issuer instead.
    ///
    /// # Errors
    ///
    /// Returns `Err` if OpenSSL fails to allocate memory.
    #[must_use = "returns a new OcspCertId that must be used or stored"]
    pub fn new() -> Result<Self, ErrorStack> {
        // SAFETY: OCSP_CERTID_new() is a plain allocator; it takes no arguments
        // and returns null only on allocation failure.
        let ptr = unsafe { sys::OCSP_CERTID_new() };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(OcspCertId { ptr })
    }

    /// Build a cert ID from a subject certificate and its direct issuer.
    ///
    /// `digest` is the hash algorithm used to hash the issuer name and key
    /// (default: SHA-1 when `None`, per RFC 6960).  SHA-1 is required by most
    /// deployed OCSP responders; pass `Some(sha256_alg)` only when the responder
    /// is known to support it.
    ///
    /// # Errors
    pub fn from_cert(
        digest: Option<&crate::digest::DigestAlg>,
        subject: &crate::x509::X509,
        issuer: &crate::x509::X509,
    ) -> Result<Self, ErrorStack> {
        let dgst_ptr = digest.map_or(std::ptr::null(), crate::digest::DigestAlg::as_ptr);
        let ptr = unsafe { sys::OCSP_cert_to_id(dgst_ptr, subject.as_ptr(), issuer.as_ptr()) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(OcspCertId { ptr })
    }
}

// ── OcspRequest ───────────────────────────────────────────────────────────────

/// An OCSP request (`OCSP_REQUEST*`).
///
/// Build with [`OcspRequest::new`], populate with [`OcspRequest::add_cert_id`],
/// then encode with [`OcspRequest::to_der`] and send to the OCSP responder.
pub struct OcspRequest {
    ptr: *mut sys::OCSP_REQUEST,
}

unsafe impl Send for OcspRequest {}

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

impl OcspRequest {
    /// Create a new, empty OCSP request.
    ///
    /// # Errors
    pub fn new() -> Result<Self, ErrorStack> {
        let ptr = unsafe { sys::OCSP_REQUEST_new() };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(OcspRequest { ptr })
    }

    /// Decode an OCSP request from DER bytes.
    ///
    /// # Errors
    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack> {
        let mut ptr = std::ptr::null_mut::<sys::OCSP_REQUEST>();
        let mut p = der.as_ptr();
        let len = i64::try_from(der.len()).unwrap_or(i64::MAX);
        let result = unsafe {
            sys::d2i_OCSP_REQUEST(std::ptr::addr_of_mut!(ptr), std::ptr::addr_of_mut!(p), len)
        };
        if result.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(OcspRequest { ptr })
    }

    /// Add a certificate identifier to the request.
    ///
    /// `cert_id` ownership is transferred to the request (`add0` semantics);
    /// the `OcspCertId` is consumed.
    ///
    /// # Errors
    pub fn add_cert_id(&mut self, cert_id: OcspCertId) -> Result<(), ErrorStack> {
        // OCSP_request_add0_id transfers ownership of the CERTID on success only.
        // Do not forget cert_id until after the null check — on failure the CERTID
        // is NOT consumed by OpenSSL and must still be freed by our Drop impl.
        let rc = unsafe { sys::OCSP_request_add0_id(self.ptr, cert_id.ptr) };
        if rc.is_null() {
            return Err(ErrorStack::drain());
        }
        // Success: ownership transferred; suppress Drop.
        std::mem::forget(cert_id);
        Ok(())
    }

    /// Encode the OCSP request to DER bytes.
    ///
    /// # Errors
    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack> {
        let len = unsafe { sys::i2d_OCSP_REQUEST(self.ptr, std::ptr::null_mut()) };
        if len < 0 {
            return Err(ErrorStack::drain());
        }
        #[allow(clippy::cast_sign_loss)] // len > 0 checked above
        let mut buf = vec![0u8; len as usize];
        let mut out_ptr = buf.as_mut_ptr();
        let written = unsafe { sys::i2d_OCSP_REQUEST(self.ptr, std::ptr::addr_of_mut!(out_ptr)) };
        if written < 0 {
            return Err(ErrorStack::drain());
        }
        #[allow(clippy::cast_sign_loss)] // written >= 0 checked above
        buf.truncate(written as usize);
        Ok(buf)
    }
}

// ── OcspSingleResp / BorrowedOcspSingleResp ───────────────────────────────────

/// An individual `SingleResponse` entry inside an `OCSP_BASICRESP`
/// (`OCSP_SINGLERESP*`).
///
/// Not usually constructed directly; obtain a borrowed view via
/// [`OcspBasicResp::get_response`].
pub struct OcspSingleResp {
    ptr: *mut sys::OCSP_SINGLERESP,
}

unsafe impl Send for OcspSingleResp {}

impl Drop for OcspSingleResp {
    fn drop(&mut self) {
        // OCSP_SINGLERESP objects are owned by their parent OCSP_BASICRESP.
        // They must NOT be freed independently; this Drop is intentionally a
        // no-op.  The owning OcspBasicResp will free them via OCSP_BASICRESP_free.
    }
}

/// A borrowed `OCSP_SINGLERESP*` whose lifetime is tied to its parent
/// [`OcspBasicResp`].
///
/// Obtained from [`OcspBasicResp::get_response`].  Does **not** free the
/// underlying pointer on drop — the pointer is owned by the `OCSP_BASICRESP`
/// and freed when the [`OcspBasicResp`] is dropped.
pub struct BorrowedOcspSingleResp<'a> {
    inner: std::mem::ManuallyDrop<OcspSingleResp>,
    _lifetime: std::marker::PhantomData<&'a OcspBasicResp>,
}

/// Status of a single certificate entry, returned by
/// [`BorrowedOcspSingleResp::status`].
#[derive(Debug, Clone)]
pub struct SingleRespStatus {
    /// Per-certificate revocation status.
    pub cert_status: OcspCertStatus,
    /// Revocation reason, set only when `cert_status == Revoked` and the
    /// responder included a reason code.
    pub reason: Option<OcspRevokeReason>,
    /// `thisUpdate` time as a human-readable UTC string, if present.
    pub this_update: Option<String>,
    /// `nextUpdate` time as a human-readable UTC string, if present.
    pub next_update: Option<String>,
    /// Revocation time as a human-readable UTC string; `Some` only when
    /// `cert_status == Revoked` and the time was provided.
    pub revocation_time: Option<String>,
}

impl BorrowedOcspSingleResp<'_> {
    /// Extract the revocation status from this `SingleResponse` entry.
    ///
    /// Calls `OCSP_single_get0_status` to retrieve the certificate status,
    /// optional revocation reason, and the `thisUpdate`/`nextUpdate`/
    /// `revocationTime` timestamps.
    ///
    /// # Errors
    ///
    /// Returns `Err` if OpenSSL returns an unrecognised status code (< 0).
    pub fn status(&self) -> Result<SingleRespStatus, ErrorStack> {
        let mut reason: i32 = -1;
        let mut revtime: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
        let mut thisupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
        let mut nextupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();

        // SAFETY: self.inner.ptr is a valid non-null OCSP_SINGLERESP* borrowed
        // from the parent OcspBasicResp for lifetime 'a.  The out-pointer
        // arguments are all valid stack-allocated locals.  The returned
        // ASN1_GENERALIZEDTIME* pointers are borrowed from the single response
        // and are valid for the same lifetime.
        let raw_status = unsafe {
            sys::OCSP_single_get0_status(
                self.inner.ptr,
                std::ptr::addr_of_mut!(reason),
                std::ptr::addr_of_mut!(revtime),
                std::ptr::addr_of_mut!(thisupd),
                std::ptr::addr_of_mut!(nextupd),
            )
        };

        if raw_status < 0 {
            return Err(ErrorStack::drain());
        }

        let cert_status = OcspCertStatus::from_raw(raw_status);
        let revoke_reason = if cert_status == OcspCertStatus::Revoked {
            OcspRevokeReason::from_raw(reason)
        } else {
            None
        };

        Ok(SingleRespStatus {
            cert_status,
            reason: revoke_reason,
            this_update: generalizedtime_to_str(thisupd),
            next_update: generalizedtime_to_str(nextupd),
            revocation_time: generalizedtime_to_str(revtime),
        })
    }
}

// ── OcspBasicResp ─────────────────────────────────────────────────────────────

/// The signed inner OCSP response (`OCSP_BASICRESP*`).
///
/// Extracted from an [`OcspResponse`] via [`OcspResponse::basic`], or
/// allocated fresh with [`OcspBasicResp::new`] for responder-side use.
/// Provides signature verification and per-certificate status lookup.
pub struct OcspBasicResp {
    ptr: *mut sys::OCSP_BASICRESP,
}

unsafe impl Send for OcspBasicResp {}

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

impl OcspBasicResp {
    /// Allocate a new, empty `OCSP_BASICRESP`.
    ///
    /// Useful on the responder side when building a basic response from scratch.
    ///
    /// # Errors
    ///
    /// Returns `Err` if OpenSSL fails to allocate memory.
    #[must_use = "returns a new OcspBasicResp that must be used or stored"]
    pub fn new() -> Result<Self, ErrorStack> {
        // SAFETY: OCSP_BASICRESP_new() is a plain allocator; it takes no
        // arguments and returns null only on allocation failure.
        let ptr = unsafe { sys::OCSP_BASICRESP_new() };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(OcspBasicResp { ptr })
    }

    /// Get the `SingleResponse` at position `idx` in this basic response.
    ///
    /// Returns `None` if `idx` is out of range (>= [`Self::count`]).
    ///
    /// The returned [`BorrowedOcspSingleResp`] is borrowed from `self` and
    /// must not outlive it.
    #[must_use]
    pub fn get_response(&self, idx: usize) -> Option<BorrowedOcspSingleResp<'_>> {
        // Convert usize → c_int; treat overflow as out-of-range.
        let idx_c = i32::try_from(idx).ok()?;
        // SAFETY: self.ptr is a valid non-null OCSP_BASICRESP*.
        // OCSP_resp_get0 returns a borrowed pointer into the BASICRESP's
        // internal stack; lifetime is tied to self via BorrowedOcspSingleResp.
        let ptr = unsafe { sys::OCSP_resp_get0(self.ptr, idx_c) };
        if ptr.is_null() {
            return None;
        }
        Some(BorrowedOcspSingleResp {
            inner: std::mem::ManuallyDrop::new(OcspSingleResp { ptr }),
            _lifetime: std::marker::PhantomData,
        })
    }

    /// Find the index of the first `SingleResponse` matching `id`.
    ///
    /// Searches from the beginning of the response list.  Returns `Some(idx)`
    /// with the zero-based index of the matching entry, or `None` if no
    /// matching entry was found.
    ///
    /// Use [`Self::get_response`] with the returned index to access the entry.
    #[must_use]
    pub fn find_response(&self, id: &OcspCertId) -> Option<usize> {
        // SAFETY: self.ptr and id.ptr are both valid non-null pointers.
        // OCSP_resp_find does not store the id pointer after returning.
        // last = -1 means "start from the beginning".
        let idx = unsafe { sys::OCSP_resp_find(self.ptr, id.ptr, -1) };
        if idx < 0 {
            return None;
        }
        // idx >= 0 is safe to cast.
        #[allow(clippy::cast_sign_loss)]
        Some(idx as usize)
    }

    /// Verify the response signature against `store`.
    ///
    /// `flags` is passed directly to `OCSP_basic_verify` (use 0 for defaults,
    /// which verifies the signature and checks the signing certificate chain).
    ///
    /// Returns `Ok(true)` if the signature is valid.
    ///
    /// # Errors
    pub fn verify(&self, store: &crate::x509::X509Store, flags: u64) -> Result<bool, ErrorStack> {
        match unsafe {
            sys::OCSP_basic_verify(self.ptr, std::ptr::null_mut(), store.as_ptr(), flags)
        } {
            1 => Ok(true),
            0 => Ok(false),
            _ => Err(ErrorStack::drain()),
        }
    }

    /// Number of `SingleResponse` entries in this basic response.
    #[must_use]
    pub fn count(&self) -> usize {
        let n = unsafe { sys::OCSP_resp_count(self.ptr) };
        usize::try_from(n).unwrap_or(0)
    }

    /// Look up the status for a specific certificate by its [`OcspCertId`].
    ///
    /// Returns `Ok(Some(status))` if the responder included a `SingleResponse`
    /// for that certificate, `Ok(None)` if not found, or `Err` on a fatal
    /// OpenSSL error.
    ///
    /// The `cert_id` is passed by shared reference; its pointer is only used
    /// for the duration of this call (`OCSP_resp_find_status` does not store it).
    ///
    /// # Errors
    pub fn find_status(
        &self,
        cert_id: &OcspCertId,
    ) -> Result<Option<OcspSingleStatus>, ErrorStack> {
        let mut status: i32 = -1;
        let mut reason: i32 = -1;
        let mut revtime: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
        let mut thisupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
        let mut nextupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();

        let rc = unsafe {
            sys::OCSP_resp_find_status(
                self.ptr,
                cert_id.ptr,
                std::ptr::addr_of_mut!(status),
                std::ptr::addr_of_mut!(reason),
                std::ptr::addr_of_mut!(revtime),
                std::ptr::addr_of_mut!(thisupd),
                std::ptr::addr_of_mut!(nextupd),
            )
        };

        // rc == 1 → found; rc == 0 → not found; anything else → error.
        match rc {
            1 => Ok(Some(OcspSingleStatus {
                cert_status: OcspCertStatus::from_raw(status),
                reason: OcspRevokeReason::from_raw(reason),
                this_update: generalizedtime_to_str(thisupd),
                next_update: generalizedtime_to_str(nextupd),
                revocation_time: generalizedtime_to_str(revtime),
            })),
            0 => Ok(None),
            _ => Err(ErrorStack::drain()),
        }
    }

    /// Validate the `thisUpdate` / `nextUpdate` window of a `SingleResponse`.
    ///
    /// `sec` is the acceptable clock-skew in seconds (typically 300).
    /// `maxsec` limits how far in the future `nextUpdate` may be (-1 = no limit).
    ///
    /// # Errors
    pub fn check_validity(
        &self,
        cert_id: &OcspCertId,
        sec: i64,
        maxsec: i64,
    ) -> Result<bool, ErrorStack> {
        // Re-run find_status to get thisupd / nextupd pointers.
        let mut thisupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
        let mut nextupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
        let rc = unsafe {
            sys::OCSP_resp_find_status(
                self.ptr,
                cert_id.ptr,
                std::ptr::null_mut(), // status
                std::ptr::null_mut(), // reason
                std::ptr::null_mut(), // revtime
                std::ptr::addr_of_mut!(thisupd),
                std::ptr::addr_of_mut!(nextupd),
            )
        };
        // rc == 1 → found; rc == 0 → not found; negative → fatal error.
        match rc {
            1 => {}
            0 => return Ok(false),
            _ => return Err(ErrorStack::drain()),
        }
        match unsafe { sys::OCSP_check_validity(thisupd, nextupd, sec, maxsec) } {
            1 => Ok(true),
            0 => Ok(false),
            _ => Err(ErrorStack::drain()),
        }
    }
}

// ── OcspResponse ──────────────────────────────────────────────────────────────

/// An OCSP response (`OCSP_RESPONSE*`).
///
/// Decode from DER with [`OcspResponse::from_der`].  Check the top-level
/// [`OcspResponse::status`], then extract the signed inner response with
/// [`OcspResponse::basic`] for per-certificate status lookup.
pub struct OcspResponse {
    ptr: *mut sys::OCSP_RESPONSE,
}

unsafe impl Send for OcspResponse {}

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

impl OcspResponse {
    /// Allocate a new, empty `OCSP_RESPONSE`.
    ///
    /// Useful on the responder side when building a response from scratch.
    ///
    /// # Errors
    ///
    /// Returns `Err` if OpenSSL fails to allocate memory.
    #[must_use = "returns a new OcspResponse that must be used or stored"]
    pub fn new() -> Result<Self, ErrorStack> {
        // SAFETY: OCSP_RESPONSE_new() is a plain allocator; it takes no
        // arguments and returns null only on allocation failure.
        let ptr = unsafe { sys::OCSP_RESPONSE_new() };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(OcspResponse { ptr })
    }

    /// Decode an OCSP response from DER bytes.
    ///
    /// # Errors
    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack> {
        let mut ptr = std::ptr::null_mut::<sys::OCSP_RESPONSE>();
        let mut p = der.as_ptr();
        let len = i64::try_from(der.len()).unwrap_or(i64::MAX);
        let result = unsafe {
            sys::d2i_OCSP_RESPONSE(std::ptr::addr_of_mut!(ptr), std::ptr::addr_of_mut!(p), len)
        };
        if result.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(OcspResponse { ptr })
    }

    /// Encode the OCSP response to DER bytes.
    ///
    /// # Errors
    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack> {
        let len = unsafe { sys::i2d_OCSP_RESPONSE(self.ptr, std::ptr::null_mut()) };
        if len < 0 {
            return Err(ErrorStack::drain());
        }
        #[allow(clippy::cast_sign_loss)] // len > 0 checked above
        let mut buf = vec![0u8; len as usize];
        let mut out_ptr = buf.as_mut_ptr();
        let written = unsafe { sys::i2d_OCSP_RESPONSE(self.ptr, std::ptr::addr_of_mut!(out_ptr)) };
        if written < 0 {
            return Err(ErrorStack::drain());
        }
        #[allow(clippy::cast_sign_loss)] // written >= 0 checked above
        buf.truncate(written as usize);
        Ok(buf)
    }

    /// Overall OCSP response status (top-level packet status, not cert status).
    ///
    /// A `Successful` value means the server processed the request; it does not
    /// mean any individual certificate is good.  Use [`Self::basic`] and then
    /// [`OcspBasicResp::find_status`] for per-certificate results.
    #[must_use]
    pub fn status(&self) -> OcspResponseStatus {
        OcspResponseStatus::from(unsafe { sys::OCSP_response_status(self.ptr) })
    }

    /// Extract the signed inner response (`OCSP_BASICRESP*`).
    ///
    /// Only valid when [`Self::status`] is [`OcspResponseStatus::Successful`].
    ///
    /// # Errors
    ///
    /// Returns `Err` if the response has no basic response body (e.g. the
    /// top-level status is not `Successful`).
    pub fn basic(&self) -> Result<OcspBasicResp, ErrorStack> {
        let ptr = unsafe { sys::OCSP_response_get1_basic(self.ptr) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(OcspBasicResp { ptr })
    }

    /// Convenience: verify the basic response signature and look up a cert status
    /// in one call.
    ///
    /// Equivalent to `resp.basic()?.verify(store, 0)?; resp.basic()?.find_status(id)`.
    ///
    /// # Errors
    pub fn verified_status(
        &self,
        store: &crate::x509::X509Store,
        cert_id: &OcspCertId,
    ) -> Result<Option<OcspSingleStatus>, ErrorStack> {
        let basic = self.basic()?;
        // verify() returns Ok(false) when the signature is invalid — treat that
        // as an error to prevent certificate status from an unverified response.
        if !basic.verify(store, 0)? {
            return Err(ErrorStack::drain());
        }
        basic.find_status(cert_id)
    }

    /// Build a minimal `OCSP_RESPONSE` (status = successful, no basic response)
    /// and return it as DER. Used for testing only.
    #[cfg(test)]
    fn new_successful_der() -> Vec<u8> {
        // DER: SEQUENCE { ENUMERATED 0 }
        // OCSPResponseStatus successful(0) with no responseBytes.
        vec![0x30, 0x03, 0x0A, 0x01, 0x00]
    }
}

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

/// Convert an `ASN1_GENERALIZEDTIME*` (which is really `ASN1_STRING*`) to a
/// human-readable string via `ASN1_TIME_print` on a memory BIO.
fn generalizedtime_to_str(t: *mut sys::ASN1_GENERALIZEDTIME) -> Option<String> {
    if t.is_null() {
        return None;
    }
    // ASN1_GENERALIZEDTIME is typedef'd to asn1_string_st, same as ASN1_TIME.
    // ASN1_TIME_print handles both UTCTime and GeneralizedTime.
    let Ok(mut bio) = MemBio::new() else {
        // BIO allocation failed; clear the error queue so callers do not
        // see a stale allocation error as if it came from their own call.
        unsafe { sys::ERR_clear_error() };
        return None;
    };
    let rc = unsafe { sys::ASN1_TIME_print(bio.as_ptr(), t.cast::<sys::ASN1_TIME>()) };
    if rc != 1 {
        unsafe { sys::ERR_clear_error() };
        return None;
    }
    String::from_utf8(bio.into_vec()).ok()
}

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

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

    /// Build a minimal CA + end-entity certificate pair for testing.
    fn make_ca_and_ee() -> (
        crate::x509::X509,
        Pkey<Private>,
        crate::x509::X509,
        Pkey<Private>,
    ) {
        // CA key + cert (self-signed)
        let mut ca_kgen = KeygenCtx::new(c"ED25519").unwrap();
        let ca_priv = ca_kgen.generate().unwrap();
        let ca_pub = Pkey::<Public>::from(ca_priv.clone());

        let mut ca_name = X509NameOwned::new().unwrap();
        ca_name.add_entry_by_txt(c"CN", b"OCSP Test CA").unwrap();

        let ca_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(&ca_name)
            .unwrap()
            .set_issuer_name(&ca_name)
            .unwrap()
            .set_public_key(&ca_pub)
            .unwrap()
            .sign(&ca_priv, None)
            .unwrap()
            .build();

        // EE key + cert (signed by CA)
        let mut ee_kgen = KeygenCtx::new(c"ED25519").unwrap();
        let ee_priv = ee_kgen.generate().unwrap();
        let ee_pub = Pkey::<Public>::from(ee_priv.clone());

        let mut ee_name = X509NameOwned::new().unwrap();
        ee_name.add_entry_by_txt(c"CN", b"OCSP Test EE").unwrap();

        let ee_cert = X509Builder::new()
            .unwrap()
            .set_version(2)
            .unwrap()
            .set_serial_number(2)
            .unwrap()
            .set_not_before_offset(0)
            .unwrap()
            .set_not_after_offset(365 * 86400)
            .unwrap()
            .set_subject_name(&ee_name)
            .unwrap()
            .set_issuer_name(&ca_name)
            .unwrap()
            .set_public_key(&ee_pub)
            .unwrap()
            .sign(&ca_priv, None)
            .unwrap()
            .build();

        (ca_cert, ca_priv, ee_cert, ee_priv)
    }

    // ── OcspCertId tests ──────────────────────────────────────────────────────

    #[test]
    fn cert_id_from_cert() {
        let (ca_cert, _, ee_cert, _) = make_ca_and_ee();
        // SHA-1 is the OCSP default; pass None for the digest.
        let id = OcspCertId::from_cert(None, &ee_cert, &ca_cert).unwrap();
        // Clone must not crash.
        let _id2 = id.clone();
    }

    // ── OcspRequest tests ─────────────────────────────────────────────────────

    #[test]
    fn ocsp_request_new_and_to_der() {
        let req = OcspRequest::new().unwrap();
        let der = req.to_der().unwrap();
        assert!(!der.is_empty());
    }

    #[test]
    fn ocsp_request_with_cert_id() {
        let (ca_cert, _, ee_cert, _) = make_ca_and_ee();
        let id = OcspCertId::from_cert(None, &ee_cert, &ca_cert).unwrap();

        let mut req = OcspRequest::new().unwrap();
        req.add_cert_id(id).unwrap();
        let der = req.to_der().unwrap();
        assert!(!der.is_empty());
        // DER with a cert ID is larger than an empty request.
        let empty_der = OcspRequest::new().unwrap().to_der().unwrap();
        assert!(der.len() > empty_der.len());
    }

    #[test]
    fn ocsp_request_der_roundtrip() {
        let req = OcspRequest::new().unwrap();
        let der = req.to_der().unwrap();
        let req2 = OcspRequest::from_der(&der).unwrap();
        assert_eq!(req2.to_der().unwrap(), der);
    }

    // ── OcspResponse tests ────────────────────────────────────────────────────

    #[test]
    fn ocsp_response_status_decode() {
        let der = OcspResponse::new_successful_der();
        let resp = OcspResponse::from_der(&der).unwrap();
        assert_eq!(resp.status(), OcspResponseStatus::Successful);
    }

    #[test]
    fn ocsp_response_der_roundtrip() {
        let der = OcspResponse::new_successful_der();
        let resp = OcspResponse::from_der(&der).unwrap();
        assert_eq!(resp.to_der().unwrap(), der);
    }

    #[test]
    fn ocsp_response_basic_fails_without_body() {
        // A response with only a status code and no responseBytes has no basic resp.
        let der = OcspResponse::new_successful_der();
        let resp = OcspResponse::from_der(&der).unwrap();
        // basic() should return Err because there is no responseBytes.
        assert!(resp.basic().is_err());
    }

    // ── OcspBasicResp / find_status tests ────────────────────────────────────
    //
    // Building a real OCSP_BASICRESP from scratch requires the full OCSP
    // responder stack (OCSP_basic_sign, OCSP_basic_add1_status) which is
    // outside the scope of unit tests. Instead we verify that find_status
    // returns None when the cert is not in the response (requires a real
    // OCSP response DER), and test the X509Store/X509StoreCtx path via
    // the integration-level store tests in x509.rs.
    //
    // The important invariants (OcspCertId::from_cert, add_cert_id, DER
    // round-trip) are covered by the tests above.
    //
    // If a real OCSP response is available (e.g. from a test OCSP responder),
    // use OcspResponse::from_der + basic() + find_status() to validate the
    // full stack.

    // ── Constructor smoke tests ───────────────────────────────────────────────

    #[test]
    fn ocsp_basicresp_new_and_drop() {
        // OcspBasicResp::new() must succeed and the value must drop cleanly.
        let resp = OcspBasicResp::new().unwrap();
        drop(resp);
    }

    #[test]
    fn ocsp_certid_new_and_drop() {
        // OcspCertId::new() must succeed and the value must drop cleanly.
        let id = OcspCertId::new().unwrap();
        drop(id);
    }

    #[test]
    fn ocsp_response_new_and_drop() {
        // OcspResponse::new() must succeed and the value must drop cleanly.
        let resp = OcspResponse::new().unwrap();
        drop(resp);
    }

    // ── get_response / find_response tests ───────────────────────────────────

    #[test]
    fn ocsp_resp_get0_out_of_range() {
        // A fresh, empty OcspBasicResp has no entries; get_response(0) must
        // return None rather than panicking or returning a dangling pointer.
        let resp = OcspBasicResp::new().unwrap();
        assert!(resp.get_response(0).is_none());
    }

    #[test]
    fn ocsp_find_response_empty() {
        // find_response on an empty basic response must return None.
        let resp = OcspBasicResp::new().unwrap();
        let id = OcspCertId::new().unwrap();
        assert!(resp.find_response(&id).is_none());
    }

    // NOTE: Testing find_response and BorrowedOcspSingleResp::status with a
    // real populated response requires the full responder stack
    // (OCSP_basic_add1_status, OCSP_basic_sign).  That is integration-level
    // work tracked as a follow-up TODO.
}
