use crate::deserialize::{self, FromSql};
use crate::serialize::{self, Output, ToSql};
use crate::sql_types;
use crate::sqlite::connection::SqliteValue;
use crate::sqlite::Sqlite;

#[cfg(feature = "chrono")]
mod chrono;
#[cfg(feature = "time")]
mod time;

#[cfg(feature = "sqlite")]
impl FromSql<sql_types::Date, Sqlite> for String {
    fn from_sql(value: SqliteValue<'_, '_, '_>) -> deserialize::Result<Self> {
        FromSql::<sql_types::Text, Sqlite>::from_sql(value)
    }
}

#[cfg(feature = "sqlite")]
impl ToSql<sql_types::Date, Sqlite> for str {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
        ToSql::<sql_types::Text, Sqlite>::to_sql(self, out)
    }
}

#[cfg(feature = "sqlite")]
impl ToSql<sql_types::Date, Sqlite> for String {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
        <str as ToSql<sql_types::Date, Sqlite>>::to_sql(self as &str, out)
    }
}

#[cfg(feature = "sqlite")]
impl FromSql<sql_types::Time, Sqlite> for String {
    fn from_sql(value: SqliteValue<'_, '_, '_>) -> deserialize::Result<Self> {
        FromSql::<sql_types::Text, Sqlite>::from_sql(value)
    }
}

#[cfg(feature = "sqlite")]
impl ToSql<sql_types::Time, Sqlite> for str {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
        ToSql::<sql_types::Text, Sqlite>::to_sql(self, out)
    }
}

#[cfg(feature = "sqlite")]
impl ToSql<sql_types::Time, Sqlite> for String {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
        <str as ToSql<sql_types::Time, Sqlite>>::to_sql(self as &str, out)
    }
}

#[cfg(feature = "sqlite")]
impl FromSql<sql_types::Timestamp, Sqlite> for String {
    fn from_sql(value: SqliteValue<'_, '_, '_>) -> deserialize::Result<Self> {
        FromSql::<sql_types::Text, Sqlite>::from_sql(value)
    }
}

#[cfg(feature = "sqlite")]
impl ToSql<sql_types::Timestamp, Sqlite> for str {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
        ToSql::<sql_types::Text, Sqlite>::to_sql(self, out)
    }
}

#[cfg(feature = "sqlite")]
impl ToSql<sql_types::Timestamp, Sqlite> for String {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
        <str as ToSql<sql_types::Timestamp, Sqlite>>::to_sql(self as &str, out)
    }
}

#[cfg(feature = "sqlite")]
impl FromSql<sql_types::TimestamptzSqlite, Sqlite> for String {
    fn from_sql(value: SqliteValue<'_, '_, '_>) -> deserialize::Result<Self> {
        FromSql::<sql_types::Text, Sqlite>::from_sql(value)
    }
}

#[cfg(feature = "sqlite")]
impl ToSql<sql_types::TimestamptzSqlite, Sqlite> for str {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
        ToSql::<sql_types::Text, Sqlite>::to_sql(self, out)
    }
}

#[cfg(feature = "sqlite")]
impl ToSql<sql_types::TimestamptzSqlite, Sqlite> for String {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
        <str as ToSql<sql_types::TimestamptzSqlite, Sqlite>>::to_sql(self as &str, out)
    }
}

#[cfg(all(test, feature = "chrono", feature = "time"))]
#[allow(clippy::cast_possible_truncation)] // it's a test
mod tests {
    extern crate chrono;
    extern crate time;

    use chrono::{
        DateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc,
    };
    use time::{
        macros::{date, datetime, offset, time},
        Date, OffsetDateTime, PrimitiveDateTime, Time,
    };

    use crate::insert_into;
    use crate::prelude::*;
    use crate::test_helpers::connection;

    crate::table! {
        table_timestamp_tz(id) {
            id -> Integer,
            timestamp_with_tz -> TimestamptzSqlite,
        }
    }
    crate::table! {
      table_timestamp(id) {
          id -> Integer,
          timestamp -> Timestamp
      }
    }
    crate::table! {
        table_date(id) {
            id -> Integer,
            date -> Date
        }
    }
    crate::table! {
        table_time(id) {
            id -> Integer,
            time -> Time
        }
    }

    fn eq_date(left: Date, right: NaiveDate) -> bool {
        left.year() == right.year()
            && left.month() as u8 == right.month() as u8
            && left.day() == right.day() as u8
    }

    fn eq_time(left: Time, right: NaiveTime) -> bool {
        left.hour() == right.hour() as u8
            && left.minute() == right.minute() as u8
            && left.second() == right.second() as u8
            && left.nanosecond() == right.nanosecond()
    }

    fn eq_datetime(left: PrimitiveDateTime, right: NaiveDateTime) -> bool {
        eq_date(left.date(), right.date()) && eq_time(left.time(), right.time())
    }

    fn eq_datetime_utc(left: OffsetDateTime, right: DateTime<Utc>) -> bool {
        left.unix_timestamp_nanos() == right.timestamp_nanos_opt().unwrap() as i128
    }

    fn eq_datetime_offset(left: OffsetDateTime, right: DateTime<FixedOffset>) -> bool {
        left.unix_timestamp_nanos() == right.timestamp_nanos_opt().unwrap() as i128
    }

    fn create_tables(conn: &mut SqliteConnection) {
        crate::sql_query(
            "CREATE TABLE table_timestamp_tz(id INTEGER PRIMARY KEY, timestamp_with_tz TEXT);",
        )
        .execute(conn)
        .unwrap();

        crate::sql_query("CREATE TABLE table_timestamp(id INTEGER PRIMARY KEY, timestamp TEXT);")
            .execute(conn)
            .unwrap();

        crate::sql_query("CREATE TABLE table_date(id INTEGER PRIMARY KEY, date TEXT);")
            .execute(conn)
            .unwrap();

        crate::sql_query("CREATE TABLE table_time(id INTEGER PRIMARY KEY, time TEXT);")
            .execute(conn)
            .unwrap();
    }

    #[test]
    fn time_to_chrono_date() {
        let conn = &mut connection();
        create_tables(conn);

        let original = date!(2000 - 1 - 1);

        insert_into(table_date::table)
            .values(vec![(table_date::id.eq(1), table_date::date.eq(original))])
            .execute(conn)
            .unwrap();

        let translated = table_date::table
            .select(table_date::date)
            .get_result::<NaiveDate>(conn)
            .unwrap();

        assert!(eq_date(original, translated))
    }

    #[test]
    fn chrono_to_time_date() {
        let conn = &mut connection();
        create_tables(conn);

        let original = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();

        insert_into(table_date::table)
            .values(vec![(table_date::id.eq(1), table_date::date.eq(original))])
            .execute(conn)
            .unwrap();

        let translated = table_date::table
            .select(table_date::date)
            .get_result::<Date>(conn)
            .unwrap();

        assert!(eq_date(translated, original))
    }

    #[test]
    fn time_to_chrono_time() {
        let conn = &mut connection();
        create_tables(conn);

        let original = time!(1:1:1.001);

        insert_into(table_time::table)
            .values(vec![(table_time::id.eq(1), table_time::time.eq(original))])
            .execute(conn)
            .unwrap();

        let translated = table_time::table
            .select(table_time::time)
            .get_result::<NaiveTime>(conn)
            .unwrap();

        assert!(eq_time(original, translated))
    }

    #[test]
    fn chrono_to_time_time() {
        let conn = &mut connection();
        create_tables(conn);

        let original = NaiveTime::from_hms_milli_opt(1, 1, 1, 1).unwrap();

        insert_into(table_time::table)
            .values(vec![(table_time::id.eq(1), table_time::time.eq(original))])
            .execute(conn)
            .unwrap();

        let translated = table_time::table
            .select(table_time::time)
            .get_result::<Time>(conn)
            .unwrap();

        assert!(eq_time(translated, original))
    }

    #[test]
    fn time_to_chrono_datetime() {
        let conn = &mut connection();
        create_tables(conn);

        let original = datetime!(2000-1-1 1:1:1.001);

        insert_into(table_timestamp::table)
            .values(vec![(
                table_timestamp::id.eq(1),
                table_timestamp::timestamp.eq(original),
            )])
            .execute(conn)
            .unwrap();

        let translated = table_timestamp::table
            .select(table_timestamp::timestamp)
            .get_result::<NaiveDateTime>(conn)
            .unwrap();

        assert!(eq_datetime(original, translated))
    }

    #[test]
    fn chrono_to_time_datetime() {
        let conn = &mut connection();
        create_tables(conn);

        let original = NaiveDate::from_ymd_opt(2000, 1, 1)
            .unwrap()
            .and_hms_milli_opt(1, 1, 1, 1)
            .unwrap();

        insert_into(table_timestamp::table)
            .values(vec![(
                table_timestamp::id.eq(1),
                table_timestamp::timestamp.eq(original),
            )])
            .execute(conn)
            .unwrap();

        let translated = table_timestamp::table
            .select(table_timestamp::timestamp)
            .get_result::<PrimitiveDateTime>(conn)
            .unwrap();

        assert!(eq_datetime(translated, original))
    }

    #[test]
    fn chrono_to_time_datetime_utc() {
        let conn = &mut connection();
        create_tables(conn);

        let original = Utc::now();

        insert_into(table_timestamp_tz::table)
            .values(vec![(
                table_timestamp_tz::id.eq(1),
                table_timestamp_tz::timestamp_with_tz.eq(original),
            )])
            .execute(conn)
            .unwrap();

        let translated = table_timestamp_tz::table
            .select(table_timestamp_tz::timestamp_with_tz)
            .get_result::<OffsetDateTime>(conn)
            .unwrap();

        assert!(eq_datetime_utc(translated, original))
    }

    #[test]
    fn time_to_chrono_datetime_utc() {
        let conn = &mut connection();
        create_tables(conn);

        let original = OffsetDateTime::now_utc();

        insert_into(table_timestamp_tz::table)
            .values(vec![(
                table_timestamp_tz::id.eq(1),
                table_timestamp_tz::timestamp_with_tz.eq(original),
            )])
            .execute(conn)
            .unwrap();

        let translated = table_timestamp_tz::table
            .select(table_timestamp_tz::timestamp_with_tz)
            .get_result::<DateTime<Utc>>(conn)
            .unwrap();

        assert!(eq_datetime_utc(original, translated))
    }

    #[test]
    fn chrono_to_time_datetime_timezone() {
        let conn = &mut connection();
        create_tables(conn);

        let original = Utc::now().with_timezone(&FixedOffset::east_opt(5 * 3600).unwrap());

        insert_into(table_timestamp_tz::table)
            .values(vec![(
                table_timestamp_tz::id.eq(1),
                table_timestamp_tz::timestamp_with_tz.eq(original),
            )])
            .execute(conn)
            .unwrap();

        let translated = table_timestamp_tz::table
            .select(table_timestamp_tz::timestamp_with_tz)
            .get_result::<OffsetDateTime>(conn)
            .unwrap();

        assert!(eq_datetime_offset(translated, original))
    }

    #[test]
    fn time_to_chrono_datetime_offset() {
        let conn = &mut connection();
        create_tables(conn);

        let original = OffsetDateTime::now_utc().to_offset(offset!(+5));

        insert_into(table_timestamp_tz::table)
            .values(vec![(
                table_timestamp_tz::id.eq(1),
                table_timestamp_tz::timestamp_with_tz.eq(original),
            )])
            .execute(conn)
            .unwrap();

        let translated = table_timestamp_tz::table
            .select(table_timestamp_tz::timestamp_with_tz)
            .get_result::<DateTime<Utc>>(conn)
            .unwrap();

        assert!(eq_datetime_utc(original, translated))
    }
}
