use num_conv::prelude::*;
use quickcheck::{Arbitrary, TestResult};
use quickcheck_macros::quickcheck;
use time::Weekday::*;
use time::macros::{format_description, time};
use time::{Date, Duration, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday};

macro_rules! test_shrink {
    ($type:ty,
     $fn_name:ident,
     $($method:ident()).+
     $(, min=$min_value:literal)?
    ) => {
        #[quickcheck]
        fn $fn_name(v: $type) -> TestResult {
            let method_value = v.$($method()).+;
            if method_value == test_shrink!(@min_or_zero $($min_value)?) {
                TestResult::discard()
            } else {
                TestResult::from_bool(v.shrink().any(|shrunk|
                    if method_value > 0 {
                        shrunk.$($method()).+ < v.$($method()).+
                    } else {
                        shrunk.$($method()).+ > v.$($method()).+
                    }
                ))
            }
        }
    };
    (@min_or_zero) => { 0 };
    (@min_or_zero $min:literal) => { $min };
}

macro_rules! no_panic {
    ($($x:tt)*) => {
        std::panic::catch_unwind(|| {
            $($x)*
        })
        .is_ok()
    }
}

#[quickcheck]
fn date_yo_roundtrip(d: Date) -> bool {
    Date::from_ordinal_date(d.year(), d.ordinal()) == Ok(d)
}

#[quickcheck]
fn date_ymd_roundtrip(d: Date) -> bool {
    Date::from_calendar_date(d.year(), d.month(), d.day()) == Ok(d)
}

#[quickcheck]
fn date_ywd_roundtrip(d: Date) -> bool {
    let (year, week, weekday) = d.to_iso_week_date();
    Date::from_iso_week_date(year, week, weekday) == Ok(d)
}

#[quickcheck]
fn date_format_century_last_two_equivalent(d: Date) -> bool {
    let split_format = format_description!("[year repr:century][year repr:last_two]-[month]-[day]");
    let split = d.format(&split_format).expect("formatting failed");

    let combined_format = format_description!("[year]-[month]-[day]");
    let combined = d.format(&combined_format).expect("formatting failed");

    split == combined
}

#[quickcheck]
fn date_parse_century_last_two_equivalent_extended(d: Date) -> TestResult {
    // With the extended range, there is an ambiguity when parsing a year with fewer than six
    // digits, as the first four are consumed by the century, leaving at most one for the last
    // two digits.
    if !matches!(d.year().unsigned_abs().to_string().len(), 6) {
        return TestResult::discard();
    }

    let split_format = format_description!("[year repr:century][year repr:last_two]-[month]-[day]");
    let combined_format = format_description!("[year]-[month]-[day]");
    let combined = d.format(&combined_format).expect("formatting failed");

    TestResult::from_bool(Date::parse(&combined, &split_format).expect("parsing failed") == d)
}

#[quickcheck]
fn date_parse_century_last_two_equivalent_standard(d: Date) -> TestResult {
    // With the standard range, the year must be at most four digits.
    if !matches!(d.year(), -9999..=9999) {
        return TestResult::discard();
    }

    let split_format = format_description!(
        "[year repr:century range:standard][year repr:last_two range:standard]-[month]-[day]"
    );
    let combined_format = format_description!("[year range:standard]-[month]-[day]");
    let combined = d.format(&combined_format).expect("formatting failed");

    TestResult::from_bool(Date::parse(&combined, &split_format).expect("parsing failed") == d)
}

#[quickcheck]
fn julian_day_roundtrip(d: Date) -> bool {
    Date::from_julian_day(d.to_julian_day()) == Ok(d)
}

#[quickcheck]
fn duration_roundtrip(d: Duration) -> bool {
    Duration::new(d.whole_seconds(), d.subsec_nanoseconds()) == d
}

#[quickcheck]
fn time_roundtrip(t: Time) -> bool {
    Time::from_hms_nano(t.hour(), t.minute(), t.second(), t.nanosecond()) == Ok(t)
}

#[quickcheck]
fn primitive_date_time_roundtrip(a: PrimitiveDateTime) -> bool {
    PrimitiveDateTime::new(a.date(), a.time()) == a
}

#[quickcheck]
fn utc_offset_roundtrip(o: UtcOffset) -> bool {
    let (hours, minutes, seconds) = o.as_hms();
    UtcOffset::from_hms(hours, minutes, seconds) == Ok(o)
}

#[quickcheck]
fn offset_date_time_roundtrip(a: OffsetDateTime) -> TestResult {
    // Values near the edge of what is allowed may panic if the conversion brings the underlying
    // value outside the valid range of values.
    if a.date() == Date::MIN || a.date() == Date::MAX {
        return TestResult::discard();
    }

    TestResult::from_bool(PrimitiveDateTime::new(a.date(), a.time()).assume_offset(a.offset()) == a)
}

#[quickcheck]
fn unix_timestamp_roundtrip(odt: OffsetDateTime) -> TestResult {
    match odt.date() {
        Date::MIN | Date::MAX => TestResult::discard(),
        _ => TestResult::from_bool({
            // nanoseconds are not stored in the basic Unix timestamp
            let odt = odt - Duration::nanoseconds(odt.nanosecond().into());
            OffsetDateTime::from_unix_timestamp(odt.unix_timestamp()) == Ok(odt)
        }),
    }
}

#[quickcheck]
fn unix_timestamp_nanos_roundtrip(odt: OffsetDateTime) -> TestResult {
    match odt.date() {
        Date::MIN | Date::MAX => TestResult::discard(),
        _ => TestResult::from_bool(
            OffsetDateTime::from_unix_timestamp_nanos(odt.unix_timestamp_nanos()) == Ok(odt),
        ),
    }
}

#[quickcheck]
fn number_from_monday_roundtrip(w: Weekday) -> bool {
    Monday.nth_next(w.number_from_monday() + 7 - 1) == w
}

#[quickcheck]
fn number_from_sunday_roundtrip(w: Weekday) -> bool {
    Sunday.nth_next(w.number_from_sunday() + 7 - 1) == w
}

#[quickcheck]
fn number_days_from_monday_roundtrip(w: Weekday) -> bool {
    Monday.nth_next(w.number_days_from_monday()) == w
}

#[quickcheck]
fn number_days_from_sunday_roundtrip(w: Weekday) -> bool {
    Sunday.nth_next(w.number_days_from_sunday()) == w
}

#[quickcheck]
fn weekday_supports_arbitrary(w: Weekday) -> bool {
    (1..=7).contains(&w.number_from_monday())
}

#[quickcheck]
fn weekday_can_shrink(w: Weekday) -> bool {
    match w {
        Monday => w.shrink().next().is_none(),
        _ => w.shrink().next() == Some(w.previous()),
    }
}

#[quickcheck]
fn month_supports_arbitrary(m: Month) -> bool {
    (1..=12).contains(&u8::from(m))
}

#[quickcheck]
fn month_can_shrink(m: Month) -> bool {
    match m {
        Month::January => m.shrink().next().is_none(),
        _ => m.shrink().next() == Some(m.previous()),
    }
}

#[quickcheck]
fn date_replace_year(date: Date, year: i32) -> bool {
    date.replace_year(year) == Date::from_calendar_date(year, date.month(), date.day())
}

#[quickcheck]
fn date_replace_month(date: Date, month: Month) -> bool {
    date.replace_month(month) == Date::from_calendar_date(date.year(), month, date.day())
}

#[quickcheck]
fn date_replace_day(date: Date, day: u8) -> bool {
    date.replace_day(day) == Date::from_calendar_date(date.year(), date.month(), day)
}

#[quickcheck]
fn time_replace_hour(time: Time, hour: u8) -> bool {
    time.replace_hour(hour)
        == Time::from_hms_nano(hour, time.minute(), time.second(), time.nanosecond())
}

#[quickcheck]
fn pdt_replace_year(pdt: PrimitiveDateTime, year: i32) -> bool {
    pdt.replace_year(year)
        == Date::from_calendar_date(year, pdt.month(), pdt.day())
            .map(|date| date.with_time(pdt.time()))
}

#[quickcheck]
fn pdt_replace_month(pdt: PrimitiveDateTime, month: Month) -> bool {
    pdt.replace_month(month)
        == Date::from_calendar_date(pdt.year(), month, pdt.day())
            .map(|date| date.with_time(pdt.time()))
}

#[quickcheck]
fn pdt_replace_day(pdt: PrimitiveDateTime, day: u8) -> bool {
    pdt.replace_day(day)
        == Date::from_calendar_date(pdt.year(), pdt.month(), day)
            .map(|date| date.with_time(pdt.time()))
}

// Regression test for #481
#[quickcheck]
fn time_sub_time_no_panic(time_a: Time, time_b: Time) -> bool {
    no_panic! {
        let _ = time_a - time_b;
        let _ = time_b - time_a;
    }
}

#[quickcheck]
fn time_duration_until_since_range(time_a: Time, time_b: Time) -> bool {
    let a_until_b = time_a.duration_until(time_b);
    let b_until_a = time_b.duration_until(time_a);

    let a_since_b = time_a.duration_since(time_b);
    let b_since_a = time_b.duration_since(time_a);

    (Duration::ZERO..Duration::DAY).contains(&a_until_b)
        && (Duration::ZERO..Duration::DAY).contains(&b_until_a)
        && (Duration::ZERO..Duration::DAY).contains(&a_since_b)
        && (Duration::ZERO..Duration::DAY).contains(&b_since_a)
}

#[quickcheck]
fn time_duration_until_since_arithmetic(time_a: Time, time_b: Time) -> bool {
    let a_until_b = time_a.duration_until(time_b);
    let b_until_a = time_b.duration_until(time_a);

    let a_since_b = time_a.duration_since(time_b);
    let b_since_a = time_b.duration_since(time_a);

    time_a + a_until_b == time_b
        && time_b + b_until_a == time_a
        && time_b + a_since_b == time_a
        && time_a + b_since_a == time_b
}

#[quickcheck]
fn from_julian_day_no_panic(julian_day: i32) -> TestResult {
    if !(Date::MIN.to_julian_day()..=Date::MAX.to_julian_day()).contains(&julian_day) {
        return TestResult::discard();
    }

    TestResult::from_bool(
        std::panic::catch_unwind(|| {
            let _ = Date::from_julian_day(julian_day);
        })
        .is_ok(),
    )
}

#[quickcheck]
fn odt_eq_no_panic(left: OffsetDateTime, right: OffsetDateTime) -> bool {
    no_panic! {
        let _ = left == right;
    }
}

#[quickcheck]
fn odt_ord_no_panic(left: OffsetDateTime, right: OffsetDateTime) -> bool {
    no_panic! {
        let _ = left < right;
        let _ = left > right;
    }
}

#[quickcheck]
fn odt_sub_no_panic(left: OffsetDateTime, right: OffsetDateTime) -> bool {
    no_panic! {
        let _ = left - right;
    }
}

#[quickcheck]
fn odt_to_offset_no_panic(odt: OffsetDateTime, offset: UtcOffset) -> TestResult {
    let offset_difference = offset.whole_seconds() - odt.offset().whole_seconds();
    if Date::MIN
        .midnight()
        .assume_utc()
        .checked_add(Duration::seconds(offset_difference.widen()))
        .is_none()
        || Date::MAX
            .with_time(time!(23:59:59.999_999_999))
            .assume_utc()
            .checked_add(Duration::seconds(offset_difference.widen()))
            .is_none()
    {
        return TestResult::discard();
    }

    TestResult::from_bool(no_panic! {
        let _ = odt.to_offset(offset);
    })
}

#[quickcheck]
fn odt_replace_offset_no_panic(odt: OffsetDateTime, offset: UtcOffset) -> TestResult {
    if Date::MIN
        .midnight()
        .assume_offset(odt.offset())
        .checked_add(Duration::seconds(offset.whole_seconds().widen()))
        .is_none()
        || Date::MAX
            .with_time(time!(23:59:59.999_999_999))
            .assume_offset(odt.offset())
            .checked_add(Duration::seconds(offset.whole_seconds().widen()))
            .is_none()
    {
        return TestResult::discard();
    }

    TestResult::from_bool(no_panic! {
        let _ = odt.replace_offset(offset);
    })
}

test_shrink!(Date, date_can_shrink_year, year());
test_shrink!(Date, date_can_shrink_ordinal, ordinal(), min = 1);

test_shrink!(Duration, duration_can_shrink_seconds, whole_seconds());
test_shrink!(Duration, duration_can_shrink_ns, subsec_nanoseconds());

test_shrink!(Time, time_can_shrink_hour, hour());
test_shrink!(Time, time_can_shrink_minute, minute());
test_shrink!(Time, time_can_shrink_second, second());
test_shrink!(Time, time_can_shrink_nanosecond, nanosecond());

test_shrink!(
    PrimitiveDateTime,
    primitive_date_time_can_shrink_year,
    year()
);
test_shrink!(
    PrimitiveDateTime,
    primitive_date_time_can_shrink_ordinal,
    ordinal(),
    min = 1
);
test_shrink!(
    PrimitiveDateTime,
    primitive_date_time_can_shrink_hour,
    hour()
);
test_shrink!(
    PrimitiveDateTime,
    primitive_date_time_can_shrink_minute,
    minute()
);
test_shrink!(
    PrimitiveDateTime,
    primitive_date_time_can_shrink_second,
    second()
);
test_shrink!(
    PrimitiveDateTime,
    primitive_date_time_can_shrink_nanosecond,
    nanosecond()
);

test_shrink!(UtcOffset, utc_offset_can_shrink, whole_seconds());

test_shrink!(
    OffsetDateTime,
    offset_date_time_can_shrink_offset,
    offset().whole_seconds()
);
test_shrink!(OffsetDateTime, offset_date_time_can_shrink_year, year());
test_shrink!(
    OffsetDateTime,
    offset_date_time_can_shrink_ordinal,
    ordinal(),
    min = 1
);
test_shrink!(OffsetDateTime, offset_date_time_can_shrink_hour, hour());
test_shrink!(OffsetDateTime, offset_date_time_can_shrink_minute, minute());
test_shrink!(OffsetDateTime, offset_date_time_can_shrink_second, second());
test_shrink!(
    OffsetDateTime,
    offset_date_time_can_shrink_nanosecond,
    nanosecond()
);
