#![cfg(feature = "sqlite")]

use std::{
    fs,
    path::{Path, PathBuf},
    time::{SystemTime, UNIX_EPOCH},
};

use quex::{self, FromRow, Pool, Row, SqliteConnectOptions};

use crate::{AlterTableBlueprint, MigrationError, Migrator, SchemaDialect, TableBlueprint};

#[derive(Debug)]
pub struct UserCount {
    value: i64,
}

impl FromRow for UserCount {
    fn from_row(row: &Row) -> quex::Result<Self> {
        Ok(Self {
            value: row.get("value")?,
        })
    }
}

#[derive(Debug)]
struct ForeignKeyRule {
    on_delete: String,
}

impl FromRow for ForeignKeyRule {
    fn from_row(row: &Row) -> quex::Result<Self> {
        Ok(Self {
            on_delete: row.get("on_delete")?,
        })
    }
}

#[derive(Debug)]
struct TableColumn {
    data_type: String,
}

impl FromRow for TableColumn {
    fn from_row(row: &Row) -> quex::Result<Self> {
        Ok(Self {
            data_type: row.get("type")?,
        })
    }
}

#[derive(Debug)]
struct UserEmailValues {
    email: String,
    normalized_email: String,
}

impl FromRow for UserEmailValues {
    fn from_row(row: &Row) -> quex::Result<Self> {
        Ok(Self {
            email: row.get("email")?,
            normalized_email: row.get("normalized_email")?,
        })
    }
}

#[derive(Debug)]
struct PrimaryKeyColumn {
    name: String,
    pk: i64,
}

impl FromRow for PrimaryKeyColumn {
    fn from_row(row: &Row) -> quex::Result<Self> {
        Ok(Self {
            name: row.get("name")?,
            pk: row.get("pk")?,
        })
    }
}

async fn sqlite_pool() -> quex::Result<Pool> {
    quex::Pool::connect(SqliteConnectOptions::new().in_memory())?
        .max_size(1)
        .build()
        .await
}

fn temp_migration_dir(name: &str) -> PathBuf {
    let mut path = std::env::temp_dir();
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    path.push(format!("lift-migration-{name}-{nanos}"));
    fs::create_dir_all(&path).unwrap();
    path
}

fn write_migration(dir: &Path, name: &str, contents: &str) {
    fs::write(dir.join(name), contents).unwrap();
}

#[test]
fn schema_builder_renders_sqlite_and_postgres_sql() {
    let mut table = TableBlueprint::new("users");
    table.id();
    table.string("name");
    table.unique(["name"]);
    table.check("length(name) > 0");
    table.index("users_name_idx", ["name"]);
    table.unique_index("users_name_unique_idx", ["name"]);
    table
        .foreign_id("team_id")
        .constrained()
        .cascade_on_delete()
        .cascade_on_update();
    table.timestamps();

    assert_eq!(
        table.create_sql(SchemaDialect::Sqlite),
        r#"create table "users" ("id" integer primary key autoincrement, "name" varchar(255) not null, "team_id" bigint not null, "created_at" datetime not null default current_timestamp, "updated_at" datetime not null default current_timestamp, foreign key ("team_id") references "teams"("id") on delete cascade on update cascade, unique ("name"), check (length(name) > 0));"#
    );
    assert_eq!(
        table.create_sql(SchemaDialect::Postgres),
        r#"create table "users" ("id" bigserial primary key, "name" varchar(255) not null, "team_id" bigint not null, "created_at" timestamp not null default current_timestamp, "updated_at" timestamp not null default current_timestamp, foreign key ("team_id") references "teams"("id") on delete cascade on update cascade, unique ("name"), check (length(name) > 0));"#
    );
}

#[test]
fn alter_table_builder_renders_drop_columns_for_each_dialect() {
    let mut table = AlterTableBlueprint::new("users");
    table.drop_column("nickname");
    table.drop_timestamps();

    assert_eq!(
        table.sql_statements(SchemaDialect::Sqlite),
        vec![
            r#"alter table "users" drop column "nickname";"#.to_owned(),
            r#"alter table "users" drop column "created_at";"#.to_owned(),
            r#"alter table "users" drop column "updated_at";"#.to_owned(),
        ]
    );
    assert_eq!(
        table.sql_statements(SchemaDialect::Postgres),
        vec![
            r#"alter table "users" drop column "nickname", drop column "created_at", drop column "updated_at";"#.to_owned(),
        ]
    );
}

#[test]
fn alter_table_builder_renders_add_columns() {
    let mut table = AlterTableBlueprint::new("users");
    table.string("slug").nullable().unique();

    assert_eq!(
        table.sql_statements(SchemaDialect::Sqlite),
        vec![r#"alter table "users" add column "slug" varchar(255) unique;"#.to_owned()]
    );
}

#[tokio::test]
async fn sqlite_migrator_runs_lift_files_and_rolls_back() {
    let dir = temp_migration_dir("run-and-rollback");
    write_migration(
        &dir,
        "202604030001_create_users.lift",
        r#"
create users {
  id
  email text unique
  team_id bigint
}

down {
  drop users
}
"#,
    );

    let pool = sqlite_pool().await.unwrap();
    let migrator = Migrator::new(&pool).path(&dir);

    let report = migrator.run().await.unwrap();
    assert_eq!(report.batch, Some(1));
    assert_eq!(report.applied, vec![202604030001]);

    quex::query("insert into users(email, team_id) values(?, ?)")
        .bind("alice@example.com")
        .bind(1_i64)
        .execute(&pool)
        .await
        .unwrap();

    let users = quex::query("select count(*) as value from users")
        .one::<UserCount>(&pool)
        .await
        .unwrap();
    assert_eq!(users.value, 1);

    let rollback = migrator.rollback_last_batch().await.unwrap();
    assert_eq!(rollback.batch, Some(1));
    assert_eq!(rollback.rolled_back, vec![202604030001]);

    fs::remove_dir_all(dir).unwrap();
}

#[tokio::test]
async fn sqlite_migrator_detects_checksum_drift() {
    let dir = temp_migration_dir("checksum");
    let file = dir.join("202604030002_create_users.lift");
    fs::write(
        &file,
        r#"
create users {
  id primary
  email text
}

down {
  drop users
}
"#,
    )
    .unwrap();

    let pool = sqlite_pool().await.unwrap();
    let migrator = Migrator::new(&pool).path(&dir);
    migrator.run().await.unwrap();

    fs::write(
        &file,
        r#"
create users {
  id primary
  email text unique
}

down {
  drop users
}
"#,
    )
    .unwrap();

    let err = migrator.run().await.err().unwrap();
    assert!(matches!(
        err,
        MigrationError::ChecksumMismatch { id: 202604030002 }
    ));

    fs::remove_dir_all(dir).unwrap();
}

#[tokio::test]
async fn sqlite_migrator_runs_lift_alter_and_rename_table() {
    let dir = temp_migration_dir("alter-and-rename");
    write_migration(
        &dir,
        "202604030003_create_and_rename_users.lift",
        r#"
create users {
  id primary
  email text unique
}

alter users {
  add column timezone text default "UTC"
  rename column email to login
  add index (timezone)
}

rename users to accounts

down {
  rename accounts to users

  alter users {
    drop index users_timezone_idx
    rename column login to email
    drop column timezone
  }

  drop users
}
"#,
    );

    let pool = sqlite_pool().await.unwrap();
    let migrator = Migrator::new(&pool).path(&dir);

    migrator.run().await.unwrap();

    let table_count = quex::query(
        "select count(*) as value from sqlite_master where type = 'table' and name = ?",
    )
    .bind("accounts")
    .one::<UserCount>(&pool)
    .await
    .unwrap();
    assert_eq!(table_count.value, 1);

    let column_count =
        quex::query("select count(*) as value from pragma_table_info('accounts') where name = ?")
            .bind("login")
            .one::<UserCount>(&pool)
            .await
            .unwrap();
    assert_eq!(column_count.value, 1);

    let index_count = quex::query(
        "select count(*) as value from sqlite_master where type = 'index' and name = ?",
    )
    .bind("users_timezone_idx")
    .one::<UserCount>(&pool)
    .await
    .unwrap();
    assert_eq!(index_count.value, 1);

    migrator.rollback_last_batch().await.unwrap();

    fs::remove_dir_all(dir).unwrap();
}

#[tokio::test]
async fn sqlite_migrator_executes_enum_statements() {
    let dir = temp_migration_dir("enum-ops");
    write_migration(
        &dir,
        "202604030004_create_status_enum.lift",
        r#"
create enum post_status { draft, published }

create posts {
  id primary
  status post_status
}

alter enum post_status {
  add value archived
}

rename enum post_status to article_status

down {
  drop posts
  drop enum article_status
}
"#,
    );

    let pool = sqlite_pool().await.unwrap();
    let migrator = Migrator::new(&pool).path(&dir);
    migrator.run().await.unwrap();

    let column = quex::query("select type from pragma_table_info('posts') where name = ?")
        .bind("status")
        .one::<TableColumn>(&pool)
        .await
        .unwrap();
    assert_eq!(column.data_type.to_ascii_lowercase(), "post_status");

    fs::remove_dir_all(dir).unwrap();
}

#[tokio::test]
async fn sqlite_migrator_executes_composite_primary_keys() {
    let dir = temp_migration_dir("composite-primary");
    write_migration(
        &dir,
        "202604030005_create_memberships.lift",
        r#"
create memberships {
  user_id uuid
  org_id uuid
  primary (user_id, org_id)
}
"#,
    );

    let pool = sqlite_pool().await.unwrap();
    let migrator = Migrator::new(&pool).path(&dir);
    migrator.run().await.unwrap();

    let primary = quex::query(
        "select name, pk from pragma_table_info('memberships') where pk > 0 order by pk",
    )
    .all::<PrimaryKeyColumn>(&pool)
    .await
    .unwrap();
    assert_eq!(primary.len(), 2);
    assert_eq!(primary[0].name, "user_id");
    assert_eq!(primary[0].pk, 1);
    assert_eq!(primary[1].name, "org_id");
    assert_eq!(primary[1].pk, 2);

    fs::remove_dir_all(dir).unwrap();
}

#[tokio::test]
async fn sqlite_migrator_supports_implicit_remember_token_helper() {
    let dir = temp_migration_dir("remember-token-helper");
    write_migration(
        &dir,
        "202604030006_create_users.lift",
        r#"
create users {
  id primary
  remember_token
}
"#,
    );

    let pool = sqlite_pool().await.unwrap();
    let migrator = Migrator::new(&pool).path(&dir);
    migrator.run().await.unwrap();

    let column_type = quex::query(
        "select count(*) as value from pragma_table_info('users') where name = ? and type = ?",
    )
    .bind("remember_token")
    .bind("varchar(100)")
    .one::<UserCount>(&pool)
    .await
    .unwrap();
    assert_eq!(column_type.value, 1);

    fs::remove_dir_all(dir).unwrap();
}

#[tokio::test]
async fn sqlite_migrator_infers_relation_type_and_auto_indexes() {
    let dir = temp_migration_dir("relation-defaults");
    let pool = sqlite_pool().await.unwrap();

    quex::query("create table countries (code varchar(2) primary key not null)")
        .execute(&pool)
        .await
        .unwrap();

    write_migration(
        &dir,
        "202604030007_create_addresses.lift",
        r#"
create addresses {
  id primary
  country_code ref countries.code
}
"#,
    );

    let migrator = Migrator::new(&pool).path(&dir);
    migrator.run().await.unwrap();

    let column = quex::query("select type from pragma_table_info('addresses') where name = ?")
        .bind("country_code")
        .one::<TableColumn>(&pool)
        .await
        .unwrap();
    assert_eq!(column.data_type.to_ascii_lowercase(), "varchar(2)");

    let index = quex::query(
        "select count(*) as value from sqlite_master where type = 'index' and name = ?",
    )
    .bind("addresses_country_code_idx")
    .one::<UserCount>(&pool)
    .await
    .unwrap();
    assert_eq!(index.value, 1);

    let fk = quex::query("select on_delete from pragma_foreign_key_list('addresses') limit 1")
        .one::<ForeignKeyRule>(&pool)
        .await
        .unwrap();
    assert_eq!(fk.on_delete.to_ascii_lowercase(), "restrict");

    fs::remove_dir_all(dir).unwrap();
}

#[tokio::test]
async fn sqlite_migrator_honors_relation_no_index_override() {
    let dir = temp_migration_dir("relation-no-index");
    let pool = sqlite_pool().await.unwrap();

    quex::query("create table users (id integer primary key not null)")
        .execute(&pool)
        .await
        .unwrap();

    write_migration(
        &dir,
        "202604030008_create_posts.lift",
        r#"
create posts {
  id primary
  author_id ref users no index
}
"#,
    );

    let migrator = Migrator::new(&pool).path(&dir);
    migrator.run().await.unwrap();

    let index = quex::query(
        "select count(*) as value from sqlite_master where type = 'index' and name = ?",
    )
    .bind("posts_author_id_idx")
    .one::<UserCount>(&pool)
    .await
    .unwrap();
    assert_eq!(index.value, 0);

    fs::remove_dir_all(dir).unwrap();
}

#[tokio::test]
async fn sqlite_migrator_supports_relation_add_column_in_alter_table() {
    let dir = temp_migration_dir("relation-alter-add");
    let pool = sqlite_pool().await.unwrap();

    quex::query("create table users (id integer primary key not null)")
        .execute(&pool)
        .await
        .unwrap();

    write_migration(
        &dir,
        "202604030009_create_posts.lift",
        r#"
create posts {
  id primary
}

alter posts {
  add column author_id ref users
}
"#,
    );

    let migrator = Migrator::new(&pool).path(&dir);
    migrator.run().await.unwrap();

    let column = quex::query("select type from pragma_table_info('posts') where name = ?")
        .bind("author_id")
        .one::<TableColumn>(&pool)
        .await
        .unwrap();
    assert_eq!(column.data_type.to_ascii_lowercase(), "integer");

    let fk = quex::query("select on_delete from pragma_foreign_key_list('posts') limit 1")
        .one::<ForeignKeyRule>(&pool)
        .await
        .unwrap();
    assert_eq!(fk.on_delete.to_ascii_lowercase(), "restrict");

    let index = quex::query(
        "select count(*) as value from sqlite_master where type = 'index' and name = ?",
    )
    .bind("posts_author_id_idx")
    .one::<UserCount>(&pool)
    .await
    .unwrap();
    assert_eq!(index.value, 1);

    fs::remove_dir_all(dir).unwrap();
}

#[tokio::test]
async fn sqlite_migrator_runs_forward_only_sql_migration() {
    let dir = temp_migration_dir("sql-forward-only");
    write_migration(
        &dir,
        "202604030009_create_logs.sql",
        "create table logs (id integer primary key, message text not null);",
    );

    let pool = sqlite_pool().await.unwrap();
    let migrator = Migrator::new(&pool).path(&dir);
    let report = migrator.run().await.unwrap();
    assert_eq!(report.applied, vec![202604030009]);

    let table = quex::query(
        "select count(*) as value from sqlite_master where type = 'table' and name = ?",
    )
    .bind("logs")
    .one::<UserCount>(&pool)
    .await
    .unwrap();
    assert_eq!(table.value, 1);

    let rollback = migrator.rollback_last_batch().await.unwrap();
    assert_eq!(rollback.rolled_back, vec![202604030009]);

    let table = quex::query(
        "select count(*) as value from sqlite_master where type = 'table' and name = ?",
    )
    .bind("logs")
    .one::<UserCount>(&pool)
    .await
    .unwrap();
    assert_eq!(table.value, 1);

    fs::remove_dir_all(dir).unwrap();
}

#[tokio::test]
async fn sqlite_migrator_runs_sql_migration_pair_and_rolls_back() {
    let dir = temp_migration_dir("sql-pair");
    write_migration(
        &dir,
        "202604030010_create_tags.sql",
        "create table tags (id integer primary key, name text not null);",
    );
    write_migration(
        &dir,
        "202604030010_create_tags.down.sql",
        "drop table tags;",
    );

    let pool = sqlite_pool().await.unwrap();
    let migrator = Migrator::new(&pool).path(&dir);
    migrator.run().await.unwrap();

    let table = quex::query(
        "select count(*) as value from sqlite_master where type = 'table' and name = ?",
    )
    .bind("tags")
    .one::<UserCount>(&pool)
    .await
    .unwrap();
    assert_eq!(table.value, 1);

    let rollback = migrator.rollback_last_batch().await.unwrap();
    assert_eq!(rollback.rolled_back, vec![202604030010]);

    let table = quex::query(
        "select count(*) as value from sqlite_master where type = 'table' and name = ?",
    )
    .bind("tags")
    .one::<UserCount>(&pool)
    .await
    .unwrap();
    assert_eq!(table.value, 0);

    fs::remove_dir_all(dir).unwrap();
}

#[tokio::test]
async fn sqlite_migrator_rejects_down_sql_without_forward_pair() {
    let dir = temp_migration_dir("sql-down-only");
    write_migration(
        &dir,
        "202604030011_create_audit_logs.down.sql",
        "drop table audit_logs;",
    );

    let pool = sqlite_pool().await.unwrap();
    let migrator = Migrator::new(&pool).path(&dir);
    let err = migrator.run().await.err().unwrap();
    assert!(matches!(err, MigrationError::InvalidMigrationFile { .. }));

    fs::remove_dir_all(dir).unwrap();
}

#[tokio::test]
async fn sqlite_migrator_runs_inline_sql_blocks_in_lift() {
    let dir = temp_migration_dir("inline-sql");
    write_migration(
        &dir,
        "202604030012_inline_sql.lift",
        r#"
sql """
create table logs (
  id integer primary key,
  message text not null
);
"""

down {
  sql """
drop table logs;
"""
}
"#,
    );

    let pool = sqlite_pool().await.unwrap();
    let migrator = Migrator::new(&pool).path(&dir);
    migrator.run().await.unwrap();

    let table = quex::query(
        "select count(*) as value from sqlite_master where type = 'table' and name = ?",
    )
    .bind("logs")
    .one::<UserCount>(&pool)
    .await
    .unwrap();
    assert_eq!(table.value, 1);

    migrator.rollback_last_batch().await.unwrap();

    let table = quex::query(
        "select count(*) as value from sqlite_master where type = 'table' and name = ?",
    )
    .bind("logs")
    .one::<UserCount>(&pool)
    .await
    .unwrap();
    assert_eq!(table.value, 0);

    fs::remove_dir_all(dir).unwrap();
}

#[tokio::test]
async fn sqlite_migrator_runs_backfill_blocks_in_lift() {
    let dir = temp_migration_dir("backfill");
    write_migration(
        &dir,
        "202604030013_backfill_users.lift",
        r#"
create users {
  id primary
  email text
}

sql """
insert into users (email) values ('Alice@Example.com');
"""

alter users {
  add column normalized_email text nullable
}

backfill {
  update users
  set normalized_email = lower(email)
  where normalized_email is null;
}

down {
  backfill {
    update users
    set normalized_email = null;
  }

  alter users {
    drop column normalized_email
  }

  drop users
}
"#,
    );

    let pool = sqlite_pool().await.unwrap();
    let migrator = Migrator::new(&pool).path(&dir);
    migrator.run().await.unwrap();

    let user = quex::query("select email, normalized_email from users limit 1")
        .one::<UserEmailValues>(&pool)
        .await
        .unwrap();
    assert_eq!(user.email, "Alice@Example.com");
    assert_eq!(user.normalized_email, "alice@example.com");

    migrator.rollback_last_batch().await.unwrap();

    let table = quex::query(
        "select count(*) as value from sqlite_master where type = 'table' and name = ?",
    )
    .bind("users")
    .one::<UserCount>(&pool)
    .await
    .unwrap();
    assert_eq!(table.value, 0);

    fs::remove_dir_all(dir).unwrap();
}
