//! Time helpers and the `MonthYear` type used for card expiries.

use serde::{Deserialize, Serialize};

use crate::error::{RelicarioError, Result};

/// Current Unix timestamp in seconds.
pub fn now_unix() -> i64 {
    chrono::Utc::now().timestamp()
}

/// Month + year (1-12 / e.g. 2026). Used for card expiries.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct MonthYear {
    pub month: u8,
    pub year: u16,
}

impl MonthYear {
    pub fn new(month: u8, year: u16) -> std::result::Result<Self, &'static str> {
        if !(1..=12).contains(&month) {
            return Err("month must be 1..=12");
        }
        if !(2000..=2099).contains(&year) {
            return Err("year must be 2000..=2099");
        }
        Ok(Self { month, year })
    }

    /// Parse a card-expiry string. Accepts `MM/YYYY`, `MM-YYYY`, and `MM/YY`
    /// (two-digit year is taken as 20YY).
    pub fn parse(s: &str) -> Result<Self> {
        let invalid = |detail: String| RelicarioError::InvalidMonthYear(detail);
        let (m_str, y_str) = s
            .split_once(['/', '-'])
            .ok_or_else(|| invalid(format!("expected MM/YYYY, got {s:?}")))?;
        let month: u8 = m_str
            .parse()
            .map_err(|_| invalid(format!("bad month {m_str:?}")))?;
        let year: u16 = if y_str.len() == 2 {
            2000 + y_str
                .parse::<u16>()
                .map_err(|_| invalid(format!("bad 2-digit year {y_str:?}")))?
        } else {
            y_str
                .parse()
                .map_err(|_| invalid(format!("bad year {y_str:?}")))?
        };
        Self::new(month, year).map_err(|e| invalid(e.into()))
    }
}

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

    #[test]
    fn now_unix_is_positive_and_recent() {
        let t = now_unix();
        assert!(t > 1_700_000_000); // after late 2023
        assert!(t < 4_000_000_000); // before 2096
    }

    #[test]
    fn month_year_constructor_rejects_bad_month() {
        assert!(MonthYear::new(0, 2026).is_err());
        assert!(MonthYear::new(13, 2026).is_err());
        assert!(MonthYear::new(1, 2026).is_ok());
        assert!(MonthYear::new(12, 2026).is_ok());
    }

    #[test]
    fn month_year_constructor_rejects_bad_year() {
        assert!(MonthYear::new(1, 1999).is_err());
        assert!(MonthYear::new(1, 2100).is_err());
        assert!(MonthYear::new(1, 2000).is_ok());
        assert!(MonthYear::new(1, 2099).is_ok());
    }

    #[test]
    fn month_year_round_trips_through_json() {
        let my = MonthYear::new(7, 2030).unwrap();
        let json = serde_json::to_string(&my).unwrap();
        let parsed: MonthYear = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed, my);
    }

    #[test]
    fn parse_accepts_mm_slash_yyyy_and_mm_dash_yyyy() {
        assert_eq!(MonthYear::parse("01/2026").unwrap(), MonthYear::new(1, 2026).unwrap());
        assert_eq!(MonthYear::parse("12/2099").unwrap(), MonthYear::new(12, 2099).unwrap());
        assert_eq!(MonthYear::parse("07-2030").unwrap(), MonthYear::new(7, 2030).unwrap());
    }

    #[test]
    fn parse_accepts_mm_slash_yy() {
        assert_eq!(MonthYear::parse("01/26").unwrap(), MonthYear::new(1, 2026).unwrap());
        assert_eq!(MonthYear::parse("12/99").unwrap(), MonthYear::new(12, 2099).unwrap());
    }

    #[test]
    fn parse_rejects_malformed() {
        assert!(matches!(
            MonthYear::parse("garbage"),
            Err(RelicarioError::InvalidMonthYear(_))
        ));
        assert!(MonthYear::parse("13/2026").is_err());      // bad month
        assert!(MonthYear::parse("01/1999").is_err());      // pre-2000
        assert!(MonthYear::parse("01/2100").is_err());      // post-2099
        assert!(MonthYear::parse("/2026").is_err());        // empty month
        assert!(MonthYear::parse("01/").is_err());          // empty year
    }
}
