use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, Weekday};
use std::sync::OnceLock;
use whichtime::{Component, FastComponents, ReferenceWithTimezone, WhichTime};

static WHICHTIME: OnceLock<WhichTime> = OnceLock::new();

#[derive(Debug, Clone)]
pub struct ParsedDate {
    pub date: NaiveDate,
    pub time: Option<(u32, u32)>, // (hour 0-23, minute 0-59)
    pub end_date: Option<NaiveDate>,
    pub end_time: Option<(u32, u32)>,
    pub is_date_range: bool,
    pub duration_minutes: Option<i64>,
}

#[derive(Debug, Clone)]
pub struct InterpretedDateTime {
    pub is_all_day: bool,
    pub start_date: String,
    pub start_time: String,
    pub end_date: String,
    pub end_time: String,
}

pub fn interpret_parsed_date_time(
    parsed: &ParsedDate,
    current_start_date: &str,
    current_start_time: &str,
    current_end_date: &str,
    current_end_time: &str,
) -> InterpretedDateTime {
    let start_date = parsed.date.format("%Y-%m-%d").to_string();

    let Some((h, m)) = parsed.time else {
        return InterpretedDateTime {
            is_all_day: true,
            start_date,
            start_time: String::new(),
            end_date: parsed
                .end_date
                .unwrap_or(parsed.date)
                .format("%Y-%m-%d")
                .to_string(),
            end_time: String::new(),
        };
    };

    let start = parsed.date.and_hms_opt(h, m, 0).unwrap();
    let end = if let Some(duration_minutes) = parsed.duration_minutes {
        start + Duration::minutes(duration_minutes)
    } else if let Some((eh, em)) = parsed.end_time {
        let end_date = parsed.end_date.unwrap_or(parsed.date);
        let end = end_date.and_hms_opt(eh, em, 0).unwrap();
        if parsed.end_date.is_none() && end <= start {
            end + Duration::days(1)
        } else {
            end
        }
    } else if let Some(duration) = current_duration(
        current_start_date,
        current_start_time,
        current_end_date,
        current_end_time,
    ) {
        start + duration
    } else {
        start + Duration::hours(1)
    };

    InterpretedDateTime {
        is_all_day: false,
        start_date,
        start_time: format!("{:02}:{:02}", h, m),
        end_date: end.date().format("%Y-%m-%d").to_string(),
        end_time: end.time().format("%H:%M").to_string(),
    }
}

pub fn current_duration(
    start_date: &str,
    start_time: &str,
    end_date: &str,
    end_time: &str,
) -> Option<Duration> {
    let start = parse_datetime_fields(start_date, start_time)?;
    let end = parse_datetime_fields(end_date, end_time)?;
    let duration = end - start;
    if duration > Duration::zero() {
        Some(duration)
    } else {
        None
    }
}

pub fn shift_end_to_preserve_duration(
    new_start_date: &str,
    new_start_time: &str,
    duration: Duration,
) -> Option<(String, String)> {
    let new_start = parse_datetime_fields(new_start_date, new_start_time)?;
    let new_end = new_start + duration;
    Some((
        new_end.date().format("%Y-%m-%d").to_string(),
        new_end.time().format("%H:%M").to_string(),
    ))
}

fn parse_datetime_fields(date: &str, time: &str) -> Option<NaiveDateTime> {
    let date = NaiveDate::parse_from_str(date, "%Y-%m-%d").ok()?;
    let time = chrono::NaiveTime::parse_from_str(time, "%H:%M").ok()?;
    Some(date.and_time(time))
}

pub fn parse_natural_date(input: &str) -> Option<ParsedDate> {
    parse_natural_date_with_reference(input, Local::now())
}

fn parse_natural_date_with_reference(
    input: &str,
    reference: DateTime<Local>,
) -> Option<ParsedDate> {
    let today = reference.date_naive();
    let s = input.trim();
    if s.is_empty() {
        return None;
    }

    if let Some(r) = parse_inline_time_range(s, reference) {
        return Some(r);
    }

    // Range patterns first: "from X until Y", "between X and Y"
    if let Some(r) = parse_range(s, reference) {
        return Some(r);
    }

    // Extract duration suffix: "for N minutes/hours/days/weeks"
    let (base, dur_mins, dur_days) = extract_duration(s);

    // Shorthand: +1d, +2w, -3d
    if let Some(date) = parse_shorthand(&base, today) {
        let end_date = dur_days.map(|d| date + Duration::days(d));
        return Some(ParsedDate {
            date,
            time: None,
            end_date,
            end_time: None,
            is_date_range: dur_days.is_some(),
            duration_minutes: dur_mins,
        });
    }

    if let Some((date, time)) = parse_date_time(&base, reference) {
        return Some(parsed_with_duration(
            date, time, None, None, dur_mins, dur_days,
        ));
    }

    let parsed = parse_whichtime(&base, reference)?;
    Some(parsed_with_duration(
        parsed.date,
        parsed.time,
        parsed.end_date,
        parsed.end_time,
        dur_mins,
        dur_days,
    ))
}

fn parsed_with_duration(
    date: NaiveDate,
    time: Option<(u32, u32)>,
    parsed_end_date: Option<NaiveDate>,
    parsed_end_time: Option<(u32, u32)>,
    dur_mins: Option<i64>,
    dur_days: Option<i64>,
) -> ParsedDate {
    let end_time = if let (Some(mins), Some((h, m))) = (dur_mins, time) {
        let total = h as i64 * 60 + m as i64 + mins;
        Some(((total / 60 % 24) as u32, (total % 60) as u32))
    } else {
        parsed_end_time
    };
    let end_date = dur_days
        .map(|d| date + Duration::days(d))
        .or(parsed_end_date);
    let is_date_range = end_date.is_some() && time.is_none();
    ParsedDate {
        date,
        time,
        end_date,
        end_time,
        is_date_range,
        duration_minutes: dur_mins,
    }
}

fn parse_range(s: &str, reference: DateTime<Local>) -> Option<ParsedDate> {
    let sl = s.to_lowercase();

    let (start_str, end_str) = if sl.starts_with("from ") || sl.starts_with("starting ") {
        let skip = if sl.starts_with("from ") { 5 } else { 9 };
        let rest = &s[skip..];
        let rest_lower = rest.to_lowercase();
        let separators = [" until ", " to ", " till "];
        let mut found = None;
        for sep in &separators {
            if let Some(pos) = rest_lower.find(sep) {
                found = Some((&rest[..pos], &rest[pos + sep.len()..]));
                break;
            }
        }
        found?
    } else if sl.starts_with("between ") {
        let rest = &s[8..];
        let rest_lower = rest.to_lowercase();
        let pos = rest_lower.find(" and ")?;
        (&rest[..pos], &rest[pos + 5..])
    } else {
        return None;
    };

    let (start_date, start_time) = parse_date_time_flexible(start_str.trim(), reference)?;
    let (end_date, end_time) = parse_date_time_flexible(end_str.trim(), reference)?;

    Some(ParsedDate {
        date: start_date,
        time: start_time,
        end_date: Some(end_date),
        end_time,
        is_date_range: true,
        duration_minutes: None,
    })
}

fn parse_inline_time_range(s: &str, reference: DateTime<Local>) -> Option<ParsedDate> {
    let normalized = s.replace(['–', '—'], "-");
    let (start_str, end_str) = normalized.split_once('-')?;
    let (date, start_time) = parse_date_time(start_str.trim(), reference)?;
    let start_time = start_time?;
    let end_time = parse_time_with_meridiem_hint(end_str.trim(), start_time)?;

    Some(ParsedDate {
        date,
        time: Some(start_time),
        end_date: None,
        end_time: Some(end_time),
        is_date_range: false,
        duration_minutes: None,
    })
}

fn extract_duration(s: &str) -> (String, Option<i64>, Option<i64>) {
    let sl = s.to_lowercase();
    if let Some(pos) = sl.rfind(" for ") {
        let after = sl[pos + 5..].trim();
        let parts: Vec<&str> = after.splitn(2, ' ').collect();
        if let (Some(n_str), Some(unit)) = (parts.first(), parts.get(1)) {
            if let Ok(n) = n_str.parse::<i64>() {
                let unit = unit.trim();
                let base = s[..pos].to_string();
                match unit {
                    "minute" | "minutes" | "min" | "mins" | "m" => {
                        return (base, Some(n), None);
                    }
                    "hour" | "hours" | "hr" | "hrs" | "h" => {
                        return (base, Some(n * 60), None);
                    }
                    "day" | "days" | "d" => {
                        return (base, None, Some(n));
                    }
                    "week" | "weeks" | "wk" | "wks" | "w" => {
                        return (base, None, Some(n * 7));
                    }
                    _ => {}
                }
            }
        }
    }
    (s.to_string(), None, None)
}

fn parse_shorthand(s: &str, today: NaiveDate) -> Option<NaiveDate> {
    let s = s.trim();
    if s.len() < 3 {
        return None;
    }
    let first = s.chars().next()?;
    if first != '+' && first != '-' {
        return None;
    }
    let sign: i64 = if first == '+' { 1 } else { -1 };
    let rest = &s[1..];
    let last = rest.chars().last()?;
    if !last.is_alphabetic() {
        return None;
    }
    let n_str = &rest[..rest.len() - last.len_utf8()];
    let n: i64 = n_str.parse().ok()?;
    let amount = n * sign;
    match last.to_ascii_lowercase() {
        'd' => Some(today + Duration::days(amount)),
        'w' => Some(today + Duration::weeks(amount)),
        _ => None,
    }
}

fn parse_date_time_flexible(
    s: &str,
    reference: DateTime<Local>,
) -> Option<(NaiveDate, Option<(u32, u32)>)> {
    parse_date_time(s, reference)
        .or_else(|| parse_whichtime(s, reference).map(|parsed| (parsed.date, parsed.time)))
}

fn parse_date_time(s: &str, reference: DateTime<Local>) -> Option<(NaiveDate, Option<(u32, u32)>)> {
    let today = reference.date_naive();
    let sl = s.to_lowercase();
    let sl = sl.trim();

    for prefix in &["on ", "at "] {
        if sl.starts_with(prefix) {
            return parse_date_time(s[prefix.len()..].trim(), reference);
        }
    }

    if let Some(parsed) = parse_trailing_date_time(s.trim(), today) {
        return Some(parsed);
    }

    // Relative keywords: today, tomorrow, yesterday, tonight
    for (kw, offset) in &[
        ("tonight", 0i64),
        ("today", 0),
        ("tomorrow", 1),
        ("yesterday", -1),
    ] {
        if sl == *kw {
            return Some((today + Duration::days(*offset), None));
        }
        let with_space = format!("{} ", kw);
        if sl.starts_with(&with_space) {
            let date = today + Duration::days(*offset);
            let rest = strip_at_prefix(sl[kw.len()..].trim());
            return Some((date, parse_time(rest)));
        }
    }

    // "next <weekday>" or "this <weekday>"
    for prefix in &["next ", "this "] {
        if sl.starts_with(prefix) {
            let rest = s[prefix.len()..].trim();
            let parts: Vec<&str> = rest.splitn(2, ' ').collect();
            if let Some(wd) = parse_weekday(&parts[0].to_lowercase()) {
                let force_next = *prefix == "next ";
                let date = next_weekday_from(today, wd, force_next);
                let time_str = parts
                    .get(1)
                    .map(|t| strip_at_prefix(t.trim()))
                    .unwrap_or("");
                return Some((
                    date,
                    if time_str.is_empty() {
                        None
                    } else {
                        parse_time(time_str)
                    },
                ));
            }
        }
    }

    // "<weekday>" or "<weekday> <time>"
    {
        let parts: Vec<&str> = s.trim().splitn(2, ' ').collect();
        if let Some(wd) = parse_weekday(&parts[0].to_lowercase()) {
            let date = next_weekday_from(today, wd, false);
            let time_str = parts
                .get(1)
                .map(|t| strip_at_prefix(t.trim()))
                .unwrap_or("");
            return Some((
                date,
                if time_str.is_empty() {
                    None
                } else {
                    parse_time(time_str)
                },
            ));
        }
    }

    // "in N days/weeks/months"
    if sl.starts_with("in ") {
        let rest = &sl[3..];
        let parts: Vec<&str> = rest.splitn(3, ' ').collect();
        if let (Some(n_str), Some(unit)) = (parts.first(), parts.get(1)) {
            if let Ok(n) = n_str.parse::<i64>() {
                let days = match unit.trim().trim_end_matches('s') {
                    "day" | "d" => n,
                    "week" | "wk" | "w" => n * 7,
                    "month" | "mo" => n * 30,
                    _ => return None,
                };
                let date = today + Duration::days(days);
                let time_str = parts
                    .get(2)
                    .map(|t| strip_at_prefix(t.trim()))
                    .unwrap_or("");
                return Some((
                    date,
                    if time_str.is_empty() {
                        None
                    } else {
                        parse_time(time_str)
                    },
                ));
            }
        }
    }

    // Just a time (today)
    if let Some(t) = parse_time(sl) {
        return Some((today, Some(t)));
    }

    // ISO date: YYYY-MM-DD
    if let Ok(d) = NaiveDate::parse_from_str(s.trim(), "%Y-%m-%d") {
        return Some((d, None));
    }

    // M/D/YYYY or M/D/YY
    for fmt in &["%m/%d/%Y", "%m/%d/%y"] {
        if let Ok(d) = NaiveDate::parse_from_str(s.trim(), fmt) {
            return Some((d, None));
        }
    }

    // M/D (current or next year)
    if let Some(d) = parse_numeric_month_day(s.trim(), today) {
        return Some((d, None));
    }

    // "Mar 15", "March 15", "Mar 15 2026", etc.
    if let Some(d) = parse_month_day(s.trim(), today) {
        return Some((d, None));
    }

    None
}

fn parse_whichtime(s: &str, reference: DateTime<Local>) -> Option<ParsedDate> {
    let parser = WHICHTIME.get_or_init(WhichTime::new);
    let results = parser.parse(s, Some(reference)).ok()?;
    let result = results
        .into_iter()
        .max_by_key(|r| (r.end_index.saturating_sub(r.index), usize::MAX - r.index))?;
    let ref_tz = ReferenceWithTimezone::new(reference, None);
    let start = result.date(&ref_tz)?;
    let date = start.date_naive();
    let time = component_time(&result.start);

    let (end_date, end_time) = if let Some(end_components) = result.end {
        if let Some(end) = end_components.to_datetime(&ref_tz) {
            (Some(end.date_naive()), component_time(&end_components))
        } else {
            (None, None)
        }
    } else {
        (None, None)
    };

    Some(ParsedDate {
        date,
        time,
        end_date,
        end_time,
        is_date_range: end_date.is_some() && time.is_none(),
        duration_minutes: None,
    })
}

fn component_time(components: &FastComponents) -> Option<(u32, u32)> {
    let has_explicit_time = components.is_certain(Component::Hour)
        || components.is_certain(Component::Minute)
        || components.is_certain(Component::Second)
        || components.is_certain(Component::Meridiem);
    if !has_explicit_time {
        return None;
    }

    let hour = components.get(Component::Hour)? as u32;
    let minute = components.get(Component::Minute).unwrap_or(0) as u32;
    if hour < 24 && minute < 60 {
        Some((hour, minute))
    } else {
        None
    }
}

fn parse_trailing_date_time(s: &str, today: NaiveDate) -> Option<(NaiveDate, Option<(u32, u32)>)> {
    let parts: Vec<&str> = s.split_whitespace().collect();
    if parts.len() < 2 {
        return None;
    }

    for time_start in 1..parts.len() {
        let date_str = parts[..time_start].join(" ");
        let time_part = parts[time_start..].join(" ");
        let time_str = strip_at_prefix(&time_part);
        if let (Some(date), Some(time)) = (
            parse_standalone_date(&date_str, today),
            parse_time(time_str),
        ) {
            return Some((date, Some(time)));
        }
    }

    None
}

fn parse_standalone_date(s: &str, today: NaiveDate) -> Option<NaiveDate> {
    let s = s.trim();
    let sl = s.to_lowercase();
    match sl.as_str() {
        "tonight" | "today" => return Some(today),
        "tomorrow" => return Some(today + Duration::days(1)),
        "yesterday" => return Some(today - Duration::days(1)),
        _ => {}
    }

    if let Some(rest) = sl.strip_prefix("next ") {
        return parse_weekday(rest).map(|wd| next_weekday_from(today, wd, true));
    }
    if let Some(rest) = sl.strip_prefix("this ") {
        return parse_weekday(rest).map(|wd| next_weekday_from(today, wd, false));
    }
    if let Some(wd) = parse_weekday(&sl) {
        return Some(next_weekday_from(today, wd, false));
    }
    if let Some(date) = parse_shorthand(s, today) {
        return Some(date);
    }
    if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
        return Some(d);
    }
    for fmt in &["%m/%d/%Y", "%m/%d/%y"] {
        if let Ok(d) = NaiveDate::parse_from_str(s, fmt) {
            return Some(d);
        }
    }
    if let Some(d) = parse_numeric_month_day(s, today) {
        return Some(d);
    }
    parse_month_day(s, today)
}

fn strip_at_prefix<'a>(s: &'a str) -> &'a str {
    if let Some(rest) = s.strip_prefix("at ") {
        rest.trim()
    } else {
        s
    }
}

fn parse_time(s: &str) -> Option<(u32, u32)> {
    let s = s.trim();
    if s.is_empty() {
        return None;
    }

    // HH:MM (24h)
    if s.len() == 5 && s.chars().nth(2) == Some(':') {
        if let (Ok(h), Ok(m)) = (s[..2].parse::<u32>(), s[3..].parse::<u32>()) {
            if h < 24 && m < 60 {
                return Some((h, m));
            }
        }
    }

    // H:MM or HH:MM (24h, no strict length check)
    if let Some(colon) = s.find(':') {
        let h_str = &s[..colon];
        let rest = &s[colon + 1..];
        let sl = rest.to_lowercase();
        let is_pm = sl.ends_with("pm");
        let is_am = sl.ends_with("am");
        let m_str = if is_pm || is_am {
            &rest[..rest.len() - 2]
        } else {
            rest
        };
        if let (Ok(h), Ok(m)) = (h_str.parse::<u32>(), m_str.parse::<u32>()) {
            if m < 60 {
                let h24 = if is_pm {
                    if h == 12 {
                        12
                    } else {
                        (h + 12) % 24
                    }
                } else if is_am {
                    if h == 12 {
                        0
                    } else {
                        h
                    }
                } else {
                    h
                };
                if h24 < 24 {
                    return Some((h24, m));
                }
            }
        }
    }

    // Npm or Npm (no colon)
    let sl = s.to_lowercase();
    let is_pm = sl.ends_with("pm");
    let is_am = sl.ends_with("am");
    if is_pm || is_am {
        let h_str = &sl[..sl.len() - 2].trim();
        if let Ok(h) = h_str.parse::<u32>() {
            let h24 = if is_pm {
                if h == 12 {
                    12
                } else {
                    (h + 12) % 24
                }
            } else {
                if h == 12 {
                    0
                } else {
                    h
                }
            };
            if h24 < 24 {
                return Some((h24, 0));
            }
        }
    }

    if let Ok(h) = sl.parse::<u32>() {
        if h < 24 {
            return Some((h, 0));
        }
    }

    None
}

fn parse_time_with_meridiem_hint(s: &str, start_time: (u32, u32)) -> Option<(u32, u32)> {
    let end = parse_time(s)?;
    let sl = s.trim().to_lowercase();
    if sl.ends_with("am") || sl.ends_with("pm") || end.0 >= 12 || start_time.0 < 12 {
        return Some(end);
    }

    let pm_end = (end.0 + 12, end.1);
    if pm_end.0 < 24 && pm_end > start_time {
        Some(pm_end)
    } else {
        Some(end)
    }
}

fn parse_weekday(s: &str) -> Option<Weekday> {
    match s {
        "monday" | "mon" => Some(Weekday::Mon),
        "tuesday" | "tue" | "tues" => Some(Weekday::Tue),
        "wednesday" | "wed" => Some(Weekday::Wed),
        "thursday" | "thu" | "thur" | "thurs" => Some(Weekday::Thu),
        "friday" | "fri" => Some(Weekday::Fri),
        "saturday" | "sat" => Some(Weekday::Sat),
        "sunday" | "sun" => Some(Weekday::Sun),
        _ => None,
    }
}

fn next_weekday_from(today: NaiveDate, target: Weekday, force_next: bool) -> NaiveDate {
    let today_n = today.weekday().num_days_from_monday() as i64;
    let target_n = target.num_days_from_monday() as i64;
    let mut days = target_n - today_n;
    if days < 0 || (days == 0 && force_next) {
        days += 7;
    }
    today + Duration::days(days)
}

fn adjust_year(d: NaiveDate, today: NaiveDate) -> NaiveDate {
    let year = today.year();
    let candidate = d.with_year(year).unwrap_or(d);
    if candidate < today {
        candidate.with_year(year + 1).unwrap_or(candidate)
    } else {
        candidate
    }
}

fn parse_month_day(s: &str, today: NaiveDate) -> Option<NaiveDate> {
    if let Some(d) = parse_named_month_day(s, today) {
        return Some(d);
    }
    for fmt in &[
        "%b %d %Y", "%B %d %Y", "%b %d", "%B %d", "%d %b %Y", "%d %B %Y", "%d %b", "%d %B",
    ] {
        if let Ok(d) = NaiveDate::parse_from_str(s, fmt) {
            return Some(if fmt.contains("%Y") {
                d
            } else {
                adjust_year(d, today)
            });
        }
    }
    None
}

fn parse_numeric_month_day(s: &str, today: NaiveDate) -> Option<NaiveDate> {
    let mut parts = s.split('/');
    let month = parts.next()?.parse::<u32>().ok()?;
    let day = parts.next()?.parse::<u32>().ok()?;
    if parts.next().is_some() {
        return None;
    }
    let date = NaiveDate::from_ymd_opt(today.year(), month, day)?;
    Some(adjust_year(date, today))
}

fn parse_named_month_day(s: &str, today: NaiveDate) -> Option<NaiveDate> {
    let cleaned = s.replace(',', " ");
    let parts: Vec<&str> = cleaned.split_whitespace().collect();
    if parts.len() != 2 && parts.len() != 3 {
        return None;
    }

    let (month_str, day_str, year) = if let Some(month) = month_number(parts[0]) {
        (
            Some(month),
            parts[1],
            parts.get(2).and_then(|y| y.parse::<i32>().ok()),
        )
    } else if let Some(month) = month_number(parts[1]) {
        (
            Some(month),
            parts[0],
            parts.get(2).and_then(|y| y.parse::<i32>().ok()),
        )
    } else {
        (None, "", None)
    };

    let month = month_str?;
    let day = day_str.parse::<u32>().ok()?;
    let year = year.unwrap_or_else(|| today.year());
    let date = NaiveDate::from_ymd_opt(year, month, day)?;
    Some(if parts.len() == 3 {
        date
    } else {
        adjust_year(date, today)
    })
}

fn month_number(s: &str) -> Option<u32> {
    match s.to_lowercase().as_str() {
        "jan" | "january" => Some(1),
        "feb" | "february" => Some(2),
        "mar" | "march" => Some(3),
        "apr" | "april" => Some(4),
        "may" => Some(5),
        "jun" | "june" => Some(6),
        "jul" | "july" => Some(7),
        "aug" | "august" => Some(8),
        "sep" | "sept" | "september" => Some(9),
        "oct" | "october" => Some(10),
        "nov" | "november" => Some(11),
        "dec" | "december" => Some(12),
        _ => None,
    }
}

pub fn format_parsed_preview(parsed: &ParsedDate) -> String {
    let date_str = crate::domain::time::format_day_header(parsed.date);

    if parsed.is_date_range {
        if let Some(end) = parsed.end_date {
            let end_str = crate::domain::time::format_day_header(end);
            let days = (end - parsed.date).num_days();
            return format!("{} – {} ({} days)", date_str, end_str, days);
        }
        return date_str;
    }

    let time_str = if let Some((h, m)) = parsed.time {
        let ampm = if h < 12 { "AM" } else { "PM" };
        let h12 = match h {
            0 => 12,
            h if h > 12 => h - 12,
            h => h,
        };
        if let Some((eh, em)) = parsed.end_time {
            let eampm = if eh < 12 { "AM" } else { "PM" };
            let eh12 = match eh {
                0 => 12,
                h if h > 12 => h - 12,
                h => h,
            };
            format!(
                " at {:02}:{:02} {} – {:02}:{:02} {}",
                h12, m, ampm, eh12, em, eampm
            )
        } else {
            format!(" at {:02}:{:02} {}", h12, m, ampm)
        }
    } else {
        " (all day)".to_string()
    };

    format!("{}{}", date_str, time_str)
}

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

    fn reference() -> DateTime<Local> {
        Local.with_ymd_and_hms(2026, 6, 10, 9, 0, 0).unwrap()
    }

    #[test]
    fn interpreted_time_without_explicit_end_preserves_current_duration() {
        let parsed = ParsedDate {
            date: NaiveDate::from_ymd_opt(2026, 6, 10).unwrap(),
            time: Some((13, 15)),
            end_date: None,
            end_time: None,
            is_date_range: false,
            duration_minutes: None,
        };

        let interpreted =
            interpret_parsed_date_time(&parsed, "2026-06-10", "09:00", "2026-06-10", "10:30");

        assert!(!interpreted.is_all_day);
        assert_eq!(interpreted.start_date, "2026-06-10");
        assert_eq!(interpreted.start_time, "13:15");
        assert_eq!(interpreted.end_date, "2026-06-10");
        assert_eq!(interpreted.end_time, "14:45");
    }

    #[test]
    fn interpreted_explicit_end_crosses_midnight_when_needed() {
        let parsed = ParsedDate {
            date: NaiveDate::from_ymd_opt(2026, 6, 10).unwrap(),
            time: Some((23, 30)),
            end_date: None,
            end_time: Some((0, 15)),
            is_date_range: false,
            duration_minutes: None,
        };

        let interpreted =
            interpret_parsed_date_time(&parsed, "2026-06-10", "09:00", "2026-06-10", "10:00");

        assert_eq!(interpreted.start_time, "23:30");
        assert_eq!(interpreted.end_date, "2026-06-11");
        assert_eq!(interpreted.end_time, "00:15");
    }

    #[test]
    fn parses_common_when_field_forms() {
        let time_only = parse_natural_date("3pm").unwrap();
        assert_eq!(time_only.time, Some((15, 0)));

        let bare_hour = parse_natural_date("15").unwrap();
        assert_eq!(bare_hour.time, Some((15, 0)));

        let tomorrow_at_bare_hour = parse_natural_date("tomorrow at 4").unwrap();
        assert_eq!(tomorrow_at_bare_hour.time, Some((4, 0)));

        let slash_date_with_time = parse_natural_date("6/10 3pm").unwrap();
        assert_eq!(slash_date_with_time.time, Some((15, 0)));

        let month_date_with_time = parse_natural_date("June 10 3pm").unwrap();
        assert_eq!(month_date_with_time.time, Some((15, 0)));

        let weekday_with_time = parse_natural_date("next fri 3pm").unwrap();
        assert_eq!(weekday_with_time.time, Some((15, 0)));
    }

    #[test]
    fn parses_inline_time_ranges() {
        let range = parse_natural_date("tomorrow 3pm-4").unwrap();
        assert_eq!(range.time, Some((15, 0)));
        assert_eq!(range.end_time, Some((16, 0)));

        let spaced_range = parse_natural_date("6/10 09:30 - 10:15").unwrap();
        assert_eq!(spaced_range.time, Some((9, 30)));
        assert_eq!(spaced_range.end_time, Some((10, 15)));
    }

    #[test]
    fn falls_back_to_whichtime_for_spelled_relative_dates() {
        let parsed = parse_natural_date_with_reference("in two weeks", reference()).unwrap();

        assert_eq!(parsed.date, NaiveDate::from_ymd_opt(2026, 6, 24).unwrap());
        assert_eq!(parsed.time, None);
    }

    #[test]
    fn falls_back_to_whichtime_for_relative_times() {
        let parsed = parse_natural_date_with_reference("in two hours", reference()).unwrap();

        assert_eq!(parsed.date, NaiveDate::from_ymd_opt(2026, 6, 10).unwrap());
        assert_eq!(parsed.time, Some((11, 0)));
    }

    #[test]
    fn parses_prefixed_weekday_with_existing_upcoming_semantics() {
        let parsed = parse_natural_date_with_reference("on Monday at 3pm", reference()).unwrap();

        assert_eq!(parsed.date, NaiveDate::from_ymd_opt(2026, 6, 15).unwrap());
        assert_eq!(parsed.time, Some((15, 0)));
    }

    #[test]
    fn falls_back_to_whichtime_for_overmorrow() {
        let parsed = parse_natural_date_with_reference("overmorrow", reference()).unwrap();

        assert_eq!(parsed.date, NaiveDate::from_ymd_opt(2026, 6, 12).unwrap());
        assert_eq!(parsed.time, None);
    }
}
