defmodule Ecto.Integration.MigrationTest do
  use ExUnit.Case, async: true

  alias Ecto.Integration.{TestRepo, PoolRepo}

  defmodule CreateMigration do
    use Ecto.Migration

    @table table(:create_table_migration)
    @index index(:create_table_migration, [:value], unique: true)

    def up do
      create @table do
        add :value, :integer
      end
      create @index
    end

    def down do
      drop @index
      drop @table
    end
  end

  defmodule AddColumnMigration do
    use Ecto.Migration

    def up do
      create table(:add_col_migration) do
        add :value, :integer
      end

      alter table(:add_col_migration) do
        add :to_be_added, :integer
      end

      execute "INSERT INTO add_col_migration (value, to_be_added) VALUES (1, 2)"
    end

    def down do
      drop table(:add_col_migration)
    end
  end

  defmodule AlterColumnMigration do
    use Ecto.Migration

    def up do
      create table(:alter_col_migration) do
        add :from_null_to_not_null, :integer
        add :from_not_null_to_null, :integer, null: false

        add :from_default_to_no_default, :integer, default: 0
        add :from_no_default_to_default, :integer
      end

      alter table(:alter_col_migration) do
        modify :from_null_to_not_null, :string, null: false
        modify :from_not_null_to_null, :string, null: true

        modify :from_default_to_no_default, :integer, default: nil
        modify :from_no_default_to_default, :integer, default: 0
      end

      execute "INSERT INTO alter_col_migration (from_null_to_not_null) VALUES ('foo')"
    end

    def down do
      drop table(:alter_col_migration)
    end
  end

  defmodule AlterColumnFromMigration do
    use Ecto.Migration

    def change do
      create table(:modify_from_products) do
        add :value, :integer
        add :nullable, :integer, null: false
      end

      if direction() == :up do
        flush()
        PoolRepo.insert_all "modify_from_products", [[value: 1, nullable: 1]]
      end

      alter table(:modify_from_products) do
        modify :value, :bigint, from: :integer
        modify :nullable, :bigint, null: true, from: {:integer, null: false}
      end
    end
  end

  defmodule AlterColumnFromPkeyMigration do
    use Ecto.Migration

    def change do
      create table(:modify_from_authors, primary_key: false) do
        add :id, :integer, primary_key: true
      end
      create table(:modify_from_posts) do
        add :author_id, references(:modify_from_authors, type: :integer)
      end

      if direction() == :up do
        flush()
        PoolRepo.insert_all "modify_from_authors", [[id: 1]]
        PoolRepo.insert_all "modify_from_posts", [[author_id: 1]]
      end

      alter table(:modify_from_posts) do
        # remove the constraints modify_from_posts_author_id_fkey
        modify :author_id, :integer, from: references(:modify_from_authors, type: :integer)
      end
      alter table(:modify_from_authors) do
        modify :id, :bigint, from: :integer
      end
      alter table(:modify_from_posts) do
        # add the constraints modify_from_posts_author_id_fkey
        modify :author_id, references(:modify_from_authors, type: :bigint), from: :integer
      end
    end
  end

  defmodule AlterForeignKeyOnDeleteMigration do
    use Ecto.Migration

    def up do
      create table(:alter_fk_users)

      create table(:alter_fk_posts) do
        add :alter_fk_user_id, :id
      end

      alter table(:alter_fk_posts) do
        modify :alter_fk_user_id, references(:alter_fk_users, on_delete: :nilify_all)
      end
    end

    def down do
      drop table(:alter_fk_posts)
      drop table(:alter_fk_users)
    end
  end

  defmodule AlterForeignKeyOnUpdateMigration do
    use Ecto.Migration

    def up do
      create table(:alter_fk_users)

      create table(:alter_fk_posts) do
        add :alter_fk_user_id, :id
      end

      alter table(:alter_fk_posts) do
        modify :alter_fk_user_id, references(:alter_fk_users, on_update: :update_all)
      end
    end

    def down do
      drop table(:alter_fk_posts)
      drop table(:alter_fk_users)
    end
  end

  defmodule DropColumnMigration do
    use Ecto.Migration

    def up do
      create table(:drop_col_migration) do
        add :value, :integer
        add :to_be_removed, :integer
      end

      execute "INSERT INTO drop_col_migration (value, to_be_removed) VALUES (1, 2)"

      alter table(:drop_col_migration) do
        remove :to_be_removed
      end
    end

    def down do
      drop table(:drop_col_migration)
    end
  end

  defmodule RenameColumnMigration do
    use Ecto.Migration

    def up do
      create table(:rename_col_migration) do
        add :to_be_renamed, :integer
      end

      rename table(:rename_col_migration), :to_be_renamed, to: :was_renamed

      execute "INSERT INTO rename_col_migration (was_renamed) VALUES (1)"
    end

    def down do
      drop table(:rename_col_migration)
    end
  end

  defmodule OnDeleteMigration do
    use Ecto.Migration

    def up do
      create table(:parent1)
      create table(:parent2)

      create table(:ref_migration) do
        add :parent1, references(:parent1, on_delete: :nilify_all)
      end

      alter table(:ref_migration) do
        add :parent2, references(:parent2, on_delete: :delete_all)
      end
    end

    def down do
      drop table(:ref_migration)
      drop table(:parent1)
      drop table(:parent2)
    end
  end

  defmodule OnDeleteNilifyColumnsMigration do
    use Ecto.Migration

    def up do
      create table(:parent) do
        add :col1, :integer
        add :col2, :integer
      end

      create unique_index(:parent, [:id, :col1, :col2])

      create table(:ref) do
        add :col1, :integer
        add :col2, :integer
        add :parent_id,
            references(:parent,
              with: [col1: :col1, col2: :col2],
              on_delete: {:nilify, [:parent_id, :col2]}
            )
      end
    end

    def down do
      drop table(:ref)
      drop table(:parent)
    end
  end

  defmodule CompositeForeignKeyMigration do
    use Ecto.Migration

    def change do
      create table(:composite_parent) do
        add :key_id, :integer
      end

      create unique_index(:composite_parent, [:id, :key_id])

      create table(:composite_child) do
        add :parent_key_id, :integer
        add :parent_id, references(:composite_parent, with: [parent_key_id: :key_id])
      end
    end
  end

  defmodule RenameIndexMigration do
    use Ecto.Migration

    def change do
      create table(:composite_parent) do
        add :key_id, :integer
      end

      create unique_index(:composite_parent, [:id, :key_id], name: "old_index_name")

      rename index(:composite_parent, [:id, :key_id], name: "old_index_name"), to: "new_index_name"
    end
  end

  defmodule ReferencesRollbackMigration do
    use Ecto.Migration

    def change do
      create table(:parent) do
        add :name, :string
      end

      create table(:child) do
        add :parent_id, references(:parent)
      end
    end
  end

  defmodule RenameMigration do
    use Ecto.Migration

    @table_current table(:posts_migration)
    @table_new table(:new_posts_migration)

    def up do
      create @table_current
      rename @table_current, to: @table_new
    end

    def down do
      drop @table_new
    end
  end

  defmodule PrefixMigration do
    use Ecto.Migration

    @prefix "ecto_prefix_test"

    def up do
      execute TestRepo.create_prefix(@prefix)
      create table(:first, prefix: @prefix)
      create table(:second, prefix: @prefix) do
        add :first_id, references(:first)
      end
    end

    def down do
      drop table(:second, prefix: @prefix)
      drop table(:first, prefix: @prefix)
      execute TestRepo.drop_prefix(@prefix)
    end
  end

  defmodule NoSQLMigration do
    use Ecto.Migration

    def up do
      create table(:collection, options: [capped: true])
      execute create: "collection"
    end
  end

  defmodule Parent do
    use Ecto.Schema

    schema "parent" do
    end
  end

  defmodule NoErrorTableMigration do
    use Ecto.Migration

    def change do
      create_if_not_exists table(:existing) do
        add :name, :string
      end

      create_if_not_exists table(:existing) do
        add :name, :string
      end

      create_if_not_exists table(:existing)

      drop_if_exists table(:existing)
      drop_if_exists table(:existing)
    end
  end

  defmodule NoErrorIndexMigration do
    use Ecto.Migration

    def change do
      create_if_not_exists index(:posts, [:title])
      create_if_not_exists index(:posts, [:title])
      drop_if_exists index(:posts, [:title])
      drop_if_exists index(:posts, [:title])
    end
  end

  defmodule InferredDropIndexMigration do
    use Ecto.Migration

    def change do
      create index(:posts, [:title])
    end
  end

  defmodule AlterPrimaryKeyMigration do
    use Ecto.Migration

    def change do
      create table(:no_pk, primary_key: false) do
        add :dummy, :string
      end
      alter table(:no_pk) do
        add :id, :serial, primary_key: true
      end
    end
  end


  defmodule AddColumnIfNotExistsMigration do
    use Ecto.Migration

    def up do
      create table(:add_col_if_not_exists_migration)

      alter table(:add_col_if_not_exists_migration) do
        add_if_not_exists :value, :integer
        add_if_not_exists :to_be_added, :integer
      end

      execute "INSERT INTO add_col_if_not_exists_migration (value, to_be_added) VALUES (1, 2)"
    end

    def down do
      drop table(:add_col_if_not_exists_migration)
    end
  end

  defmodule DropColumnIfExistsMigration do
    use Ecto.Migration

    def up do
      create table(:drop_col_if_exists_migration) do
        add :value, :integer
        add :to_be_removed, :integer
      end

      execute "INSERT INTO drop_col_if_exists_migration (value, to_be_removed) VALUES (1, 2)"

      alter table(:drop_col_if_exists_migration) do
        remove_if_exists :to_be_removed, :integer
      end
    end

    def down do
      drop table(:drop_col_if_exists_migration)
    end
  end

  defmodule NoErrorOnConditionalColumnMigration do
    use Ecto.Migration

    def up do
      create table(:no_error_on_conditional_column_migration)

      alter table(:no_error_on_conditional_column_migration) do
        add_if_not_exists  :value, :integer
        add_if_not_exists  :value, :integer

        remove_if_exists :value, :integer
        remove_if_exists :value, :integer
      end
    end

    def down do
      drop table(:no_error_on_conditional_column_migration)
    end
  end

  import Ecto.Query, only: [from: 2]
  import Ecto.Migrator, only: [up: 4, down: 4]

  # Avoid migration out of order warnings
  @moduletag :capture_log
  @base_migration 1_000_000

  setup do
    {:ok, migration_number: System.unique_integer([:positive]) + @base_migration}
  end

  test "create and drop table and indexes", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, CreateMigration, log: false)
    assert :ok == down(PoolRepo, num, CreateMigration, log: false)
  end

  test "correctly infers how to drop index", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, InferredDropIndexMigration, log: false)
    assert :ok == down(PoolRepo, num, InferredDropIndexMigration, log: false)
  end

  test "supports on delete", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, OnDeleteMigration, log: false)

    parent1 = PoolRepo.insert! Ecto.put_meta(%Parent{}, source: "parent1")
    parent2 = PoolRepo.insert! Ecto.put_meta(%Parent{}, source: "parent2")

    writer = "INSERT INTO ref_migration (parent1, parent2) VALUES (#{parent1.id}, #{parent2.id})"
    PoolRepo.query!(writer)

    reader = from r in "ref_migration", select: {r.parent1, r.parent2}
    assert PoolRepo.all(reader) == [{parent1.id, parent2.id}]

    PoolRepo.delete!(parent1)
    assert PoolRepo.all(reader) == [{nil, parent2.id}]

    PoolRepo.delete!(parent2)
    assert PoolRepo.all(reader) == []

    assert :ok == down(PoolRepo, num, OnDeleteMigration, log: false)
  end

  test "composite foreign keys", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, CompositeForeignKeyMigration, log: false)

    PoolRepo.insert_all("composite_parent", [[key_id: 2]])
    assert [id] = PoolRepo.all(from p in "composite_parent", select: p.id)

    catch_error(PoolRepo.insert_all("composite_child", [[parent_id: id, parent_key_id: 1]]))
    assert {1, nil} = PoolRepo.insert_all("composite_child", [[parent_id: id, parent_key_id: 2]])

    assert :ok == down(PoolRepo, num, CompositeForeignKeyMigration, log: false)
  end

  test "rename index", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, RenameIndexMigration, log: false)
    assert :ok == down(PoolRepo, num, RenameIndexMigration, log: false)
  end

  test "rolls back references in change/1", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, ReferencesRollbackMigration, log: false)
    assert :ok == down(PoolRepo, num, ReferencesRollbackMigration, log: false)
  end

  test "create table if not exists and drop table if exists does not raise on failure", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, NoErrorTableMigration, log: false)
  end

  @tag :create_index_if_not_exists
  test "create index if not exists and drop index if exists does not raise on failure", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, NoErrorIndexMigration, log: false)
  end

  test "raises on NoSQL migrations", %{migration_number: num} do
    assert_raise ArgumentError, ~r"does not support keyword lists in :options", fn ->
      up(PoolRepo, num, NoSQLMigration, log: false)
    end
  end

  @tag :add_column
  test "add column", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, AddColumnMigration, log: false)
    assert [2] == PoolRepo.all from p in "add_col_migration", select: p.to_be_added
    :ok = down(PoolRepo, num, AddColumnMigration, log: false)
  end

  @tag :modify_column
  test "modify column", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, AlterColumnMigration, log: false)

    assert ["foo"] ==
           PoolRepo.all from p in "alter_col_migration", select: p.from_null_to_not_null
    assert [nil] ==
           PoolRepo.all from p in "alter_col_migration", select: p.from_not_null_to_null
    assert [nil] ==
           PoolRepo.all from p in "alter_col_migration", select: p.from_default_to_no_default
    assert [0] ==
           PoolRepo.all from p in "alter_col_migration", select: p.from_no_default_to_default

    query = "INSERT INTO `alter_col_migration` (\"from_not_null_to_null\") VALUES ('foo')"
    assert catch_error(PoolRepo.query!(query))

    :ok = down(PoolRepo, num, AlterColumnMigration, log: false)
  end

  @tag :modify_column
  test "modify column with from", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, AlterColumnFromMigration, log: false)

    assert [1] ==
           PoolRepo.all from p in "modify_from_products", select: p.value

    :ok = down(PoolRepo, num, AlterColumnFromMigration, log: false)
  end

  @tag :alter_primary_key
  test "modify column with from and pkey", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, AlterColumnFromPkeyMigration, log: false)

    assert [1] ==
           PoolRepo.all from p in "modify_from_posts", select: p.author_id

    :ok = down(PoolRepo, num, AlterColumnFromPkeyMigration, log: false)
  end

  @tag :alter_foreign_key
  test "modify foreign key's on_delete constraint", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, AlterForeignKeyOnDeleteMigration, log: false)

    PoolRepo.insert_all("alter_fk_users", [[]])
    assert [id] = PoolRepo.all from p in "alter_fk_users", select: p.id

    PoolRepo.insert_all("alter_fk_posts", [[alter_fk_user_id: id]])
    PoolRepo.delete_all("alter_fk_users")
    assert [nil] == PoolRepo.all from p in "alter_fk_posts", select: p.alter_fk_user_id

    :ok = down(PoolRepo, num, AlterForeignKeyOnDeleteMigration, log: false)
  end

  @tag :assigns_id_type
  test "modify foreign key's on_update constraint", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, AlterForeignKeyOnUpdateMigration, log: false)

    PoolRepo.insert_all("alter_fk_users", [[]])
    assert [id] = PoolRepo.all from p in "alter_fk_users", select: p.id

    PoolRepo.insert_all("alter_fk_posts", [[alter_fk_user_id: id]])
    PoolRepo.update_all("alter_fk_users", set: [id: 12345])
    assert [12345] == PoolRepo.all from p in "alter_fk_posts", select: p.alter_fk_user_id

    PoolRepo.delete_all("alter_fk_posts")
    :ok = down(PoolRepo, num, AlterForeignKeyOnUpdateMigration, log: false)
  end

  @tag :remove_column
  test "remove column", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, DropColumnMigration, log: false)
    assert catch_error(PoolRepo.all from p in "drop_col_migration", select: p.to_be_removed)
    :ok = down(PoolRepo, num, DropColumnMigration, log: false)
  end

  @tag :rename_column
  test "rename column", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, RenameColumnMigration, log: false)
    assert [1] == PoolRepo.all from p in "rename_col_migration", select: p.was_renamed
    :ok = down(PoolRepo, num, RenameColumnMigration, log: false)
  end

  @tag :rename_table
  test "rename table", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, RenameMigration, log: false)
    assert :ok == down(PoolRepo, num, RenameMigration, log: false)
  end

  @tag :prefix
  test "prefix", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, PrefixMigration, log: false)
    assert :ok == down(PoolRepo, num, PrefixMigration, log: false)
  end

  @tag :alter_primary_key
  test "alter primary key", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, AlterPrimaryKeyMigration, log: false)
    assert :ok == down(PoolRepo, num, AlterPrimaryKeyMigration, log: false)
  end

  @tag :add_column_if_not_exists
  @tag :remove_column_if_exists
  test "add if not exists and remove if exists does not raise on failure", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, NoErrorOnConditionalColumnMigration, log: false)
    assert :ok == down(PoolRepo, num, NoErrorOnConditionalColumnMigration, log: false)
  end

  @tag :add_column_if_not_exists
  test "add column if not exists", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, AddColumnIfNotExistsMigration, log: false)
    assert [2] == PoolRepo.all from p in "add_col_if_not_exists_migration", select: p.to_be_added
    :ok = down(PoolRepo, num, AddColumnIfNotExistsMigration, log: false)
  end

  @tag :remove_column_if_exists
  test "remove column when exists", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, DropColumnIfExistsMigration, log: false)
    assert catch_error(PoolRepo.all from p in "drop_col_if_exists_migration", select: p.to_be_removed)
    :ok = down(PoolRepo, num, DropColumnIfExistsMigration, log: false)
  end

  @tag :on_delete_nilify_column_list
  test "nilify list of columns on_delete constraint", %{migration_number: num} do
    assert :ok == up(PoolRepo, num, OnDeleteNilifyColumnsMigration, log: false)

    PoolRepo.insert_all("parent", [%{col1: 1, col2: 2}])
    assert [{id, col1, col2}] = PoolRepo.all from p in "parent", select: {p.id, p.col1, p.col2}

    PoolRepo.insert_all("ref", [[parent_id: id, col1: col1, col2: col2]])
    PoolRepo.delete_all("parent")
    assert [{nil, col1, nil}] == PoolRepo.all from r in "ref", select: {r.parent_id, r.col1, r.col2}

    :ok = down(PoolRepo, num, OnDeleteNilifyColumnsMigration, log: false)
  end
end
