#![cfg(all(feature = "chrono", not(Py_LIMITED_API)))]

//! Conversions to and from [chrono](https://docs.rs/chrono/)’s `Duration`,
//! `NaiveDate`, `NaiveTime`, `DateTime<Tz>`, `FixedOffset`, and `Utc`.
//!
//! Unavailable with the `abi3` feature.
//!
//! # Setup
//!
//! To use this feature, add this to your **`Cargo.toml`**:
//!
//! ```toml
//! [dependencies]
//! # change * to the latest versions
//! pyo3 = { version = "*", features = ["chrono"] }
//! chrono = "0.4"
// workaround for `extended_key_value_attributes`: https://github.com/rust-lang/rust/issues/82768#issuecomment-803935643
#![cfg_attr(docsrs, cfg_attr(docsrs, doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"),  "\", features = [\"chrono\"] }")))]
#![cfg_attr(
    not(docsrs),
    doc = "pyo3 = { version = \"*\", features = [\"chrono\"] }"
)]
//! ```
//!
//! Note that you must use compatible versions of chrono and PyO3.
//! The required chrono version may vary based on the version of PyO3.
//!
//! # Example: Convert a `PyDateTime` to chrono's `DateTime<Utc>`
//!
//! ```rust
//! use chrono::{Utc, DateTime};
//! use pyo3::{Python, ToPyObject, types::PyDateTime};
//!
//! fn main() {
//!     pyo3::prepare_freethreaded_python();
//!     Python::with_gil(|py| {
//!         // Create an UTC datetime in python
//!         let py_tz = Utc.to_object(py);
//!         let py_tz = py_tz.downcast(py).unwrap();
//!         let pydatetime = PyDateTime::new(py, 2022, 1, 1, 12, 0, 0, 0, Some(py_tz)).unwrap();
//!         println!("PyDateTime: {}", pydatetime);
//!         // Now convert it to chrono's DateTime<Utc>
//!         let chrono_datetime: DateTime<Utc> = pydatetime.extract().unwrap();
//!         println!("DateTime<Utc>: {}", chrono_datetime);
//!     });
//! }
//! ```
use crate::exceptions::{PyTypeError, PyValueError};
use crate::types::{
    timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess,
    PyTzInfo, PyTzInfoAccess, PyUnicode,
};
use crate::{AsPyPointer, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject};
use chrono::offset::{FixedOffset, Utc};
use chrono::{
    DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike,
};
use pyo3_ffi::{PyDateTime_IMPORT, PyTimeZone_FromOffset};
use std::convert::TryInto;

impl ToPyObject for Duration {
    fn to_object(&self, py: Python<'_>) -> PyObject {
        // Total number of days
        let days = self.num_days();
        // Remainder of seconds
        let secs_dur = *self - Duration::days(days);
        // .try_into() converts i64 to i32, but this should never overflow
        // since it's at most the number of seconds per day
        let secs = secs_dur.num_seconds().try_into().unwrap();
        // Fractional part of the microseconds
        let micros = (secs_dur - Duration::seconds(secs_dur.num_seconds()))
            .num_microseconds()
            // This should never panic since we are just getting the fractional
            // part of the total microseconds, which should never overflow.
            .unwrap()
            // Same for the conversion from i64 to i32
            .try_into()
            .unwrap();

        // We do not need to check i64 to i32 cast from rust because
        // python will panic with OverflowError.
        // We pass true as the `normalize` parameter since we'd need to do several checks here to
        // avoid that, and it shouldn't have a big performance impact.
        let delta = PyDelta::new(py, days.try_into().unwrap_or(i32::MAX), secs, micros, true)
            .expect("Failed to construct delta");
        delta.into()
    }
}

impl IntoPy<PyObject> for Duration {
    fn into_py(self, py: Python<'_>) -> PyObject {
        ToPyObject::to_object(&self, py)
    }
}

impl FromPyObject<'_> for Duration {
    fn extract(ob: &PyAny) -> PyResult<Duration> {
        let delta: &PyDelta = ob.downcast()?;
        // Python size are much lower than rust size so we do not need bound checks.
        // 0 <= microseconds < 1000000
        // 0 <= seconds < 3600*24
        // -999999999 <= days <= 999999999
        Ok(Duration::days(delta.get_days().into())
            + Duration::seconds(delta.get_seconds().into())
            + Duration::microseconds(delta.get_microseconds().into()))
    }
}

impl ToPyObject for NaiveDate {
    fn to_object(&self, py: Python<'_>) -> PyObject {
        let month = self.month() as u8;
        let day = self.day() as u8;
        let date = PyDate::new(py, self.year(), month, day).expect("Failed to construct date");
        date.into()
    }
}

impl IntoPy<PyObject> for NaiveDate {
    fn into_py(self, py: Python<'_>) -> PyObject {
        ToPyObject::to_object(&self, py)
    }
}

impl FromPyObject<'_> for NaiveDate {
    fn extract(ob: &PyAny) -> PyResult<NaiveDate> {
        let date: &PyDate = ob.downcast()?;
        NaiveDate::from_ymd_opt(
            date.get_year(),
            date.get_month() as u32,
            date.get_day() as u32,
        )
        .ok_or_else(|| PyValueError::new_err("invalid or out-of-range date"))
    }
}

impl ToPyObject for NaiveTime {
    fn to_object(&self, py: Python<'_>) -> PyObject {
        let h = self.hour() as u8;
        let m = self.minute() as u8;
        let s = self.second() as u8;
        let ns = self.nanosecond();
        let (ms, fold) = match ns.checked_sub(1_000_000_000) {
            Some(ns) => (ns / 1000, true),
            None => (ns / 1000, false),
        };
        let time =
            PyTime::new_with_fold(py, h, m, s, ms, None, fold).expect("Failed to construct time");
        time.into()
    }
}

impl IntoPy<PyObject> for NaiveTime {
    fn into_py(self, py: Python<'_>) -> PyObject {
        ToPyObject::to_object(&self, py)
    }
}

impl FromPyObject<'_> for NaiveTime {
    fn extract(ob: &PyAny) -> PyResult<NaiveTime> {
        let time: &PyTime = ob.downcast()?;
        let ms = time.get_fold() as u32 * 1_000_000 + time.get_microsecond();
        let h = time.get_hour() as u32;
        let m = time.get_minute() as u32;
        let s = time.get_second() as u32;
        NaiveTime::from_hms_micro_opt(h, m, s, ms)
            .ok_or_else(|| PyValueError::new_err("invalid or out-of-range time"))
    }
}

impl ToPyObject for NaiveDateTime {
    fn to_object(&self, py: Python<'_>) -> PyObject {
        let date = self.date();
        let time = self.time();
        let yy = date.year();
        let mm = date.month() as u8;
        let dd = date.day() as u8;
        let h = time.hour() as u8;
        let m = time.minute() as u8;
        let s = time.second() as u8;
        let ns = time.nanosecond();
        let (ms, fold) = match ns.checked_sub(1_000_000_000) {
            Some(ns) => (ns / 1000, true),
            None => (ns / 1000, false),
        };
        let datetime = PyDateTime::new_with_fold(py, yy, mm, dd, h, m, s, ms, None, fold)
            .expect("Failed to construct datetime");
        datetime.into()
    }
}

impl IntoPy<PyObject> for NaiveDateTime {
    fn into_py(self, py: Python<'_>) -> PyObject {
        ToPyObject::to_object(&self, py)
    }
}

impl FromPyObject<'_> for NaiveDateTime {
    fn extract(ob: &PyAny) -> PyResult<NaiveDateTime> {
        let dt: &PyDateTime = ob.downcast()?;
        // If the user tries to convert a timezone aware datetime into a naive one,
        // we return a hard error. We could silently remove tzinfo, or assume local timezone
        // and do a conversion, but better leave this decision to the user of the library.
        if dt.get_tzinfo().is_some() {
            return Err(PyTypeError::new_err(
                "Trying to convert a timezone aware datetime into a NaiveDateTime.",
            ));
        }
        let h = dt.get_hour().into();
        let m = dt.get_minute().into();
        let s = dt.get_second().into();
        let ms = dt.get_microsecond();
        let dt = NaiveDateTime::new(
            NaiveDate::from_ymd_opt(dt.get_year(), dt.get_month().into(), dt.get_day().into())
                .ok_or_else(|| PyValueError::new_err("invalid or out-of-range date"))?,
            NaiveTime::from_hms_micro_opt(h, m, s, ms)
                .ok_or_else(|| PyValueError::new_err("invalid or out-of-range time"))?,
        );
        Ok(dt)
    }
}

impl<Tz: TimeZone> ToPyObject for DateTime<Tz> {
    fn to_object(&self, py: Python<'_>) -> PyObject {
        let date = self.naive_local().date();
        let time = self.naive_local().time();
        let yy = date.year();
        let mm = date.month() as u8;
        let dd = date.day() as u8;
        let h = time.hour() as u8;
        let m = time.minute() as u8;
        let s = time.second() as u8;
        let ns = time.nanosecond();
        let (ms, fold) = match ns.checked_sub(1_000_000_000) {
            Some(ns) => (ns / 1000, true),
            None => (ns / 1000, false),
        };
        let tz = self.offset().fix().to_object(py);
        let tz = tz.downcast(py).unwrap();
        let datetime = PyDateTime::new_with_fold(py, yy, mm, dd, h, m, s, ms, Some(tz), fold)
            .expect("Failed to construct datetime");
        datetime.into()
    }
}

impl<Tz: TimeZone> IntoPy<PyObject> for DateTime<Tz> {
    fn into_py(self, py: Python<'_>) -> PyObject {
        ToPyObject::to_object(&self, py)
    }
}

impl FromPyObject<'_> for DateTime<FixedOffset> {
    fn extract(ob: &PyAny) -> PyResult<DateTime<FixedOffset>> {
        let dt: &PyDateTime = ob.downcast()?;
        let ms = dt.get_microsecond();
        let h = dt.get_hour().into();
        let m = dt.get_minute().into();
        let s = dt.get_second().into();
        let tz = if let Some(tzinfo) = dt.get_tzinfo() {
            tzinfo.extract()?
        } else {
            return Err(PyTypeError::new_err("Not datetime.tzinfo"));
        };
        let dt = NaiveDateTime::new(
            NaiveDate::from_ymd_opt(dt.get_year(), dt.get_month().into(), dt.get_day().into())
                .ok_or_else(|| PyValueError::new_err("invalid or out-of-range date"))?,
            NaiveTime::from_hms_micro_opt(h, m, s, ms)
                .ok_or_else(|| PyValueError::new_err("invalid or out-of-range time"))?,
        );
        // `FixedOffset` cannot have ambiguities so we don't have to worry about DST folds and such
        Ok(DateTime::from_local(dt, tz))
    }
}

impl FromPyObject<'_> for DateTime<Utc> {
    fn extract(ob: &PyAny) -> PyResult<DateTime<Utc>> {
        let dt: &PyDateTime = ob.downcast()?;
        let ms = dt.get_fold() as u32 * 1_000_000 + dt.get_microsecond();
        let h = dt.get_hour().into();
        let m = dt.get_minute().into();
        let s = dt.get_second().into();
        let tz = if let Some(tzinfo) = dt.get_tzinfo() {
            tzinfo.extract()?
        } else {
            return Err(PyTypeError::new_err("Not datetime.timezone.utc"));
        };
        let dt = NaiveDateTime::new(
            NaiveDate::from_ymd_opt(dt.get_year(), dt.get_month().into(), dt.get_day().into())
                .ok_or_else(|| PyValueError::new_err("invalid or out-of-range date"))?,
            NaiveTime::from_hms_micro_opt(h, m, s, ms)
                .ok_or_else(|| PyValueError::new_err("invalid or out-of-range time"))?,
        );
        Ok(DateTime::from_utc(dt, tz))
    }
}

// Utiliy function used to convert PyDelta to timezone
fn pytimezone_fromoffset<'a>(py: &Python<'a>, td: &PyDelta) -> &'a PyAny {
    // Safety: py.from_owned_ptr needs the cast to be valid.
    // Since we are forcing a &PyDelta as input, the cast should always be valid.
    unsafe {
        PyDateTime_IMPORT();
        py.from_owned_ptr(PyTimeZone_FromOffset(td.as_ptr()))
    }
}

impl ToPyObject for FixedOffset {
    fn to_object(&self, py: Python<'_>) -> PyObject {
        let seconds_offset = self.local_minus_utc();
        let td =
            PyDelta::new(py, 0, seconds_offset, 0, true).expect("Failed to contruct timedelta");
        pytimezone_fromoffset(&py, td).into()
    }
}

impl IntoPy<PyObject> for FixedOffset {
    fn into_py(self, py: Python<'_>) -> PyObject {
        ToPyObject::to_object(&self, py)
    }
}

impl FromPyObject<'_> for FixedOffset {
    /// Convert python tzinfo to rust [`FixedOffset`].
    ///
    /// Note that the conversion will result in precision lost in microseconds as chrono offset
    /// does not supports microseconds.
    fn extract(ob: &PyAny) -> PyResult<FixedOffset> {
        let py_tzinfo: &PyTzInfo = ob.downcast()?;
        // Passing `ob.py().None()` (so Python's None) to the `utcoffset` function will only
        // work for timezones defined as fixed offsets in Python.
        // Any other timezone would require a datetime as the parameter, and return
        // None if the datetime is not provided.
        // Trying to convert None to a PyDelta in the next line will then fail.
        let py_timedelta = py_tzinfo.call_method1("utcoffset", (ob.py().None(),))?;
        let py_timedelta: &PyDelta = py_timedelta.downcast().map_err(|_| {
            PyTypeError::new_err(format!(
                "{:?} is not a fixed offset timezone",
                py_tzinfo
                    .repr()
                    .unwrap_or_else(|_| PyUnicode::new(ob.py(), "repr failed"))
            ))
        })?;
        let days = py_timedelta.get_days() as i64;
        let seconds = py_timedelta.get_seconds() as i64;
        // Here we won't get microseconds as noted before
        // let microseconds = py_timedelta.get_microseconds() as i64;
        let total_seconds = Duration::days(days) + Duration::seconds(seconds);
        // This cast is safe since the timedelta is limited to -24 hours and 24 hours.
        let total_seconds = total_seconds.num_seconds() as i32;
        FixedOffset::east_opt(total_seconds)
            .ok_or_else(|| PyValueError::new_err("fixed offset out of bounds"))
    }
}

impl ToPyObject for Utc {
    fn to_object(&self, py: Python<'_>) -> PyObject {
        timezone_utc(py).to_object(py)
    }
}

impl IntoPy<PyObject> for Utc {
    fn into_py(self, py: Python<'_>) -> PyObject {
        ToPyObject::to_object(&self, py)
    }
}

impl FromPyObject<'_> for Utc {
    fn extract(ob: &PyAny) -> PyResult<Utc> {
        let py_tzinfo: &PyTzInfo = ob.downcast()?;
        let py_utc = timezone_utc(ob.py());
        if py_tzinfo.eq(py_utc)? {
            Ok(Utc)
        } else {
            Err(PyTypeError::new_err("Not datetime.timezone.utc"))
        }
    }
}

#[cfg(test)]
mod tests {
    use std::{cmp::Ordering, panic};

    use super::*;

    #[test]
    // Only Python>=3.9 has the zoneinfo package
    // We skip the test on windows too since we'd need to install
    // tzdata there to make this work.
    #[cfg(all(Py_3_9, not(target_os = "windows")))]
    fn test_zoneinfo_is_not_fixedoffset() {
        Python::with_gil(|py| {
            let locals = crate::types::PyDict::new(py);
            py.run(
                "import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')",
                None,
                Some(locals),
            )
            .unwrap();
            let result: PyResult<FixedOffset> = locals.get_item("zi").unwrap().extract();
            assert!(result.is_err());
            let res = result.err().unwrap();
            // Also check the error message is what we expect
            let msg = res.value(py).repr().unwrap().to_string();
            assert_eq!(msg, "TypeError('\"zoneinfo.ZoneInfo(key=\\'Europe/London\\')\" is not a fixed offset timezone')");
        });
    }

    #[test]
    fn test_timezone_aware_to_naive_fails() {
        // Test that if a user tries to convert a python's timezone aware datetime into a naive
        // one, the conversion fails.
        Python::with_gil(|py| {
            let utc = timezone_utc(py);
            let py_datetime = PyDateTime::new(py, 2022, 1, 1, 1, 0, 0, 0, Some(utc)).unwrap();
            // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
            let res: PyResult<NaiveDateTime> = py_datetime.extract();
            assert!(res.is_err());
            let res = res.err().unwrap();
            // Also check the error message is what we expect
            let msg = res.value(py).repr().unwrap().to_string();
            assert_eq!(
                msg,
                "TypeError('Trying to convert a timezone aware datetime into a NaiveDateTime.')"
            );
        });
    }

    #[test]
    fn test_pyo3_timedelta_topyobject() {
        // Utility function used to check different durations.
        // The `name` parameter is used to identify the check in case of a failure.
        let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| {
            Python::with_gil(|py| {
                let delta = delta.to_object(py);
                let delta: &PyDelta = delta.extract(py).unwrap();
                let py_delta = PyDelta::new(py, py_days, py_seconds, py_ms, true).unwrap();
                assert!(
                    delta.eq(py_delta).unwrap(),
                    "{}: {} != {}",
                    name,
                    delta,
                    py_delta
                );
            });
        };

        let delta = Duration::days(-1) + Duration::seconds(1) + Duration::microseconds(-10);
        check("delta normalization", delta, -1, 1, -10);

        // Check the minimum value allowed by PyDelta, which is different
        // from the minimum value allowed in Duration. This should pass.
        let delta = Duration::seconds(-86399999913600); // min
        check("delta min value", delta, -999999999, 0, 0);

        // Same, for max value
        let delta = Duration::seconds(86399999999999) + Duration::nanoseconds(999999000); // max
        check("delta max value", delta, 999999999, 86399, 999999);

        // Also check that trying to convert an out of bound value panics.
        Python::with_gil(|py| {
            assert!(panic::catch_unwind(|| Duration::min_value().to_object(py)).is_err());
            assert!(panic::catch_unwind(|| Duration::max_value().to_object(py)).is_err());
        });
    }

    #[test]
    fn test_pyo3_timedelta_frompyobject() {
        // Utility function used to check different durations.
        // The `name` parameter is used to identify the check in case of a failure.
        let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| {
            Python::with_gil(|py| {
                let py_delta = PyDelta::new(py, py_days, py_seconds, py_ms, true).unwrap();
                let py_delta: Duration = py_delta.extract().unwrap();
                assert_eq!(py_delta, delta, "{}: {} != {}", name, py_delta, delta);
            })
        };

        // Check the minimum value allowed by PyDelta, which is different
        // from the minimum value allowed in Duration. This should pass.
        check(
            "min pydelta value",
            Duration::seconds(-86399999913600),
            -999999999,
            0,
            0,
        );
        // Same, for max value
        check(
            "max pydelta value",
            Duration::seconds(86399999999999) + Duration::microseconds(999999),
            999999999,
            86399,
            999999,
        );

        // This check is to assert that we can't construct every possible Duration from a PyDelta
        // since they have different bounds.
        Python::with_gil(|py| {
            let low_days: i32 = -1000000000;
            // This is possible
            assert!(panic::catch_unwind(|| Duration::days(low_days as i64)).is_ok());
            // This panics on PyDelta::new
            assert!(panic::catch_unwind(|| {
                let pydelta = PyDelta::new(py, low_days, 0, 0, true).unwrap();
                if let Ok(_duration) = pydelta.extract::<Duration>() {
                    // So we should never get here
                }
            })
            .is_err());

            let high_days: i32 = 1000000000;
            // This is possible
            assert!(panic::catch_unwind(|| Duration::days(high_days as i64)).is_ok());
            // This panics on PyDelta::new
            assert!(panic::catch_unwind(|| {
                let pydelta = PyDelta::new(py, high_days, 0, 0, true).unwrap();
                if let Ok(_duration) = pydelta.extract::<Duration>() {
                    // So we should never get here
                }
            })
            .is_err());
        });
    }

    #[test]
    fn test_pyo3_date_topyobject() {
        let eq_ymd = |name: &'static str, year, month, day| {
            Python::with_gil(|py| {
                let date = NaiveDate::from_ymd_opt(year, month, day)
                    .unwrap()
                    .to_object(py);
                let date: &PyDate = date.extract(py).unwrap();
                let py_date = PyDate::new(py, year, month as u8, day as u8).unwrap();
                assert_eq!(
                    date.compare(py_date).unwrap(),
                    Ordering::Equal,
                    "{}: {} != {}",
                    name,
                    date,
                    py_date
                );
            })
        };

        eq_ymd("past date", 2012, 2, 29);
        eq_ymd("min date", 1, 1, 1);
        eq_ymd("future date", 3000, 6, 5);
        eq_ymd("max date", 9999, 12, 31);
    }

    #[test]
    fn test_pyo3_date_frompyobject() {
        let eq_ymd = |name: &'static str, year, month, day| {
            Python::with_gil(|py| {
                let py_date = PyDate::new(py, year, month as u8, day as u8).unwrap();
                let py_date: NaiveDate = py_date.extract().unwrap();
                let date = NaiveDate::from_ymd_opt(year, month, day).unwrap();
                assert_eq!(py_date, date, "{}: {} != {}", name, date, py_date);
            })
        };

        eq_ymd("past date", 2012, 2, 29);
        eq_ymd("min date", 1, 1, 1);
        eq_ymd("future date", 3000, 6, 5);
        eq_ymd("max date", 9999, 12, 31);
    }

    #[test]
    fn test_pyo3_datetime_topyobject() {
        let check_utc =
            |name: &'static str, year, month, day, hour, minute, second, ms, py_ms, fold| {
                Python::with_gil(|py| {
                    let datetime = NaiveDate::from_ymd_opt(year, month, day)
                        .unwrap()
                        .and_hms_micro_opt(hour, minute, second, ms)
                        .unwrap();
                    let datetime = DateTime::<Utc>::from_utc(datetime, Utc).to_object(py);
                    let datetime: &PyDateTime = datetime.extract(py).unwrap();
                    let py_tz = Utc.to_object(py);
                    let py_tz = py_tz.downcast(py).unwrap();
                    let py_datetime = PyDateTime::new_with_fold(
                        py,
                        year,
                        month as u8,
                        day as u8,
                        hour as u8,
                        minute as u8,
                        second as u8,
                        py_ms,
                        Some(py_tz),
                        fold,
                    )
                    .unwrap();
                    assert_eq!(
                        datetime.compare(py_datetime).unwrap(),
                        Ordering::Equal,
                        "{}: {} != {}",
                        name,
                        datetime,
                        py_datetime
                    );
                })
            };

        check_utc("fold", 2014, 5, 6, 7, 8, 9, 1_999_999, 999_999, true);
        check_utc("non fold", 2014, 5, 6, 7, 8, 9, 999_999, 999_999, false);

        let check_fixed_offset =
            |name: &'static str, year, month, day, hour, minute, ssecond, ms, py_ms, fold| {
                Python::with_gil(|py| {
                    let offset = FixedOffset::east_opt(3600).unwrap();
                    let datetime = NaiveDate::from_ymd_opt(year, month, day)
                        .unwrap()
                        .and_hms_micro_opt(hour, minute, ssecond, ms)
                        .unwrap();
                    let datetime =
                        DateTime::<FixedOffset>::from_local(datetime, offset).to_object(py);
                    let datetime: &PyDateTime = datetime.extract(py).unwrap();
                    let py_tz = offset.to_object(py);
                    let py_tz = py_tz.downcast(py).unwrap();
                    let py_datetime = PyDateTime::new_with_fold(
                        py,
                        year,
                        month as u8,
                        day as u8,
                        hour as u8,
                        minute as u8,
                        ssecond as u8,
                        py_ms,
                        Some(py_tz),
                        fold,
                    )
                    .unwrap();
                    assert_eq!(
                        datetime.compare(py_datetime).unwrap(),
                        Ordering::Equal,
                        "{}: {} != {}",
                        name,
                        datetime,
                        py_datetime
                    );
                })
            };

        check_fixed_offset("fold", 2014, 5, 6, 7, 8, 9, 1_999_999, 999_999, true);
        check_fixed_offset("non fold", 2014, 5, 6, 7, 8, 9, 999_999, 999_999, false);
    }

    #[test]
    fn test_pyo3_datetime_frompyobject() {
        let check_utc =
            |name: &'static str, year, month, day, hour, minute, second, ms, py_ms, fold| {
                Python::with_gil(|py| {
                    let py_tz = Utc.to_object(py);
                    let py_tz = py_tz.downcast(py).unwrap();
                    let py_datetime = PyDateTime::new_with_fold(
                        py,
                        year,
                        month as u8,
                        day as u8,
                        hour as u8,
                        minute as u8,
                        second as u8,
                        py_ms,
                        Some(py_tz),
                        fold,
                    )
                    .unwrap();
                    let py_datetime: DateTime<Utc> = py_datetime.extract().unwrap();
                    let datetime = NaiveDate::from_ymd_opt(year, month, day)
                        .unwrap()
                        .and_hms_micro_opt(hour, minute, second, ms)
                        .unwrap();
                    let datetime = DateTime::<Utc>::from_utc(datetime, Utc);
                    assert_eq!(
                        py_datetime, datetime,
                        "{}: {} != {}",
                        name, datetime, py_datetime
                    );
                })
            };

        check_utc("fold", 2014, 5, 6, 7, 8, 9, 1_999_999, 999_999, true);
        check_utc("non fold", 2014, 5, 6, 7, 8, 9, 999_999, 999_999, false);

        let check_fixed_offset = |year, month, day, hour, minute, second, ms| {
            Python::with_gil(|py| {
                let offset = FixedOffset::east_opt(3600).unwrap();
                let py_tz = offset.to_object(py);
                let py_tz = py_tz.downcast(py).unwrap();
                let py_datetime = PyDateTime::new_with_fold(
                    py,
                    year,
                    month as u8,
                    day as u8,
                    hour as u8,
                    minute as u8,
                    second as u8,
                    ms,
                    Some(py_tz),
                    false, // No such thing as fold for fixed offset timezones
                )
                .unwrap();
                let py_datetime: DateTime<FixedOffset> = py_datetime.extract().unwrap();
                let datetime = NaiveDate::from_ymd_opt(year, month, day)
                    .unwrap()
                    .and_hms_micro_opt(hour, minute, second, ms)
                    .unwrap();
                let datetime = DateTime::<FixedOffset>::from_local(datetime, offset);

                assert_eq!(py_datetime, datetime, "{} != {}", datetime, py_datetime);
            })
        };

        check_fixed_offset(2014, 5, 6, 7, 8, 9, 999_999);

        Python::with_gil(|py| {
            let py_tz = Utc.to_object(py);
            let py_tz = py_tz.downcast(py).unwrap();
            let py_datetime =
                PyDateTime::new_with_fold(py, 2014, 5, 6, 7, 8, 9, 999_999, Some(py_tz), false)
                    .unwrap();
            assert!(py_datetime.extract::<DateTime<FixedOffset>>().is_ok());
            let offset = FixedOffset::east_opt(3600).unwrap();
            let py_tz = offset.to_object(py);
            let py_tz = py_tz.downcast(py).unwrap();
            let py_datetime =
                PyDateTime::new_with_fold(py, 2014, 5, 6, 7, 8, 9, 999_999, Some(py_tz), false)
                    .unwrap();
            assert!(py_datetime.extract::<DateTime<Utc>>().is_err());
        })
    }

    #[test]
    fn test_pyo3_offset_fixed_topyobject() {
        Python::with_gil(|py| {
            // Chrono offset
            let offset = FixedOffset::east_opt(3600).unwrap().to_object(py);
            // Python timezone from timedelta
            let td = PyDelta::new(py, 0, 3600, 0, true).unwrap();
            let py_timedelta = pytimezone_fromoffset(&py, td);
            // Should be equal
            assert!(offset.as_ref(py).eq(py_timedelta).unwrap());

            // Same but with negative values
            let offset = FixedOffset::east_opt(-3600).unwrap().to_object(py);
            let td = PyDelta::new(py, 0, -3600, 0, true).unwrap();
            let py_timedelta = pytimezone_fromoffset(&py, td);
            assert!(offset.as_ref(py).eq(py_timedelta).unwrap());
        })
    }

    #[test]
    fn test_pyo3_offset_fixed_frompyobject() {
        Python::with_gil(|py| {
            let py_timedelta = PyDelta::new(py, 0, 3600, 0, true).unwrap();
            let py_tzinfo = pytimezone_fromoffset(&py, py_timedelta);
            let offset: FixedOffset = py_tzinfo.extract().unwrap();
            assert_eq!(FixedOffset::east_opt(3600).unwrap(), offset);
        })
    }

    #[test]
    fn test_pyo3_offset_utc_topyobject() {
        Python::with_gil(|py| {
            let utc = Utc.to_object(py);
            let py_utc = timezone_utc(py);
            assert!(utc.as_ref(py).is(py_utc));
        })
    }

    #[test]
    fn test_pyo3_offset_utc_frompyobject() {
        Python::with_gil(|py| {
            let py_utc = timezone_utc(py);
            let py_utc: Utc = py_utc.extract().unwrap();
            assert_eq!(Utc, py_utc);

            let py_timedelta = PyDelta::new(py, 0, 0, 0, true).unwrap();
            let py_timezone_utc = pytimezone_fromoffset(&py, py_timedelta);
            let py_timezone_utc: Utc = py_timezone_utc.extract().unwrap();
            assert_eq!(Utc, py_timezone_utc);

            let py_timedelta = PyDelta::new(py, 0, 3600, 0, true).unwrap();
            let py_timezone = pytimezone_fromoffset(&py, py_timedelta);
            assert!(py_timezone.extract::<Utc>().is_err());
        })
    }

    #[test]
    fn test_pyo3_time_topyobject() {
        let check_time = |name: &'static str, hour, minute, second, ms, py_ms, fold| {
            Python::with_gil(|py| {
                let time = NaiveTime::from_hms_micro_opt(hour, minute, second, ms)
                    .unwrap()
                    .to_object(py);
                let time: &PyTime = time.extract(py).unwrap();
                let py_time = PyTime::new_with_fold(
                    py,
                    hour as u8,
                    minute as u8,
                    second as u8,
                    py_ms,
                    None,
                    fold,
                )
                .unwrap();
                assert_eq!(
                    time.compare(py_time).unwrap(),
                    Ordering::Equal,
                    "{}: {} != {}",
                    name,
                    time,
                    py_time
                );
            })
        };

        check_time("fold", 3, 5, 7, 1_999_999, 999_999, true);
        check_time("non fold", 3, 5, 7, 999_999, 999_999, false);
    }

    #[test]
    fn test_pyo3_time_frompyobject() {
        let check_time = |name: &'static str, hour, minute, second, ms, py_ms, fold| {
            Python::with_gil(|py| {
                let py_time = PyTime::new_with_fold(
                    py,
                    hour as u8,
                    minute as u8,
                    second as u8,
                    py_ms,
                    None,
                    fold,
                )
                .unwrap();
                let py_time: NaiveTime = py_time.extract().unwrap();
                let time = NaiveTime::from_hms_micro_opt(hour, minute, second, ms).unwrap();
                assert_eq!(py_time, time, "{}: {} != {}", name, py_time, time);
            })
        };

        check_time("fold", 3, 5, 7, 1_999_999, 999_999, true);
        check_time("non fold", 3, 5, 7, 999_999, 999_999, false);
    }

    #[cfg(all(test, not(target_arch = "wasm32")))]
    mod proptests {
        use super::*;
        use crate::types::IntoPyDict;

        use proptest::prelude::*;

        proptest! {

            // Range is limited to 1970 to 2038 due to windows limitations
            #[test]
            fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) {
                Python::with_gil(|py| {

                    let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py);
                    let code = format!("datetime.datetime.fromtimestamp({}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={})))", timestamp, timedelta);
                    let t = py.eval(&code, Some(globals), None).unwrap();

                    // Get ISO 8601 string from python
                    let py_iso_str = t.call_method0("isoformat").unwrap();

                    // Get ISO 8601 string from rust
                    let t = t.extract::<DateTime<FixedOffset>>().unwrap();
                    // Python doesn't print the seconds of the offset if they are 0
                    let rust_iso_str = if timedelta % 60 == 0 {
                        t.format("%Y-%m-%dT%H:%M:%S%:z").to_string()
                    } else {
                        t.format("%Y-%m-%dT%H:%M:%S%::z").to_string()
                    };

                    // They should be equal
                    assert_eq!(py_iso_str.to_string(), rust_iso_str);
                })
            }

            #[test]
            fn test_duration_roundtrip(days in -999999999i64..=999999999i64) {
                // Test roundtrip convertion rust->python->rust for all allowed
                // python values of durations (from -999999999 to 999999999 days),
                Python::with_gil(|py| {
                    let dur = Duration::days(days);
                    let pydelta = dur.into_py(py);
                    let roundtripped: Duration = pydelta.extract(py).expect("Round trip");
                    assert_eq!(dur, roundtripped);
                })
            }

            #[test]
            fn test_fixedoffset_roundtrip(secs in -86399i32..=86399i32) {
                Python::with_gil(|py| {
                    let offset = FixedOffset::east_opt(secs).unwrap();
                    let pyoffset = offset.into_py(py);
                    let roundtripped: FixedOffset = pyoffset.extract(py).expect("Round trip");
                    assert_eq!(offset, roundtripped);
                })
            }

            #[test]
            fn test_naivedate_roundtrip(
                year in 1i32..=9999i32,
                month in 1u32..=12u32,
                day in 1u32..=31u32
            ) {
                // Test roundtrip convertion rust->python->rust for all allowed
                // python dates (from year 1 to year 9999)
                Python::with_gil(|py| {
                    // We use to `from_ymd_opt` constructor so that we only test valid `NaiveDate`s.
                    // This is to skip the test if we are creating an invalid date, like February 31.
                    if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) {
                        let pydate = date.into_py(py);
                        let roundtripped: NaiveDate = pydate.extract(py).expect("Round trip");
                        assert_eq!(date, roundtripped);
                    }
                })
            }

            #[test]
            fn test_naivetime_roundtrip(
                hour in 0u32..=24u32,
                min in 0u32..=60u32,
                sec in 0u32..=60u32,
                micro in 0u32..=2_000_000u32
            ) {
                // Test roundtrip convertion rust->python->rust for naive times.
                // Python time has a resolution of microseconds, so we only test
                // NaiveTimes with microseconds resolution, even if NaiveTime has nanosecond
                // resolution.
                Python::with_gil(|py| {
                    // We use to `from_hms_micro_opt` constructor so that we only test valid `NaiveTime`s.
                    // This is to skip the test if we are creating an invalid time
                    if let Some(time) = NaiveTime::from_hms_micro_opt(hour, min, sec, micro) {
                        let pytime = time.into_py(py);
                        let roundtripped: NaiveTime = pytime.extract(py).expect("Round trip");
                        assert_eq!(time, roundtripped);
                    }
                })
            }

            #[test]
            fn test_utc_datetime_roundtrip(
                year in 1i32..=9999i32,
                month in 1u32..=12u32,
                day in 1u32..=31u32,
                hour in 0u32..=24u32,
                min in 0u32..=60u32,
                sec in 0u32..=60u32,
                micro in 0u32..=2_000_000u32
            ) {
                Python::with_gil(|py| {
                    let date_opt = NaiveDate::from_ymd_opt(year, month, day);
                    let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
                    if let (Some(date), Some(time)) = (date_opt, time_opt) {
                        let dt: DateTime<Utc> = DateTime::from_utc(NaiveDateTime::new(date, time), Utc);
                        let pydt = dt.into_py(py);
                        let roundtripped: DateTime<Utc> = pydt.extract(py).expect("Round trip");
                        assert_eq!(dt, roundtripped);
                    }
                })
            }

            #[test]
            fn test_fixedoffset_datetime_roundtrip(
                year in 1i32..=9999i32,
                month in 1u32..=12u32,
                day in 1u32..=31u32,
                hour in 0u32..=24u32,
                min in 0u32..=60u32,
                sec in 0u32..=60u32,
                micro in 0u32..=1_000_000u32,
                offset_secs in -86399i32..=86399i32
            ) {
                Python::with_gil(|py| {
                    let date_opt = NaiveDate::from_ymd_opt(year, month, day);
                    let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
                    let offset = FixedOffset::east_opt(offset_secs).unwrap();
                    if let (Some(date), Some(time)) = (date_opt, time_opt) {
                        let dt: DateTime<FixedOffset> = DateTime::from_utc(NaiveDateTime::new(date, time), offset);
                        let pydt = dt.into_py(py);
                        let roundtripped: DateTime<FixedOffset> = pydt.extract(py).expect("Round trip");
                        assert_eq!(dt, roundtripped);
                    }
                })
            }
        }
    }
}
