use crate::{
    schedule::{MAX_YEAR, MIN_YEAR, MIN_YEAR_STR},
    series::SeriesWithStep,
    utils::{self, days_in_month},
    CronError, Result,
};
use chrono::{DateTime, Datelike, TimeZone, Timelike};
use std::{collections::BTreeSet, fmt::Display};

/// Common type for internal values of date and time parts.
pub(crate) type PatternValueType = u16;

/// Pattern is a single element (part) of a schedule expression.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct Pattern {
    type_: PatternType,
    pattern: PatternItem,
}

impl Pattern {
    #[inline]
    /// Getter for pattern reference.
    pub(crate) fn pattern(&self) -> &PatternItem {
        &self.pattern
    }

    /// Parses and validates single element of schedule expression.
    pub(crate) fn parse(type_: PatternType, input: &str) -> Result<Self> {
        if input.is_empty() {
            return Err(CronError::InvalidCronPattern(input.to_owned(), type_.to_string()));
        }

        let mut error_indicator = Ok(());
        let mut splitted = input
            .split(',')
            .map(|value| {
                if value == "*" {
                    Ok(PatternItem::All)
                } else if value == "?" && [PatternType::Dows, PatternType::Doms].contains(&type_) {
                    // valid for DOM and DOW element types
                    Ok(PatternItem::Any)
                } else if value.ends_with('L') && type_ == PatternType::Dows {
                    // finishing `L` is allowed in the DOW element only
                    let value = value.trim_end_matches('L');
                    Ok(PatternItem::LastDow(type_.parse(value)?))
                } else if value == "L" && type_ == PatternType::Doms {
                    // single `L` is allowed in the DOM element only
                    Ok(PatternItem::LastDom)
                } else if value.ends_with('W') && type_ == PatternType::Doms {
                    // the nearest weekday is allowed in the DOM only
                    let value = value.trim_end_matches('W');
                    Ok(PatternItem::Weekday(type_.parse(value)?))
                } else if value.contains('/') && type_ != PatternType::Dows {
                    // two types of a repeating element:
                    // - with particular starting value,
                    //   finishing value is the max possible for this type;
                    // - and with range of possible values.
                    let (base, repeater) = value.split_once('/').unwrap();
                    let base = if base == "*" {
                        // `*` means we start from the minimum possible value
                        match type_ {
                            PatternType::Doms | PatternType::Months => "1",
                            PatternType::Years => MIN_YEAR_STR,
                            _ => "0",
                        }
                    } else {
                        base
                    };

                    let repeater = if let Ok(repeater) = repeater.parse() {
                        let (_min, max) = type_.min_max();
                        if repeater < 2 || repeater > max {
                            return Err(CronError::InvalidRepeatingPattern(value.to_owned(), type_.to_string()));
                        }
                        repeater
                    } else {
                        return Err(CronError::InvalidRepeatingPattern(value.to_owned(), type_.to_string()));
                    };

                    if base.contains('-') {
                        let (start, end) = base.split_once('-').unwrap();
                        let start = type_.parse(start)?;
                        let end = type_.parse(end)?;
                        if start >= end {
                            return Err(CronError::InvalidRangeValue(value.to_owned(), type_.to_string()));
                        }
                        Ok(PatternItem::RepeatingRange(start, end, repeater))
                    } else {
                        Ok(PatternItem::RepeatingValue(type_.parse(base)?, repeater))
                    }
                } else if value.contains('-') {
                    // Range
                    let (start, end) = value.split_once('-').unwrap();
                    let start = type_.parse(start)?;
                    let end = type_.parse(end)?;
                    if start >= end {
                        return Err(CronError::InvalidRangeValue(value.to_owned(), type_.to_string()));
                    }
                    Ok(PatternItem::Range(start, end))
                } else if value.contains('#') && type_ == PatternType::Dows {
                    // Sharp/hash is allowed for day of week only
                    let mut parts = value.split('#');
                    let dow = parts.next().unwrap();
                    let number = parts.next().unwrap();
                    let number = utils::parse_digital_value(number, 1, 4);
                    if number.is_none() {
                        return Err(CronError::InvalidDayOfWeekValue(value.to_owned(), type_.to_string()));
                    }
                    Ok(PatternItem::Hash(type_.parse(dow)?, number.unwrap()))
                } else {
                    // everything else is a defined single (particular) value
                    Ok(PatternItem::Particular(type_.parse(value)?))
                }
            })
            // we use this to detect that at least one element of the list has an error, so
            // a whole pattern should be invalidated
            .scan(&mut error_indicator, |err, res| match res {
                Ok(o) => Some(o),
                Err(e) => {
                    **err = Err(e);
                    None
                }
            })
            .collect::<Vec<_>>();

        // we use this to detect that at least one element of the list has an error, so
        // a whole pattern should be invalidated
        error_indicator?;

        // sanity checks
        if splitted.is_empty()
            || (splitted.len() > 1 && (splitted.contains(&PatternItem::All) || splitted.contains(&PatternItem::Any)))
        {
            return Err(CronError::InvalidCronPattern(input.to_owned(), type_.to_string()));
        }

        let pattern = if splitted.len() > 1 {
            PatternItem::List(splitted)
        } else {
            splitted.remove(0)
        };

        Ok(Self { type_, pattern })
    }

    /// Returns next possible and valid value of this pattern element,
    /// starting form the current timestamp (inclusively) or None.
    pub(crate) fn next<Tz: TimeZone>(&self, current: &mut DateTime<Tz>) -> Option<PatternValueType> {
        // determine max element value depending on its type
        let max = if self.type_ == PatternType::Doms || self.type_ == PatternType::Dows {
            days_in_month(current.year() as PatternValueType, current.month() as PatternValueType)
        } else {
            let (_min, max) = self.type_.min_max();
            max
        };

        // we start calculations from the current value of the element
        let start = match self.type_ {
            PatternType::Seconds => current.second() as PatternValueType,
            PatternType::Minutes => current.minute() as PatternValueType,
            PatternType::Hours => current.hour() as PatternValueType,
            PatternType::Doms | PatternType::Dows => current.day() as PatternValueType,
            PatternType::Months => current.month() as PatternValueType,
            PatternType::Years => current.year() as PatternValueType,
        };

        let value: Option<PatternValueType> = match &self.pattern {
            PatternItem::List(values) => {
                // iterate over all elements of the list and determine the minimum
                // possible valid value if it exists
                let mut min: Option<PatternValueType> = None;

                for pattern in values {
                    let item = Self {
                        type_: self.type_,
                        pattern: pattern.clone(),
                    };
                    let mut current = current.clone();
                    if let Some(next) = item.next(&mut current) {
                        if let Some(prev) = min {
                            if next < prev {
                                min = Some(next);
                            }
                        } else {
                            min = Some(next)
                        }
                    }
                }
                min
            }
            // just the first possible in a row
            PatternItem::All => Some(start),
            // specific single value if not DOW
            PatternItem::Particular(value) if self.type_ != PatternType::Dows => {
                if *value >= start && *value <= max {
                    Some(*value)
                } else {
                    None
                }
            }
            // or day of month for specific day of week
            PatternItem::Particular(value) if self.type_ == PatternType::Dows => (start..=max).find(|&day| {
                utils::day_of_week(
                    current.year() as PatternValueType,
                    current.month() as PatternValueType,
                    day,
                ) == *value
            }),
            // the first value in a range >= than start
            PatternItem::Range(begin, end) if self.type_ != PatternType::Dows => {
                SeriesWithStep::new(*begin, *end, 1, *begin)
                    .filter(|v| *v >= start && *v <= max)
                    .collect::<BTreeSet<_>>()
                    .first()
                    .copied()
            }
            // the same, but for DOW range we return day of month represented by DOW
            PatternItem::Range(first_dow, last_dow) if self.type_ == PatternType::Dows => (start..=max).find(|&day| {
                let dow = utils::day_of_week(
                    current.year() as PatternValueType,
                    current.month() as PatternValueType,
                    day,
                );
                dow >= *first_dow && dow <= *last_dow
            }),
            PatternItem::RepeatingValue(range_start, step) => {
                SeriesWithStep::new(*range_start, max, *step, *range_start)
                    .filter(|v| *v >= start)
                    .collect::<BTreeSet<_>>()
                    .first()
                    .copied()
            }
            PatternItem::RepeatingRange(min, max, step) => SeriesWithStep::new(*min, *max, *step, *min)
                .filter(|v| *v >= start)
                .collect::<BTreeSet<_>>()
                .first()
                .copied(),
            PatternItem::LastDow(dow) => {
                let last_dow = utils::last_dow(
                    current.year() as PatternValueType,
                    current.month() as PatternValueType,
                    *dow,
                );
                if last_dow >= current.day() as PatternValueType {
                    Some(last_dow)
                } else {
                    None
                }
            }
            PatternItem::LastDom => Some(days_in_month(
                current.year() as PatternValueType,
                current.month() as PatternValueType,
            )),
            PatternItem::Weekday(dom) => {
                let weekday = utils::nearest_weekday(
                    current.year() as PatternValueType,
                    current.month() as PatternValueType,
                    *dom,
                );
                if weekday >= start {
                    Some(weekday)
                } else {
                    None
                }
            }
            PatternItem::Hash(dow, number) => {
                let day = utils::nth_dow(
                    current.year() as PatternValueType,
                    current.month() as PatternValueType,
                    *dow,
                    *number,
                );
                if day >= current.day() as PatternValueType {
                    Some(day)
                } else {
                    None
                }
            }
            // theoretically, we shouldn't call this `next` method for indifferent types.
            PatternItem::Any => None,
            _ => unreachable!(),
        };

        // if we got value greater than current,
        // we have to update all dependent elements to the first valid values,
        // because we leaped over to the next day/hour/minute/...
        // and should start from the beginning
        if let Some(value) = value {
            if value > start {
                match self.type_ {
                    PatternType::Years => {
                        *current = current
                            .with_day(1)?
                            .with_month(1)?
                            .with_year(value as i32)?
                            .with_hour(0)?
                            .with_minute(0)?
                            .with_second(0)?;
                    }
                    PatternType::Months => {
                        *current = current
                            .with_day(1)?
                            .with_month(value as u32)?
                            .with_hour(0)?
                            .with_minute(0)?
                            .with_second(0)?;
                    }
                    PatternType::Doms | PatternType::Dows => {
                        *current = current
                            .with_day(value as u32)?
                            .with_hour(0)?
                            .with_minute(0)?
                            .with_second(0)?;
                    }
                    PatternType::Hours => {
                        *current = current.with_hour(value as u32)?.with_minute(0)?.with_second(0)?;
                    }
                    PatternType::Minutes => {
                        *current = current.with_minute(value as u32)?.with_second(0)?;
                    }
                    PatternType::Seconds => {
                        *current = current.with_second(value as u32)?;
                    }
                }
            }
        }

        value
    }
}

impl Display for Pattern {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.pattern)
    }
}

/// Represents a type of the pattern element.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) enum PatternType {
    Seconds,
    Minutes,
    Hours,
    Doms,
    Months,
    Dows,
    Years,
}

impl Display for PatternType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            match self {
                Self::Seconds => "seconds",
                Self::Minutes => "minutes",
                Self::Hours => "hours",
                Self::Doms => "days of month",
                Self::Months => "months",
                Self::Dows => "days of week",
                Self::Years => "years",
            }
        )
    }
}

impl PatternType {
    const DAYS_OF_WEEK: [&'static str; 7] = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
    const MONTHS: [&'static str; 12] = [
        "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC",
    ];

    /// Returns minimum and maximum valid value of specific pattern element type.
    fn min_max(&self) -> (PatternValueType, PatternValueType) {
        match self {
            Self::Seconds => (0, 59),
            Self::Minutes => (0, 59),
            Self::Hours => (0, 23),
            Self::Doms => (1, 31),
            Self::Months => (1, 12),
            Self::Dows => (0, 6),
            Self::Years => (MIN_YEAR, MAX_YEAR),
        }
    }

    /// Parse a single value of the particular pattern element, depending on an element type.
    fn parse(&self, input: &str) -> Result<PatternValueType> {
        // determine valid ranges
        let (min, max) = self.min_max();
        let (variants, starter_shift) = match self {
            PatternType::Seconds
            | PatternType::Minutes
            | PatternType::Hours
            | PatternType::Doms
            | PatternType::Years => (vec![], 0),
            PatternType::Months => (Self::MONTHS.to_vec(), 1),
            PatternType::Dows => (Self::DAYS_OF_WEEK.to_vec(), 0),
        };

        // and convert string to the unsigned integer
        match self {
            PatternType::Seconds
            | PatternType::Minutes
            | PatternType::Hours
            | PatternType::Doms
            | PatternType::Years => {
                if let Some(value) = utils::parse_digital_value(input, min, max) {
                    Ok(value)
                } else {
                    Err(CronError::InvalidDigitalValue(input.to_owned(), self.to_string()))
                }
            }
            PatternType::Months | PatternType::Dows => {
                if let Some(value) = utils::parse_digital_value(input, min, max) {
                    Ok(value)
                } else if let Some(value) = utils::parse_string_value(input, &variants) {
                    Ok(value + starter_shift)
                } else {
                    Err(CronError::InvalidMnemonicValue(input.to_owned(), self.to_string()))
                }
            }
        }
    }
}

/// Represents a schedule of the whole pattern element, including lists.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) enum PatternItem {
    All,
    Any,
    Particular(PatternValueType),
    List(Vec<PatternItem>),
    // start-finish
    Range(PatternValueType, PatternValueType),
    // start/step
    RepeatingValue(PatternValueType, PatternValueType),
    // start-finish/step
    RepeatingRange(PatternValueType, PatternValueType, PatternValueType),
    // weekday
    LastDow(PatternValueType),
    LastDom,
    // month
    Weekday(PatternValueType),
    // weekday#nth
    Hash(PatternValueType, PatternValueType),
}

impl Display for PatternItem {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PatternItem::All => write!(f, "*"),
            PatternItem::Any => write!(f, "?"),
            PatternItem::LastDow(dow) => write!(f, "{dow}L"),
            PatternItem::LastDom => write!(f, "L"),
            PatternItem::Weekday(dom) => write!(f, "{dom}W"),
            PatternItem::RepeatingValue(value, repeater) => write!(f, "{value}/{repeater}"),
            PatternItem::RepeatingRange(start, end, repeater) => {
                write!(f, "{start}-{end}/{repeater}")
            }
            PatternItem::Range(start, end) => write!(f, "{start}-{end}"),
            PatternItem::Particular(value) => write!(f, "{value}"),
            PatternItem::List(vec) => {
                let values = vec.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(",");
                write!(f, "{values}")
            }
            PatternItem::Hash(dow, number) => write!(f, "{dow}#{number}"),
        }
    }
}

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

    const MAX_YEAR_STR: &str = "2099";

    #[rstest]
    #[case(PatternType::Seconds)]
    #[case(PatternType::Minutes)]
    #[case(PatternType::Hours)]
    #[case(PatternType::Doms)]
    #[case(PatternType::Months)]
    #[case(PatternType::Dows)]
    #[case(PatternType::Years)]
    fn test_pattern_item_display(#[case] type_: PatternType) {
        let test_cases = vec![
            (PatternItem::All, "*"),
            (PatternItem::Any, "?"),
            (PatternItem::Particular(5), "5"),
            (
                PatternItem::List(vec![PatternItem::Particular(3), PatternItem::Particular(1)]),
                "3,1",
            ),
            (PatternItem::Range(2, 5), "2-5"),
            (PatternItem::RepeatingValue(15, 30), "15/30"),
            (PatternItem::RepeatingRange(0, 30, 5), "0-30/5"),
            (PatternItem::LastDow(1), "1L"),
            (PatternItem::LastDom, "L"),
            (PatternItem::Weekday(15), "15W"),
            (PatternItem::Hash(3, 2), "3#2"),
            (
                PatternItem::List(vec![
                    PatternItem::Particular(3),
                    PatternItem::Particular(1),
                    PatternItem::Range(2, 5),
                    PatternItem::RepeatingValue(12, 3),
                    PatternItem::RepeatingRange(10, 22, 4),
                ]),
                "3,1,2-5,12/3,10-22/4",
            ),
        ];

        for (item, expected) in test_cases {
            assert_eq!(item.to_string(), expected);
            let pattern = Pattern { type_, pattern: item };
            assert_eq!(pattern.to_string(), expected);
        }
    }

    #[rstest]
    #[case(PatternType::Seconds)]
    #[case(PatternType::Minutes)]
    fn test_pattern_item_parse_valid_1(#[case] type_: PatternType) {
        let test_cases = vec![
            ("*", PatternItem::All),
            ("5", PatternItem::Particular(5)),
            (
                "3,1",
                PatternItem::List(vec![PatternItem::Particular(3), PatternItem::Particular(1)]),
            ),
            ("2-5", PatternItem::Range(2, 5)),
            ("15/30", PatternItem::RepeatingValue(15, 30)),
            ("*/10", PatternItem::RepeatingValue(0, 10)),
            ("0/5", PatternItem::RepeatingValue(0, 5)),
            ("0-30/5", PatternItem::RepeatingRange(0, 30, 5)),
            (
                "3,1,2-5,12/3,10-22/4",
                PatternItem::List(vec![
                    PatternItem::Particular(3),
                    PatternItem::Particular(1),
                    PatternItem::Range(2, 5),
                    PatternItem::RepeatingValue(12, 3),
                    PatternItem::RepeatingRange(10, 22, 4),
                ]),
            ),
        ];

        for (item, expected) in test_cases {
            let pattern = Pattern::parse(type_, item);
            assert!(
                pattern.is_ok(),
                "type = {type_:?}, item = {item}, error = {}",
                pattern.err().unwrap()
            );
            let pattern = pattern.unwrap();
            assert_eq!(pattern.pattern, expected, "item = {item}");
        }
    }

    #[test]
    fn test_pattern_item_parse_valid_dows() {
        let test_cases = vec![
            ("*", PatternItem::All),
            ("?", PatternItem::Any),
            ("5", PatternItem::Particular(5)),
            ("Mon", PatternItem::Particular(1)),
            ("WED", PatternItem::Particular(3)),
            ("fri", PatternItem::Particular(5)),
            ("sun#1", PatternItem::Hash(0, 1)),
            ("3#2", PatternItem::Hash(3, 2)),
            ("4L", PatternItem::LastDow(4)),
            (
                "3,1",
                PatternItem::List(vec![PatternItem::Particular(3), PatternItem::Particular(1)]),
            ),
            (
                "MON,FRI",
                PatternItem::List(vec![PatternItem::Particular(1), PatternItem::Particular(5)]),
            ),
            ("2-5", PatternItem::Range(2, 5)),
            ("Wed-sat", PatternItem::Range(3, 6)),
            (
                "3,1,2-5",
                PatternItem::List(vec![
                    PatternItem::Particular(3),
                    PatternItem::Particular(1),
                    PatternItem::Range(2, 5),
                ]),
            ),
            (
                "WEd,mon,tue-fri",
                PatternItem::List(vec![
                    PatternItem::Particular(3),
                    PatternItem::Particular(1),
                    PatternItem::Range(2, 5),
                ]),
            ),
        ];

        for (item, expected) in test_cases {
            let pattern = Pattern::parse(PatternType::Dows, item);
            assert!(pattern.is_ok(), "item = {item}, error = {}", pattern.err().unwrap());
            let pattern = pattern.unwrap();
            assert_eq!(pattern.pattern, expected, "item = {item}");
        }
    }

    #[test]
    fn test_pattern_item_parse_valid_months() {
        let test_cases = vec![
            ("*", PatternItem::All),
            ("5", PatternItem::Particular(5)),
            ("Jan", PatternItem::Particular(1)),
            ("JUN", PatternItem::Particular(6)),
            ("dec", PatternItem::Particular(12)),
            (
                "3,1",
                PatternItem::List(vec![PatternItem::Particular(3), PatternItem::Particular(1)]),
            ),
            (
                "mar,may",
                PatternItem::List(vec![PatternItem::Particular(3), PatternItem::Particular(5)]),
            ),
            ("2-5", PatternItem::Range(2, 5)),
            ("auG-DEC", PatternItem::Range(8, 12)),
            (
                "3,1,2-5",
                PatternItem::List(vec![
                    PatternItem::Particular(3),
                    PatternItem::Particular(1),
                    PatternItem::Range(2, 5),
                ]),
            ),
            (
                "feb,mar,oct-nov",
                PatternItem::List(vec![
                    PatternItem::Particular(2),
                    PatternItem::Particular(3),
                    PatternItem::Range(10, 11),
                ]),
            ),
            ("2/2", PatternItem::RepeatingValue(2, 2)),
            ("mar/2", PatternItem::RepeatingValue(3, 2)),
            ("*/5", PatternItem::RepeatingValue(1, 5)),
            ("1/6", PatternItem::RepeatingValue(1, 6)),
            ("apr/6", PatternItem::RepeatingValue(4, 6)),
            ("1-12/4", PatternItem::RepeatingRange(1, 12, 4)),
            ("jun-sep/2", PatternItem::RepeatingRange(6, 9, 2)),
            (
                "3,1,2-5,2/6,10-12/4,*/4,apR/2",
                PatternItem::List(vec![
                    PatternItem::Particular(3),
                    PatternItem::Particular(1),
                    PatternItem::Range(2, 5),
                    PatternItem::RepeatingValue(2, 6),
                    PatternItem::RepeatingRange(10, 12, 4),
                    PatternItem::RepeatingValue(1, 4),
                    PatternItem::RepeatingValue(4, 2),
                ]),
            ),
        ];

        for (item, expected) in test_cases {
            let pattern = Pattern::parse(PatternType::Months, item);
            assert!(pattern.is_ok(), "item = {item}, error = {}", pattern.err().unwrap());
            let pattern = pattern.unwrap();
            assert_eq!(pattern.pattern, expected, "item = {item}");
        }
    }

    #[test]
    fn test_pattern_item_parse_valid_doms() {
        let test_cases = vec![
            ("*", PatternItem::All),
            ("?", PatternItem::Any),
            ("5", PatternItem::Particular(5)),
            ("L", PatternItem::LastDom),
            ("15W", PatternItem::Weekday(15)),
            (
                "3,1",
                PatternItem::List(vec![PatternItem::Particular(3), PatternItem::Particular(1)]),
            ),
            ("2-5", PatternItem::Range(2, 5)),
            ("15/30", PatternItem::RepeatingValue(15, 30)),
            ("*/10", PatternItem::RepeatingValue(1, 10)),
            ("1/11", PatternItem::RepeatingValue(1, 11)),
            ("1-30/5", PatternItem::RepeatingRange(1, 30, 5)),
            (
                "3,1,2-5,12/3,10-22/4",
                PatternItem::List(vec![
                    PatternItem::Particular(3),
                    PatternItem::Particular(1),
                    PatternItem::Range(2, 5),
                    PatternItem::RepeatingValue(12, 3),
                    PatternItem::RepeatingRange(10, 22, 4),
                ]),
            ),
        ];

        for (item, expected) in test_cases {
            let pattern = Pattern::parse(PatternType::Doms, item);
            assert!(pattern.is_ok(), "item = {item}, error = {}", pattern.err().unwrap());
            let pattern = pattern.unwrap();
            assert_eq!(pattern.pattern, expected, "item = {item}");
        }
    }

    #[test]
    fn test_pattern_item_parse_valid_year() {
        let test_cases = vec![
            ("*", PatternItem::All),
            ("1975", PatternItem::Particular(1975)),
            (
                "2000,2001",
                PatternItem::List(vec![PatternItem::Particular(2000), PatternItem::Particular(2001)]),
            ),
            ("1982-1999", PatternItem::Range(1982, 1999)),
            ("2015/30", PatternItem::RepeatingValue(2015, 30)),
            ("*/10", PatternItem::RepeatingValue(1970, 10)),
            ("2011/11", PatternItem::RepeatingValue(2011, 11)),
            ("1971-2030/5", PatternItem::RepeatingRange(1971, 2030, 5)),
            (
                "2003,2001,2002-2005,2012/3,2010-2022/4",
                PatternItem::List(vec![
                    PatternItem::Particular(2003),
                    PatternItem::Particular(2001),
                    PatternItem::Range(2002, 2005),
                    PatternItem::RepeatingValue(2012, 3),
                    PatternItem::RepeatingRange(2010, 2022, 4),
                ]),
            ),
        ];

        for (item, expected) in test_cases {
            let pattern = Pattern::parse(PatternType::Years, item);
            assert!(pattern.is_ok(), "item = {item}, error = {}", pattern.err().unwrap());
            let pattern = pattern.unwrap();
            assert_eq!(pattern.pattern, expected, "item = {item}");
        }
    }

    #[rstest]
    #[case(PatternType::Seconds, vec!["2-2/2", "5-1/2", "*,1", "1-1", "5-1", "W", "?", "L", "", " ", ",", "/", "*/", "5/", "-", "1-", "a,b,c", "a-b,c", "1-2-3", ",1", "1,", "1, 2", "1#1", "0/-5", "0/0", "0/60", "60", "0/1"])]
    #[case(PatternType::Minutes, vec!["2-2/2", "5-1/2", "*,1", "1-1", "5-1", "W", "?", "L", "", " ", ",", "/", "*/", "5/", "-", "1-", "a,b,c", "a-b,c", "1-2-3", ",1", "1,", "1, 2", "1#1", "0/-5", "0/0", "0/60", "60", "0/1"])]
    #[case(PatternType::Hours,   vec!["2-2/2", "5-1/2", "*,1", "1-1", "5-1", "W", "?", "L", "", " ", ",", "/", "*/", "5/", "-", "1-", "a,b,c", "a-b,c", "1-2-3", ",1", "1,", "1, 2", "1#1", "0/-5", "0/0", "0/24", "24", "0/1"])]
    #[case(PatternType::Doms,    vec!["2-2/2", "5-1/2", "?,4", "*,1", "1-1", "5-1", "", " ", ",", "/", "*/", "5/", "-", "1-", "a,b,c", "a-b,c", "1-2-3", ",1", "1,", "1, 2", "1#5", "0/-5", "0/0", "0/31", "32", "0/1", "0"])]
    #[case(PatternType::Months,  vec!["2-2/2", "5-1/2", "*,1", "1-1", "5-1", "W", "?", "L", "", " ", ",", "/", "*/", "5/", "-", "1-", "a,b,c", "a-b,c", "1-2-3", ",1", "1,", "1, 2", "1#1", "0/-5", "0/0", "0/12", "32", "0/1", "0"])]
    #[case(PatternType::Dows,    vec!["?, 3", "*,1", "1-1", "5-1", "W", "L", "", " ", ",", "/", "*/", "5/", "-", "1-", "a,b,c", "a-b,c", "1-2-3", ",1", "1,", "1, 2", "1#5", "0/-5", "0/0", "0/2", "7"])]
    #[case(PatternType::Years,   vec!["1972-1972/2", "2005-2001/2", "*,1", "2001-2001", "2005-2001", "W", "?", "L", "", " ", ",", "/", "*/", "5/", "-", "1-", "a,b,c", "a-b,c", "1-2-3", ",1", "1,", "1, 2", "1#5", "0/-5", "0/0", "0/2", "1969", "2100", "2000/2100"])]
    fn test_pattern_item_parse_invalid(#[case] type_: PatternType, #[case] input: Vec<&str>) {
        for item in input {
            let r = Pattern::parse(type_, item);
            assert!(r.is_err(), "type = {type_:?}, pattern = '{item}'");
        }
    }

    #[rstest]
    #[case(PatternType::Seconds, "0", 0)]
    #[case(PatternType::Seconds, "33", 33)]
    #[case(PatternType::Seconds, "59", 59)]
    #[case(PatternType::Minutes, "0", 0)]
    #[case(PatternType::Minutes, "33", 33)]
    #[case(PatternType::Minutes, "59", 59)]
    #[case(PatternType::Hours, "0", 0)]
    #[case(PatternType::Hours, "13", 13)]
    #[case(PatternType::Hours, "23", 23)]
    #[case(PatternType::Doms, "1", 1)]
    #[case(PatternType::Doms, "15", 15)]
    #[case(PatternType::Doms, "31", 31)]
    #[case(PatternType::Years, MIN_YEAR_STR, MIN_YEAR)]
    #[case(PatternType::Years, MAX_YEAR_STR, MAX_YEAR)]
    #[case(PatternType::Years, "1999", 1999)]
    #[case(PatternType::Years, "2001", 2001)]
    #[case(PatternType::Months, "1", 1)]
    #[case(PatternType::Months, "6", 6)]
    #[case(PatternType::Months, "12", 12)]
    #[case(PatternType::Months, "Jan", 1)]
    #[case(PatternType::Months, "JUN", 6)]
    #[case(PatternType::Months, "dec", 12)]
    #[case(PatternType::Dows, "0", 0)]
    #[case(PatternType::Dows, "3", 3)]
    #[case(PatternType::Dows, "6", 6)]
    #[case(PatternType::Dows, "Sun", 0)]
    #[case(PatternType::Dows, "WED", 3)]
    #[case(PatternType::Dows, "fri", 5)]
    fn test_parse_valid_pattern_type(
        #[case] type_: PatternType,
        #[case] input: &str,
        #[case] expected: PatternValueType,
    ) {
        assert_eq!(type_.parse(input).unwrap(), expected);
    }

    #[rstest]
    #[case(PatternType::Seconds, "60")]
    #[case(PatternType::Seconds, "-1")]
    #[case(PatternType::Seconds, "256")]
    #[case(PatternType::Seconds, "abc")]
    #[case(PatternType::Minutes, "60")]
    #[case(PatternType::Minutes, "-1")]
    #[case(PatternType::Minutes, "256")]
    #[case(PatternType::Minutes, "abc")]
    #[case(PatternType::Hours, "24")]
    #[case(PatternType::Hours, "-1")]
    #[case(PatternType::Hours, "256")]
    #[case(PatternType::Hours, "abc")]
    #[case(PatternType::Doms, "0")]
    #[case(PatternType::Doms, "-12")]
    #[case(PatternType::Doms, "32")]
    #[case(PatternType::Doms, "abc")]
    #[case(PatternType::Years, "1969")]
    #[case(PatternType::Years, "1900")]
    #[case(PatternType::Years, "2100")]
    #[case(PatternType::Years, "-12")]
    #[case(PatternType::Years, "abc")]
    #[case(PatternType::Months, "-2")]
    #[case(PatternType::Months, "0")]
    #[case(PatternType::Months, "13")]
    #[case(PatternType::Months, "256")]
    #[case(PatternType::Months, "abc")]
    #[case(PatternType::Dows, "-3")]
    #[case(PatternType::Dows, "7")]
    #[case(PatternType::Dows, "abc")]
    #[case(PatternType::Months, "")]
    #[case(PatternType::Months, "invalid")]
    #[case(PatternType::Months, "j@n")]
    #[case(PatternType::Months, "ja")]
    #[case(PatternType::Dows, "")]
    #[case(PatternType::Dows, "invalid")]
    #[case(PatternType::Dows, "we")]
    #[case(PatternType::Dows, "M@n")]
    fn test_parse_invalid_pattern_type(#[case] type_: PatternType, #[case] input: &str) {
        assert!(
            matches!(type_.parse(input), Err(CronError::InvalidDigitalValue(e, t)) | Err(CronError::InvalidMnemonicValue(e, t)) if e == input && t == type_.to_string())
        );
    }

    #[rstest]
    // Seconds
    #[case("00:00:00", "0", PatternType::Seconds, Some(0))]
    #[case("00:00:00", "00", PatternType::Seconds, Some(0))]
    #[case("00:00:00", "05", PatternType::Seconds, Some(5))]
    #[case("00:00:00", "59", PatternType::Seconds, Some(59))]
    #[case("00:00:00", "*", PatternType::Seconds, Some(0))]
    #[case("00:00:00", "*/6", PatternType::Seconds, Some(0))]
    #[case("00:00:00", "0/2", PatternType::Seconds, Some(0))]
    #[case("00:00:00", "20/5", PatternType::Seconds, Some(20))]
    #[case("00:00:00", "30/5", PatternType::Seconds, Some(30))]
    #[case("00:00:00", "10-30/5", PatternType::Seconds, Some(10))]
    #[case("00:00:00", "50-59/10", PatternType::Seconds, Some(50))]
    #[case("00:00:00", "0-5", PatternType::Seconds, Some(0))]
    #[case("00:00:00", "25-30", PatternType::Seconds, Some(25))]
    #[case("00:00:00", "12,22,30", PatternType::Seconds, Some(12))]
    #[case("00:00:00", "10,12,20/5,25-30,40-45/2,*/30", PatternType::Seconds, Some(0))]
    #[case("00:00:22", "0", PatternType::Seconds, None)]
    #[case("00:00:22", "59", PatternType::Seconds, Some(59))]
    #[case("00:00:22", "*", PatternType::Seconds, Some(22))]
    #[case("00:00:22", "*/6", PatternType::Seconds, Some(24))]
    #[case("00:00:22", "0/2", PatternType::Seconds, Some(22))]
    #[case("00:00:22", "20/5", PatternType::Seconds, Some(25))]
    #[case("00:00:22", "10-30/5", PatternType::Seconds, Some(25))]
    #[case("00:00:22", "50-59/10", PatternType::Seconds, Some(50))]
    #[case("00:00:22", "0-5", PatternType::Seconds, None)]
    #[case("00:00:22", "25-30", PatternType::Seconds, Some(25))]
    #[case("00:00:22", "12,22,30", PatternType::Seconds, Some(22))]
    #[case("00:00:22", "10,12,20/5,25-30,40-45/2,*/30", PatternType::Seconds, Some(25))]
    // Minutes
    #[case("00:00:00", "0", PatternType::Minutes, Some(0))]
    #[case("00:00:00", "59", PatternType::Minutes, Some(59))]
    #[case("00:00:00", "*", PatternType::Minutes, Some(0))]
    #[case("00:00:00", "*/6", PatternType::Minutes, Some(0))]
    #[case("00:00:00", "0/2", PatternType::Minutes, Some(0))]
    #[case("00:00:00", "20/5", PatternType::Minutes, Some(20))]
    #[case("00:00:00", "10-30/5", PatternType::Minutes, Some(10))]
    #[case("00:00:00", "50-59/10", PatternType::Minutes, Some(50))]
    #[case("00:00:00", "0-5", PatternType::Minutes, Some(0))]
    #[case("00:00:00", "25-30", PatternType::Minutes, Some(25))]
    #[case("00:00:00", "12,22,30", PatternType::Minutes, Some(12))]
    #[case("00:00:00", "10,12,20/5,25-30,40-45/2,*/30", PatternType::Minutes, Some(0))]
    #[case("00:00:22", "10,12,20/5,25-30,40-45/2,*/30", PatternType::Minutes, Some(0))]
    #[case("00:22:00", "0", PatternType::Minutes, None)]
    #[case("00:22:00", "59", PatternType::Minutes, Some(59))]
    #[case("00:22:00", "*", PatternType::Minutes, Some(22))]
    #[case("00:22:00", "*/6", PatternType::Minutes, Some(24))]
    #[case("00:22:00", "0/2", PatternType::Minutes, Some(22))]
    #[case("00:22:00", "20/5", PatternType::Minutes, Some(25))]
    #[case("00:22:00", "10-30/5", PatternType::Minutes, Some(25))]
    #[case("00:22:00", "50-59/10", PatternType::Minutes, Some(50))]
    #[case("00:22:00", "0-5", PatternType::Minutes, None)]
    #[case("00:22:00", "25-30", PatternType::Minutes, Some(25))]
    #[case("00:22:00", "12,22,30", PatternType::Minutes, Some(22))]
    #[case("00:22:00", "10,12,20/5,25-30,40-45/2,*/30", PatternType::Minutes, Some(25))]
    #[case("00:22:22", "10,12,20/5,25-30,40-45/2,*/30", PatternType::Minutes, Some(25))]
    // Hours
    #[case("00:00:00", "0", PatternType::Hours, Some(0))]
    #[case("00:00:00", "23", PatternType::Hours, Some(23))]
    #[case("00:00:00", "*", PatternType::Hours, Some(0))]
    #[case("00:00:00", "*/6", PatternType::Hours, Some(0))]
    #[case("00:00:00", "0/2", PatternType::Hours, Some(0))]
    #[case("00:00:00", "12/5", PatternType::Hours, Some(12))]
    #[case("00:00:00", "10-20/5", PatternType::Hours, Some(10))]
    #[case("00:00:00", "18-23/2", PatternType::Hours, Some(18))]
    #[case("00:00:00", "0-5", PatternType::Hours, Some(0))]
    #[case("00:00:00", "10-15", PatternType::Hours, Some(10))]
    #[case("00:00:00", "1,5,15,22,23", PatternType::Hours, Some(1))]
    #[case("00:00:00", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(0))]
    #[case("00:00:22", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(0))]
    #[case("00:22:00", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(0))]
    #[case("00:22:15", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(0))]
    #[case("05:00:00", "0", PatternType::Hours, None)]
    #[case("05:00:00", "23", PatternType::Hours, Some(23))]
    #[case("05:00:00", "*", PatternType::Hours, Some(5))]
    #[case("05:00:00", "*/6", PatternType::Hours, Some(6))]
    #[case("05:00:00", "0/2", PatternType::Hours, Some(6))]
    #[case("05:00:00", "12/5", PatternType::Hours, Some(12))]
    #[case("05:00:00", "10-20/5", PatternType::Hours, Some(10))]
    #[case("05:00:00", "18-23/2", PatternType::Hours, Some(18))]
    #[case("05:00:00", "0-5", PatternType::Hours, Some(5))]
    #[case("05:00:00", "10-15", PatternType::Hours, Some(10))]
    #[case("05:00:00", "1,5,15,22,23", PatternType::Hours, Some(5))]
    #[case("05:00:00", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(5))]
    #[case("05:00:35", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(5))]
    #[case("05:33:00", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(5))]
    #[case("05:29:43", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(5))]
    #[case("12:00:00", "0", PatternType::Hours, None)]
    #[case("12:00:00", "23", PatternType::Hours, Some(23))]
    #[case("12:00:00", "*", PatternType::Hours, Some(12))]
    #[case("12:00:00", "*/6", PatternType::Hours, Some(12))]
    #[case("12:00:00", "0/2", PatternType::Hours, Some(12))]
    #[case("12:00:00", "12/5", PatternType::Hours, Some(12))]
    #[case("12:00:00", "10-20/5", PatternType::Hours, Some(15))]
    #[case("12:00:00", "18-23/2", PatternType::Hours, Some(18))]
    #[case("12:00:00", "0-5", PatternType::Hours, None)]
    #[case("12:00:00", "10-15", PatternType::Hours, Some(12))]
    #[case("12:00:00", "1,5,15,22,23", PatternType::Hours, Some(15))]
    #[case("12:00:00", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(12))]
    #[case("12:00:35", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(12))]
    #[case("12:13:00", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(12))]
    #[case("12:23:59", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(12))]
    #[case("19:00:00", "0", PatternType::Hours, None)]
    #[case("19:00:00", "23", PatternType::Hours, Some(23))]
    #[case("19:00:00", "*", PatternType::Hours, Some(19))]
    #[case("19:00:00", "*/6", PatternType::Hours, None)]
    #[case("19:00:00", "0/2", PatternType::Hours, Some(20))]
    #[case("19:00:00", "12/5", PatternType::Hours, Some(22))]
    #[case("19:00:00", "10-20/5", PatternType::Hours, Some(20))]
    #[case("19:00:00", "18-23/2", PatternType::Hours, Some(20))]
    #[case("19:00:00", "0-5", PatternType::Hours, None)]
    #[case("19:00:00", "10-15", PatternType::Hours, None)]
    #[case("19:00:00", "1,5,15,22,23", PatternType::Hours, Some(22))]
    #[case("19:00:00", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(20))]
    #[case("19:00:45", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(20))]
    #[case("19:45:00", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(20))]
    #[case("19:45:12", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, Some(20))]
    #[case("23:00:00", "0", PatternType::Hours, None)]
    #[case("23:00:00", "23", PatternType::Hours, Some(23))]
    #[case("23:00:00", "*", PatternType::Hours, Some(23))]
    #[case("23:00:00", "*/6", PatternType::Hours, None)]
    #[case("23:00:00", "0/2", PatternType::Hours, None)]
    #[case("23:00:00", "12/5", PatternType::Hours, None)]
    #[case("23:00:00", "10-20/5", PatternType::Hours, None)]
    #[case("23:00:00", "18-23/2", PatternType::Hours, None)]
    #[case("23:00:00", "0-5", PatternType::Hours, None)]
    #[case("23:00:00", "10-15", PatternType::Hours, None)]
    #[case("23:00:00", "1,5,15,22,23", PatternType::Hours, Some(23))]
    #[case("23:00:19", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, None)]
    #[case("23:29:00", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, None)]
    #[case("23:33:11", "10,12,20/5,10-14,3-9/2,*/4", PatternType::Hours, None)]
    fn test_pattern_next_time_part(
        #[case] time: &str,
        #[case] input: &str,
        #[case] type_: PatternType,
        #[case] expected: Option<PatternValueType>,
    ) {
        let date_times = [
            DateTime::parse_from_rfc3339(format!("2024-01-01T{time}Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("2024-02-01T{time}Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("2024-02-29T{time}Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("2023-02-28T{time}Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("1999-05-13T{time}Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("1999-12-31T{time}Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("1970-06-01T{time}Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("1970-06-30T{time}Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("2099-01-01T{time}Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("2099-12-31T{time}Z").as_str()).unwrap(),
        ];

        let pattern = Pattern::parse(type_, input);

        assert!(
            pattern.is_ok(),
            "type = {type_:?}, pattern = {input}, error = {}",
            pattern.err().unwrap()
        );

        for mut now in date_times {
            assert_eq!(
                pattern.as_ref().unwrap().next(&mut now),
                expected,
                "first: type = {type_:?}, time = {now}, pattern = {input}",
                now = now.to_rfc3339()
            );
        }
    }

    #[rstest]
    // Days of month, 2024
    #[case("2024-01-01", "1", PatternType::Doms, Some(1))]
    #[case("2024-01-01", "01", PatternType::Doms, Some(1))]
    #[case("2024-01-01", "21", PatternType::Doms, Some(21))]
    #[case("2024-01-01", "31", PatternType::Doms, Some(31))]
    #[case("2024-01-05", "5", PatternType::Doms, Some(5))]
    #[case("2024-01-06", "5", PatternType::Doms, None)]
    #[case("2024-01-01", "*", PatternType::Doms, Some(1))]
    #[case("2024-01-01", "*/5", PatternType::Doms, Some(1))]
    #[case("2024-01-12", "*", PatternType::Doms, Some(12))]
    #[case("2024-01-22", "*/5", PatternType::Doms, Some(26))]
    #[case("2024-01-13", "5/5", PatternType::Doms, Some(15))]
    #[case("2024-01-22", "12/3", PatternType::Doms, Some(24))]
    #[case("2024-01-01", "5-10", PatternType::Doms, Some(5))]
    #[case("2024-01-01", "15-22/2", PatternType::Doms, Some(15))]
    #[case("2024-01-16", "15-20", PatternType::Doms, Some(16))]
    #[case("2024-01-17", "15-22/3", PatternType::Doms, Some(18))]
    #[case("2024-01-01", "L", PatternType::Doms, Some(31))]
    #[case("2024-02-01", "L", PatternType::Doms, Some(29))]
    #[case("2024-04-01", "L", PatternType::Doms, Some(30))]
    #[case("2021-02-01", "L", PatternType::Doms, Some(28))]
    #[case("2024-01-01", "15W", PatternType::Doms, Some(15))]
    #[case("2024-01-01", "14W", PatternType::Doms, Some(15))]
    #[case("2024-01-01", "13W", PatternType::Doms, Some(12))]
    #[case("2024-01-01", "7,2,15-19/2,*/5,L,3W", PatternType::Doms, Some(1))]
    #[case("2024-01-21", "7,2,15-19/2,*/5,L,3W", PatternType::Doms, Some(21))]
    #[case("2024-02-01", "7,2,15-19/2,*/5,L,3W", PatternType::Doms, Some(1))]
    #[case("2024-02-15", "7,2,15-19/2,*/5,L,3W", PatternType::Doms, Some(15))]
    #[case("2024-05-01", "7,2,15-19/2,*/5,L,7W", PatternType::Doms, Some(1))]
    #[case("2024-05-11", "7,2,15-19/2,*/5,L,12W", PatternType::Doms, Some(11))]
    #[case("2024-05-31", "7,2,15-19/2,*/5,L,12W", PatternType::Doms, Some(31))]
    #[case("2024-11-01", "7,2,15-19/2,*/5,L,7W", PatternType::Doms, Some(1))]
    #[case("2024-11-11", "7,2,15-19/2,*/5,L,12W", PatternType::Doms, Some(11))]
    #[case("2024-11-30", "7,2,15-19/2,*/5,L,12W", PatternType::Doms, Some(30))]
    // Days of month 1999
    #[case("1999-01-01", "7,2,15-19/2,*/5,L,3W", PatternType::Doms, Some(1))]
    #[case("1999-01-21", "7,2,15-19/2,*/5,L,3W", PatternType::Doms, Some(21))]
    #[case("1999-02-01", "7,2,15-19/2,*/5,L,3W", PatternType::Doms, Some(1))]
    #[case("1999-02-15", "7,2,15-19/2,*/5,L,3W", PatternType::Doms, Some(15))]
    #[case("1999-05-01", "7,2,15-19/2,*/5,L,7W", PatternType::Doms, Some(1))]
    #[case("1999-05-11", "7,2,15-19/2,*/5,L,12W", PatternType::Doms, Some(11))]
    #[case("1999-05-31", "7,2,15-19/2,*/5,L,12W", PatternType::Doms, Some(31))]
    #[case("1999-11-11", "7,2,15-19/2,*/5,L,12W", PatternType::Doms, Some(11))]
    #[case("1999-11-30", "7,2,15-19/2,*/5,L,12W", PatternType::Doms, Some(30))]
    //
    #[case("1999-12-30", "1-31", PatternType::Doms, Some(30))]
    #[case("1999-12-31", "1-31", PatternType::Doms, Some(31))]
    #[case("2004-02-27", "1-31", PatternType::Doms, Some(27))]
    #[case("2004-02-28", "1-31", PatternType::Doms, Some(28))]
    #[case("2004-02-29", "1-31", PatternType::Doms, Some(29))]
    #[case("2001-02-28", "1-31", PatternType::Doms, Some(28))]
    // Months
    #[case("2024-01-01", "1", PatternType::Months, Some(1))]
    #[case("2024-01-01", "01", PatternType::Months, Some(1))]
    #[case("2024-02-01", "1", PatternType::Months, None)]
    #[case("2024-02-01", "12", PatternType::Months, Some(12))]
    #[case("2024-01-01", "*", PatternType::Months, Some(1))]
    #[case("2024-01-01", "*/3", PatternType::Months, Some(1))]
    #[case("2024-05-01", "*/3", PatternType::Months, Some(7))]
    #[case("2024-03-01", "1/3", PatternType::Months, Some(4))]
    #[case("2024-06-01", "5/2", PatternType::Months, Some(7))]
    #[case("2024-05-01", "2-8", PatternType::Months, Some(5))]
    #[case("2024-01-01", "2-5", PatternType::Months, Some(2))]
    #[case("2024-01-01", "4,12,1-2,5/3,10-12/2", PatternType::Months, Some(1))]
    #[case("2024-06-01", "4,12,1-2,5/3,10-12/2", PatternType::Months, Some(8))]
    #[case("2024-12-11", "4,12,1-2,5/3,10-12/2", PatternType::Months, Some(12))]
    #[case("2024-01-01", "mar/3", PatternType::Months, Some(3))]
    #[case("2024-01-01", "jun-aug", PatternType::Months, Some(6))]
    // Years
    #[case("1970-01-01", "1970", PatternType::Years, Some(1970))]
    #[case("1970-01-01", "1971", PatternType::Years, Some(1971))]
    #[case("1980-01-01", "1970", PatternType::Years, None)]
    #[case("1999-01-01", "*", PatternType::Years, Some(1999))]
    #[case("2001-01-01", "*/10", PatternType::Years, Some(2010))]
    #[case("2001-01-01", "1970-2003", PatternType::Years, Some(2001))]
    #[case("1980-01-01", "1970-2000/7", PatternType::Years, Some(1984))]
    #[case("2001-01-01", "1970-2000/10", PatternType::Years, None)]
    #[case("2001-01-01", "1970-2055/10", PatternType::Years, Some(2010))]
    #[case("1991-11-11", "*/10,2090-2099/2,1999", PatternType::Years, Some(1999))]
    #[case("2027-11-11", "*/10,2090-2099/2,1999", PatternType::Years, Some(2030))]
    #[case("2091-02-28", "*/10,2090-2099/2,1999", PatternType::Years, Some(2092))]
    // Days of the week
    #[case("1970-01-01", "0", PatternType::Dows, Some(4))]
    #[case("1970-02-02", "sun", PatternType::Dows, Some(8))]
    #[case("1970-03-03", "mon-fri", PatternType::Dows, Some(3))]
    #[case("1970-01-01", "2-4", PatternType::Dows, Some(1))]
    #[case("1970-01-01", "*", PatternType::Dows, Some(1))]
    #[case("1970-01-01", "6,3", PatternType::Dows, Some(3))]
    #[case("1999-02-01", "4L", PatternType::Dows, Some(25))]
    #[case("2020-02-01", "6L", PatternType::Dows, Some(29))]
    #[case("2020-03-29", "6L", PatternType::Dows, None)]
    #[case("1970-01-01", "3#3", PatternType::Dows, Some(21))]
    #[case("2024-03-01", "1#1", PatternType::Dows, Some(4))]
    #[case("2020-02-01", "6#4", PatternType::Dows, Some(22))]
    #[case("2020-02-23", "6#4", PatternType::Dows, None)]
    #[case("2020-03-29", "6#4", PatternType::Dows, None)]
    #[case("1970-01-01", "1,3-5", PatternType::Dows, Some(1))]
    #[case("1970-01-03", "1,3-5", PatternType::Dows, Some(5))]
    #[case("1970-01-06", "1,3-5", PatternType::Dows, Some(7))]
    #[timeout(Duration::from_secs(1))]
    fn test_pattern_next_date_part(
        #[case] date: &str,
        #[case] input: &str,
        #[case] type_: PatternType,
        #[case] expected: Option<PatternValueType>,
    ) {
        let date_times = [
            DateTime::parse_from_rfc3339(format!("{date}T00:00:00Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("{date}T00:15:33Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("{date}T01:00:00Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("{date}T01:29:12Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("{date}T11:59:59Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("{date}T12:00:00Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("{date}T12:13:14Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("{date}T19:00:00Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("{date}T23:00:00Z").as_str()).unwrap(),
            DateTime::parse_from_rfc3339(format!("{date}T23:59:59Z").as_str()).unwrap(),
        ];

        let pattern = Pattern::parse(type_, input);

        assert!(
            pattern.is_ok(),
            "type = {type_:?}, pattern = {input}, error = {}",
            pattern.err().unwrap()
        );

        for now in date_times {
            let mut current = now;
            assert_eq!(
                pattern.as_ref().unwrap().next(&mut current),
                expected,
                "type = {type_:?}, time = {now}, pattern = {input}",
                now = now.to_rfc3339()
            );
        }
    }

    #[test]
    fn test_pattern_type_display() {
        assert_eq!(PatternType::Seconds.to_string(), "seconds");
        assert_eq!(PatternType::Minutes.to_string(), "minutes");
        assert_eq!(PatternType::Hours.to_string(), "hours");
        assert_eq!(PatternType::Doms.to_string(), "days of month");
        assert_eq!(PatternType::Months.to_string(), "months");
        assert_eq!(PatternType::Dows.to_string(), "days of week");
        assert_eq!(PatternType::Years.to_string(), "years");
    }
}
