//! Object Identifiers — [`Oid`] wraps `ASN1_OBJECT` for arbitrary OID comparison.
//!
//! Unlike [`crate::x509::nid_from_text`], which only recognises OIDs registered in
//! OpenSSL's built-in OBJ table, [`Oid::from_text`] creates an object for any dotted
//! decimal OID string, including vendor- and application-specific OIDs that have no
//! registered NID.

use crate::error::ErrorStack;
use native_ossl_sys as sys;
use std::ffi::CString;
use std::fmt;

// ── Oid ───────────────────────────────────────────────────────────────────────

/// An owned Object Identifier wrapping an OpenSSL `ASN1_OBJECT*`.
///
/// [`Oid::from_text`] accepts any dotted decimal OID string (`"1.3.6.1.5.2.3.5"`)
/// or registered short name (`"sha256"`), returning an `Oid` even for OIDs that are
/// not in OpenSSL's built-in NID table.  For registered OIDs, [`Oid::nid`] returns
/// the corresponding NID; for unregistered OIDs it returns 0 (`NID_undef`).
///
/// Two `Oid` values compare equal when their ASN.1 encodings are identical,
/// regardless of whether either has a registered NID.
pub struct Oid {
    ptr: *mut sys::ASN1_OBJECT,
}

// SAFETY: ASN1_OBJECT is a read-only value object after construction; no
// thread-local state is mutated through a shared reference.
unsafe impl Send for Oid {}
unsafe impl Sync for Oid {}

impl Clone for Oid {
    fn clone(&self) -> Self {
        // SAFETY: self.ptr is a valid, non-null ASN1_OBJECT*; OBJ_dup allocates
        // a new copy on the heap and returns it (or NULL on OOM, which would
        // cause a panic in the unwrap — acceptable since this matches Rust's
        // global allocator contract).
        let dup = unsafe { sys::OBJ_dup(self.ptr) };
        assert!(!dup.is_null(), "OBJ_dup: allocation failed");
        Oid { ptr: dup }
    }
}

impl Drop for Oid {
    fn drop(&mut self) {
        // SAFETY: ptr is a valid, non-null, owned ASN1_OBJECT* allocated by
        // OBJ_txt2obj(..., 1) (copy=1 → heap-allocated).
        unsafe { sys::ASN1_OBJECT_free(self.ptr) };
    }
}

impl Oid {
    /// Wrap a raw, owned `ASN1_OBJECT*`.
    ///
    /// # Safety
    ///
    /// `ptr` must be a valid, non-null `ASN1_OBJECT*` allocated on the heap
    /// (e.g. by `OBJ_dup` or `OBJ_txt2obj` with copy=1).  Ownership is
    /// transferred to the returned `Oid`; the caller must not free `ptr`.
    pub(crate) unsafe fn from_ptr(ptr: *mut sys::ASN1_OBJECT) -> Self {
        Oid { ptr }
    }

    /// Construct from a dotted decimal OID string (`"1.3.6.1.5.2.3.5"`) or a
    /// registered short name (`"sha256"`).
    ///
    /// Unlike [`crate::x509::nid_from_text`], this succeeds for OIDs that are
    /// not registered in OpenSSL's built-in NID table.
    ///
    /// # Errors
    ///
    /// Returns an [`ErrorStack`] if `s` is not a valid OID string or name, or
    /// if `s` contains an interior NUL byte.
    pub fn from_text(s: &str) -> Result<Self, ErrorStack> {
        let cs = CString::new(s).map_err(|_| ErrorStack::drain())?;
        // SAFETY:
        // - cs.as_ptr() is valid, NUL-terminated, for the duration of this call
        // - copy=1 → OBJ_txt2obj allocates a new ASN1_OBJECT on the heap; we own it
        // - no mutable global state is accessed (the OBJ table may be read under
        //   a shared lock in threaded builds, which OpenSSL itself guarantees)
        let ptr = unsafe { sys::OBJ_txt2obj(cs.as_ptr(), 0) };
        if ptr.is_null() {
            Err(ErrorStack::drain())
        } else {
            Ok(Oid { ptr })
        }
    }

    /// Return the NID for this OID, or `0` (`NID_undef`) if the OID is not
    /// registered in OpenSSL's built-in NID table.
    #[must_use]
    pub fn nid(&self) -> i32 {
        // SAFETY: self.ptr is a valid, non-null ASN1_OBJECT* for the lifetime of self.
        unsafe { sys::OBJ_obj2nid(self.ptr) }
    }
}

impl PartialEq for Oid {
    fn eq(&self, other: &Self) -> bool {
        // SAFETY: both ptrs are valid, non-null, owned ASN1_OBJECT* values.
        // OBJ_cmp compares the DER encoding; negative/zero/positive like memcmp.
        unsafe { sys::OBJ_cmp(self.ptr, other.ptr) == 0 }
    }
}

impl Eq for Oid {}

impl fmt::Debug for Oid {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Oid({self})")
    }
}

impl fmt::Display for Oid {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // First call with a zero-length buffer to learn the required length.
        // SAFETY: buf=NULL, buf_len=0 is a documented way to query length in OpenSSL.
        let len = unsafe { sys::OBJ_obj2txt(std::ptr::null_mut(), 0, self.ptr, 1) };
        // len <= 0 means error or empty; bail out early.
        let Ok(len_usize) = usize::try_from(len) else {
            return f.write_str("<invalid-oid>");
        };
        let mut buf = vec![0u8; len_usize + 1];
        // SAFETY: buf is allocated to len+1 bytes; OBJ_obj2txt writes at most buf_len
        // bytes including the NUL terminator; no_name=1 forces dotted decimal output.
        let written = unsafe { sys::OBJ_obj2txt(buf.as_mut_ptr().cast(), len + 1, self.ptr, 1) };
        let Ok(written_usize) = usize::try_from(written) else {
            return f.write_str("<invalid-oid>");
        };
        buf.truncate(written_usize);
        f.write_str(std::str::from_utf8(&buf).unwrap_or("<invalid-oid>"))
    }
}

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

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

    #[test]
    fn oid_from_registered_name() {
        // "sha256" is a registered short name; nid() must return a known NID
        let oid = Oid::from_text("sha256").expect("sha256 must be recognised");
        assert_ne!(oid.nid(), 0, "registered OID must have a NID");
    }

    #[test]
    fn oid_from_dotted_decimal() {
        // 2.5.4.3 is commonName — registered, but supplied as dotted decimal
        let oid = Oid::from_text("2.5.4.3").expect("commonName OID must parse");
        assert_ne!(oid.nid(), 0);
    }

    #[test]
    fn oid_unregistered_dotted_decimal() {
        // A made-up OID that is not in the OpenSSL NID table.
        // Using a private-use arc (2.25.*) with a random UUID-based component
        // that is guaranteed not to be registered.
        let oid = Oid::from_text("2.25.999999999999999999999999")
            .expect("arbitrary dotted OID must parse with OBJ_txt2obj");
        // nid() returns 0 (NID_undef) for unregistered OIDs
        assert_eq!(oid.nid(), 0);
    }

    #[test]
    fn oid_equality_same_oid() {
        let a = Oid::from_text("2.5.4.3").unwrap();
        let b = Oid::from_text("2.5.4.3").unwrap();
        assert_eq!(a, b);
    }

    #[test]
    fn oid_equality_different_oids() {
        let a = Oid::from_text("2.5.4.3").unwrap();
        let b = Oid::from_text("2.5.4.6").unwrap();
        assert_ne!(a, b);
    }

    #[test]
    fn oid_display_dotted_decimal() {
        let oid = Oid::from_text("2.5.4.3").unwrap();
        let s = oid.to_string();
        assert_eq!(s, "2.5.4.3");
    }

    #[test]
    fn oid_display_unregistered() {
        // Unregistered OID must round-trip through Display
        let dotted = "1.3.6.1.5.2.3.5";
        let oid = Oid::from_text(dotted).unwrap();
        assert_eq!(oid.to_string(), dotted);
    }

    #[test]
    fn oid_registered_name_display_is_dotted() {
        // When no_name=1 is set, Display always produces dotted decimal, never a name.
        let oid = Oid::from_text("sha256").unwrap();
        let s = oid.to_string();
        // Must be dotted decimal, not "sha256"
        assert!(
            s.contains('.'),
            "Display must produce dotted decimal, got: {s}"
        );
        assert!(
            !s.contains("sha"),
            "Display must not contain short name: {s}"
        );
    }

    #[test]
    fn oid_interior_nul_returns_error() {
        assert!(Oid::from_text("1.2\x003.4").is_err());
    }
}
