//! Civil-date math shared by the auth layer.
//!
//! Why this lives on its own: the RFC 3339 parser in `refresh.rs` needs the
//! forward conversion (year/month/day to days-since-epoch) and the RFC 3339
//! formatter in `oauth_pkce.rs` needs the inverse. Both are Howard Hinnant's
//! civil-from/to-days algorithm. Keeping one copy here means the two
//! directions stay each other's exact inverse instead of drifting apart.

/// Convert a civil date to days since the Unix epoch (1970-01-01).
///
/// Returns `None` for dates before the epoch, since the auth layer only ever
/// formats or parses timestamps at or after 1970. Implements the forward half
/// of Howard Hinnant's `days_from_civil`, specialized for UTC.
pub(crate) fn ymd_to_epoch_days(year: i64, month: u32, day: u32) -> Option<i64> {
    let y = if month <= 2 { year - 1 } else { year };
    let era = y.div_euclid(400);
    let yoe = (y - era * 400) as u64;
    let m = month as u64;
    let d = day as u64;
    let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
    let days_since_epoch = era * 146_097 + doe as i64 - 719_468;
    if days_since_epoch < 0 {
        return None;
    }
    Some(days_since_epoch)
}

/// Convert days since the Unix epoch (1970-01-01) back to a civil date.
///
/// Returns `(year, month, day)`. Implements the inverse half of Howard
/// Hinnant's `civil_from_days`, the exact inverse of `ymd_to_epoch_days`.
/// Exposed at the auth-module boundary so the doctor pre-flight can render
/// a certificate expiry date without adding a date-formatting dependency.
pub fn epoch_days_to_ymd(days: i64) -> (i32, u32, u32) {
    let z = days + 719_468;
    let era = if z >= 0 { z } else { z - 146_096 }.div_euclid(146_097);
    let doe = (z - era * 146_097) as u32;
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
    let y = yoe as i32 + (era * 400) as i32;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let year = if m <= 2 { y + 1 } else { y };
    (year, m, d)
}

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

    #[test]
    fn epoch_anchor_is_zero_days() {
        assert_eq!(ymd_to_epoch_days(1970, 1, 1), Some(0));
        assert_eq!(epoch_days_to_ymd(0), (1970, 1, 1));
    }

    #[test]
    fn known_anchor_round_trips() {
        // 2024-01-01 is 19723 days after the epoch (1704067200 / 86400).
        let days = ymd_to_epoch_days(2024, 1, 1).unwrap();
        assert_eq!(days, 19_723);
        assert_eq!(epoch_days_to_ymd(days), (2024, 1, 1));
    }

    #[test]
    fn pre_epoch_dates_return_none() {
        assert_eq!(ymd_to_epoch_days(1969, 12, 31), None);
    }

    #[test]
    fn directions_are_inverse_across_a_range() {
        // Walk a few thousand days and confirm both directions agree.
        for days in 0..4000 {
            let (y, m, d) = epoch_days_to_ymd(days);
            assert_eq!(
                ymd_to_epoch_days(y as i64, m, d),
                Some(days),
                "round-trip failed at day {days} -> {y}-{m}-{d}"
            );
        }
    }

    #[test]
    fn leap_day_round_trips() {
        let days = ymd_to_epoch_days(2024, 2, 29).unwrap();
        assert_eq!(epoch_days_to_ymd(days), (2024, 2, 29));
    }
}
