use std::error::Error;
use std::fmt::{Debug, Display, Formatter};

/// Provides functionality for incremental string parsing
///
/// Per default, each single function in this module skips leading whitespace characters (if not mentioned otherwise).
pub trait Parse: Sized {
    /// Parse the content starting at buffer\[pos\] as [Self]. Returns an `Err` if not successful.
    /// If successful, the parsed value is returned and `pos` is incremented to point at the pos right after the 'consumed' value-string.
    /// In the unsuccessful case we return Err and `pos` stays __unmodified__.
    ///
    /// NB: `*pos` must always point to a char boundary.
    fn parse_at(
        buffer: &str,
        pos: &mut usize,
    ) -> Result<Self, ParseErr>;

    /// Convenience function
    /// iff successful, the number of consumed bytes and the parsed instance of the implementing type are returned
    fn parse(buffer: &str) -> Result<Self, ParseErr> {
        let mut pos = 0;
        Self::parse_at(buffer, &mut pos)
    }
}

/// Atomic parsing feature.
///
/// Tries to parse `<T>` using function `f` – and mutate `pos` only in case the parse was completed successfully.
/// NB: `*pos` must always point to a char boundary.
pub fn atomic<F, T>(
    pos: &mut usize,
    mut f: F,
) -> Result<T, ParseErr>
where
    F: FnMut(&mut usize) -> Result<T, ParseErr>,
{
    let mut work_pos = *pos;
    let result = f(&mut work_pos)?;
    *pos = work_pos;
    Ok(result)
}

/// Name – starts with (Unicode) alphabetic character and can continue with (Unicode) alphanumeric characters or '_'.
/// For pure ascii characters it would match regexp `[a-zA-Z][_0-9a-zA-Z]*`.
///
/// Skips whitespaces.
/// NB: `*pos` must always point to a char boundary.
pub fn name<'a>(
    buffer: &'a str,
    pos: &mut usize,
) -> Result<&'a str, ParseErr> {
    generic_name("name", |c| c.is_alphanumeric() || matches!(c, '_'), buffer, pos)
}

/// Identifier – starts with (Unicode) alphabetic character and can continue with (Unicode) alphanumeric characters or
/// one of `-` `$` `.` `_`.
/// For pure ascii characters it would match regexp `[a-zA-Z][-$._0-9a-zA-Z]*`.
///
/// Skips whitespaces.
/// NB: `*pos` must always point to a char boundary.
pub fn identifier<'a>(
    buffer: &'a str,
    pos: &mut usize,
) -> Result<&'a str, ParseErr> {
    generic_name(
        "identifier",
        |c| c.is_alphanumeric() || matches!(c, '-' | '$' | '.' | '_'),
        buffer,
        pos,
    )
}

/// Generic name – Starts with (Unicode) alphabetic character and a flexible continuation.
///
/// * `type_name` - Used for error messages (e.g. `"name"`, `"identifier"`)
/// * `continuation_char_matcher` - Predicate that determines valid continuation characters
/// * `buffer` - The input string to parse
/// * `pos` - Current position in `buffer`; updated to point past the parsed name on success
///
/// Skips whitespaces.
/// NB: `*pos` must always point to a char boundary.
pub fn generic_name<'a>(
    type_name: &str,
    continuation_char_matcher: fn(char) -> bool,
    buffer: &'a str,
    pos: &mut usize,
) -> Result<&'a str, ParseErr> {
    atomic(pos, |pos| {
        skip_whitespaces(buffer, pos);

        let result_start_idx = *pos;
        let mut result_len = 0;
        let mut iter = buffer[*pos..].chars();

        match iter.next() {
            Some(c) if c.is_alphabetic() => result_len += c.len_utf8(),
            Some(x) => {
                return Err(ParseErr::new(format!("Unexpected '{}' at {} start", x, type_name), buffer, pos));
            }
            None => return Err(ParseErr::new("Unexpected EOL".to_owned(), buffer, pos)),
        };

        loop {
            match iter.next() {
                None => break,
                Some(c) => {
                    if continuation_char_matcher(c) {
                        result_len += c.len_utf8()
                    } else {
                        break;
                    }
                }
            }
        }

        *pos += result_len;
        Ok(&buffer[result_start_idx..result_start_idx + result_len])
    })
}

/// Custom String token.
///
/// Skips whitespaces.
/// NB: `*pos` must always point to a char boundary.
pub fn token<'a>(
    token: &str,
    buffer: &'a str,
    pos: &mut usize,
) -> Result<&'a str, ParseErr> {
    atomic(pos, |pos| {
        skip_whitespaces(buffer, pos);
        // Note: need to compare on byte-buffer-level, because we can't assure our index into `buffer[pos + token.len()]` ends at a char boundary.
        if buffer[*pos..].len() >= token.len() && &buffer.as_bytes()[*pos..*pos + token.len()] == token.as_bytes() {
            let prev_pos = *pos;
            *pos += token.len();
            Ok(&buffer[prev_pos..*pos])
        } else {
            Err(ParseErr::new(format!("Expected '{}'", token), buffer, pos))
        }
    })
}

/// Skips whitespaces.
///
/// NB: `*pos` must always point to a char boundary.
pub fn skip_whitespaces(
    buffer: &str,
    pos: &mut usize,
) {
    let mut iter = buffer[*pos..].chars();
    loop {
        match iter.next() {
            Some(c) if c.is_whitespace() => *pos += c.len_utf8(),
            _ => break,
        }
    }
}

/// Direct sequence of one or more digits.
///
/// This function does NOT skip leading whitespaces – the digit(s) are expected directly.
/// NB: `*pos` must always point to a char boundary.
pub fn digits_direct<'a>(
    buffer: &'a str,
    pos: &mut usize,
) -> Result<&'a str, ParseErr> {
    atomic(pos, |pos| {
        let mut len = 0;
        for c in buffer[*pos..].chars() {
            if !c.is_ascii_digit() {
                break;
            }
            len += c.len_utf8()
        }

        if len > 0 {
            let result = &buffer[*pos..*pos + len];
            *pos += len;
            Ok(result)
        } else {
            Err(ParseErr::new("Expected a digit".to_owned(), buffer, pos))
        }
    })
}

/// Sequence of digits
///
/// NB: `*pos` must always point to a char boundary.
pub fn digits<'a>(
    buffer: &'a str,
    pos: &mut usize,
) -> Result<&'a str, ParseErr> {
    atomic(pos, |pos| {
        skip_whitespaces(buffer, pos);
        digits_direct(buffer, pos)
    })
}

/// Tries to apply a sequence of mini-try-parse functions - which return a string slice when successful.
///
/// Only if all sequence elements could be parsed successfully, we return a result and modify `pos` to the buffer-index after the parsed sequence.
/// Skips whitespaces.
/// NB: `*pos` must always point to a char boundary.
///
/// Returns the corresponding string slices from the elements of the parsing sequence in order.
pub fn string_sequence<'a, F, const LENGTH: usize>(
    seq: &[F; LENGTH],
    buffer: &'a str,
    pos: &mut usize,
) -> Result<[&'a str; LENGTH], ParseErr>
where
    F: Fn(&'a str, &mut usize) -> Result<&'a str, ParseErr>,
{
    atomic(pos, |pos| {
        let mut result = [""; LENGTH];
        for (i, f) in seq.iter().enumerate() {
            result[i] = f(buffer, pos)?;
        }
        Ok(result)
    })
}

/// Expects one of the strings presented in `option`.
///
/// Skips whitespaces.
/// NB: `*pos` must always point to a char boundary.
pub fn option<'a>(
    options: &[&'a str],
    buffer: &str,
    pos: &mut usize,
) -> Result<&'a str, ParseErr> {
    atomic(pos, |pos| {
        skip_whitespaces(buffer, pos);
        if let Some(&result) = options
            .iter()
            .find(|&o| buffer[*pos..].len() >= o.len() && &buffer.as_bytes()[*pos..*pos + o.len()] == o.as_bytes())
        {
            *pos += result.len();
            Ok(result)
        } else {
            Err(ParseErr::new(
                format!(
                    "One of these expected: [{}]",
                    options.iter().map(|&e| format!("'{}'", e)).collect::<Vec<_>>().join(",")
                ),
                buffer,
                pos,
            ))
        }
    })
}

/// Parses a list of same-type objects.
///
/// The parsed object-type needs to have an implementation for the [`Parse`] trait.
/// The object-list can have a prefix, a delimiter and a postfix.
///
/// - NB: One should make sure, that the `A`-object parse-function does not consume `separator`-strings.
/// - NB: `*pos` must always point to a char boundary.
pub fn obj_list<A: Parse>(
    pre: &str,
    separator: &str,
    post: &str,
    buffer: &str,
    pos: &mut usize,
) -> Result<Vec<A>, ParseErr> {
    atomic(pos, |pos| {
        token(pre, buffer, pos)?;
        let mut result = vec![];
        let mut last_pos = *pos;
        while let Ok(e) = A::parse_at(buffer, pos) {
            result.push(e);
            if token(separator, buffer, pos).is_err() {
                break;
            }
            // prevent infinite loops in edge case when `A::parse_at` and `token(separator,..)` do not advance `pos`
            if *pos == last_pos {
                break;
            }
            last_pos = *pos;
        }
        token(post, buffer, pos)?;
        Ok(result)
    })
}

/// Tuple/Sequence of two (possibly different) objects-types
///
/// Leading and intermediate whitespace are skipped.
/// Each object-type must have an implementation of [`Parse`].
/// Parsing of the sequence is handled atomically.
impl<T1: Parse, T2: Parse> Parse for (T1, T2) {
    fn parse_at(
        buffer: &str,
        pos: &mut usize,
    ) -> Result<Self, ParseErr> {
        atomic(pos, |pos| Ok((T1::parse_at(buffer, pos)?, T2::parse_at(buffer, pos)?)))
    }
}

/// Tuple/Sequence of three (possibly different) objects-types
///
/// Leading and intermediate whitespace are skipped.
/// Each object-type must have an implementation of [`Parse`].
/// Parsing of the sequence is handled atomically.
impl<T1: Parse, T2: Parse, T3: Parse> Parse for (T1, T2, T3) {
    fn parse_at(
        buffer: &str,
        pos: &mut usize,
    ) -> Result<Self, ParseErr> {
        atomic(pos, |pos| {
            Ok((T1::parse_at(buffer, pos)?, T2::parse_at(buffer, pos)?, T3::parse_at(buffer, pos)?))
        })
    }
}

/// Tuple/Sequence of four (possibly different) objects-types
///
/// Leading and intermediate whitespace are skipped.
/// Each object-type must have an implementation of [`Parse`].
/// Parsing of the sequence is handled atomically.
impl<T1: Parse, T2: Parse, T3: Parse, T4: Parse> Parse for (T1, T2, T3, T4) {
    fn parse_at(
        buffer: &str,
        pos: &mut usize,
    ) -> Result<Self, ParseErr> {
        atomic(pos, |pos| {
            Ok((
                T1::parse_at(buffer, pos)?,
                T2::parse_at(buffer, pos)?,
                T3::parse_at(buffer, pos)?,
                T4::parse_at(buffer, pos)?,
            ))
        })
    }
}

/// Tuple/Sequence of five (possibly different) objects-types
///
/// Leading and intermediate whitespace are skipped.
/// Each object-type must have an implementation of [`Parse`].
/// Parsing of the sequence is handled atomically.
impl<T1: Parse, T2: Parse, T3: Parse, T4: Parse, T5: Parse> Parse for (T1, T2, T3, T4, T5) {
    fn parse_at(
        buffer: &str,
        pos: &mut usize,
    ) -> Result<Self, ParseErr> {
        atomic(pos, |pos| {
            Ok((
                T1::parse_at(buffer, pos)?,
                T2::parse_at(buffer, pos)?,
                T3::parse_at(buffer, pos)?,
                T4::parse_at(buffer, pos)?,
                T5::parse_at(buffer, pos)?,
            ))
        })
    }
}

/// Tuple/Sequence of six (possibly different) objects-types
///
/// Leading and intermediate whitespace are skipped.
/// Each object-type must have an implementation of [`Parse`].
/// Parsing of the sequence is handled atomically.
impl<T1: Parse, T2: Parse, T3: Parse, T4: Parse, T5: Parse, T6: Parse> Parse for (T1, T2, T3, T4, T5, T6) {
    fn parse_at(
        buffer: &str,
        pos: &mut usize,
    ) -> Result<Self, ParseErr> {
        atomic(pos, |pos| {
            Ok((
                T1::parse_at(buffer, pos)?,
                T2::parse_at(buffer, pos)?,
                T3::parse_at(buffer, pos)?,
                T4::parse_at(buffer, pos)?,
                T5::parse_at(buffer, pos)?,
                T6::parse_at(buffer, pos)?,
            ))
        })
    }
}

/// Tuple/Sequence of seven (possibly different) objects-types
///
/// Leading and intermediate whitespace are skipped.
/// Each object-type must have an implementation of [`Parse`].
/// Parsing of the sequence is handled atomically.
impl<T1: Parse, T2: Parse, T3: Parse, T4: Parse, T5: Parse, T6: Parse, T7: Parse> Parse for (T1, T2, T3, T4, T5, T6, T7) {
    fn parse_at(
        buffer: &str,
        pos: &mut usize,
    ) -> Result<Self, ParseErr> {
        atomic(pos, |pos| {
            Ok((
                T1::parse_at(buffer, pos)?,
                T2::parse_at(buffer, pos)?,
                T3::parse_at(buffer, pos)?,
                T4::parse_at(buffer, pos)?,
                T5::parse_at(buffer, pos)?,
                T6::parse_at(buffer, pos)?,
                T7::parse_at(buffer, pos)?,
            ))
        })
    }
}

/// Tuple/Sequence of eight (possibly different) objects-types
///
/// Leading and intermediate whitespace are skipped.
/// Each object-type must have an implementation of [`Parse`].
/// Parsing of the sequence is handled atomically.
impl<T1: Parse, T2: Parse, T3: Parse, T4: Parse, T5: Parse, T6: Parse, T7: Parse, T8: Parse> Parse for (T1, T2, T3, T4, T5, T6, T7, T8) {
    fn parse_at(
        buffer: &str,
        pos: &mut usize,
    ) -> Result<Self, ParseErr> {
        atomic(pos, |pos| {
            Ok((
                T1::parse_at(buffer, pos)?,
                T2::parse_at(buffer, pos)?,
                T3::parse_at(buffer, pos)?,
                T4::parse_at(buffer, pos)?,
                T5::parse_at(buffer, pos)?,
                T6::parse_at(buffer, pos)?,
                T7::parse_at(buffer, pos)?,
                T8::parse_at(buffer, pos)?,
            ))
        })
    }
}

// -----------------------------------------------------------------------
// Parse impls for common primitive types
// -----------------------------------------------------------------------

macro_rules! impl_unsigned {
    ($ty:ty) => {
        impl Parse for $ty {
            fn parse_at(
                buffer: &str,
                pos: &mut usize,
            ) -> Result<Self, ParseErr> {
                let s = digits(buffer, pos)?;
                s.parse()
                    .map_err(|e| ParseErr::new(format!("Invalid {}: '{}' ({})", stringify!($ty), s, e), buffer, pos))
            }
        }
    };
}

macro_rules! impl_signed {
    ($ty:ty) => {
        impl Parse for $ty {
            fn parse_at(
                buffer: &str,
                pos: &mut usize,
            ) -> Result<Self, ParseErr> {
                atomic(pos, |pos| {
                    skip_whitespaces(buffer, pos);
                    let start = *pos;
                    if start < buffer.len() && buffer.as_bytes()[start] == b'-' {
                        *pos += 1;
                    }
                    digits_direct(buffer, pos)?;
                    let num_str = &buffer[start..*pos];
                    num_str
                        .parse()
                        .map_err(|e| ParseErr::new(format!("Invalid {}: '{}' ({})", stringify!($ty), num_str, e), buffer, pos))
                })
            }
        }
    };
}

impl_unsigned!(u32);
impl_unsigned!(u64);
impl_signed!(i32);
impl_signed!(i64);

impl Parse for bool {
    fn parse_at(
        buffer: &str,
        pos: &mut usize,
    ) -> Result<Self, ParseErr> {
        match option(&["true", "false"], buffer, pos) {
            Ok("true") => Ok(true),
            Ok("false") => Ok(false),
            Ok(_) => unreachable!(),
            Err(e) => Err(e),
        }
    }
}

impl Parse for f64 {
    fn parse_at(
        buffer: &str,
        pos: &mut usize,
    ) -> Result<Self, ParseErr> {
        atomic(pos, |pos| {
            skip_whitespaces(buffer, pos);
            let start = *pos;
            if start < buffer.len() && buffer.as_bytes()[start] == b'-' {
                *pos += 1;
            }
            digits_direct(buffer, pos)?;
            if buffer[*pos..].starts_with('.') {
                *pos += 1;
                let _ = digits_direct(buffer, pos)?;
            }
            let num_str = &buffer[start..*pos];
            num_str
                .parse()
                .map_err(|e| ParseErr::new(format!("Invalid f64: '{}' ({})", num_str, e), buffer, pos))
        })
    }
}

macro_rules! define_seq {
    ($n:literal, $func:ident, ($($T:ident),+)) => {
        #[doc = concat!("Sequentially parse ", $n, " objects in one transaction.")]
        #[doc = concat!("It's just another syntax to consume a tuple with ", $n, " elements.")]
        pub fn $func<$($T: Parse),+>(
            buffer: &str,
            pos: &mut usize,
        ) -> Result<($($T,)+), ParseErr> {
            <($($T,)+)>::parse_at(buffer, pos)
        }
    };
}

define_seq!(2, seq2, (T1, T2));
define_seq!(3, seq3, (T1, T2, T3));
define_seq!(4, seq4, (T1, T2, T3, T4));
define_seq!(5, seq5, (T1, T2, T3, T4, T5));
define_seq!(6, seq6, (T1, T2, T3, T4, T5, T6));
define_seq!(7, seq7, (T1, T2, T3, T4, T5, T6, T7));
define_seq!(8, seq8, (T1, T2, T3, T4, T5, T6, T7, T8));

#[derive(Debug)]
pub struct ParseErr {
    pub error: String,
    pub context_snapshot: Option<String>,
}

impl ParseErr {
    pub fn new(
        error: String,
        buffer: &str,
        pos: &usize,
    ) -> Self {
        let context_snapshot = Some(buffer[*pos..].chars().zip(0..20).map(|(c, _)| c).collect());
        ParseErr { error, context_snapshot }
    }

    pub fn msg(error: String) -> Self {
        ParseErr {
            error,
            context_snapshot: None,
        }
    }
}

impl Error for ParseErr {}

impl Display for ParseErr {
    fn fmt(
        &self,
        f: &mut Formatter<'_>,
    ) -> std::fmt::Result {
        f.write_str(&self.error)?;
        if let Some(context) = &self.context_snapshot {
            write!(f, " — near '{}'", context)?;
        }
        Ok(())
    }
}

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

    // -----------------------------------------------------------------------
    // skip_whitespaces
    // -----------------------------------------------------------------------

    #[rstest]
    #[case::no_whitespace("hello", 0, 0)]
    #[case::leading_spaces("   hello", 0, 3)]
    #[case::tabs("\t\thello", 0, 2)]
    #[case::newlines("\n\nhello", 0, 2)]
    #[case::mixed(" \t \n hello", 0, 5)]
    #[case::unicode_whitespace("\u{2002}hello", 0, 3)]
    #[case::already_past("hello", 3, 3)]
    fn test_skip_whitespaces(
        #[case] input: &str,
        #[case] start: usize,
        #[case] expected: usize,
    ) {
        let mut pos = start;
        skip_whitespaces(input, &mut pos);
        assert_eq!(pos, expected);
    }

    // -----------------------------------------------------------------------
    // atomic
    // -----------------------------------------------------------------------

    #[test]
    fn test_atomic_pos_unchanged_on_failure() {
        let mut pos = 5;
        let result: Result<(), ParseErr> = atomic(&mut pos, |p| {
            *p = 6;
            Err(ParseErr::msg("nope".into()))
        });
        assert!(result.is_err());
        assert_eq!(pos, 5);
    }

    #[test]
    fn test_atomic_pos_updated_on_success() {
        let mut pos = 0;
        let result: Result<i32, ParseErr> = atomic(&mut pos, |p| {
            *p = 10;
            Ok(99)
        });
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), 99);
        assert_eq!(pos, 10);
    }

    // -----------------------------------------------------------------------
    // identifier
    // -----------------------------------------------------------------------

    #[rstest]
    #[case::simple_ascii("hello", 0, "hello", 5)]
    #[case::with_underscore("foo_bar", 0, "foo_bar", 7)]
    #[case::with_dash("a-b", 0, "a-b", 3)]
    #[case::with_dollar("x$y", 0, "x$y", 3)]
    #[case::with_dot("a.b", 0, "a.b", 3)]
    #[case::mixed_special("a_b-c$d.e", 0, "a_b-c$d.e", 9)]
    #[case::unicode_alpha("λόγος", 0, "λόγος", 10)]
    #[case::unicode_then_ascii("α42", 0, "α42", 4)]
    #[case::leading_whitespace("  abc", 0, "abc", 5)]
    #[case::stop_at_non_id("abc def", 0, "abc", 3)]
    #[case::stop_at_paren("abc(x)", 0, "abc", 3)]
    #[case::single_char("x", 0, "x", 1)]
    #[case::unicode_whitespace_before("\u{2002}id", 0, "id", 5)]
    fn test_identifier_ok(
        #[case] input: &str,
        #[case] start: usize,
        #[case] expected_str: &str,
        #[case] expected_pos: usize,
    ) {
        let mut pos = start;
        let result = identifier(input, &mut pos);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), expected_str);
        assert_eq!(pos, expected_pos);
    }

    #[rstest]
    #[case::starts_with_digit("123abc")]
    #[case::starts_with_underscore("_abc")]
    #[case::starts_with_dash("-abc")]
    #[case::starts_with_dollar("$abc")]
    #[case::starts_with_dot(".abc")]
    #[case::starts_with_punct("!abc")]
    #[case::empty("")]
    #[case::only_whitespace("   ")]
    fn test_identifier_err(#[case] input: &str) {
        let mut pos = 0;
        let result = identifier(input, &mut pos);
        assert!(result.is_err());
        assert_eq!(pos, 0); // atomic guarantees rollback
    }

    // -----------------------------------------------------------------------
    // name
    // -----------------------------------------------------------------------

    #[rstest]
    #[case::simple_ascii("hello", 0, "hello", 5)]
    #[case::with_underscore("foo_bar", 0, "foo_bar", 7)]
    #[case::unicode_alpha("λόγος", 0, "λόγος", 10)]
    #[case::unicode_then_ascii("α42", 0, "α42", 4)]
    #[case::leading_whitespace("  abc", 0, "abc", 5)]
    #[case::stop_at_whitespace("abc def", 0, "abc", 3)]
    #[case::stop_at_paren("abc(x)", 0, "abc", 3)]
    #[case::stop_at_dash("a-b", 0, "a", 1)]
    #[case::stop_at_dollar("x$y", 0, "x", 1)]
    #[case::stop_at_dot("a.b", 0, "a", 1)]
    #[case::single_char("x", 0, "x", 1)]
    #[case::unicode_whitespace_before("\u{2002}id", 0, "id", 5)]
    fn test_name_ok(
        #[case] input: &str,
        #[case] start: usize,
        #[case] expected_str: &str,
        #[case] expected_pos: usize,
    ) {
        let mut pos = start;
        let result = name(input, &mut pos);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), expected_str);
        assert_eq!(pos, expected_pos);
    }

    #[rstest]
    #[case::starts_with_digit("123abc")]
    #[case::starts_with_underscore("_abc")]
    #[case::starts_with_dash("-abc")]
    #[case::starts_with_dollar("$abc")]
    #[case::starts_with_dot(".abc")]
    #[case::starts_with_punct("!abc")]
    #[case::empty("")]
    #[case::only_whitespace("   ")]
    fn test_name_err(#[case] input: &str) {
        let mut pos = 0;
        let result = name(input, &mut pos);
        assert!(result.is_err());
        assert_eq!(pos, 0);
    }

    // -----------------------------------------------------------------------
    // token
    // -----------------------------------------------------------------------

    #[rstest]
    #[case::exact("hello", "hello", 0, "hello", 5)]
    #[case::with_whitespace("  hello", "hello", 0, "hello", 7)]
    #[case::substring_in_middle("xx helloxyz", "hello", 3, "hello", 8)]
    #[case::unicode("αβγ", "αβγ", 0, "αβγ", 6)]
    fn test_token_ok(
        #[case] input: &str,
        #[case] tok: &str,
        #[case] start: usize,
        #[case] expected_str: &str,
        #[case] expected_pos: usize,
    ) {
        let mut pos = start;
        let result = token(tok, input, &mut pos);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), expected_str);
        assert_eq!(pos, expected_pos);
    }

    #[rstest]
    #[case::wrong_token("hello", "world", 0)]
    #[case::too_short("ab", "abc", 0)]
    fn test_token_err(
        #[case] input: &str,
        #[case] tok: &str,
        #[case] start: usize,
    ) {
        let mut pos = start;
        let result = token(tok, input, &mut pos);
        assert!(result.is_err());
        assert_eq!(pos, start); // rollback
    }

    // -----------------------------------------------------------------------
    // digits_direct
    // -----------------------------------------------------------------------

    #[rstest]
    #[case::simple("123", 0, "123", 3)]
    #[case::stops_at_non_digit("123abc", 0, "123", 3)]
    #[case::single_digit("7", 0, "7", 1)]
    fn test_digits_direct_ok(
        #[case] input: &str,
        #[case] start: usize,
        #[case] expected_str: &str,
        #[case] expected_pos: usize,
    ) {
        let mut pos = start;
        let result = digits_direct(input, &mut pos);
        assert!(result.is_ok(), "expected Ok but got Err");
        assert_eq!(result.unwrap(), expected_str);
        assert_eq!(pos, expected_pos);
    }

    #[rstest]
    #[case::no_digits("abc")]
    #[case::empty("")]
    #[case::whitespace_first(" 1")] // does not skip whitespace
    fn test_digits_direct_err(#[case] input: &str) {
        let mut pos = 0;
        let result = digits_direct(input, &mut pos);
        assert!(result.is_err());
        assert_eq!(pos, 0);
    }

    // -----------------------------------------------------------------------
    // digits
    // -----------------------------------------------------------------------

    #[rstest]
    #[case::simple("42", 0, "42", 2)]
    #[case::with_whitespace("  99", 0, "99", 4)]
    #[case::stops_at_non_digit("42abc", 0, "42", 2)]
    fn test_digits_ok(
        #[case] input: &str,
        #[case] start: usize,
        #[case] expected_str: &str,
        #[case] expected_pos: usize,
    ) {
        let mut pos = start;
        let result = digits(input, &mut pos);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), expected_str);
        assert_eq!(pos, expected_pos);
    }

    #[rstest]
    #[case::no_digits("abc")]
    #[case::empty("")]
    fn test_digits_err(#[case] input: &str) {
        let mut pos = 0;
        let result = digits(input, &mut pos);
        assert!(result.is_err());
        assert_eq!(pos, 0);
    }

    // -----------------------------------------------------------------------
    // string_sequence
    // -----------------------------------------------------------------------

    fn id_fn<'a>(
        buffer: &'a str,
        pos: &mut usize,
    ) -> Result<&'a str, ParseErr> {
        identifier(buffer, pos)
    }
    fn dig_fn<'a>(
        buffer: &'a str,
        pos: &mut usize,
    ) -> Result<&'a str, ParseErr> {
        digits(buffer, pos)
    }

    #[test]
    fn test_string_sequence_ok() {
        let mut pos = 0;
        let result = string_sequence(&[id_fn, dig_fn], "abc 123", &mut pos);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), ["abc", "123"]);
        assert_eq!(pos, 7);
    }

    #[test]
    fn test_string_sequence_fails_partway() {
        let mut pos = 0;
        let result = string_sequence(&[dig_fn, id_fn], "abc", &mut pos);
        assert!(result.is_err());
        assert_eq!(pos, 0); // atomic rollback
    }

    // -----------------------------------------------------------------------
    // option
    // -----------------------------------------------------------------------

    #[rstest]
    #[case::first("foo", &["foo", "bar"], 0, "foo", 3)]
    #[case::second("bar", &["foo", "bar"], 0, "bar", 3)]
    #[case::with_whitespace("  baz", &["baz", "qux"], 0, "baz", 5)]
    #[case::single_option("only", &["only"], 0, "only", 4)]
    fn test_option_ok(
        #[case] input: &str,
        #[case] options: &[&str],
        #[case] start: usize,
        #[case] expected_str: &str,
        #[case] expected_pos: usize,
    ) {
        let mut pos = start;
        let result = option(options, input, &mut pos);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), expected_str);
        assert_eq!(pos, expected_pos);
    }

    #[rstest]
    #[case::no_match("xyz", &["foo", "bar"])]
    #[case::empty_input("", &["foo"])]
    fn test_option_err(
        #[case] input: &str,
        #[case] options: &[&str],
    ) {
        let mut pos = 0;
        let result = option(options, input, &mut pos);
        assert!(result.is_err());
        assert_eq!(pos, 0);
    }

    // -----------------------------------------------------------------------
    // obj_list
    // -----------------------------------------------------------------------

    /// Wrapper type that delegates to `identifier` for parse_at.
    /// We collect into a Vec<String> just for test convenience — but since
    /// Parse returns Self, we define a thin newtype.
    #[derive(Debug, PartialEq)]
    struct Ident(String);

    impl Parse for Ident {
        fn parse_at(
            buffer: &str,
            pos: &mut usize,
        ) -> Result<Self, ParseErr> {
            identifier(buffer, pos).map(|s| Ident(s.to_string()))
        }
    }

    #[rstest]
    #[case::three_elems("[a, b, c]", "[", ",", "]", &["a", "b", "c"])]
    #[case::single_elem("[x]", "[", ",", "]", &["x"])]
    #[case::no_spaces("[a,b,c]", "[", ",", "]", &["a", "b", "c"])]
    #[case::empty_list("[]", "[", ",", "]", &[])]
    fn test_obj_list_ok(
        #[case] input: &str,
        #[case] pre: &str,
        #[case] sep: &str,
        #[case] post: &str,
        #[case] expected: &[&str],
    ) {
        let mut pos = 0;
        let result: Result<Vec<Ident>, _> = obj_list(pre, sep, post, input, &mut pos);
        assert!(result.is_ok(), "expected Ok, got {:?}", result);
        let ids: Vec<String> = result.unwrap().into_iter().map(|i| i.0).collect();
        let expected_strs: Vec<String> = expected.iter().map(|s| s.to_string()).collect();
        assert_eq!(ids, expected_strs);
        assert_eq!(pos, input.len());
    }

    #[rstest]
    #[case::missing_prefix("a,b,c]", "[", ",", "]")]
    #[case::missing_postfix("[a,b,c", "[", ",", "]")]
    fn test_obj_list_err(
        #[case] input: &str,
        #[case] pre: &str,
        #[case] sep: &str,
        #[case] post: &str,
    ) {
        let mut pos = 0;
        let result: Result<Vec<Ident>, _> = obj_list(pre, sep, post, input, &mut pos);
        assert!(result.is_err());
        assert_eq!(pos, 0);
    }

    // -----------------------------------------------------------------------
    // Parse trait — custom impl and tuple impls
    // -----------------------------------------------------------------------

    #[derive(Debug, PartialEq)]
    struct Point(i32, i32);

    impl Parse for Point {
        fn parse_at(
            buffer: &str,
            pos: &mut usize,
        ) -> Result<Self, ParseErr> {
            token("(", buffer, pos)?;
            let sx = digits(buffer, pos)?;
            token(",", buffer, pos)?;
            let sy = digits(buffer, pos)?;
            token(")", buffer, pos)?;
            let x: i32 = sx.parse().unwrap();
            let y: i32 = sy.parse().unwrap();
            Ok(Point(x, y))
        }
    }

    /// Thin wrapper around `identifier` for use in tuple impl tests.
    #[derive(Debug, PartialEq)]
    struct Id1(String);

    impl Parse for Id1 {
        fn parse_at(
            buffer: &str,
            pos: &mut usize,
        ) -> Result<Self, ParseErr> {
            name(buffer, pos).map(|s| Id1(s.to_string()))
        }
    }

    #[derive(Debug, PartialEq)]
    struct Id2(String);

    impl Parse for Id2 {
        fn parse_at(
            buffer: &str,
            pos: &mut usize,
        ) -> Result<Self, ParseErr> {
            identifier(buffer, pos).map(|s| Id2(s.to_string()))
        }
    }

    #[derive(Debug, PartialEq)]
    struct C(String);

    impl Parse for C {
        fn parse_at(
            buffer: &str,
            pos: &mut usize,
        ) -> Result<Self, ParseErr> {
            option(&[";", ".", "/"], buffer, pos).map(|s| C(s.to_string()))
        }
    }

    #[test]
    fn test_custom_parse() {
        let result = Point::parse("(42, 99)");
        assert_eq!(result.unwrap(), Point(42, 99));
    }

    #[test]
    fn test_tuple2_parse() {
        let result = <(Id1, Id1)>::parse("abc xyz");
        assert!(result.is_ok());
        let (a, b) = result.unwrap();
        assert_eq!(a.0, "abc");
        assert_eq!(b.0, "xyz");
    }

    #[test]
    fn test_tuple3_parse() {
        let result = <(Id1, Id1, Id1)>::parse("a b c");
        assert!(result.is_ok());
        let (a, b, c) = result.unwrap();
        assert_eq!(a.0, "a");
        assert_eq!(b.0, "b");
        assert_eq!(c.0, "c");
    }

    #[test]
    fn test_tuple_parse_failure_rollback() {
        let mut pos = 0;
        // second parse fails → pos must be unchanged
        let result = <(Id1, Id1)>::parse_at("abc ???", &mut pos);
        assert!(result.is_err());
        assert_eq!(pos, 0);
    }

    #[test]
    fn test_tuple_showcase() -> Result<(), ParseErr> {
        let (a, b, c): (Id1, Id2, C) = <(Id1, Id2, C)>::parse("Foo Bar$ /")?;
        assert_eq!(a.0, "Foo");
        assert_eq!(b.0, "Bar$");
        assert_eq!(c.0, "/");
        Ok(())
    }

    // -----------------------------------------------------------------------
    // ParseErr
    // -----------------------------------------------------------------------

    #[test]
    fn test_parse_err_display_with_context() {
        let err = ParseErr::new("boom".into(), "hello world", &6);
        let msg = err.to_string();
        assert!(msg.contains("boom"));
        assert!(msg.contains("world"));
    }

    #[test]
    fn test_parse_err_display_no_context() {
        let err = ParseErr::msg("just an error".into());
        let msg = err.to_string();
        assert_eq!(msg, "just an error");
    }

    #[test]
    fn test_parse_err_context_snapshot_truncated() {
        let long = "x".repeat(100);
        let err = ParseErr::new("err".into(), &long, &0);
        let ctx = err.context_snapshot.unwrap();
        assert_eq!(ctx.len(), 20);
    }

    // -----------------------------------------------------------------------
    // Parse impls for primitive types
    // -----------------------------------------------------------------------

    #[rstest]
    #[case::positive("42", 42u32)]
    #[case::with_whitespace("  99", 99u32)]
    #[case::zero("0", 0u32)]
    fn test_u32_ok(
        #[case] input: &str,
        #[case] expected: u32,
    ) {
        let result = u32::parse(input);
        assert_eq!(result.unwrap(), expected);
    }

    #[rstest]
    #[case::no_digits("abc")]
    #[case::empty("")]
    fn test_u32_err(#[case] input: &str) {
        assert!(u32::parse(input).is_err());
    }

    #[rstest]
    #[case::positive("42", 42u64)]
    #[case::large("9999999999999", 9999999999999u64)]
    fn test_u64_ok(
        #[case] input: &str,
        #[case] expected: u64,
    ) {
        let result = u64::parse(input);
        assert_eq!(result.unwrap(), expected);
    }

    #[rstest]
    #[case::positive("42", 42i32)]
    #[case::negative("-42", -42i32)]
    #[case::with_whitespace("  -99", -99i32)]
    #[case::zero("0", 0i32)]
    fn test_i32_ok(
        #[case] input: &str,
        #[case] expected: i32,
    ) {
        let result = i32::parse(input);
        assert_eq!(result.unwrap(), expected);
    }

    #[rstest]
    #[case::no_digits("abc")]
    #[case::only_minus("-")]
    fn test_i32_err(#[case] input: &str) {
        assert!(i32::parse(input).is_err());
    }

    #[rstest]
    #[case::positive("42", 42i64)]
    #[case::negative("-42", -42i64)]
    fn test_i64_ok(
        #[case] input: &str,
        #[case] expected: i64,
    ) {
        let result = i64::parse(input);
        assert_eq!(result.unwrap(), expected);
    }

    #[rstest]
    #[case::true_token("true", true)]
    #[case::false_token("false", false)]
    #[case::with_whitespace("  true", true)]
    fn test_bool_ok(
        #[case] input: &str,
        #[case] expected: bool,
    ) {
        let result = bool::parse(input);
        assert_eq!(result.unwrap(), expected);
    }

    #[rstest]
    #[case::invalid("tru")]
    #[case::empty("")]
    fn test_bool_err(#[case] input: &str) {
        assert!(bool::parse(input).is_err());
    }

    #[rstest]
    #[case::integer("42", 42.0f64)]
    #[case::negative("-3.5", -3.5f64)]
    #[case::fractional("3.74", 3.74f64)]
    #[case::with_whitespace("  -2.5", -2.5f64)]
    fn test_f64_ok(
        #[case] input: &str,
        #[case] expected: f64,
    ) {
        let result = f64::parse(input);
        let val = result.unwrap();
        assert!((val - expected).abs() < 1e-10, "got {} expected {}", val, expected);
    }

    #[rstest]
    #[case::no_digits("abc")]
    #[case::empty("")]
    #[case::only_dot(".")]
    fn test_f64_err(#[case] input: &str) {
        assert!(f64::parse(input).is_err());
    }

    #[test]
    fn test_primitive_rollback_on_failure() {
        let mut pos = 0;
        assert!(i32::parse_at("abc", &mut pos).is_err());
        assert_eq!(pos, 0);
        assert!(f64::parse_at("abc", &mut pos).is_err());
        assert_eq!(pos, 0);
        assert!(bool::parse_at("xyz", &mut pos).is_err());
        assert_eq!(pos, 0);
    }

    // -----------------------------------------------------------------------
    // NamedPoint – a more complex mixed szenario with atomic and seq3
    // -----------------------------------------------------------------------
    #[derive(Debug)]
    struct NamedPoint {
        name: Id1,
        point: Point,
    }
    impl Parse for NamedPoint {
        fn parse_at(
            buffer: &str,
            pos: &mut usize,
        ) -> Result<Self, ParseErr> {
            atomic(pos, |pos| {
                token("NamedPoint", buffer, pos)?;
                let (name, point) = seq2::<Id1, Point>(buffer, pos)?;
                token(";", buffer, pos)?;
                Ok(NamedPoint { name, point })
            })
        }
    }
    #[test]
    fn test_named_point() {
        let buffer = ".. NamedPoint A(9,20); ..";
        let mut pos = 3;

        match NamedPoint::parse_at(buffer, &mut pos) {
            Ok(NamedPoint { name, point }) => {
                assert_eq!(name, Id1("A".to_string()));
                assert_eq!(point, Point(9, 20));
            }
            Err(e) => panic!("{}", e),
        }
    }
}
