use std::env;

use chrono::{Duration, TimeZone, Utc};
use num_traits::Zero;
use serde_json::Value as JsonValue;
use shopless_database::addresses::{self, Country, NewAddress};
use shopless_database::invoices::{self, NewInvoice};
use shopless_database::line_item_options::{self, NewLineItemOption};
use shopless_database::line_items::{self, NewLineItem};
use shopless_database::orders::{self, NewOrder, OrderState};
use shopless_database::Cents;
use sqlx::{Connection, PgConnection};
use uuid::Uuid;

async fn setup() -> PgConnection {
    let database_url = env::var("DATABASE_URL")
        .unwrap_or_else(|_| "postgresql://shopless@localhost/shopless".to_string());
    let test_database_url = env::var("TEST_DATABASE_URL")
        .unwrap_or_else(|_| "postgresql://shopless@localhost/shopless_test".to_string());
    shopless_migration::reset_test_db(&database_url)
        .await
        .unwrap();
    PgConnection::connect(&test_database_url).await.unwrap()
}

#[tokio::test]
async fn database_tests() {
    let mut conn = setup().await;

    // Address

    let new_address = NewAddress {
        recipient: "Name",
        line1: "Street 1",
        line2: "Floor 2",
        postal_code: "12345",
        city: "The City",
        province_code: Some("TP"),
        province_name: Some("The Province"),
        country_code: Country::DE,
        country_name: "Germany",
        phone: Some("0123456789"),
    };
    let shipping_address = addresses::insert(&new_address, &mut conn).await.unwrap();
    {
        // deconstruct to not miss new properties in the future
        let NewAddress {
            recipient,
            line1,
            line2,
            postal_code,
            city,
            province_code,
            province_name,
            country_code,
            country_name,
            phone,
        } = new_address;
        assert_eq!(shipping_address.recipient, recipient);
        assert_eq!(shipping_address.line1, line1);
        assert_eq!(shipping_address.line2, line2);
        assert_eq!(shipping_address.postal_code, postal_code);
        assert_eq!(shipping_address.city, city);
        assert_eq!(shipping_address.province_code.as_deref(), province_code);
        assert_eq!(shipping_address.province_name.as_deref(), province_name);
        assert_eq!(shipping_address.country_code, country_code);
        assert_eq!(shipping_address.country_name, country_name);
        assert_eq!(shipping_address.phone.as_deref(), phone);
    }
    assert_eq!(
        addresses::fetch(shipping_address.id, &mut conn)
            .await
            .unwrap()
            .as_ref(),
        Some(&shipping_address)
    );
    let invoice_address = addresses::insert(&new_address, &mut conn).await.unwrap();

    // Order

    let new_order = NewOrder {
        uuid: Uuid::new_v4(),
        origin: "http://localhost",
        locale: "de",
        state: OrderState::Created,
        custom_status: "new",
        date: Utc.ymd(2020, 7, 11).and_hms(17, 3, 42),
        payment_method: "cash",
        payment_id: "1234",
        payment_meta: &JsonValue::from(true),
        currency: "EUR",
        shipping_method: "mail",
        shipping_method_name: "Mail",
        coupon: None,
        shipping: Cents::from(500),
        tax: Cents::from(239),
        discount: Cents::zero(),
        total: Cents::from(1500),
        email: "foo@bar.foobar",
        token: "1hg234i12hk3whj1k2j",
        invoice_address_id: invoice_address.id,
        shipping_address_id: shipping_address.id,
    };
    let order = orders::insert(&new_order, &mut conn).await.unwrap();
    {
        // deconstruct to not miss new properties in the future
        let NewOrder {
            uuid,
            origin,
            locale,
            state,
            custom_status,
            date,
            payment_method,
            payment_id,
            payment_meta,
            currency,
            shipping_method,
            shipping_method_name,
            coupon,
            shipping,
            tax,
            discount,
            total,
            email,
            token,
            invoice_address_id,
            shipping_address_id,
        } = new_order;
        assert_eq!(order.uuid, uuid);
        assert_eq!(order.origin, origin);
        assert_eq!(order.locale, locale);
        assert_eq!(order.state, state);
        assert_eq!(order.custom_status, custom_status);
        assert_eq!(order.date, date);
        assert_eq!(order.payment_method, payment_method);
        assert_eq!(order.payment_id, payment_id);
        assert_eq!(&order.payment_meta, payment_meta);
        assert_eq!(order.currency, currency);
        assert_eq!(order.shipping_method, shipping_method);
        assert_eq!(order.shipping_method_name, shipping_method_name);
        assert_eq!(order.coupon.as_deref(), coupon);
        assert_eq!(order.shipping, shipping);
        assert_eq!(order.tax, tax);
        assert_eq!(order.discount, discount);
        assert_eq!(order.total, total);
        assert_eq!(order.email, email);
        assert_eq!(order.token, token);
        assert_eq!(order.invoice_address_id, invoice_address_id);
        assert_eq!(order.shipping_address_id, shipping_address_id);
    }
    assert_eq!(
        orders::fetch(order.id, &mut conn).await.unwrap().as_ref(),
        Some(&order)
    );

    // Line Item

    let new_line_item = NewLineItem {
        order_id: order.id,
        sku: "test",
        url: "http://localhost/test",
        name: "Test",
        quantity: 2,
        price: Cents::from(1000),
        taxrate: 19,
        total: Cents::from(2000),
    };
    let line_item = line_items::insert(&new_line_item, &mut conn).await.unwrap();
    {
        // deconstruct to not miss new properties in the future
        let NewLineItem {
            order_id,
            sku,
            url,
            name,
            quantity,
            price,
            taxrate,
            total,
        } = new_line_item;
        assert_eq!(line_item.order_id, order_id);
        assert_eq!(line_item.sku, sku);
        assert_eq!(line_item.url, url);
        assert_eq!(line_item.name, name);
        assert_eq!(line_item.quantity, quantity);
        assert_eq!(line_item.price, price);
        assert_eq!(line_item.taxrate, taxrate);
        assert_eq!(line_item.total, total);
    }
    assert_eq!(
        line_items::fetch(line_item.id, &mut conn)
            .await
            .unwrap()
            .as_ref(),
        Some(&line_item)
    );

    // Line Item Option

    let new_line_item_option = NewLineItemOption {
        line_item_id: line_item.id,
        sku: "length",
        name: "Length",
        value: JsonValue::from(42),
        price: Cents::from(200),
    };
    let line_item_option = line_item_options::insert(&new_line_item_option, &mut conn)
        .await
        .unwrap();
    {
        // deconstruct to not miss new properties in the future
        let NewLineItemOption {
            line_item_id,
            sku,
            name,
            value,
            price,
        } = new_line_item_option;
        assert_eq!(line_item_option.line_item_id, line_item_id);
        assert_eq!(line_item_option.sku, sku);
        assert_eq!(line_item_option.name, name);
        assert_eq!(line_item_option.value, value);
        assert_eq!(line_item_option.price, price);
    }
    assert_eq!(
        line_item_options::fetch(line_item_option.id, &mut conn)
            .await
            .unwrap()
            .as_ref(),
        Some(&line_item_option)
    );

    // Invoice

    let shipment = JsonValue::from("tracking-number: 123124124124");
    let new_invoice = NewInvoice {
        order_id: order.id,
        invoice_date: Utc.ymd(2020, 7, 11).naive_utc(),
        payment_method_name: "Cash",
        payment_transaction_id: "41263567781892312",
        payment_instructions: Some("no further steps necessary"),
        shipment: Some(&shipment),
    };
    let invoice = invoices::insert(&new_invoice, &mut conn).await.unwrap();
    {
        // deconstruct to not miss new properties in the future
        let NewInvoice {
            order_id,
            invoice_date,
            payment_method_name,
            payment_transaction_id,
            payment_instructions,
            shipment,
        } = new_invoice;
        assert_eq!(invoice.order_id, order_id);
        assert_eq!(invoice.invoice_date, invoice_date);
        assert_eq!(invoice.payment_method_name, payment_method_name);
        assert_eq!(invoice.payment_transaction_id, payment_transaction_id);
        assert_eq!(
            invoice.payment_instructions.as_deref(),
            payment_instructions
        );
        assert_eq!(invoice.shipment.as_ref(), shipment);
    }
    assert_eq!(
        invoices::fetch(invoice.invoice_id, &mut conn)
            .await
            .unwrap()
            .as_ref(),
        Some(&invoice)
    );

    // test payment_method/_id conflict
    match orders::insert(&new_order, &mut conn).await {
        Err(sqlx::Error::RowNotFound) => {}
        _ => panic!("expected sqlx::Error::RowNotFound"),
    }

    // test updated_at when updating order
    sqlx::query("select pg_sleep(0.1);")
        .execute(&mut conn)
        .await
        .unwrap();
    sqlx::query("UPDATE orders SET custom_status = 'payment-received' WHERE id = $1")
        .bind(order.id)
        .execute(&mut conn)
        .await
        .unwrap();
    let order = orders::fetch(order.id, &mut conn).await.unwrap().unwrap();
    let delta = Utc::now() - order.updated_at;
    if delta > Duration::milliseconds(10) {
        panic!(
            "expected updated_at to get updated when updating order (delta: {}ms)",
            delta.num_milliseconds()
        );
    }

    // test updated_at when updating line item
    sqlx::query("select pg_sleep(0.1);")
        .execute(&mut conn)
        .await
        .unwrap();
    sqlx::query("UPDATE line_items SET sku = 'foobar' WHERE id = $1")
        .bind(line_item.id)
        .execute(&mut conn)
        .await
        .unwrap();
    let order = orders::fetch(order.id, &mut conn).await.unwrap().unwrap();
    let delta = Utc::now() - order.updated_at;
    if delta > Duration::milliseconds(10) {
        panic!(
            "expected updated_at to get updated when updating line item (delta: {}ms)",
            delta.num_milliseconds()
        );
    }

    // test updated_at when updating line item option
    sqlx::query("select pg_sleep(0.1);")
        .execute(&mut conn)
        .await
        .unwrap();
    sqlx::query("UPDATE line_item_options SET sku = 'foobar' WHERE id = $1")
        .bind(line_item_option.id)
        .execute(&mut conn)
        .await
        .unwrap();
    let order = orders::fetch(order.id, &mut conn).await.unwrap().unwrap();
    let delta = Utc::now() - order.updated_at;
    if delta > Duration::milliseconds(10) {
        panic!(
            "expected updated_at to get updated when updating line item option (delta: {}ms)",
            delta.num_milliseconds()
        );
    }

    // test updated_at when updating address
    sqlx::query("select pg_sleep(0.1);")
        .execute(&mut conn)
        .await
        .unwrap();
    sqlx::query("UPDATE addresses SET line2 = 'foobar' WHERE id = $1")
        .bind(shipping_address.id)
        .execute(&mut conn)
        .await
        .unwrap();
    let order = orders::fetch(order.id, &mut conn).await.unwrap().unwrap();
    let delta = Utc::now() - order.updated_at;
    if delta > Duration::milliseconds(10) {
        panic!(
            "expected updated_at to get updated when updating line item option (delta: {}ms)",
            delta.num_milliseconds()
        );
    }
}
