utf8class.rs raw

//! UTF-8 character class parsing with Unicode support.
//!
//! This module provides the [`Utf8Class`] parser for matching sequences of UTF-8
//! characters that belong to a specific character class. Unlike the byte-level
//! [`Class`](crate::class::Class) parser, this handles full Unicode characters correctly.
//!
//! The parser includes built-in support for Unicode-aware character classes
//! such as alphabetic characters, digits, alphanumeric, and whitespace across
//! all Unicode categories. It uses efficient UTF-8 decoding with ASCII
//! optimization for performance.

use std::collections::HashSet;

use crate::{
    cache::ParsingCache,
    parser::{Parsable, Parser, Source},
    result::{Error, ParseResult},
    utf8util::read_utf8_char,
};

/// A parser that matches a sequence of UTF-8 characters that are all present in a character class.
///
/// The Utf8Class parser consumes UTF-8 encoded characters from the input as long as each
/// character is present in the specified set. It returns a `String` containing all matched
/// characters. The parser succeeds even if it matches zero characters (unless a minimum
/// length is specified).
///
/// # Examples
///
/// ```rust
/// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
/// use std::io::Cursor;
///
/// // Match digits: parses "123" from "123abc"
/// let digits = Utf8Class::new("0123456789");
/// let mut input1 = Cursor::new("123abc".as_bytes());
/// let mut source1 = Source::new(input1);
/// let result1 = parse(digits, &mut source1).unwrap();
/// assert_eq!(result1, "123");
///
/// // Match whitespace including Unicode spaces
/// let whitespace = Utf8Class::new(" \t\r\n\u{00A0}\u{2000}\u{2001}");
/// let mut input2 = Cursor::new("  \t\u{00A0}hello".as_bytes());
/// let mut source2 = Source::new(input2);
/// let result2 = parse(whitespace, &mut source2).unwrap();
/// assert_eq!(result2, "  \t\u{00A0}");
///
/// // Match emoji
/// let emoji = Utf8Class::new("😀😁😂🤣😃😄😅");
/// let mut input3 = Cursor::new("😀😁😂abc".as_bytes());
/// let mut source3 = Source::new(input3);
/// let result3 = parse(emoji, &mut source3).unwrap();
/// assert_eq!(result3, "😀😁😂");
/// ```
#[derive(Debug, Clone)]
pub struct Utf8Class<F = fn(char) -> bool> {
    allowed: HashSet<char>,
    predicate: Option<F>,
    min_length: usize,
    max_length: Option<usize>,
    negated: bool,
}

impl<F> PartialEq for Utf8Class<F> {
    fn eq(&self, other: &Self) -> bool {
        self.allowed == other.allowed
            && self.min_length == other.min_length
            && self.max_length == other.max_length
            && self.negated == other.negated
        // Note: We can't compare function pointers reliably, so we ignore predicate
    }
}

impl<F> Eq for Utf8Class<F> {}

impl Utf8Class<fn(char) -> bool> {
    /// Create a new Utf8Class parser that matches any character in the given string.
    ///
    /// The parser will match zero or more characters that are present in the class.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let digits = Utf8Class::new("0123456789");
    ///
    /// // Matches: "", "1", "123", "999999", etc.
    /// let mut input1 = Cursor::new("123abc".as_bytes());
    /// let mut source1 = Source::new(input1);
    /// let result1 = parse(digits, &mut source1).unwrap();
    /// assert_eq!(result1, "123");
    ///
    /// // Stops at first non-digit
    /// let digits2 = Utf8Class::new("0123456789");
    /// let mut input2 = Cursor::new("abc123".as_bytes());
    /// let mut source2 = Source::new(input2);
    /// let result2 = parse(digits2, &mut source2).unwrap();
    /// assert_eq!(result2, ""); // matches empty string
    /// ```
    pub fn new(chars: &str) -> Self {
        Self {
            allowed: chars.chars().collect(),
            predicate: None,
            min_length: 0,
            max_length: None,
            negated: false,
        }
    }

    /// Create a new Utf8Class parser that matches any character NOT in the given string.
    ///
    /// The parser will match zero or more characters that are NOT present in the class.
    /// This uses the negation flag instead of predicates for better performance.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let not_digits = Utf8Class::not_in("0123456789");
    ///
    /// // Matches: "abc", "hello", "!@#", etc.
    /// let mut input1 = Cursor::new("abc123".as_bytes());
    /// let mut source1 = Source::new(input1);
    /// let result1 = parse(not_digits, &mut source1).unwrap();
    /// assert_eq!(result1, "abc");
    ///
    /// // Stops at first digit
    /// let not_digits2 = Utf8Class::not_in("0123456789");
    /// let mut input2 = Cursor::new("123abc".as_bytes());
    /// let mut source2 = Source::new(input2);
    /// let result2 = parse(not_digits2, &mut source2).unwrap();
    /// assert_eq!(result2, ""); // matches empty string
    /// ```
    pub fn not_in(chars: &str) -> Utf8Class {
        Utf8Class {
            allowed: chars.chars().collect(),
            predicate: None,
            min_length: 0,
            max_length: None,
            negated: true,
        }
    }

    /// Create a new Utf8Class parser that matches any character NOT in the given string
    /// with a minimum required length.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let not_digits = Utf8Class::not_in_with_min("0123456789", 2);
    ///
    /// // Must match at least 2 non-digit characters
    /// let mut input = Cursor::new("abc123".as_bytes());
    /// let mut source = Source::new(input);
    /// let result = parse(not_digits, &mut source).unwrap();
    /// assert_eq!(result, "abc");
    /// ```
    pub fn not_in_with_min(chars: &str, min_length: usize) -> Utf8Class {
        Utf8Class {
            allowed: chars.chars().collect(),
            predicate: None,
            min_length,
            max_length: None,
            negated: true,
        }
    }

    /// Create a new Utf8Class parser with a minimum required length.
    ///
    /// The parser must match at least `min_length` characters or it fails.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let digits = Utf8Class::with_min("0123456789", 2);
    ///
    /// // Matches: "12", "999", "12345", etc.
    /// let mut input1 = Cursor::new("123abc".as_bytes());
    /// let mut source1 = Source::new(input1);
    /// let result1 = parse(digits, &mut source1).unwrap();
    /// assert_eq!(result1, "123");
    ///
    /// // Fails on: "", "1"
    /// let digits2 = Utf8Class::with_min("0123456789", 2);
    /// let mut input2 = Cursor::new("1abc".as_bytes());
    /// let mut source2 = Source::new(input2);
    /// let result2 = parse(digits2, &mut source2);
    /// assert!(result2.is_err()); // fails because only 1 digit
    /// ```
    pub fn with_min(chars: &str, min_length: usize) -> Self {
        Self {
            allowed: chars.chars().collect(),
            predicate: None,
            min_length,
            max_length: None,
            negated: false,
        }
    }

    /// Create a new Utf8Class parser with a maximum length limit.
    ///
    /// The parser will stop after matching `max_length` characters, even if more
    /// matching characters are available.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let digits = Utf8Class::with_max("0123456789", 3);
    ///
    /// // From "12345", matches "123" and stops
    /// let mut input = Cursor::new("12345abc".as_bytes());
    /// let mut source = Source::new(input);
    /// let result = parse(digits, &mut source).unwrap();
    /// assert_eq!(result, "123");
    /// ```
    pub fn with_max(chars: &str, max_length: usize) -> Self {
        Self {
            allowed: chars.chars().collect(),
            predicate: None,
            min_length: 0,
            max_length: Some(max_length),
            negated: false,
        }
    }

    /// Create a new Utf8Class parser with both minimum and maximum length limits.
    ///
    /// The parser must match at least `min_length` characters and will stop after
    /// `max_length` characters.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let digits = Utf8Class::with_bounds("0123456789", 2, 4);
    ///
    /// // Matches 2-4 digits: "12", "123", "1234"
    /// let mut input1 = Cursor::new("123abc".as_bytes());
    /// let mut source1 = Source::new(input1);
    /// let result1 = parse(digits, &mut source1).unwrap();
    /// assert_eq!(result1, "123");
    ///
    /// // Stops at 4 even from "123456"
    /// let digits2 = Utf8Class::with_bounds("0123456789", 2, 4);
    /// let mut input2 = Cursor::new("123456abc".as_bytes());
    /// let mut source2 = Source::new(input2);
    /// let result2 = parse(digits2, &mut source2).unwrap();
    /// assert_eq!(result2, "1234");
    /// ```
    pub fn with_bounds(chars: &str, min_length: usize, max_length: usize) -> Self {
        Self {
            allowed: chars.chars().collect(),
            predicate: None,
            min_length,
            max_length: Some(max_length),
            negated: false,
        }
    }

    /// Create a Utf8Class parser for ASCII digits (0-9).
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let digits = Utf8Class::digits();
    /// // Equivalent to: Utf8Class::with_min("0123456789", 1)
    ///
    /// let mut input = Cursor::new("123abc".as_bytes());
    /// let mut source = Source::new(input);
    /// let result = parse(digits, &mut source).unwrap();
    /// assert_eq!(result, "123");
    /// ```
    pub fn digits() -> Self {
        Self::with_min("0123456789", 1)
    }

    /// Create a Utf8Class parser for ASCII alphabetic characters (a-z, A-Z).
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let alpha = Utf8Class::alpha();
    ///
    /// // Matches: "abc", "XYZ", "Hello", etc.
    /// let mut input = Cursor::new("Hello123".as_bytes());
    /// let mut source = Source::new(input);
    /// let result = parse(alpha, &mut source).unwrap();
    /// assert_eq!(result, "Hello");
    /// ```
    pub fn alpha() -> Self {
        Self::with_min("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 1)
    }

    /// Create a Utf8Class parser for ASCII alphanumeric characters (a-z, A-Z, 0-9).
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let alnum = Utf8Class::alphanumeric();
    ///
    /// // Matches: "abc123", "Hello42", etc.
    /// let mut input = Cursor::new("Hello42_world".as_bytes());
    /// let mut source = Source::new(input);
    /// let result = parse(alnum, &mut source).unwrap();
    /// assert_eq!(result, "Hello42");
    /// ```
    pub fn alphanumeric() -> Self {
        Self::with_min(
            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
            1,
        )
    }

    /// Create a Utf8Class parser for ASCII whitespace characters.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let whitespace = Utf8Class::whitespace();
    ///
    /// // Matches: " ", "\t\n", "   ", etc.
    /// let mut input = Cursor::new("  \t\r\nhello".as_bytes());
    /// let mut source = Source::new(input);
    /// let result = parse(whitespace, &mut source).unwrap();
    /// assert_eq!(result, "  \t\r\n");
    /// ```
    pub fn whitespace() -> Self {
        Self::with_min(" \t\r\n\u{0b}\u{0c}", 1)
    }

    /// Create a Utf8Class parser for hexadecimal digits (0-9, a-f, A-F).
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let hex = Utf8Class::hex_digits();
    ///
    /// // Matches: "1a2b", "DEADBEEF", "0xff", etc.
    /// let mut input = Cursor::new("DEADBEEFghij".as_bytes());
    /// let mut source = Source::new(input);
    /// let result = parse(hex, &mut source).unwrap();
    /// assert_eq!(result, "DEADBEEF");
    /// ```
    pub fn hex_digits() -> Self {
        Self::with_min("0123456789abcdefABCDEF", 1)
    }

    /// Create a Utf8Class parser for Unicode letters (using char::is_alphabetic).
    ///
    /// This matches Unicode letters including accented characters, Greek letters, etc.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let unicode_alpha = Utf8Class::unicode_alpha();
    ///
    /// // Matches: "café", "αβγ", "こんにちは", etc.
    /// let mut input = Cursor::new("café123".as_bytes());
    /// let mut source = Source::new(input);
    /// let result = parse(unicode_alpha, &mut source).unwrap();
    /// assert_eq!(result, "café");
    /// ```
    pub fn unicode_alpha() -> Self {
        Self::from_predicate_min(char::is_alphabetic, 1)
    }

    /// Create a Utf8Class parser for Unicode digits (using char::is_numeric).
    ///
    /// This matches Unicode numeric characters including Arabic-Indic digits, etc.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let unicode_digits = Utf8Class::unicode_digits();
    ///
    /// // Matches: "123", "۱۲۳", "123", etc.
    /// let mut input = Cursor::new("123abc".as_bytes());
    /// let mut source = Source::new(input);
    /// let result = parse(unicode_digits, &mut source).unwrap();
    /// assert_eq!(result, "123");
    /// ```
    pub fn unicode_digits() -> Self {
        Self::from_predicate_min(char::is_numeric, 1)
    }

    /// Create a Utf8Class parser for Unicode whitespace (using char::is_whitespace).
    ///
    /// This matches all Unicode whitespace characters including various space types.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let unicode_ws = Utf8Class::unicode_whitespace();
    ///
    /// // Matches: " ", "\u{00A0}", "\u{2000}", etc.
    /// let mut input = Cursor::new("  \u{00A0}\u{2000}hello".as_bytes());
    /// let mut source = Source::new(input);
    /// let result = parse(unicode_ws, &mut source).unwrap();
    /// assert_eq!(result, "  \u{00A0}\u{2000}");
    /// ```
    pub fn unicode_whitespace() -> Self {
        Self::from_predicate_min(char::is_whitespace, 1)
    }
}

impl<F> Utf8Class<F>
where
    F: Fn(char) -> bool,
{
    /// Create a new Utf8Class parser that uses a predicate function to test characters.
    ///
    /// The predicate function will be called for each character to determine if it
    /// should be matched. This allows for complex matching logic.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// // Match uppercase letters
    /// let uppercase = Utf8Class::from_predicate(|c| c.is_uppercase());
    /// let mut input1 = Cursor::new("HELLO world".as_bytes());
    /// let mut source1 = Source::new(input1);
    /// let result1 = parse(uppercase, &mut source1).unwrap();
    /// assert_eq!(result1, "HELLO");
    ///
    /// // Match vowels
    /// let vowels = Utf8Class::from_predicate(|c| matches!(c, 'a' | 'e' | 'i' | 'o' | 'u' | 'A' | 'E' | 'I' | 'O' | 'U'));
    /// let mut input2 = Cursor::new("aeiou123".as_bytes());
    /// let mut source2 = Source::new(input2);
    /// let result2 = parse(vowels, &mut source2).unwrap();
    /// assert_eq!(result2, "aeiou");
    /// ```
    pub fn from_predicate(predicate: F) -> Self {
        Self {
            allowed: HashSet::new(), // Ignored when predicate is present
            predicate: Some(predicate),
            min_length: 0,
            max_length: None,
            negated: false,
        }
    }

    /// Create a Utf8Class parser with a predicate and minimum length.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let uppercase = Utf8Class::from_predicate_min(|c| c.is_uppercase(), 2);
    ///
    /// // Must match at least 2 uppercase letters
    /// let mut input = Cursor::new("HELLO world".as_bytes());
    /// let mut source = Source::new(input);
    /// let result = parse(uppercase, &mut source).unwrap();
    /// assert_eq!(result, "HELLO");
    /// ```
    pub fn from_predicate_min(predicate: F, min_length: usize) -> Self {
        Self {
            allowed: HashSet::new(),
            predicate: Some(predicate),
            min_length,
            max_length: None,
            negated: false,
        }
    }

    /// Create a Utf8Class parser with a predicate and maximum length.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let uppercase = Utf8Class::from_predicate_max(|c| c.is_uppercase(), 5);
    ///
    /// // Match at most 5 uppercase letters
    /// let mut input = Cursor::new("HELLOWORLD123".as_bytes());
    /// let mut source = Source::new(input);
    /// let result = parse(uppercase, &mut source).unwrap();
    /// assert_eq!(result, "HELLO"); // stops at 5 letters
    /// ```
    pub fn from_predicate_max(predicate: F, max_length: usize) -> Self {
        Self {
            allowed: HashSet::new(),
            predicate: Some(predicate),
            min_length: 0,
            max_length: Some(max_length),
            negated: false,
        }
    }

    /// Create a Utf8Class parser with a predicate and both min/max length.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{utf8class::Utf8Class, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let uppercase = Utf8Class::from_predicate_bounds(|c| c.is_uppercase(), 2, 5);
    ///
    /// // Match 2-5 uppercase letters
    /// let mut input = Cursor::new("HELLOWORLD123".as_bytes());
    /// let mut source = Source::new(input);
    /// let result = parse(uppercase, &mut source).unwrap();
    /// assert_eq!(result, "HELLO"); // matches 5 letters then stops
    /// ```
    pub fn from_predicate_bounds(predicate: F, min_length: usize, max_length: usize) -> Self {
        Self {
            allowed: HashSet::new(),
            predicate: Some(predicate),
            min_length,
            max_length: Some(max_length),
            negated: false,
        }
    }
}

impl<F> Utf8Class<F> {
    /// Check if a character matches this class
    fn char_matches(&self, c: char) -> bool
    where
        F: Fn(char) -> bool,
    {
        let matches = if let Some(ref predicate) = self.predicate {
            predicate(c)
        } else {
            self.allowed.contains(&c)
        };

        if self.negated { !matches } else { matches }
    }
}

impl<F, Ctx> Parser<Ctx> for Utf8Class<F>
where
    F: Fn(char) -> bool + 'static,
{
    type Output = String;

    fn id(&self) -> u64 {
        use std::any::TypeId;
        use std::hash::{DefaultHasher, Hash, Hasher};

        let mut hasher = DefaultHasher::new();
        TypeId::of::<Self>().hash(&mut hasher);
        // Hash the allowed character set for character set-based classes
        // We need to hash the contents of the set in a deterministic way
        let mut chars: Vec<char> = self.allowed.iter().copied().collect();
        chars.sort(); // Ensure deterministic ordering
        chars.hash(&mut hasher);
        self.min_length.hash(&mut hasher);
        self.max_length.hash(&mut hasher);
        self.negated.hash(&mut hasher);
        // Note: predicate functions can't be hashed directly, but the allowed set
        // captures the essential state for non-predicate classes
        hasher.finish()
    }

    fn read<S>(
        &self,
        source: &mut Source<S>,
        _cache: &mut impl ParsingCache,
        _context: &mut Ctx,
    ) -> ParseResult<Self::Output>
    where
        S: Parsable,
    {
        let mut result = String::new();
        let mut char_count = 0;

        loop {
            // Check max length limit
            if let Some(max_length) = self.max_length {
                if char_count >= max_length {
                    break;
                }
            }

            // Check if the next character matches before consuming it
            source.push();
            match read_utf8_char(source) {
                Ok(c) => {
                    if self.char_matches(c) {
                        // Character is in our class, consume it
                        source.commit(); // Commit the read
                        result.push(c);
                        char_count += 1;
                    } else {
                        // Character not in class, backtrack and stop matching
                        source.pop(); // Backtrack
                        break;
                    }
                }
                Err(Error::NoMatch) => {
                    // End of input or invalid UTF-8, stop matching
                    source.pop(); // Backtrack
                    break;
                }
                Err(err) => {
                    source.pop(); // Backtrack
                    return Err(err);
                }
            }
        }

        // Check minimum length requirement
        if char_count < self.min_length {
            return Err(Error::NoMatch);
        }

        Ok(result)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::parser::parse;
    use std::io::Cursor;

    #[test]
    fn test_id_implementation_different_utf8class_parsers() {
        // Test that Utf8Class implements proper id() method
        // Different Utf8Class parsers should have different IDs to avoid cache conflicts

        let class1 = Utf8Class::new("abc");
        let class2 = Utf8Class::new("xyz");

        // These should have different IDs because they have different character sets
        // This test will FAIL if Utf8Class uses default id() implementation
        assert_ne!(
            <Utf8Class as crate::parser::Parser<()>>::id(&class1),
            <Utf8Class as crate::parser::Parser<()>>::id(&class2),
            "Different Utf8Class instances should have different IDs to avoid cache collisions"
        );
    }

    #[test]
    fn test_id_implementation_same_utf8class_parsers() {
        // Test that identical Utf8Class parsers have the same ID
        let class1 = Utf8Class::new("abc");
        let class2 = Utf8Class::new("abc");

        assert_eq!(
            <Utf8Class as crate::parser::Parser<()>>::id(&class1),
            <Utf8Class as crate::parser::Parser<()>>::id(&class2),
            "Identical Utf8Class instances should have the same ID for cache efficiency"
        );
    }

    #[test]
    fn test_id_implementation_utf8class_different_bounds() {
        // Test Utf8Class parsers with different bounds
        let class1 = Utf8Class::with_min("abc", 1);
        let class2 = Utf8Class::with_min("abc", 2);

        // These should have different IDs because they have different minimum bounds
        // This test will FAIL if Utf8Class uses default id() implementation
        assert_ne!(
            <Utf8Class as crate::parser::Parser<()>>::id(&class1),
            <Utf8Class as crate::parser::Parser<()>>::id(&class2),
            "Utf8Class instances with different bounds should have different IDs"
        );
    }

    #[test]
    fn test_id_implementation_utf8class_predicate_parsers() {
        // Test Utf8Class parsers with predicates
        let class1 = Utf8Class::from_predicate(|c| c.is_alphabetic());
        let class2 = Utf8Class::from_predicate(|c| c.is_numeric());

        // These should have different IDs because they have different predicates
        // This test will FAIL if Utf8Class uses default id() implementation
        // Note: This test is complex because function pointers might not be easily comparable
        // Helper function to get ID with proper type inference
        fn get_id<P: crate::parser::Parser<()>>(parser: &P) -> u64 {
            parser.id()
        }

        assert_ne!(
            get_id(&class1),
            get_id(&class2),
            "Utf8Class instances with different predicates should have different IDs"
        );
    }

    #[test]
    fn test_utf8class_basic_functionality() {
        // Basic functionality test to ensure Utf8Class works correctly
        let class = Utf8Class::new("abc");

        let mut input = Cursor::new("aabbcc123".as_bytes());
        let mut source = crate::parser::Source::new(&mut input);

        let result = parse(class, &mut source).unwrap();
        assert_eq!(result, "aabbcc");
    }

    #[test]
    fn test_utf8class_unicode_characters() {
        // Test with various Unicode character ranges
        let emoji_class = Utf8Class::new("😀😁😂🤣😃");

        let mut input = Cursor::new("😀😁😂abc".as_bytes());
        let mut source = crate::parser::Source::new(&mut input);

        let result = parse(emoji_class, &mut source).unwrap();
        assert_eq!(result, "😀😁😂");

        // Test with mixed scripts
        let mixed_class = Utf8Class::new("aαа世界");
        let mut input2 = Cursor::new("aα世abc".as_bytes());
        let mut source2 = crate::parser::Source::new(&mut input2);

        let result2 = parse(mixed_class, &mut source2).unwrap();
        // The class "aαа世界" should match all characters that are in the set
        assert!(result2.contains('a') && result2.contains('α') && result2.contains('世'));
    }

    #[test]
    fn test_utf8class_negated_functionality() {
        // Test not_in functionality
        let not_digits = Utf8Class::not_in("0123456789");

        let mut input = Cursor::new("abc123".as_bytes());
        let mut source = crate::parser::Source::new(&mut input);

        let result = parse(not_digits, &mut source).unwrap();
        assert_eq!(result, "abc");

        // Test not_in with Unicode
        let not_emoji = Utf8Class::not_in("😀😁😂🤣");
        let mut input2 = Cursor::new("Hello世界😀more".as_bytes());
        let mut source2 = crate::parser::Source::new(&mut input2);

        let result2 = parse(not_emoji, &mut source2).unwrap();
        assert_eq!(result2, "Hello世界");
    }

    #[test]
    fn test_utf8class_boundary_conditions() {
        // Test min/max bounds with Unicode characters
        let unicode_class = Utf8Class::with_bounds("世界测试", 2, 3);

        // Should succeed with 3 characters
        let mut input1 = Cursor::new("世界测试abc".as_bytes());
        let mut source1 = crate::parser::Source::new(&mut input1);

        let result1 = parse(unicode_class, &mut source1).unwrap();
        assert_eq!(result1, "世界测"); // Stops at max of 3

        // Test failure below minimum
        let unicode_class2 = Utf8Class::with_min("世界", 3);
        let mut input2 = Cursor::new("世界abc".as_bytes());
        let mut source2 = crate::parser::Source::new(&mut input2);

        let result2 = parse(unicode_class2, &mut source2);
        assert!(result2.is_err()); // Only 2 characters, need 3
    }

    #[test]
    fn test_utf8class_predicate_functionality() {
        // Test various Unicode predicates
        let alpha_class = Utf8Class::from_predicate(char::is_alphabetic);

        let mut input1 = Cursor::new("Helloαβγ世界123".as_bytes());
        let mut source1 = crate::parser::Source::new(&mut input1);

        let result1 = parse(alpha_class, &mut source1).unwrap();
        assert_eq!(result1, "Helloαβγ世界");

        // Test numeric predicate
        let numeric_class = Utf8Class::from_predicate(char::is_numeric);
        let mut input2 = Cursor::new("123456abc".as_bytes()); // Unicode + ASCII digits
        let mut source2 = crate::parser::Source::new(&mut input2);

        let result2 = parse(numeric_class, &mut source2).unwrap();
        assert_eq!(result2, "123456");

        // Test whitespace predicate
        let ws_class = Utf8Class::from_predicate(char::is_whitespace);
        let mut input3 = Cursor::new("  \t\u{00A0}\u{2000}abc".as_bytes());
        let mut source3 = crate::parser::Source::new(&mut input3);

        let result3 = parse(ws_class, &mut source3).unwrap();
        assert_eq!(result3, "  \t\u{00A0}\u{2000}");
    }

    #[test]
    fn test_utf8class_convenience_constructors() {
        // Test convenience constructors
        let digits = Utf8Class::digits();
        let mut input1 = Cursor::new("123abc".as_bytes());
        let mut source1 = crate::parser::Source::new(&mut input1);

        let result1 = parse(digits, &mut source1).unwrap();
        assert_eq!(result1, "123");

        // Test alpha constructor
        let alpha = Utf8Class::alpha();
        let mut input2 = Cursor::new("Hello123".as_bytes());
        let mut source2 = crate::parser::Source::new(&mut input2);

        let result2 = parse(alpha, &mut source2).unwrap();
        assert_eq!(result2, "Hello");

        // Test unicode_alpha constructor
        let unicode_alpha = Utf8Class::unicode_alpha();
        let mut input3 = Cursor::new("Helloαβγ世界123".as_bytes());
        let mut source3 = crate::parser::Source::new(&mut input3);

        let result3 = parse(unicode_alpha, &mut source3).unwrap();
        assert_eq!(result3, "Helloαβγ世界");

        // Test unicode_digits constructor
        let unicode_digits = Utf8Class::unicode_digits();
        let mut input4 = Cursor::new("১২৩123abc".as_bytes()); // Bengali + ASCII digits
        let mut source4 = crate::parser::Source::new(&mut input4);

        let result4 = parse(unicode_digits, &mut source4).unwrap();
        assert_eq!(result4, "১২৩123");
    }

    #[test]
    fn test_utf8class_empty_input() {
        // Test with empty input
        let class = Utf8Class::new("abc");

        let mut input = Cursor::new("".as_bytes());
        let mut source = crate::parser::Source::new(&mut input);

        let result = parse(class, &mut source).unwrap();
        assert_eq!(result, "");

        // Test with minimum requirement on empty input
        let class_min = Utf8Class::with_min("abc", 1);
        let mut input2 = Cursor::new("".as_bytes());
        let mut source2 = crate::parser::Source::new(&mut input2);

        let result2 = parse(class_min, &mut source2);
        assert!(result2.is_err());
    }

    #[test]
    fn test_utf8class_position_tracking() {
        // Verify position is correctly tracked with Unicode characters
        let class = Utf8Class::new("世界");

        let mut input = Cursor::new("世界测XYZ".as_bytes());
        let mut source = crate::parser::Source::new(&mut input);

        let result = parse(class, &mut source).unwrap();
        assert_eq!(result, "世界");

        // Position should be at '测'
        let next_char = crate::utf8util::read_utf8_char(&mut source).unwrap();
        assert_eq!(next_char, '测');
    }

    #[test]
    fn test_utf8class_complex_unicode_scenarios() {
        // Test with combining characters
        let class = Utf8Class::new("eé\u{0301}"); // e, é, and combining acute

        let mut input = Cursor::new("eée\u{0301}abc".as_bytes());
        let mut source = crate::parser::Source::new(&mut input);

        let result = parse(class, &mut source).unwrap();
        // Note: This tests individual Unicode code points, not grapheme clusters
        assert!(result.contains('e') && result.contains('é'));

        // Test with surrogate pairs / 4-byte UTF-8
        let emoji_class = Utf8Class::new("🌍🌎🌏");
        let mut input2 = Cursor::new("🌍🌎abc".as_bytes());
        let mut source2 = crate::parser::Source::new(&mut input2);

        let result2 = parse(emoji_class, &mut source2).unwrap();
        assert_eq!(result2, "🌍🌎");
    }

    #[test]
    fn test_utf8class_large_character_set() {
        // Test with a large Unicode character set
        let large_set = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZαβγδεζηθικλμνξοπρστυφχψωабвгдежзийклмнопрстуфхцчшщъыьэюя世界测试日本語한국어Hello";
        let class = Utf8Class::new(large_set);

        let mut input = Cursor::new("Hello世界αβ한국123".as_bytes());
        let mut source = crate::parser::Source::new(&mut input);

        let result = parse(class, &mut source).unwrap();
        // Should match all characters that are in the large set
        assert!(!result.is_empty());
        assert!(result.contains("Hello"));
        assert!(result.contains("世界"));
    }

    #[test]
    fn test_utf8class_invalid_utf8_handling() {
        // Test behavior with invalid UTF-8 sequences
        let class = Utf8Class::from_predicate(|_| true); // Accept any valid character

        // Create input with invalid UTF-8 in the middle
        let mut invalid_input = Vec::new();
        invalid_input.extend_from_slice("Hello".as_bytes());
        invalid_input.push(0xFF); // Invalid UTF-8 byte
        invalid_input.extend_from_slice("World".as_bytes());

        let mut input = Cursor::new(&invalid_input);
        let mut source = crate::parser::Source::new(&mut input);

        let result = parse(class, &mut source).unwrap();
        assert_eq!(result, "Hello"); // Should stop at invalid UTF-8
    }

    #[test]
    fn test_utf8class_predicate_edge_cases() {
        // Predicate that always returns true
        let always_true = Utf8Class::from_predicate(|_| true);
        let mut input1 = Cursor::new("Hello世界123!@#".as_bytes());
        let mut source1 = crate::parser::Source::new(&mut input1);

        let result1 = parse(always_true, &mut source1).unwrap();
        assert_eq!(result1, "Hello世界123!@#");

        // Predicate that always returns false
        let always_false = Utf8Class::from_predicate(|_| false);
        let mut input2 = Cursor::new("Hello".as_bytes());
        let mut source2 = crate::parser::Source::new(&mut input2);

        let result2 = parse(always_false, &mut source2).unwrap();
        assert_eq!(result2, "");

        // Complex predicate: vowels (ASCII and some Unicode)
        let vowels = Utf8Class::from_predicate(|c| {
            matches!(
                c,
                'a' | 'e'
                    | 'i'
                    | 'o'
                    | 'u'
                    | 'A'
                    | 'E'
                    | 'I'
                    | 'O'
                    | 'U'
                    | 'α'
                    | 'ε'
                    | 'ι'
                    | 'ο'
                    | 'υ'
            )
        });
        let mut input3 = Cursor::new("aeiouαεβγ123".as_bytes());
        let mut source3 = crate::parser::Source::new(&mut input3);

        let result3 = parse(vowels, &mut source3).unwrap();
        assert_eq!(result3, "aeiouαε");
    }

    #[test]
    fn test_utf8class_character_vs_byte_counting() {
        // Verify character counting vs byte counting
        let class = Utf8Class::with_max("🌍🌎🌏abc", 3);

        // "🌍🌎🌏" are each 4 bytes but should count as 3 characters
        let mut input = Cursor::new("🌍🌎🌏abc".as_bytes());
        let mut source = crate::parser::Source::new(&mut input);

        let result = parse(class, &mut source).unwrap();
        assert_eq!(result, "🌍🌎🌏"); // Should get all 3 emoji characters

        // Test with min requirement
        let class2 = Utf8Class::with_min("🌍", 2);
        let mut input2 = Cursor::new("🌍abc".as_bytes()); // Only 1 character
        let mut source2 = crate::parser::Source::new(&mut input2);

        let result2 = parse(class2, &mut source2);
        assert!(result2.is_err()); // Should fail - only 1 character, need 2
    }

    #[test]
    fn test_utf8class_performance_large_input() {
        // Test with reasonably large Unicode input
        let large_unicode = "世界".repeat(5000);
        let class = Utf8Class::new("世界");

        let mut input = Cursor::new(large_unicode.as_bytes());
        let mut source = crate::parser::Source::new(&mut input);

        let result = parse(class, &mut source).unwrap();
        assert_eq!(result.chars().count(), 10000); // 5000 * 2 characters
        assert!(result.chars().all(|c| c == '世' || c == '界'));
    }

    #[test]
    fn test_utf8class_mixed_ascii_unicode() {
        // Test realistic mixed ASCII/Unicode scenarios
        let mixed_class = Utf8Class::new("Hello世界-_123");

        let mut input = Cursor::new("Hello-世界_123!@#".as_bytes());
        let mut source = crate::parser::Source::new(&mut input);

        let result = parse(mixed_class, &mut source).unwrap();
        assert_eq!(result, "Hello-世界_123");

        // Remaining should be "!@#"
        let remaining_bytes = source.peek(3).unwrap();
        assert_eq!(remaining_bytes, "!@#".as_bytes());
    }
}