#![allow(unused_imports, dead_code)]

use entity::{Column, Entity};
use sea_orm::{
    DerivePartialModel, IntoActiveModel, JoinType, NotSet, QueryOrder, QuerySelect, Set,
    prelude::*, sea_query::Alias,
};
use sea_query::ExprTrait;

use crate::common::TestContext;
use common::bakery_chain::*;

mod common;

mod entity {
    use sea_orm::prelude::*;

    #[derive(Debug, Clone, DeriveEntityModel)]
    #[sea_orm(table_name = "foo_table")]
    pub struct Model {
        #[sea_orm(primary_key)]
        id: i32,
        foo: i32,
        bar: String,
        foo2: bool,
        bar2: f64,
    }

    #[derive(Debug, DeriveRelation, EnumIter)]
    pub enum Relation {}

    impl ActiveModelBehavior for ActiveModel {}
}

#[derive(DerivePartialModel)]
#[sea_orm(entity = "Entity")]
struct SimpleTest {
    foo: i32,
    bar: String,
}

#[derive(DerivePartialModel)]
#[sea_orm(entity = "<entity::Model as ModelTrait>::Entity")]
struct EntityNameNotAIdent {
    #[sea_orm(from_col = "foo2")]
    foo: i32,
    #[sea_orm(from_col = "bar2")]
    bar: String,
}

#[derive(DerivePartialModel)]
#[sea_orm(entity = "Entity")]
struct FieldFromDiffNameColumnTest {
    #[sea_orm(from_col = "foo2")]
    foo: i32,
    #[sea_orm(from_col = "bar2")]
    bar: String,
}

#[derive(DerivePartialModel)]
struct FieldFromExpr {
    #[sea_orm(from_expr = "Column::Bar2.sum()")]
    foo: f64,
    #[sea_orm(from_expr = "Expr::col(Column::Id).equals(Column::Foo)")]
    bar: bool,
}

#[derive(DerivePartialModel)]
struct Nest {
    #[sea_orm(nested)]
    foo: SimpleTest,
}

#[derive(DerivePartialModel)]
struct NestOption {
    #[sea_orm(nested)]
    foo: Option<SimpleTest>,
}

#[derive(Debug, DerivePartialModel)]
#[sea_orm(entity = "bakery::Entity")]
struct Bakery {
    id: i32,
    #[sea_orm(from_col = "Name")]
    title: String,
}

#[derive(Debug, DerivePartialModel)]
#[sea_orm(entity = "cake::Entity", into_active_model)]
struct Cake {
    id: i32,
    name: String,
    #[sea_orm(nested)]
    bakery: Option<Bakery>,
    #[sea_orm(skip)]
    ignore: Ignore,
}

#[derive(DerivePartialModel)]
#[sea_orm(entity = "bakery::Entity")]
struct BakeryDetails {
    #[sea_orm(nested)]
    basics: Bakery,
    #[sea_orm(from_expr = "bakery::Column::ProfitMargin")]
    profit: f64,
}

#[derive(Debug, Default, PartialEq, Eq)]
struct Ignore {}

#[sea_orm_macros::test]
async fn partial_model_left_join_does_not_exist() {
    let ctx = TestContext::new("partial_model_left_join_does_not_exist").await;
    create_tables(&ctx.db).await.unwrap();

    seed_data::init_1(&ctx, false).await;

    let cake: Cake = cake::Entity::find()
        .left_join(bakery::Entity)
        .order_by_asc(cake::Column::Id)
        .into_partial_model()
        .one(&ctx.db)
        .await
        .expect("succeeds to get the result")
        .expect("exactly one model in DB");

    assert_eq!(cake.id, 13);
    assert_eq!(cake.name, "Cheesecake");
    assert!(cake.bakery.is_none());

    ctx.delete().await;
}

#[sea_orm_macros::test]
async fn partial_model_left_join_with_optional_nested_model_optional_fields_does_not_exist() {
    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "baker::Entity")]
    struct BakerDetails {
        id: i32,
        name: String,
        bakery_id: Option<i32>,
    }

    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "baker::Entity")]
    struct NestedBaker {
        #[sea_orm(nested)]
        details: BakerDetails,
    }

    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "cake::Entity")]
    struct CakeWithOptionalBakerModel {
        id: i32,
        name: String,
        #[sea_orm(nested)]
        baker: Option<NestedBaker>,
    }

    let ctx = TestContext::new("partial_model_left_join_deep_baker").await;
    create_tables(&ctx.db).await.unwrap();

    seed_data::init_1(&ctx, true).await;

    let cakes: Vec<CakeWithOptionalBakerModel> = cake::Entity::find()
        .left_join(baker::Entity)
        .order_by_asc(cake::Column::Id)
        .into_partial_model()
        .all(&ctx.db)
        .await
        .expect("succeeds to get the result");

    assert_eq!(
        cakes,
        [
            CakeWithOptionalBakerModel {
                id: 13,
                name: "Cheesecake".to_owned(),
                baker: Some(NestedBaker {
                    details: BakerDetails {
                        id: 22,
                        name: "Master Baker".to_owned(),
                        bakery_id: Some(42),
                    },
                }),
            },
            CakeWithOptionalBakerModel {
                id: 15,
                name: "Chocolate".to_owned(),
                baker: None,
            },
        ]
    );

    ctx.delete().await;
}

#[sea_orm_macros::test]
async fn partial_model_left_join_exists() {
    let ctx = TestContext::new("partial_model_left_join_exists").await;
    create_tables(&ctx.db).await.unwrap();

    seed_data::init_1(&ctx, true).await;

    let cake: Cake = cake::Entity::find()
        .left_join(bakery::Entity)
        .order_by_asc(cake::Column::Id)
        .into_partial_model()
        .one(&ctx.db)
        .await
        .expect("succeeds to get the result")
        .expect("exactly one model in DB");

    assert_eq!(cake.id, 13);
    assert_eq!(cake.name, "Cheesecake");
    assert!(matches!(cake.bakery, Some(Bakery { id: 42, .. })));
    assert_eq!(cake.bakery.as_ref().unwrap().title, "cool little bakery");

    #[derive(Debug, DerivePartialModel)]
    #[sea_orm(entity = "cake::Entity", into_active_model)]
    struct Cake2 {
        id: i32,
        name: String,
        #[sea_orm(nested, alias = "r0")]
        bakery: Option<Bakery>,
        #[sea_orm(skip)]
        ignore: Ignore,
    }

    let cake2: Cake2 = cake::Entity::find()
        .left_join_linked(cake::ToBakery)
        .order_by_asc(cake::Column::Id)
        .into_partial_model()
        .one(&ctx.db)
        .await
        .expect("succeeds to get the result")
        .expect("exactly one model in DB");

    assert_eq!(
        format!("{cake:?}").trim_start_matches("Cake "),
        format!("{cake2:?}").trim_start_matches("Cake2 ")
    );

    #[derive(DerivePartialModel)]
    #[sea_orm(entity = "cake::Entity")]
    struct Cake3 {
        id: i32,
        name: String,
        #[sea_orm(nested)]
        bakery: Option<bakery::Model>,
    }

    let cake: Cake3 = cake::Entity::find()
        .left_join(bakery::Entity)
        .order_by_asc(cake::Column::Id)
        .into_partial_model()
        .one(&ctx.db)
        .await
        .expect("succeeds to get the result")
        .expect("exactly one model in DB");

    assert_eq!(cake.id, 13);
    assert_eq!(cake.name, "Cheesecake");
    assert_eq!(
        cake.bakery.unwrap(),
        bakery::Model {
            id: 42,
            name: "cool little bakery".to_string(),
            profit_margin: 4.1,
        }
    );

    #[derive(DerivePartialModel)]
    #[sea_orm(entity = "cake::Entity")]
    struct Cake4 {
        id: i32,
        price: Decimal,
        #[sea_orm(from_expr = "cake::COLUMN.price.add(\"0.2\".parse::<Decimal>().unwrap())")]
        price_vat: Decimal,
    }

    let cake: Cake4 = cake::Entity::find()
        .order_by_asc(cake::COLUMN.id)
        .into_partial_model()
        .one(&ctx.db)
        .await
        .expect("succeeds to get the result")
        .expect("exactly one model in DB");

    assert_eq!(cake.id, 13);
    assert_eq!(cake.price, "2".parse().unwrap());
    assert_eq!(cake.price_vat, "2.2".parse().unwrap());

    ctx.delete().await;
}

#[sea_orm_macros::test]
async fn model_as_partial_model() {
    let ctx = TestContext::new("model_as_partial_model").await;
    create_tables(&ctx.db).await.unwrap();

    seed_data::init_1(&ctx, false).await;

    let bakery: bakery::Model = bakery::Entity::find()
        .order_by_asc(bakery::Column::Id)
        .into_partial_model()
        .one(&ctx.db)
        .await
        .expect("succeeds to get the result")
        .expect("exactly one model in DB");

    assert_eq!(
        bakery,
        bakery::Model {
            id: 42,
            name: "cool little bakery".to_string(),
            profit_margin: 4.1,
        }
    );

    ctx.delete().await;
}

#[sea_orm_macros::test]
async fn partial_model_left_join_alias() {
    #[derive(DerivePartialModel)]
    #[sea_orm(entity = "bakery::Entity")]
    struct Factory {
        id: i32,
        #[sea_orm(from_col = "name")]
        plant: String,
    }

    #[derive(DerivePartialModel)]
    #[sea_orm(entity = "cake::Entity")]
    struct CakeFactory {
        id: i32,
        name: String,
        #[sea_orm(nested, alias = "factory")]
        bakery: Option<Factory>,
    }

    // SELECT "cake"."id" AS "id", "cake"."name" AS "name", "factory"."id" AS "bakery_id", "factory"."name" AS "bakery_plant" FROM "cake" LEFT JOIN "bakery" AS "factory" ON "cake"."bakery_id" = "factory"."id" LIMIT 1
    let ctx = TestContext::new("partial_model_left_join_alias").await;
    create_tables(&ctx.db).await.unwrap();

    seed_data::init_1(&ctx, true).await;

    let cake: CakeFactory = cake::Entity::find()
        .join_as(JoinType::LeftJoin, cake::Relation::Bakery.def(), "factory")
        .order_by_asc(cake::Column::Id)
        .into_partial_model()
        .one(&ctx.db)
        .await
        .expect("succeeds to get the result")
        .expect("exactly one model in DB");

    assert_eq!(cake.id, 13);
    assert_eq!(cake.name, "Cheesecake");
    assert!(matches!(cake.bakery, Some(Factory { id: 42, .. })));
    assert_eq!(cake.bakery.unwrap().plant, "cool little bakery");

    ctx.delete().await;
}

#[sea_orm_macros::test]
async fn partial_model_left_join_alias_old() {
    #[derive(DerivePartialModel)]
    #[sea_orm(entity = "bakery::Entity", alias = "factory")]
    struct Factory {
        id: i32,
        #[sea_orm(from_col = "name")]
        plant: String,
    }

    #[derive(DerivePartialModel)]
    #[sea_orm(entity = "cake::Entity")]
    struct CakeFactory {
        id: i32,
        name: String,
        #[sea_orm(nested)]
        bakery: Option<Factory>,
    }

    let ctx = TestContext::new("partial_model_left_join_alias_old").await;
    create_tables(&ctx.db).await.unwrap();

    seed_data::init_1(&ctx, true).await;

    let cake: CakeFactory = cake::Entity::find()
        .join_as(JoinType::LeftJoin, cake::Relation::Bakery.def(), "factory")
        .order_by_asc(cake::Column::Id)
        .into_partial_model()
        .one(&ctx.db)
        .await
        .expect("succeeds to get the result")
        .expect("exactly one model in DB");

    assert_eq!(cake.id, 13);
    assert_eq!(cake.name, "Cheesecake");
    assert!(matches!(cake.bakery, Some(Factory { id: 42, .. })));
    assert_eq!(cake.bakery.unwrap().plant, "cool little bakery");

    ctx.delete().await;
}

#[sea_orm_macros::test]
async fn partial_model_flat() {
    let ctx = TestContext::new("partial_model_flat").await;
    create_tables(&ctx.db).await.unwrap();

    seed_data::init_1(&ctx, true).await;

    let bakery: Bakery = bakery::Entity::find()
        .into_partial_model()
        .one(&ctx.db)
        .await
        .expect("succeeds to get the result")
        .expect("exactly one model in DB");

    assert_eq!(bakery.id, 42);
    assert_eq!(bakery.title, "cool little bakery");

    ctx.delete().await;
}

#[sea_orm_macros::test]
async fn partial_model_nested() {
    // SELECT "bakery"."id" AS "basics_id", "bakery"."name" AS "basics_title", "bakery"."profit_margin" AS "profit" FROM "bakery" LIMIT 1
    let ctx = TestContext::new("partial_model_nested").await;
    create_tables(&ctx.db).await.unwrap();

    seed_data::init_1(&ctx, true).await;

    let bakery: BakeryDetails = bakery::Entity::find()
        .into_partial_model()
        .one(&ctx.db)
        .await
        .expect("succeeds to get the result")
        .expect("exactly one model in DB");

    assert_eq!(bakery.basics.id, 42);
    assert_eq!(bakery.basics.title, "cool little bakery");
    assert_eq!(bakery.profit, 4.1);

    ctx.delete().await;
}

#[sea_orm_macros::test]
async fn partial_model_join_three() {
    let ctx = TestContext::new("partial_model_join_three").await;
    create_tables(&ctx.db).await.unwrap();

    seed_data::init_1(&ctx, true).await;

    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "order::Entity")]
    struct OrderItem {
        id: i32,
        total: Decimal,
        #[sea_orm(nested)]
        customer: Customer,
        #[sea_orm(nested)]
        line: LineItem,
    }

    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "customer::Entity")]
    struct Customer {
        name: String,
    }

    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "lineitem::Entity")]
    struct LineItem {
        price: Decimal,
        quantity: i32,
        #[sea_orm(nested)]
        cake: Cake,
    }

    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "cake::Entity")]
    struct Cake {
        name: String,
    }

    let items: Vec<OrderItem> = order::Entity::find()
        .left_join(customer::Entity)
        .left_join(lineitem::Entity)
        .join(JoinType::LeftJoin, lineitem::Relation::Cake.def())
        .order_by_asc(order::Column::Id)
        .order_by_asc(lineitem::Column::Id)
        .into_partial_model()
        .all(&ctx.db)
        .await
        .unwrap();

    assert_eq!(
        items,
        [
            OrderItem {
                id: 101,
                total: Decimal::from(10),
                customer: Customer {
                    name: "Bob".to_owned()
                },
                line: LineItem {
                    cake: Cake {
                        name: "Cheesecake".to_owned()
                    },
                    price: Decimal::from(2),
                    quantity: 2,
                }
            },
            OrderItem {
                id: 101,
                total: Decimal::from(10),
                customer: Customer {
                    name: "Bob".to_owned()
                },
                line: LineItem {
                    cake: Cake {
                        name: "Chocolate".to_owned()
                    },
                    price: Decimal::from(3),
                    quantity: 2,
                }
            }
        ]
    );

    ctx.delete().await;
}

#[sea_orm_macros::test]
async fn partial_model_join_three_flat() {
    let ctx = TestContext::new("partial_model_join_three_flat").await;
    create_tables(&ctx.db).await.unwrap();

    seed_data::init_1(&ctx, true).await;

    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "order::Entity")]
    struct OrderItem {
        #[sea_orm(nested)]
        order: Order,
        #[sea_orm(nested)]
        customer: Customer,
        #[sea_orm(nested)]
        line: LineItem,
        #[sea_orm(nested)]
        cake: Cake,
    }

    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "order::Entity")]
    struct Order {
        #[sea_orm(from_col = "id")]
        order_id: i32,
        total: Decimal,
    }

    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "customer::Entity")]
    struct Customer {
        name: String,
    }

    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "lineitem::Entity")]
    struct LineItem {
        price: Decimal,
        quantity: i32,
    }

    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "cake::Entity")]
    struct Cake {
        name: String,
    }

    let items: Vec<OrderItem> = order::Entity::find()
        .left_join(customer::Entity)
        .left_join(lineitem::Entity)
        .join(JoinType::LeftJoin, lineitem::Relation::Cake.def())
        .order_by_asc(order::Column::Id)
        .order_by_asc(lineitem::Column::Id)
        .into_partial_model()
        .all(&ctx.db)
        .await
        .unwrap();

    assert_eq!(
        items,
        [
            OrderItem {
                order: Order {
                    order_id: 101,
                    total: Decimal::from(10),
                },
                customer: Customer {
                    name: "Bob".to_owned()
                },
                line: LineItem {
                    price: Decimal::from(2),
                    quantity: 2,
                },
                cake: Cake {
                    name: "Cheesecake".to_owned()
                },
            },
            OrderItem {
                order: Order {
                    order_id: 101,
                    total: Decimal::from(10),
                },
                customer: Customer {
                    name: "Bob".to_owned()
                },
                line: LineItem {
                    price: Decimal::from(3),
                    quantity: 2,
                },
                cake: Cake {
                    name: "Chocolate".to_owned()
                },
            }
        ]
    );

    ctx.delete().await;
}

#[sea_orm_macros::test]
async fn partial_model_join_two_linked() {
    let ctx = TestContext::new("partial_model_join_two_linked").await;
    create_tables(&ctx.db).await.unwrap();

    seed_data::init_1(&ctx, true).await;

    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "order::Entity")]
    struct OrderItem {
        #[sea_orm(nested)]
        order: Order,
        #[sea_orm(nested, alias = "r0")]
        customer: Customer,
        #[sea_orm(nested, alias = "r1")]
        line: LineItem,
    }

    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "order::Entity")]
    struct Order {
        #[sea_orm(from_col = "id")]
        order_id: i32,
        total: Decimal,
    }

    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "customer::Entity")]
    struct Customer {
        name: String,
    }

    #[derive(Debug, DerivePartialModel, PartialEq)]
    #[sea_orm(entity = "lineitem::Entity")]
    struct LineItem {
        price: Decimal,
        quantity: i32,
    }

    let items: Vec<OrderItem> = order::Entity::find()
        .left_join_linked(order::ToCustomer)
        .left_join_linked(order::ToLineitem)
        .order_by_asc(order::Column::Id)
        .order_by_asc(Expr::col(("r1", lineitem::Column::Id)))
        .into_partial_model()
        .all(&ctx.db)
        .await
        .unwrap();

    assert_eq!(
        items,
        [
            OrderItem {
                order: Order {
                    order_id: 101,
                    total: Decimal::from(10),
                },
                customer: Customer {
                    name: "Bob".to_owned()
                },
                line: LineItem {
                    price: Decimal::from(2),
                    quantity: 2,
                },
            },
            OrderItem {
                order: Order {
                    order_id: 101,
                    total: Decimal::from(10),
                },
                customer: Customer {
                    name: "Bob".to_owned()
                },
                line: LineItem {
                    price: Decimal::from(3),
                    quantity: 2,
                },
            }
        ]
    );

    ctx.delete().await;
}

#[sea_orm_macros::test]
async fn partial_model_into_active_model() {
    let mut cake = Cake {
        id: 12,
        name: "Lemon Drizzle".to_owned(),
        bakery: None,
        ignore: Ignore {},
    }
    .into_active_model();
    cake.serial = NotSet;

    assert_eq!(
        cake,
        cake::ActiveModel {
            id: Set(12),
            name: Set("Lemon Drizzle".to_owned()),
            serial: NotSet,
            ..Default::default()
        }
    );

    assert_eq!(
        cake::ActiveModel {
            ..cake.into_active_model()
        },
        cake::ActiveModel {
            id: Set(12),
            name: Set("Lemon Drizzle".to_owned()),
            serial: NotSet,
            ..Default::default()
        }
    );
}

#[derive(Debug, DerivePartialModel)]
#[sea_orm(entity = "bakery::Entity")]
struct WrongBakery {
    id: String,
    #[sea_orm(from_col = "Name")]
    title: String,
}

#[derive(Debug, DerivePartialModel)]
#[sea_orm(entity = "cake::Entity")]
struct WrongCake {
    id: i32,
    name: String,
    #[sea_orm(nested)]
    bakery: Option<WrongBakery>,
}

#[sea_orm_macros::test]
#[ignore = "This currently does not work, as sqlx does not perform type checking when a column is absent.."]
async fn partial_model_optional_field_but_type_error() {
    let ctx = TestContext::new("partial_model_nested").await;
    create_tables(&ctx.db).await.unwrap();

    seed_data::init_1(&ctx, false).await;

    let _: DbErr = cake::Entity::find()
        .left_join(bakery::Entity)
        .into_partial_model::<WrongCake>()
        .one(&ctx.db)
        .await
        .expect_err("should error instead of returning an empty Option");

    ctx.delete().await;
}
