use chrono::{NaiveDate, TimeDelta, TimeZone, Utc};

use super::*;

fn at(hour: u32, min: u32) -> chrono::DateTime<Utc> {
    Utc.with_ymd_and_hms(2026, 6, 19, hour, min, 0).unwrap()
}

#[test]
fn wire_labels_are_stable() {
    assert_eq!(Freshness::Current.as_str(), "current");
    assert_eq!(Freshness::Stale.as_str(), "stale");
    assert_eq!(Freshness::Unknown.as_str(), "unknown");
    assert_eq!(Freshness::default(), Freshness::Unknown);
}

#[test]
fn worst_ranks_stale_over_unknown_over_current() {
    assert_eq!(Freshness::Current.worse(Freshness::Stale), Freshness::Stale);
    assert_eq!(
        Freshness::Unknown.worse(Freshness::Current),
        Freshness::Unknown
    );
    assert_eq!(Freshness::Stale.worse(Freshness::Unknown), Freshness::Stale);

    assert_eq!(
        Freshness::worst([Freshness::Current, Freshness::Current]),
        Freshness::Current
    );
    assert_eq!(
        Freshness::worst([Freshness::Current, Freshness::Unknown]),
        Freshness::Unknown
    );
    assert_eq!(
        Freshness::worst([Freshness::Current, Freshness::Stale, Freshness::Unknown]),
        Freshness::Stale
    );
    assert_eq!(Freshness::worst([]), Freshness::Unknown, "empty -> unknown");
}

#[test]
fn expiry_is_stale_past_end_current_before_and_unknown_without() {
    let now = at(12, 0);
    assert_eq!(classify_expiry(now, Some(at(11, 0))), Freshness::Stale);
    assert_eq!(classify_expiry(now, Some(at(13, 0))), Freshness::Current);
    // Exactly at the end is still current (inclusive window end).
    assert_eq!(classify_expiry(now, Some(at(12, 0))), Freshness::Current);
    assert_eq!(classify_expiry(now, None), Freshness::Unknown);
}

#[test]
fn observation_uses_the_sixty_minute_metar_horizon() {
    let now = at(12, 0);
    let max_age = TimeDelta::minutes(METAR_MAX_AGE_MINUTES);
    assert_eq!(
        classify_observation(now, Some(at(11, 1)), max_age),
        Freshness::Current,
        "59 min old"
    );
    assert_eq!(
        classify_observation(now, Some(at(11, 0)), max_age),
        Freshness::Current,
        "exactly 60 min is still current"
    );
    assert_eq!(
        classify_observation(now, Some(at(10, 59)), max_age),
        Freshness::Stale,
        "61 min old"
    );
    assert_eq!(classify_observation(now, None, max_age), Freshness::Unknown);
}

#[test]
fn product_dispatch_matches_each_kind() {
    let now = at(12, 0);
    // METAR by observation age (no validity window).
    assert_eq!(
        classify_product(now, &ProductKind::Metar, Some(at(11, 30)), None),
        Freshness::Current
    );
    assert_eq!(
        classify_product(now, &ProductKind::Metar, Some(at(10, 0)), None),
        Freshness::Stale
    );
    assert_eq!(
        classify_product(now, &ProductKind::Metar, None, None),
        Freshness::Unknown
    );
    // PIREP is always unknown — no official validity — even with a window.
    assert_eq!(
        classify_product(now, &ProductKind::Pirep, Some(at(11, 59)), Some(at(13, 0))),
        Freshness::Unknown
    );
    // TAF / SIGMET classify by their validity-window end.
    assert_eq!(
        classify_product(now, &ProductKind::Taf, Some(at(6, 0)), Some(at(11, 0))),
        Freshness::Stale
    );
    assert_eq!(
        classify_product(now, &ProductKind::Sigmet, Some(at(11, 0)), Some(at(13, 0))),
        Freshness::Current
    );
}

#[test]
fn cycle_is_current_within_window_and_stale_on_rollover() {
    let next = NaiveDate::from_ymd_opt(2026, 7, 9).unwrap();
    assert_eq!(
        classify_cycle(NaiveDate::from_ymd_opt(2026, 6, 19).unwrap(), next),
        Freshness::Current
    );
    // Half-open window: the next effective date already belongs to the new cycle.
    assert_eq!(classify_cycle(next, next), Freshness::Stale);
    assert_eq!(
        classify_cycle(NaiveDate::from_ymd_opt(2026, 7, 10).unwrap(), next),
        Freshness::Stale
    );
}
