Documentation
use xsd::TimezoneOffset;

#[cfg(feature = "borsh")]
#[test]
fn borsh_offsets_use_validated_little_endian_minutes() {
    for minutes in i16::MIN..=i16::MAX {
        let bytes = minutes.to_le_bytes();
        let decoded = borsh::from_slice::<TimezoneOffset>(&bytes);
        match TimezoneOffset::from_minutes(minutes) {
            Some(offset) => {
                assert_eq!(decoded.unwrap(), offset);
                assert_eq!(borsh::to_vec(&offset).unwrap(), bytes);
            },
            None => assert_eq!(
                decoded.unwrap_err().kind(),
                borsh::io::ErrorKind::InvalidData
            ),
        }
    }
    for bytes in [&[][..], &[0][..], &[0, 0, 0][..]] {
        assert!(borsh::from_slice::<TimezoneOffset>(bytes).is_err());
    }
    assert_eq!(borsh::to_vec(&None::<TimezoneOffset>).unwrap(), [0]);
    assert_eq!(
        borsh::to_vec(&Some(TimezoneOffset::UTC)).unwrap(),
        [1, 0, 0]
    );
}

#[cfg(feature = "serde")]
#[test]
fn serde_validates_offsets_and_preserves_absence() {
    for minutes in -840..=840 {
        let offset = TimezoneOffset::from_minutes(minutes).unwrap();
        let json = serde_json::to_string(&offset).unwrap();
        assert_eq!(json, minutes.to_string());
        assert_eq!(
            serde_json::from_str::<TimezoneOffset>(&json).unwrap(),
            offset
        );
    }
    for json in ["841", "-841", "32768", "1.5", "\"Z\"", "null"] {
        assert!(
            serde_json::from_str::<TimezoneOffset>(json).is_err(),
            "{json}"
        );
    }
    for (offset, json) in [(None, "null"), (Some(TimezoneOffset::UTC), "0")] {
        assert_eq!(serde_json::to_string(&offset).unwrap(), json);
        assert_eq!(
            serde_json::from_str::<Option<TimezoneOffset>>(json).unwrap(),
            offset
        );
    }
}

#[cfg(feature = "jiff")]
#[test]
fn jiff_conversion_preserves_offsets_and_rejects_loss() {
    use xsd::TimezoneOffsetError::{OutOfRange, SubMinute};
    for minutes in -840..=840 {
        let offset = TimezoneOffset::from_minutes(minutes).unwrap();
        let jiff_offset = jiff::tz::Offset::from(offset);
        assert_eq!(jiff_offset.seconds(), i32::from(minutes) * 60);
        assert_eq!(TimezoneOffset::try_from(jiff_offset), Ok(offset));
    }
    for (seconds, error) in [
        (1, SubMinute),
        (-1, SubMinute),
        (59, SubMinute),
        (50_401, OutOfRange),
        (-50_401, OutOfRange),
        (54_000, OutOfRange),
    ] {
        let offset = jiff::tz::Offset::from_seconds(seconds).unwrap();
        assert_eq!(TimezoneOffset::try_from(offset), Err(error));
    }
}

#[test]
fn every_offset_round_trips_through_xsd_syntax() {
    for minutes in -840..=840 {
        let offset = TimezoneOffset::from_minutes(minutes).unwrap();
        assert_eq!(
            offset.to_string().parse::<TimezoneOffset>().unwrap(),
            offset
        );
    }
    for input in ["Z", "+00:00", "-00:00"] {
        assert_eq!(
            input.parse::<TimezoneOffset>().unwrap(),
            TimezoneOffset::UTC
        );
    }
}

#[test]
fn offset_parser_rejects_invalid_syntax_and_ranges() {
    use xsd::TimezoneOffsetError::{InvalidLexical, OutOfRange};
    for input in [
        "",
        "z",
        " Z",
        "Z ",
        "+1:00",
        "+0100",
        "+01",
        "+01:00:00",
        "01:00",
        "+ab:cd",
        "+0:00",
        "+01:00[UTC]",
    ] {
        assert_eq!(
            input.parse::<TimezoneOffset>(),
            Err(InvalidLexical),
            "{input}"
        );
    }
    for input in ["+14:01", "-14:01", "+15:00", "-99:59", "+00:60", "+13:99"] {
        assert_eq!(input.parse::<TimezoneOffset>(), Err(OutOfRange), "{input}");
    }
}

#[test]
fn offset_constructor_enforces_the_entire_i16_domain() {
    for minutes in i16::MIN..=i16::MAX {
        let offset = TimezoneOffset::from_minutes(minutes);
        assert_eq!(offset.is_some(), (-840..=840).contains(&minutes));
        if let Some(offset) = offset {
            assert_eq!(offset.minutes(), minutes);
        }
    }
    assert_ne!(None, Some(TimezoneOffset::UTC));
}

#[test]
fn offset_formats_xsd_timezone_syntax() {
    for (minutes, lexical) in [
        (-840, "-14:00"),
        (-1, "-00:01"),
        (0, "Z"),
        (1, "+00:01"),
        (330, "+05:30"),
        (840, "+14:00"),
    ] {
        assert_eq!(
            TimezoneOffset::from_minutes(minutes).unwrap().to_string(),
            lexical
        );
    }
}