//! Immutable identifier strings for quoted or unquoted source text.
//!
//! [`IdentStr`] keeps the identifier text together with the quote style from
//! the source, when one was present. Matching, ordering, and hashing are
//! case-insensitive by default.
//!
//! Use [`IdentStr::new`] for source text that may be quoted. Use
//! [`IdentStr::from_raw`] when the input is already identifier text.
//!
//! [`Key`] stores normalized lookup text for maps that do not need to retain
//! the original spelling or quote style.
//!
//! [`Quote`] with [`policy::Ascii`] covers common quoted identifier input.
//! Enable the `unicode` feature for Unicode matching modes and security
//! helpers.

pub mod key;
pub mod policy;
#[cfg(feature = "unicode")]
pub mod unicode;
#[cfg(feature = "unicode")]
pub use unicode::security;

mod input;
mod parse;
mod quote;
mod render;
mod repr;
mod storage;

use std::{
    borrow::Cow,
    cmp::Ordering,
    convert::Infallible,
    fmt,
    hash::{Hash, Hasher},
    marker::PhantomData,
    ops::Deref,
    rc::Rc,
    str::FromStr,
    sync::Arc,
};

pub use input::Input;
pub use key::Key;
pub use quote::Quote;
pub use storage::{ArcStorage, BoxStorage, RcStorage, Storage};

use policy::{KeyPolicy, Policy};
use repr::{INLINE_CAPACITY, QUOTED_INLINE_CAPACITY, Repr};

const INLINE_LEN_U8: [u8; INLINE_CAPACITY + 1] = [
    0,
    1,
    2,
    3,
    4,
    5,
    6,
    7,
    8,
    #[cfg(target_pointer_width = "64")]
    9,
    #[cfg(target_pointer_width = "64")]
    10,
    #[cfg(target_pointer_width = "64")]
    11,
    #[cfg(target_pointer_width = "64")]
    12,
    #[cfg(target_pointer_width = "64")]
    13,
    #[cfg(target_pointer_width = "64")]
    14,
    #[cfg(target_pointer_width = "64")]
    15,
    #[cfg(target_pointer_width = "64")]
    16,
];

/// Quote syntax used by [`IdentStr`].
///
/// [`Quote`] covers common single-byte delimiters. Implement this trait when
/// your input format uses different delimiter pairs.
pub trait QuoteTag: Copy + Eq + 'static {
    /// Returns a stable nonzero code for this quote style.
    ///
    /// Codes `1..=31` are valid. Code `0` is reserved for unquoted text.
    fn encode(self) -> u8;

    /// Restores a quote style from [`encode`](Self::encode).
    ///
    /// Return `None` for unknown codes.
    fn decode(code: u8) -> Option<Self>;

    /// Returns the ASCII opening delimiter.
    #[must_use]
    fn open_byte(self) -> u8;

    /// Returns the ASCII closing delimiter.
    #[must_use]
    fn close_byte(self) -> u8;

    /// Detects quoted source text and returns the quote style with the inner
    /// text.
    ///
    /// This only identifies the outer delimiter pair. [`IdentStr::new`] handles
    /// doubled closing delimiters after the split.
    ///
    /// Return `None` when `value` should be treated as unquoted text.
    #[must_use]
    fn split_source(value: &str) -> Option<(Self, &str)>;
}

/// Immutable identifier text with an optional quote style.
///
/// Equality, ordering, and hashing follow the configured identifier policy `P`.
/// The quote style is available separately via [`IdentStr::quote`] and does not
/// participate in those traits.
pub struct IdentStr<Q: QuoteTag = Quote, P: Policy = policy::Ascii, S: Storage = BoxStorage> {
    repr: Repr,
    marker: PhantomData<(Q, P, S, S::Owned)>,
}

/// Display adapter for rendering an identifier with its preserved quotes.
#[derive(Clone, Copy)]
pub struct Quoted<'a, Q: QuoteTag = Quote, P: Policy = policy::Ascii, S: Storage = BoxStorage> {
    ident: &'a IdentStr<Q, P, S>,
}

impl<Q: QuoteTag, P: Policy, S: Storage> IdentStr<Q, P, S> {
    /// Maximum byte length accepted by [`new_inline`](Self::new_inline).
    pub const INLINE_CAPACITY: usize = INLINE_CAPACITY;

    /// Maximum byte length accepted by [`with_quote_inline`](Self::with_quote_inline).
    pub const QUOTED_INLINE_CAPACITY: usize = QUOTED_INLINE_CAPACITY;

    /// Creates an identifier from source text.
    ///
    /// When the input is surrounded by a recognized quote pair, the surrounding
    /// quotes are removed and doubled closing delimiters are unescaped.
    /// Otherwise, including malformed quoted text, the input is stored as
    /// unquoted identifier text.
    #[must_use]
    #[inline]
    pub fn new(value: impl Input<S>) -> Self {
        value.into_source::<Q, P>()
    }

    /// Creates an identifier from source text with caller-provided inner
    /// parsing for quoted input.
    ///
    /// `Q` still decides whether `source` is wrapped in a recognized quote
    /// pair. When it is, `parser` receives the quote style and the raw inner
    /// text without the outer delimiters. The callback may return a borrowed
    /// `&str`, an owned [`String`], or a [`Cow`].
    ///
    /// The callback runs instead of the built-in doubled-delimiter unescaping.
    ///
    /// Unquoted input is stored as raw identifier text and does not call
    /// `parser`.
    ///
    /// # Errors
    ///
    /// Returns the error from `parser` when quoted input cannot be parsed.
    #[inline]
    pub fn try_new_with<'a, E, R>(
        source: &'a str,
        parser: impl FnOnce(Q, &'a str) -> Result<R, E>,
    ) -> Result<Self, E>
    where
        R: Into<Cow<'a, str>>,
    {
        let Some((quote, inner)) = Q::split_source(source) else {
            return Ok(Self::from_borrowed(source, None));
        };

        let value = parser(quote, inner)?.into();
        Ok(match value {
            Cow::Borrowed(value) => Self::from_borrowed(value, Some(quote)),
            Cow::Owned(value) => Self::from_string(value, Some(quote)),
        })
    }

    /// Creates an identifier from already-unquoted text.
    ///
    /// This stores the input as identifier text and does not parse quote
    /// delimiters.
    #[must_use]
    #[inline]
    pub fn from_raw(value: impl Input<S>) -> Self {
        value.into_raw::<Q, P>(None)
    }

    /// Creates an identifier from already-unquoted text with quote style.
    ///
    /// `quote` may be a quote style or any value that can be converted into
    /// one, such as an opening delimiter character for [`Quote`].
    ///
    /// # Panics
    ///
    /// Panics if `quote` cannot be converted into `Q`. Use
    /// [`try_with_quote`](Self::try_with_quote) when the delimiter may be
    /// invalid.
    #[must_use]
    #[inline]
    pub fn with_quote<I>(value: impl Input<S>, quote: I) -> Self
    where
        I: TryInto<Q>,
        I::Error: fmt::Debug,
    {
        Self::try_with_quote(value, quote).expect("invalid quote delimiter")
    }

    /// Creates an identifier from already-unquoted text with quote style.
    ///
    /// # Errors
    ///
    /// Returns the conversion error when `quote` cannot be converted into `Q`.
    #[inline]
    pub fn try_with_quote<I>(value: impl Input<S>, quote: I) -> Result<Self, I::Error>
    where
        I: TryInto<Q>,
    {
        let quote = quote.try_into()?;
        Ok(value.into_raw::<Q, P>(Some(quote)))
    }

    /// Returns the stored identifier text without surrounding quotes.
    #[must_use]
    #[inline]
    pub fn as_str(&self) -> &str {
        self.repr.as_str()
    }

    /// Returns the stored identifier bytes without surrounding quotes.
    #[must_use]
    #[inline]
    pub fn as_bytes(&self) -> &[u8] {
        self.repr.as_bytes()
    }

    /// Returns the preserved quote delimiter, if the identifier was quoted.
    #[must_use]
    #[inline]
    pub fn quote(&self) -> Option<Q> {
        match self.repr.quote_tag() {
            0 => None,
            tag => Q::decode(tag),
        }
    }

    /// Returns `true` when the text is held without an allocation.
    #[must_use]
    pub const fn is_inline(&self) -> bool {
        self.repr.is_inline()
    }

    /// Creates an empty identifier.
    #[must_use]
    pub const fn empty() -> Self {
        Self {
            repr: Repr::new_inline(Self::inline_bytes("", None)),
            marker: PhantomData,
        }
    }

    /// Creates an identifier without allocating.
    ///
    /// This is available in const contexts and returns `None` when `value`
    /// is too long to store without allocating.
    #[must_use]
    pub const fn new_inline(value: &str) -> Option<Self> {
        if value.len() > INLINE_CAPACITY {
            return None;
        }

        Some(Self {
            repr: Repr::new_inline(Self::inline_bytes(value, None)),
            marker: PhantomData,
        })
    }

    /// Converts this identifier into the selected owned string type.
    #[must_use]
    pub fn into_owned(self) -> S::Owned {
        let this = std::mem::ManuallyDrop::new(self);
        if this.repr.is_inline() {
            S::from_borrowed(this.as_str())
        } else {
            S::into_owned(this.repr)
        }
    }

    /// Builds an owned lookup key under `P`.
    ///
    /// Use this when a map should store only normalized lookup text rather
    /// than the original spelling and quote style.
    #[must_use]
    pub fn to_key(&self) -> Key<P>
    where
        P: KeyPolicy,
    {
        Key::from(self)
    }

    /// Returns a confusable skeleton for this identifier.
    #[cfg(feature = "unicode")]
    #[must_use]
    pub fn confusable_skeleton(&self) -> security::Skeleton {
        security::skeleton(self.as_str())
    }

    /// Returns Unicode security information for this identifier.
    #[cfg(feature = "unicode")]
    #[must_use]
    pub fn analyze_security(&self) -> security::Analysis {
        security::analyze(self.as_str())
    }

    /// Returns a formatter that renders this identifier with its quotes.
    ///
    /// Use this with `format!`, `write!`, or logging APIs when an owned
    /// [`String`] is not needed.
    #[must_use]
    pub const fn display_quoted(&self) -> Quoted<'_, Q, P, S> {
        Quoted { ident: self }
    }

    /// Writes this identifier with its quotes.
    ///
    /// # Errors
    ///
    /// Returns an error if the destination writer fails.
    pub fn write_quoted(&self, output: &mut (impl fmt::Write + ?Sized)) -> fmt::Result {
        render::write_quoted(self.as_str(), self.quote(), output)
    }

    /// Returns this identifier rendered with its quotes.
    #[must_use]
    pub fn to_quoted_string(&self) -> String {
        render::to_string(self.as_str(), self.quote())
    }

    #[inline]
    pub(crate) fn from_borrowed(value: &str, quote: Option<Q>) -> Self {
        if let Some(inline) = Self::try_inline(value, quote) {
            return inline;
        }

        Self::from_heap(S::from_borrowed(value), quote)
    }

    #[inline]
    pub(crate) fn from_owned(value: S::Owned, quote: Option<Q>) -> Self {
        if let Some(inline) = Self::try_inline(value.as_ref(), quote) {
            return inline;
        }

        Self::from_heap(value, quote)
    }

    #[inline]
    pub(crate) fn from_source_borrowed(value: &str) -> Self {
        let Some((quote, value)) = parse::quoted_source::<Q>(value) else {
            return Self::from_borrowed(value, None);
        };

        match value {
            Cow::Borrowed(value) => Self::from_borrowed(value, Some(quote)),
            Cow::Owned(value) => Self::from_string(value, Some(quote)),
        }
    }

    #[inline]
    pub(crate) fn from_source_owned(value: S::Owned) -> Self {
        let Some((quote, value)) = parse::quoted_source::<Q>(value.as_ref()) else {
            return Self::from_owned(value, None);
        };

        match value {
            Cow::Borrowed(value) => Self::from_borrowed(value, Some(quote)),
            Cow::Owned(value) => Self::from_string(value, Some(quote)),
        }
    }

    #[inline]
    pub(crate) fn from_source_string(value: String) -> Self {
        let Some((quote, value)) = parse::quoted_source::<Q>(&value) else {
            return Self::from_string(value, None);
        };

        match value {
            Cow::Borrowed(value) => Self::from_borrowed(value, Some(quote)),
            Cow::Owned(value) => Self::from_string(value, Some(quote)),
        }
    }

    #[inline]
    pub(crate) fn from_string(value: String, quote: Option<Q>) -> Self {
        if let Some(inline) = Self::try_inline(&value, quote) {
            return inline;
        }

        Self::from_heap(S::from_box(value.into_boxed_str()), quote)
    }

    #[inline]
    fn from_inline(value: &str, quote_tag: Option<u8>) -> Self {
        Self {
            repr: Repr::new_inline(Self::inline_bytes(value, quote_tag)),
            marker: PhantomData,
        }
    }

    #[inline]
    fn from_heap(value: S::Owned, quote: Option<Q>) -> Self {
        Self {
            repr: S::into_repr(value, Self::heap_tag(quote)),
            marker: PhantomData,
        }
    }

    #[inline]
    fn try_inline(value: &str, quote: Option<Q>) -> Option<Self> {
        match quote {
            Some(quote) => {
                let tag = Self::inline_tag(quote)?;
                (value.len() <= QUOTED_INLINE_CAPACITY).then(|| Self::from_inline(value, Some(tag)))
            }
            None => (value.len() <= INLINE_CAPACITY).then(|| Self::from_inline(value, None)),
        }
    }

    #[inline]
    fn inline_tag(quote: Q) -> Option<u8> {
        let tag = quote.encode();
        assert!(tag != 0, "quote tag 0 is reserved for unquoted text");
        (tag <= Repr::MAX_INLINE_QUOTE_TAG).then_some(tag)
    }

    #[inline]
    fn heap_tag(quote: Option<Q>) -> u8 {
        let Some(quote) = quote else {
            return 0;
        };

        let tag = quote.encode();
        assert!(tag != 0, "quote tag 0 is reserved for unquoted text");
        assert!(
            tag <= Repr::MAX_HEAP_QUOTE_TAG,
            "quote tag does not fit heap marker space",
        );
        tag
    }

    const fn inline_bytes(value: &str, quote_tag: Option<u8>) -> [u8; INLINE_CAPACITY] {
        let src = value.as_bytes();
        let mut bytes = [0; INLINE_CAPACITY];
        let mut index = 0;
        while index < src.len() {
            bytes[index] = src[index];
            index += 1;
        }

        match quote_tag {
            Some(tag) => {
                if src.len() < QUOTED_INLINE_CAPACITY {
                    bytes[QUOTED_INLINE_CAPACITY - 1] =
                        Repr::short_inline_byte(INLINE_LEN_U8[src.len()]);
                }
                bytes[INLINE_CAPACITY - 1] = Repr::quoted_inline_byte(tag);
            }
            None if src.len() < INLINE_CAPACITY => {
                bytes[INLINE_CAPACITY - 1] = Repr::short_inline_byte(INLINE_LEN_U8[src.len()]);
            }
            None => {}
        }

        bytes
    }
}

impl<P: Policy, S: Storage> IdentStr<Quote, P, S> {
    /// Creates a quoted identifier without allocating.
    ///
    /// This is available in const contexts and returns `None` when `value`
    /// is too long to store without allocating.
    #[must_use]
    pub const fn with_quote_inline(value: &str, quote: Quote) -> Option<Self> {
        if value.len() > QUOTED_INLINE_CAPACITY {
            return None;
        }

        Some(Self {
            repr: Repr::new_inline(Self::inline_bytes(value, Some(quote.tag()))),
            marker: PhantomData,
        })
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> fmt::Display for Quoted<'_, Q, P, S> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.ident.write_quoted(f)
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> Default for IdentStr<Q, P, S> {
    fn default() -> Self {
        Self::empty()
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> Clone for IdentStr<Q, P, S> {
    #[inline]
    fn clone(&self) -> Self {
        let repr = if self.repr.is_inline() {
            self.repr
        } else {
            S::clone_repr(&self.repr)
        };

        Self {
            repr,
            marker: PhantomData,
        }
    }
}

impl<Q: QuoteTag + fmt::Debug, P: Policy, S: Storage> fmt::Debug for IdentStr<Q, P, S> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut debug = f.debug_struct("IdentStr");
        debug.field("value", &self.as_str());
        debug.field("quote", &self.quote());
        debug.finish()
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> fmt::Display for IdentStr<Q, P, S> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> Drop for IdentStr<Q, P, S> {
    fn drop(&mut self) {
        if self.repr.is_inline() {
            return;
        }

        S::drop_repr(self.repr);
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> Deref for IdentStr<Q, P, S> {
    type Target = str;

    #[inline]
    fn deref(&self) -> &Self::Target {
        self.as_str()
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> AsRef<str> for IdentStr<Q, P, S> {
    #[inline]
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> AsRef<[u8]> for IdentStr<Q, P, S> {
    #[inline]
    fn as_ref(&self) -> &[u8] {
        self.as_bytes()
    }
}

impl<Q: QuoteTag, P: Policy, L: Storage, R: Storage> PartialEq<IdentStr<Q, P, R>>
    for IdentStr<Q, P, L>
{
    #[inline]
    fn eq(&self, other: &IdentStr<Q, P, R>) -> bool {
        P::eq(self.as_str(), other.as_str())
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> Eq for IdentStr<Q, P, S> {}

impl<Q: QuoteTag, P: Policy, L: Storage, R: Storage> PartialOrd<IdentStr<Q, P, R>>
    for IdentStr<Q, P, L>
{
    #[inline]
    fn partial_cmp(&self, other: &IdentStr<Q, P, R>) -> Option<Ordering> {
        Some(P::cmp(self.as_str(), other.as_str()))
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> Ord for IdentStr<Q, P, S> {
    #[inline]
    fn cmp(&self, other: &Self) -> Ordering {
        P::cmp(self.as_str(), other.as_str())
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> Hash for IdentStr<Q, P, S> {
    #[inline]
    fn hash<H: Hasher>(&self, state: &mut H) {
        P::hash(self.as_str(), state);
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> PartialEq<str> for IdentStr<Q, P, S> {
    #[inline]
    fn eq(&self, other: &str) -> bool {
        P::eq(self.as_str(), other)
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> PartialEq<&str> for IdentStr<Q, P, S> {
    #[inline]
    fn eq(&self, other: &&str) -> bool {
        P::eq(self.as_str(), other)
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> PartialEq<String> for IdentStr<Q, P, S> {
    #[inline]
    fn eq(&self, other: &String) -> bool {
        P::eq(self.as_str(), other)
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> PartialEq<&String> for IdentStr<Q, P, S> {
    #[inline]
    fn eq(&self, other: &&String) -> bool {
        P::eq(self.as_str(), other)
    }
}

impl<'a, Q: QuoteTag, P: Policy, S: Storage> PartialEq<Cow<'a, str>> for IdentStr<Q, P, S> {
    #[inline]
    fn eq(&self, other: &Cow<'a, str>) -> bool {
        P::eq(self.as_str(), other)
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> PartialEq<Box<str>> for IdentStr<Q, P, S> {
    #[inline]
    fn eq(&self, other: &Box<str>) -> bool {
        P::eq(self.as_str(), other)
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> PartialEq<Arc<str>> for IdentStr<Q, P, S> {
    #[inline]
    fn eq(&self, other: &Arc<str>) -> bool {
        P::eq(self.as_str(), other)
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> PartialEq<Rc<str>> for IdentStr<Q, P, S> {
    #[inline]
    fn eq(&self, other: &Rc<str>) -> bool {
        P::eq(self.as_str(), other)
    }
}

impl<Q: QuoteTag, P: KeyPolicy, S: Storage> PartialEq<Key<P>> for IdentStr<Q, P, S> {
    #[inline]
    fn eq(&self, other: &Key<P>) -> bool {
        P::eq(self.as_str(), other.as_str())
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> PartialEq<IdentStr<Q, P, S>> for str {
    #[inline]
    fn eq(&self, other: &IdentStr<Q, P, S>) -> bool {
        P::eq(self, other.as_str())
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> PartialEq<IdentStr<Q, P, S>> for &str {
    #[inline]
    fn eq(&self, other: &IdentStr<Q, P, S>) -> bool {
        P::eq(self, other.as_str())
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> PartialEq<IdentStr<Q, P, S>> for String {
    #[inline]
    fn eq(&self, other: &IdentStr<Q, P, S>) -> bool {
        P::eq(self, other.as_str())
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> PartialEq<IdentStr<Q, P, S>> for &String {
    #[inline]
    fn eq(&self, other: &IdentStr<Q, P, S>) -> bool {
        P::eq(self, other.as_str())
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> PartialEq<IdentStr<Q, P, S>> for Cow<'_, str> {
    #[inline]
    fn eq(&self, other: &IdentStr<Q, P, S>) -> bool {
        P::eq(self, other.as_str())
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> PartialEq<IdentStr<Q, P, S>> for Box<str> {
    #[inline]
    fn eq(&self, other: &IdentStr<Q, P, S>) -> bool {
        P::eq(self, other.as_str())
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> PartialEq<IdentStr<Q, P, S>> for Arc<str> {
    #[inline]
    fn eq(&self, other: &IdentStr<Q, P, S>) -> bool {
        P::eq(self, other.as_str())
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> PartialEq<IdentStr<Q, P, S>> for Rc<str> {
    #[inline]
    fn eq(&self, other: &IdentStr<Q, P, S>) -> bool {
        P::eq(self, other.as_str())
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> From<&str> for IdentStr<Q, P, S> {
    #[inline]
    fn from(value: &str) -> Self {
        Self::from_source_borrowed(value)
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> From<String> for IdentStr<Q, P, S> {
    #[inline]
    fn from(value: String) -> Self {
        Self::from_source_string(value)
    }
}

impl<'a, Q: QuoteTag, P: Policy, S: Storage> From<Cow<'a, str>> for IdentStr<Q, P, S> {
    #[inline]
    fn from(value: Cow<'a, str>) -> Self {
        match value {
            Cow::Borrowed(value) => Self::from_source_borrowed(value),
            Cow::Owned(value) => Self::from_source_string(value),
        }
    }
}

impl<Q: QuoteTag, P: Policy> From<Box<str>> for IdentStr<Q, P, BoxStorage> {
    fn from(value: Box<str>) -> Self {
        Self::from_source_owned(value)
    }
}

impl<Q: QuoteTag, P: Policy> From<Arc<str>> for IdentStr<Q, P, ArcStorage> {
    fn from(value: Arc<str>) -> Self {
        Self::from_source_owned(value)
    }
}

impl<Q: QuoteTag, P: Policy> From<Rc<str>> for IdentStr<Q, P, RcStorage> {
    fn from(value: Rc<str>) -> Self {
        Self::from_source_owned(value)
    }
}

impl<Q: QuoteTag, P: Policy> From<IdentStr<Q, P, BoxStorage>> for Box<str> {
    fn from(value: IdentStr<Q, P, BoxStorage>) -> Self {
        value.into_owned()
    }
}

impl<Q: QuoteTag, P: Policy> From<IdentStr<Q, P, BoxStorage>> for String {
    fn from(value: IdentStr<Q, P, BoxStorage>) -> Self {
        let value: Box<str> = value.into();
        value.into_string()
    }
}

impl<Q: QuoteTag, P: Policy> From<IdentStr<Q, P, ArcStorage>> for Arc<str> {
    fn from(value: IdentStr<Q, P, ArcStorage>) -> Self {
        value.into_owned()
    }
}

impl<Q: QuoteTag, P: Policy> From<IdentStr<Q, P, RcStorage>> for Rc<str> {
    fn from(value: IdentStr<Q, P, RcStorage>) -> Self {
        value.into_owned()
    }
}

impl<Q: QuoteTag, P: Policy, S: Storage> FromStr for IdentStr<Q, P, S> {
    type Err = Infallible;

    #[inline]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Self::from_source_borrowed(s))
    }
}

#[cfg(test)]
mod tests {
    #[cfg(feature = "unicode")]
    use super::security;
    use super::{ArcStorage, BoxStorage, IdentStr, Key, Quote, QuoteTag, RcStorage, policy};
    use std::{
        cmp::Ordering,
        hash::{Hash, Hasher},
        mem::size_of,
        rc::Rc,
        str::FromStr,
        sync::Arc,
    };

    #[repr(u8)]
    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    enum TestQuote {
        Double = 1,
        Single = 2,
        Backtick = 3,
        Bracket = 4,
    }

    impl QuoteTag for TestQuote {
        fn encode(self) -> u8 {
            self as u8
        }

        fn decode(tag: u8) -> Option<Self> {
            match tag {
                1 => Some(Self::Double),
                2 => Some(Self::Single),
                3 => Some(Self::Backtick),
                4 => Some(Self::Bracket),
                _ => None,
            }
        }

        fn open_byte(self) -> u8 {
            match self {
                Self::Double => b'"',
                Self::Single => b'\'',
                Self::Backtick => b'`',
                Self::Bracket => b'[',
            }
        }

        fn close_byte(self) -> u8 {
            match self {
                Self::Bracket => b']',
                Self::Double => b'"',
                Self::Single => b'\'',
                Self::Backtick => b'`',
            }
        }

        fn split_source(value: &str) -> Option<(Self, &str)> {
            let bytes = value.as_bytes();
            if bytes.len() < 2 {
                return None;
            }

            let quote = match (bytes[0], bytes[bytes.len() - 1]) {
                (b'"', b'"') => Self::Double,
                (b'\'', b'\'') => Self::Single,
                (b'`', b'`') => Self::Backtick,
                (b'[', b']') => Self::Bracket,
                _ => return None,
            };

            Some((quote, &value[1..value.len() - 1]))
        }
    }

    #[repr(u8)]
    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    enum HeapOnlyQuote {
        Custom = 0x10,
    }

    impl QuoteTag for HeapOnlyQuote {
        fn encode(self) -> u8 {
            self as u8
        }

        fn decode(tag: u8) -> Option<Self> {
            match tag {
                0x10 => Some(Self::Custom),
                _ => None,
            }
        }

        fn open_byte(self) -> u8 {
            b'<'
        }

        fn close_byte(self) -> u8 {
            b'\0'
        }

        fn split_source(_value: &str) -> Option<(Self, &str)> {
            None
        }
    }

    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    enum ZeroQuote {
        Custom,
    }

    impl QuoteTag for ZeroQuote {
        fn encode(self) -> u8 {
            match self {
                Self::Custom => 0,
            }
        }

        fn decode(_tag: u8) -> Option<Self> {
            None
        }

        fn open_byte(self) -> u8 {
            match self {
                Self::Custom => b'"',
            }
        }

        fn close_byte(self) -> u8 {
            match self {
                Self::Custom => b'"',
            }
        }

        fn split_source(_value: &str) -> Option<(Self, &str)> {
            None
        }
    }

    type TestIdentStr = IdentStr<TestQuote>;

    fn hash_value<T: Hash>(value: &T) -> u64 {
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        value.hash(&mut hasher);
        hasher.finish()
    }

    #[test]
    fn storage_uses_two_word_budget() {
        assert_eq!(size_of::<TestIdentStr>(), 2 * size_of::<usize>());
    }

    #[test]
    fn default_quote_tag_matches_sql_delimiters() {
        assert_eq!(Quote::from_open('"'), Some(Quote::Double));
        assert_eq!(Quote::from_open_byte(b'"'), Some(Quote::Double));
        assert_eq!(Quote::from_tag(Quote::Double.tag()), Some(Quote::Double));
        assert_eq!(Quote::try_from('"'), Ok(Quote::Double));
        assert_eq!(Quote::try_from(Quote::Double.tag()), Ok(Quote::Double));
        assert_eq!(Quote::from_open('\''), Some(Quote::Single));
        assert_eq!(Quote::from_open('`'), Some(Quote::Backtick));
        assert_eq!(Quote::from_open('['), Some(Quote::Bracket));
        assert_eq!(Quote::Double.open(), '"');
        assert_eq!(Quote::Double.open_byte(), b'"');
        assert_eq!(Quote::Bracket.close(), ']');
        assert_eq!(Quote::Bracket.close_byte(), b']');
    }

    #[test]
    fn uses_full_inline_capacity() {
        let inline = "a".repeat(TestIdentStr::INLINE_CAPACITY);
        let spill = "a".repeat(TestIdentStr::INLINE_CAPACITY + 1);

        let inline_name = TestIdentStr::new(inline.as_str());
        let spill_name = TestIdentStr::new(spill.as_str());

        assert!(inline_name.is_inline());
        assert!(!spill_name.is_inline());
    }

    #[test]
    fn full_inline_unquoted_roundtrips_utf8_tail() {
        let value = "a".repeat(TestIdentStr::INLINE_CAPACITY - "é".len()) + "é";
        let name = TestIdentStr::new(value.as_str());

        assert!(name.is_inline());
        assert_eq!(name.as_str(), value);
        assert_eq!(name.as_bytes(), value.as_bytes());
    }

    #[test]
    fn quoted_uses_expected_inline_capacity() {
        let inline = "a".repeat(TestIdentStr::QUOTED_INLINE_CAPACITY);
        let spill = "a".repeat(TestIdentStr::QUOTED_INLINE_CAPACITY + 1);

        let inline_name = TestIdentStr::with_quote(inline.as_str(), TestQuote::Double);
        let spill_name = TestIdentStr::with_quote(spill.as_str(), TestQuote::Double);

        assert!(inline_name.is_inline());
        assert!(!spill_name.is_inline());
    }

    #[test]
    fn quoted_full_inline_roundtrips_utf8_tail() {
        let value = "a".repeat(TestIdentStr::QUOTED_INLINE_CAPACITY - "é".len()) + "é";
        let name = TestIdentStr::with_quote(value.as_str(), TestQuote::Double);

        assert!(name.is_inline());
        assert_eq!(name.as_str(), value);
        assert_eq!(name.as_bytes(), value.as_bytes());
    }

    #[test]
    fn quoted_owned_box_uses_owned_path() {
        let name = IdentStr::<Quote, policy::Ascii, BoxStorage>::with_quote(
            Box::<str>::from("Users"),
            Quote::Double,
        );

        assert!(name.is_inline());
        assert_eq!(name.as_str(), "Users");
        assert_eq!(name.quote(), Some(Quote::Double));
    }

    #[test]
    fn const_inline_constructors_work() {
        const EMPTY: IdentStr<Quote> = IdentStr::empty();
        const INLINE: Option<IdentStr<Quote>> = IdentStr::new_inline("users");
        const QUOTED: Option<IdentStr<Quote>> = IdentStr::with_quote_inline("Users", Quote::Double);
        let inline_value = INLINE;
        let quoted_value = QUOTED;
        let inline = inline_value.as_ref().expect("fits inline");
        let quoted = quoted_value.as_ref().expect("fits inline");

        assert!(EMPTY.is_empty());
        assert_eq!(inline.as_str(), "users");
        assert_eq!(quoted.as_str(), "Users");
        assert_eq!(quoted.quote(), Some(Quote::Double));
    }

    #[test]
    fn long_heap_path_roundtrips_owned_conversions() {
        let raw = "this_identifier_name_is_long_enough_to_spill_out_of_line";
        let name = TestIdentStr::from(raw.to_owned());

        assert_eq!(name.as_str(), raw);
        assert_eq!(name.quote(), None);

        let cloned = name.clone();
        let owned: String = cloned.into();
        assert_eq!(owned, raw);

        let boxed: Box<str> = name.into();
        assert_eq!(&*boxed, raw);
    }

    #[test]
    fn arc_spill_keeps_shared_ownership() {
        let raw = "this_identifier_name_is_long_enough_to_spill_out_of_line";
        let name = IdentStr::<TestQuote, policy::Ascii, ArcStorage>::from(raw);
        let cloned = name.clone();

        let left: Arc<str> = name.into();
        let right: Arc<str> = cloned.into();

        assert!(Arc::ptr_eq(&left, &right));
        assert_eq!(&*left, raw);
    }

    #[test]
    fn rc_spill_keeps_shared_ownership() {
        let raw = "this_identifier_name_is_long_enough_to_spill_out_of_line";
        let name = IdentStr::<TestQuote, policy::Ascii, RcStorage>::from(raw);
        let cloned = name.clone();

        let left: Rc<str> = name.into();
        let right: Rc<str> = cloned.into();

        assert!(Rc::ptr_eq(&left, &right));
        assert_eq!(&*left, raw);
    }

    #[test]
    fn box_spill_roundtrips_exact_owned_value() {
        let raw = "this_identifier_name_is_long_enough_to_spill_out_of_line";
        let name = IdentStr::<TestQuote, policy::Ascii, BoxStorage>::from(Box::<str>::from(raw));
        let boxed: Box<str> = name.into();
        assert_eq!(&*boxed, raw);
    }

    #[test]
    fn short_owned_string_stays_inline() {
        let name = TestIdentStr::from(String::from("short_name"));
        assert!(name.is_inline());
    }

    #[test]
    fn byte_view_matches_stored_text() {
        let name = TestIdentStr::with_quote("Users", TestQuote::Double);
        assert_eq!(name.as_bytes(), b"Users");
        assert_eq!(AsRef::<[u8]>::as_ref(&name), b"Users");
    }

    #[test]
    fn display_quoted_renders_preserved_quote_style() {
        let unquoted = IdentStr::<Quote>::new("Users");
        let double = IdentStr::<Quote>::with_quote("User\"Table", Quote::Double);
        let single = IdentStr::<Quote>::with_quote("User'Table", Quote::Single);
        let backtick = IdentStr::<Quote>::with_quote("User`Table", Quote::Backtick);
        let bracket = IdentStr::<Quote>::with_quote("User]Table", Quote::Bracket);

        assert_eq!(unquoted.to_quoted_string(), "Users");
        assert_eq!(double.display_quoted().to_string(), "\"User\"\"Table\"");
        assert_eq!(single.to_quoted_string(), "'User''Table'");
        assert_eq!(backtick.to_quoted_string(), "`User``Table`");
        assert_eq!(bracket.to_quoted_string(), "[User]]Table]");
        assert_eq!(double.to_string(), "User\"Table");
    }

    #[test]
    fn new_parses_default_quote_delimiters() {
        let unquoted = IdentStr::<Quote>::new("Users");
        let double = IdentStr::<Quote>::new("\"User\"\"Table\"");
        let single = IdentStr::<Quote>::new("'User''Table'");
        let backtick = IdentStr::<Quote>::new("`User``Table`");
        let bracket = IdentStr::<Quote>::new("[User]]Table]");

        assert_eq!(unquoted.as_str(), "Users");
        assert_eq!(unquoted.quote(), None);
        assert_eq!(double.as_str(), "User\"Table");
        assert_eq!(double.quote(), Some(Quote::Double));
        assert_eq!(single.as_str(), "User'Table");
        assert_eq!(single.quote(), Some(Quote::Single));
        assert_eq!(backtick.as_str(), "User`Table");
        assert_eq!(backtick.quote(), Some(Quote::Backtick));
        assert_eq!(bracket.as_str(), "User]Table");
        assert_eq!(bracket.quote(), Some(Quote::Bracket));
    }

    #[test]
    fn new_keeps_unbalanced_quotes_as_raw_text() {
        let missing_close = IdentStr::<Quote>::new("\"Users");
        let missing_open = IdentStr::<Quote>::new("Users\"");

        assert_eq!(missing_close.as_str(), "\"Users");
        assert_eq!(missing_close.quote(), None);
        assert_eq!(missing_open.as_str(), "Users\"");
        assert_eq!(missing_open.quote(), None);
    }

    #[test]
    fn new_keeps_malformed_quoted_source_as_raw_text() {
        let malformed = IdentStr::<Quote>::new("\"User\"Table\"");

        assert_eq!(malformed.as_str(), "\"User\"Table\"");
        assert_eq!(malformed.quote(), None);
    }

    #[test]
    fn try_new_with_skips_inner_parser_for_unquoted_text() {
        let ident = IdentStr::<Quote>::try_new_with("Users", |_, _| -> Result<&str, ()> {
            panic!("inner parser should not run for unquoted input")
        })
        .expect("unquoted input should succeed");

        assert_eq!(ident.as_str(), "Users");
        assert_eq!(ident.quote(), None);
    }

    #[test]
    fn try_new_with_uses_callback_result_for_quoted_text() {
        let ident = IdentStr::<Quote>::try_new_with("\"User\"\"Table\"", |_quote, inner| {
            Ok::<_, ()>(inner)
        })
        .expect("quoted input should succeed");

        assert_eq!(ident.as_str(), "User\"\"Table");
        assert_eq!(ident.quote(), Some(Quote::Double));
    }

    #[test]
    fn try_new_with_accepts_owned_inner_text() {
        let ident = IdentStr::<Quote>::try_new_with("\"User\"\"Table\"", |_quote, inner| {
            Ok::<_, ()>(inner.replace("\"\"", "\""))
        })
        .expect("quoted input should succeed");

        assert_eq!(ident.as_str(), "User\"Table");
        assert_eq!(ident.quote(), Some(Quote::Double));
    }

    #[test]
    fn try_new_with_propagates_inner_parser_errors() {
        let err = IdentStr::<Quote>::try_new_with("\"Users\"", |_quote, _inner| {
            Result::<&str, _>::Err("bad quote")
        })
        .expect_err("quoted input should propagate errors");

        assert_eq!(err, "bad quote");
    }

    #[test]
    fn from_raw_does_not_parse_quote_delimiters() {
        let name = IdentStr::<Quote>::from_raw("\"Users\"");

        assert_eq!(name.as_str(), "\"Users\"");
        assert_eq!(name.quote(), None);
    }

    #[test]
    fn with_quote_accepts_opening_delimiters() {
        let name = IdentStr::<Quote>::with_quote("Users", '"');
        let invalid = IdentStr::<Quote>::try_with_quote("Users", 'x');

        assert_eq!(name.as_str(), "Users");
        assert_eq!(name.quote(), Some(Quote::Double));
        assert_eq!(invalid, Err(()));
    }

    #[test]
    fn custom_quote_tags_can_parse_source_text() {
        let name = TestIdentStr::new("[Users]");

        assert_eq!(name.as_str(), "Users");
        assert_eq!(name.quote(), Some(TestQuote::Bracket));
    }

    #[test]
    fn custom_quote_tags_can_render_preserved_quote_style() {
        let name = TestIdentStr::with_quote("User]Table", TestQuote::Bracket);
        let mut rendered = String::new();

        name.write_quoted(&mut rendered).unwrap();

        assert_eq!(name.to_quoted_string(), "[User]]Table]");
        assert_eq!(name.display_quoted().to_string(), "[User]]Table]");
        assert_eq!(rendered, "[User]]Table]");
    }

    #[test]
    fn debug_includes_quote_metadata() {
        let quoted = IdentStr::<Quote>::with_quote("Users", Quote::Double);
        let unquoted = IdentStr::<Quote>::new("users");

        assert_eq!(
            format!("{quoted:?}"),
            "IdentStr { value: \"Users\", quote: Some(Double) }"
        );
        assert_eq!(
            format!("{unquoted:?}"),
            "IdentStr { value: \"users\", quote: None }"
        );
    }

    #[test]
    fn short_name_with_heap_only_quote_spills() {
        let name = IdentStr::<HeapOnlyQuote>::with_quote("short", HeapOnlyQuote::Custom);
        assert!(!name.is_inline());
        assert_eq!(name.quote(), Some(HeapOnlyQuote::Custom));
        assert_eq!(name.as_str(), "short");
    }

    #[test]
    fn empty_name_with_heap_only_quote_spills() {
        let name = IdentStr::<HeapOnlyQuote>::with_quote("", HeapOnlyQuote::Custom);
        assert!(!name.is_inline());
        assert_eq!(name.quote(), Some(HeapOnlyQuote::Custom));
        assert_eq!(name.as_str(), "");
    }

    #[test]
    #[should_panic(expected = "quote tag 0 is reserved for unquoted text")]
    fn quote_tag_zero_is_reserved() {
        let _ = IdentStr::<ZeroQuote>::with_quote("short", ZeroQuote::Custom);
    }

    #[test]
    fn string_traits_follow_ascii_identifier_semantics() {
        let unquoted = TestIdentStr::new("Users");
        let quoted = TestIdentStr::with_quote("users", TestQuote::Double);

        assert_eq!(unquoted, quoted);
        assert_eq!(unquoted, "uSeRs");
        assert_eq!("USERs", quoted);
        assert_eq!(unquoted.cmp(&quoted), Ordering::Equal);
    }

    #[test]
    fn owned_string_traits_follow_ascii_identifier_semantics() {
        let ident = TestIdentStr::with_quote("Users", TestQuote::Double);
        let owned = String::from("users");
        let boxed = Box::<str>::from("users");
        let shared = Arc::<str>::from("users");
        let local = Rc::<str>::from("users");
        let cow = std::borrow::Cow::Borrowed("users");

        assert_eq!(ident, owned);
        assert_eq!(ident, boxed);
        assert_eq!(ident, shared);
        assert_eq!(ident, local);
        assert_eq!(ident, cow);
        assert_eq!(owned, ident);
        assert_eq!(boxed, ident);
        assert_eq!(shared, ident);
        assert_eq!(local, ident);
        assert_eq!(cow, ident);
    }

    #[test]
    fn ascii_policy_does_not_case_fold_non_ascii_bytes() {
        let upper = TestIdentStr::new("Ä");
        let lower = TestIdentStr::new("ä");

        assert_ne!(upper, lower);
    }

    #[test]
    fn hashing_uses_identifier_semantics() {
        let lhs = TestIdentStr::new("Users");
        let rhs = TestIdentStr::with_quote("users", TestQuote::Bracket);

        let mut lhs_hasher = std::collections::hash_map::DefaultHasher::new();
        let mut rhs_hasher = std::collections::hash_map::DefaultHasher::new();
        lhs.hash(&mut lhs_hasher);
        rhs.hash(&mut rhs_hasher);

        assert_eq!(lhs_hasher.finish(), rhs_hasher.finish());
    }

    #[test]
    fn equality_is_independent_of_spill_backend() {
        let boxed = IdentStr::<TestQuote>::from("Users");
        let shared = IdentStr::<TestQuote, policy::Ascii, ArcStorage>::from("users");

        assert_eq!(boxed, shared);
    }

    #[test]
    fn ordering_is_independent_of_storage_mode() {
        let boxed = IdentStr::<TestQuote, policy::Ascii, BoxStorage>::from("Users");
        let shared = IdentStr::<TestQuote, policy::Ascii, ArcStorage>::from("users");
        let local = IdentStr::<TestQuote, policy::Ascii, RcStorage>::from("zz");

        assert_eq!(boxed.partial_cmp(&shared), Some(Ordering::Equal));
        assert_eq!(boxed.partial_cmp(&local), Some(Ordering::Less));
        assert!(boxed < local);
    }

    #[test]
    fn key_stores_lookup_text() {
        let key = Key::<policy::Ascii>::new("\"Users\"");
        assert_eq!(key.as_str(), "users");
    }

    #[test]
    fn key_from_raw_does_not_parse_quote_delimiters() {
        let key = Key::<policy::Ascii>::from_raw("\"Users\"");
        assert_eq!(key.as_str(), "\"users\"");
    }

    #[test]
    fn key_compares_with_ident() {
        let ident = TestIdentStr::with_quote("Users", TestQuote::Double);
        let key = Key::<policy::Ascii>::new("Users");

        assert_eq!(key, ident);
        assert_eq!(ident, key);
    }

    #[test]
    fn key_compares_with_string_types() {
        let key = Key::<policy::Ascii>::new("\"Users\"");
        let owned = String::from("users");
        let boxed = Box::<str>::from("users");
        let shared = Arc::<str>::from("users");
        let local = Rc::<str>::from("users");
        let cow = std::borrow::Cow::Borrowed("users");

        assert_eq!(key, "uSeRs");
        assert_eq!("USERs", key);
        assert_eq!(key, owned);
        assert_eq!(key, boxed);
        assert_eq!(key, shared);
        assert_eq!(key, local);
        assert_eq!(key, cow);
        assert_ne!(key, "\"users\"");
    }

    #[test]
    fn key_hash_matches_equal_ident() {
        let ident = TestIdentStr::with_quote("Users", TestQuote::Double);
        let key = Key::<policy::Ascii>::new("users");

        assert_eq!(key, ident);
        assert_eq!(hash_value(&key), hash_value(&ident));
    }

    #[test]
    fn key_lookup_uses_key_queries() {
        let mut tables = std::collections::HashMap::new();
        tables.insert(Key::<policy::Ascii>::new("\"Users\""), 7usize);

        assert_eq!(tables.get(&Key::<policy::Ascii>::new("users")), Some(&7));
        assert_eq!(
            tables.get(&Key::<policy::Ascii>::new("\"users\"")),
            Some(&7)
        );
    }

    #[test]
    fn from_str_parses_ident_and_key() {
        let ident = IdentStr::<Quote>::from_str("\"Users\"").expect("infallible parse");
        let key = Key::<policy::Ascii>::from_str("\"Users\"").expect("infallible parse");

        assert_eq!(ident.as_str(), "Users");
        assert_eq!(ident.quote(), Some(Quote::Double));
        assert_eq!(key.as_str(), "users");
        assert_eq!(key.as_bytes(), b"users");
    }

    #[test]
    fn key_reuses_owned_ascii_buffer_for_lookup_text() {
        let key = Key::<policy::Ascii>::from(Box::<str>::from("Users"));
        assert_eq!(key.as_str(), "users");
    }

    #[test]
    fn lowercase_ascii_key_reuses_owned_box() {
        let value = Box::<str>::from("users");
        let ptr = value.as_ptr();
        let key = Key::<policy::Ascii>::from(value);
        let value: Box<str> = key.into();
        assert_eq!(ptr, value.as_ptr());
    }

    #[test]
    fn key_from_ident_keeps_raw_identifier_text() {
        let ident = IdentStr::<Quote>::from_raw("\"Users\"");
        let key = Key::<policy::Ascii>::from(&ident);

        assert_eq!(key.as_str(), "\"users\"");
    }

    #[test]
    fn key_from_custom_quoted_ident_uses_identifier_text() {
        let ident = IdentStr::<TestQuote>::new("[Users]");
        let key = Key::<policy::Ascii>::from(&ident);

        assert_eq!(key.as_str(), "users");
    }

    #[test]
    fn malformed_key_source_stays_raw() {
        let key = Key::<policy::Ascii>::new("\"User\"Table\"");

        assert_eq!(key.as_str(), "\"user\"table\"");
    }

    #[test]
    fn boxed_spill_reuses_original_allocation() {
        let value = Box::<str>::from("this_identifier_name_is_long_enough_to_spill_out_of_line");
        let ptr = value.as_ptr();
        let ident = IdentStr::<TestQuote, policy::Ascii, BoxStorage>::from(value);
        let value: Box<str> = ident.into();
        assert_eq!(ptr, value.as_ptr());
    }

    #[cfg(feature = "unicode")]
    #[test]
    fn unicode_nfc_matches_canonical_casefolded_text() {
        let lhs = IdentStr::<TestQuote, policy::UnicodeNfc>::from("É");
        let rhs = IdentStr::<TestQuote, policy::UnicodeNfc>::from("e\u{301}");

        assert_eq!(lhs, rhs);
    }

    #[cfg(feature = "unicode")]
    #[test]
    fn unicode_nfc_hashing_matches_identifier_semantics() {
        let lhs = IdentStr::<TestQuote, policy::UnicodeNfc>::from("É");
        let rhs = IdentStr::<TestQuote, policy::UnicodeNfc>::from("e\u{301}");

        assert_eq!(hash_value(&lhs), hash_value(&rhs));
        assert_eq!(lhs.cmp(&rhs), Ordering::Equal);
    }

    #[cfg(feature = "unicode")]
    #[test]
    fn unicode_key_hash_matches_equal_ident() {
        let ident = IdentStr::<TestQuote, policy::UnicodeNfc>::from("É");
        let key = Key::<policy::UnicodeNfc>::new("e\u{301}");

        assert_eq!(key, ident);
        assert_eq!(hash_value(&key), hash_value(&ident));
    }

    #[cfg(feature = "unicode")]
    #[test]
    fn unicode_nfkc_matches_compatibility_casefolded_text() {
        let lhs = IdentStr::<TestQuote, policy::UnicodeNfkc>::from("ﬀ");
        let rhs = IdentStr::<TestQuote, policy::UnicodeNfkc>::from("FF");

        assert_eq!(lhs, rhs);
    }

    #[cfg(feature = "unicode")]
    #[test]
    fn unicode_nfkc_hashing_matches_identifier_semantics() {
        let lhs = IdentStr::<TestQuote, policy::UnicodeNfkc>::from("ﬀ");
        let rhs = IdentStr::<TestQuote, policy::UnicodeNfkc>::from("FF");

        let mut lhs_hasher = std::collections::hash_map::DefaultHasher::new();
        let mut rhs_hasher = std::collections::hash_map::DefaultHasher::new();
        lhs.hash(&mut lhs_hasher);
        rhs.hash(&mut rhs_hasher);

        assert_eq!(lhs_hasher.finish(), rhs_hasher.finish());
        assert_eq!(lhs.cmp(&rhs), Ordering::Equal);
    }

    #[cfg(feature = "unicode")]
    #[test]
    fn unicode_turkic_policies_follow_dotted_i_rules() {
        let lhs = IdentStr::<TestQuote, policy::UnicodeTurkicNfc>::from("İ");
        let rhs = IdentStr::<TestQuote, policy::UnicodeTurkicNfc>::from("i");
        let upper = IdentStr::<TestQuote, policy::UnicodeTurkicNfc>::from("I");
        let dotless = IdentStr::<TestQuote, policy::UnicodeTurkicNfc>::from("ı");

        assert_eq!(lhs, rhs);
        assert_eq!(upper, dotless);
        assert_ne!(lhs, upper);
    }

    #[cfg(feature = "unicode")]
    #[test]
    fn unicode_turkic_policies_match_canonical_equivalents() {
        let composed_nfc = IdentStr::<TestQuote, policy::UnicodeTurkicNfc>::from("İ");
        let decomposed_nfc = IdentStr::<TestQuote, policy::UnicodeTurkicNfc>::from("I\u{307}");
        let composed_nfkc = IdentStr::<TestQuote, policy::UnicodeTurkicNfkc>::from("İ");
        let decomposed_nfkc = IdentStr::<TestQuote, policy::UnicodeTurkicNfkc>::from("I\u{307}");

        assert_eq!(composed_nfc, decomposed_nfc);
        assert_eq!(hash_value(&composed_nfc), hash_value(&decomposed_nfc));
        assert_eq!(composed_nfkc, decomposed_nfkc);
        assert_eq!(hash_value(&composed_nfkc), hash_value(&decomposed_nfkc));
        assert_eq!(
            Key::<policy::UnicodeTurkicNfc>::new("İ"),
            Key::<policy::UnicodeTurkicNfc>::new("I\u{307}")
        );
        assert_eq!(
            Key::<policy::UnicodeTurkicNfkc>::new("İ"),
            Key::<policy::UnicodeTurkicNfkc>::new("I\u{307}")
        );
    }

    #[cfg(feature = "unicode")]
    #[test]
    fn security_helpers_expose_skeleton_and_analysis() {
        let lhs = IdentStr::<TestQuote>::from("ｓ");
        let rhs = IdentStr::<TestQuote>::from("s");
        let safe = IdentStr::<TestQuote>::from("users");

        assert!(security::is_confusable(lhs.as_str(), rhs.as_str()));

        let analysis = safe.analyze_security();
        assert!(analysis.identifier_allowed);
        assert_eq!(lhs.confusable_skeleton().as_str(), "s");
    }
}
