# frozen_string_literal: true

require "cases/helper"
require "cases/migration/helper"

class MigratorTest < ActiveRecord::TestCase
  # Use this class to sense if migrations have gone
  # up or down.
  class Sensor < ActiveRecord::Migration::Current
    attr_reader :went_up, :went_down

    def initialize(name = self.class.name, version = nil)
      super
      @went_up = false
      @went_down = false
    end

    def up; @went_up = true; end
    def down; @went_down = true; end
  end

  def setup
    super
    @pool = ActiveRecord::Base.connection_pool
    @schema_migration = @pool.schema_migration
    @schema_migration.create_table
    @schema_migration.delete_all_versions rescue nil
    @internal_metadata = @pool.internal_metadata
    @verbose_was = ActiveRecord::Migration.verbose
    ActiveRecord::Migration.message_count = 0
    ActiveRecord::Migration.class_eval do
      undef :puts
      def puts(*)
        ActiveRecord::Migration.message_count += 1
      end
    end
  end

  teardown do
    @schema_migration.delete_all_versions rescue nil
    ActiveRecord::Migration.verbose = @verbose_was
    ActiveRecord::Migration.class_eval do
      undef :puts
      def puts(*)
        super
      end
    end
  end

  def test_migrator_with_duplicate_names
    e = assert_raises(ActiveRecord::DuplicateMigrationNameError) do
      list = [ActiveRecord::Migration.new("Chunky"), ActiveRecord::Migration.new("Chunky")]
      ActiveRecord::Migrator.new(:up, list, @schema_migration, @internal_metadata)
    end
    assert_match(/Multiple migrations have the name Chunky/, e.message)
  end

  def test_migrator_with_duplicate_versions
    assert_raises(ActiveRecord::DuplicateMigrationVersionError) do
      list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 1)]
      ActiveRecord::Migrator.new(:up, list, @schema_migration, @internal_metadata)
    end
  end

  def test_migrator_with_missing_version_numbers
    assert_raises(ActiveRecord::UnknownMigrationVersionError) do
      list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
      ActiveRecord::Migrator.new(:up, list, @schema_migration, @internal_metadata, 3).run
    end

    assert_raises(ActiveRecord::UnknownMigrationVersionError) do
      list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
      ActiveRecord::Migrator.new(:up, list, @schema_migration, @internal_metadata, -1).run
    end

    assert_raises(ActiveRecord::UnknownMigrationVersionError) do
      list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
      ActiveRecord::Migrator.new(:up, list, @schema_migration, @internal_metadata, 0).run
    end

    assert_raises(ActiveRecord::UnknownMigrationVersionError) do
      list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
      ActiveRecord::Migrator.new(:up, list, @schema_migration, @internal_metadata, 3).migrate
    end

    assert_raises(ActiveRecord::UnknownMigrationVersionError) do
      list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
      ActiveRecord::Migrator.new(:up, list, @schema_migration, @internal_metadata, -1).migrate
    end
  end

  def test_finds_migrations
    migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid", @schema_migration, @internal_metadata).migrations

    [[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i|
      assert_equal migrations[i].version, pair.first
      assert_equal migrations[i].name, pair.last
    end
  end

  def test_finds_migrations_in_subdirectories
    migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid_with_subdirectories", @schema_migration, @internal_metadata).migrations

    [[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i|
      assert_equal migrations[i].version, pair.first
      assert_equal migrations[i].name, pair.last
    end
  end

  def test_finds_migrations_from_two_directories
    directories = [MIGRATIONS_ROOT + "/valid_with_timestamps", MIGRATIONS_ROOT + "/to_copy_with_timestamps"]
    migrations = ActiveRecord::MigrationContext.new(directories, @schema_migration, @internal_metadata).migrations

    [[20090101010101, "PeopleHaveHobbies"],
     [20090101010202, "PeopleHaveDescriptions"],
     [20100101010101, "ValidWithTimestampsPeopleHaveLastNames"],
     [20100201010101, "ValidWithTimestampsWeNeedReminders"],
     [20100301010101, "ValidWithTimestampsInnocentJointable"]].each_with_index do |pair, i|
       assert_equal pair.first, migrations[i].version
       assert_equal pair.last, migrations[i].name
     end
  end

  def test_finds_migrations_in_numbered_directory
    migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/10_urban", @schema_migration, @internal_metadata).migrations
    assert_equal 9, migrations[0].version
    assert_equal "AddExpressions", migrations[0].name
  end

  def test_relative_migrations
    list = Dir.chdir(MIGRATIONS_ROOT) do
      ActiveRecord::MigrationContext.new("valid", @schema_migration, @internal_metadata).migrations
    end

    migration_proxy = list.find { |item|
      item.name == "ValidPeopleHaveLastNames"
    }
    assert migration_proxy, "should find pending migration"
  end

  def test_finds_pending_migrations
    @schema_migration.create_version("1")
    migration_list = [ActiveRecord::Migration.new("foo", 1), ActiveRecord::Migration.new("bar", 3)]
    migrations = ActiveRecord::Migrator.new(:up, migration_list, @schema_migration, @internal_metadata).pending_migrations

    assert_equal 1, migrations.size
    assert_equal migration_list.last, migrations.first
  end

  def test_migrations_status
    path = MIGRATIONS_ROOT + "/valid"

    @schema_migration.create_version(2)
    @schema_migration.create_version(10)

    assert_equal [
      ["down", "001", "Valid people have last names"],
      ["up",   "002", "We need reminders"],
      ["down", "003", "Innocent jointable"],
      ["up",   "010", "********** NO FILE **********"],
    ], ActiveRecord::MigrationContext.new(path, @schema_migration, @internal_metadata).migrations_status
  end

  def test_migrations_status_order_new_and_old_version
    path = MIGRATIONS_ROOT + "/old_and_new_versions"
    @schema_migration.create_version(230)
    @schema_migration.create_version(231)
    @schema_migration.create_version(20210716122844)
    @schema_migration.create_version(20210716123013)

    assert_equal [
      ["up", "230", "Add people hobby"],
      ["up", "231", "Add people last name"],
      ["up", "20210716122844", "Add people description"],
      ["up", "20210716123013", "Add people number of legs"],
    ], ActiveRecord::MigrationContext.new(path, @schema_migration, @internal_metadata).migrations_status
  end

  def test_migrations_status_order_new_and_old_version_applied_out_of_order
    path = MIGRATIONS_ROOT + "/old_and_new_versions"

    @schema_migration.create_version(230)
    @schema_migration.create_version(231)

    # "Apply" a newer migration and not an older to simulate out-of-order
    # migration application which should not affect ordering in status and is
    # possible if a branch is merged which contains a migration which has an
    # earlier version but is judged to be compatible with existing migrations.
    @schema_migration.create_version(20210716123013)

    assert_equal [
      ["up", "230", "Add people hobby"],
      ["up", "231", "Add people last name"],
      ["down", "20210716122844", "Add people description"],
      ["up", "20210716123013", "Add people number of legs"],
    ], ActiveRecord::MigrationContext.new(path, @schema_migration, @internal_metadata).migrations_status
  end

  def test_migrations_status_in_subdirectories
    path = MIGRATIONS_ROOT + "/valid_with_subdirectories"

    @schema_migration.create_version(2)
    @schema_migration.create_version(10)

    assert_equal [
      ["down", "001", "Valid people have last names"],
      ["up",   "002", "We need reminders"],
      ["down", "003", "Innocent jointable"],
      ["up",   "010", "********** NO FILE **********"],
    ], ActiveRecord::MigrationContext.new(path, @schema_migration, @internal_metadata).migrations_status
  end

  def test_migrations_status_with_schema_define_in_subdirectories
    path = MIGRATIONS_ROOT + "/valid_with_subdirectories"
    prev_paths = ActiveRecord::Migrator.migrations_paths
    ActiveRecord::Migrator.migrations_paths = path

    ActiveRecord::Schema.define(version: 3) do
    end

    assert_equal [
      ["up", "001", "Valid people have last names"],
      ["up", "002", "We need reminders"],
      ["up", "003", "Innocent jointable"],
    ], ActiveRecord::MigrationContext.new(path, @schema_migration, @internal_metadata).migrations_status
  ensure
    ActiveRecord::Migrator.migrations_paths = prev_paths
  end

  def test_migrations_status_from_two_directories
    paths = [MIGRATIONS_ROOT + "/valid_with_timestamps", MIGRATIONS_ROOT + "/to_copy_with_timestamps"]

    @schema_migration.create_version("20100101010101")
    @schema_migration.create_version("20160528010101")

    assert_equal [
      ["down", "20090101010101", "People have hobbies"],
      ["down", "20090101010202", "People have descriptions"],
      ["up",   "20100101010101", "Valid with timestamps people have last names"],
      ["down", "20100201010101", "Valid with timestamps we need reminders"],
      ["down", "20100301010101", "Valid with timestamps innocent jointable"],
      ["up",   "20160528010101", "********** NO FILE **********"],
    ], ActiveRecord::MigrationContext.new(paths, @schema_migration, @internal_metadata).migrations_status
  end

  def test_migrator_interleaved_migrations
    pass_one = [Sensor.new("One", 1)]

    ActiveRecord::Migrator.new(:up, pass_one, @schema_migration, @internal_metadata).migrate
    assert pass_one.first.went_up
    assert_not pass_one.first.went_down

    pass_two = [Sensor.new("One", 1), Sensor.new("Three", 3)]
    ActiveRecord::Migrator.new(:up, pass_two, @schema_migration, @internal_metadata).migrate
    assert_not pass_two[0].went_up
    assert pass_two[1].went_up
    assert pass_two.all? { |x| !x.went_down }

    pass_three = [Sensor.new("One", 1),
                  Sensor.new("Two", 2),
                  Sensor.new("Three", 3)]

    ActiveRecord::Migrator.new(:down, pass_three, @schema_migration, @internal_metadata).migrate
    assert pass_three[0].went_down
    assert_not pass_three[1].went_down
    assert pass_three[2].went_down
  end

  def test_up_calls_up
    migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
    migrator = ActiveRecord::Migrator.new(:up, migrations, @schema_migration, @internal_metadata)
    migrator.migrate
    assert migrations.all?(&:went_up)
    assert migrations.all? { |m| !m.went_down }
    assert_equal 2, migrator.current_version
  end

  def test_down_calls_down
    test_up_calls_up

    migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
    migrator = ActiveRecord::Migrator.new(:down, migrations, @schema_migration, @internal_metadata)
    migrator.migrate
    assert migrations.all? { |m| !m.went_up }
    assert migrations.all?(&:went_down)
    assert_equal 0, migrator.current_version
  end

  def test_current_version
    @schema_migration.create_version("1000")
    schema_migration = ActiveRecord::Base.connection_pool.schema_migration
    internal_metadata = ActiveRecord::Base.connection_pool.internal_metadata
    migrator = ActiveRecord::MigrationContext.new("db/migrate", schema_migration, internal_metadata)
    assert_equal 1000, migrator.current_version
  end

  def test_migrator_one_up
    calls, migrations = sensors(3)

    ActiveRecord::Migrator.new(:up, migrations, @schema_migration, @internal_metadata, 1).migrate
    assert_equal [[:up, 1]], calls
    calls.clear

    ActiveRecord::Migrator.new(:up, migrations, @schema_migration, @internal_metadata, 2).migrate
    assert_equal [[:up, 2]], calls
  end

  def test_migrator_one_down
    calls, migrations = sensors(3)

    ActiveRecord::Migrator.new(:up, migrations, @schema_migration, @internal_metadata).migrate
    assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
    calls.clear

    ActiveRecord::Migrator.new(:down, migrations, @schema_migration, @internal_metadata, 1).migrate

    assert_equal [[:down, 3], [:down, 2]], calls
  end

  def test_migrator_one_up_one_down
    calls, migrations = sensors(3)

    ActiveRecord::Migrator.new(:up, migrations, @schema_migration, @internal_metadata, 1).migrate
    assert_equal [[:up, 1]], calls
    calls.clear

    ActiveRecord::Migrator.new(:down, migrations, @schema_migration, @internal_metadata, 0).migrate
    assert_equal [[:down, 1]], calls
  end

  def test_migrator_double_up
    calls, migrations = sensors(3)
    migrator = ActiveRecord::Migrator.new(:up, migrations, @schema_migration, @internal_metadata, 1)
    assert_equal(0, migrator.current_version)

    migrator.migrate
    assert_equal [[:up, 1]], calls
    calls.clear

    migrator.migrate
    assert_equal [], calls
  end

  def test_migrator_double_down
    calls, migrations = sensors(3)
    migrator = ActiveRecord::Migrator.new(:up, migrations, @schema_migration, @internal_metadata, 1)

    assert_equal 0, migrator.current_version

    migrator.run
    assert_equal [[:up, 1]], calls
    calls.clear

    migrator = ActiveRecord::Migrator.new(:down, migrations, @schema_migration, @internal_metadata, 1)
    migrator.run
    assert_equal [[:down, 1]], calls
    calls.clear

    migrator.run
    assert_equal [], calls

    assert_equal 0, migrator.current_version
  end

  def test_migrator_verbosity
    _, migrations = sensors(3)

    ActiveRecord::Migration.verbose = true
    ActiveRecord::Migrator.new(:up, migrations, @schema_migration, @internal_metadata, 1).migrate
    assert_not_equal 0, ActiveRecord::Migration.message_count

    ActiveRecord::Migration.message_count = 0

    ActiveRecord::Migrator.new(:down, migrations, @schema_migration, @internal_metadata, 0).migrate
    assert_not_equal 0, ActiveRecord::Migration.message_count
  end

  def test_migrator_verbosity_off
    _, migrations = sensors(3)

    ActiveRecord::Migration.verbose = false
    ActiveRecord::Migrator.new(:up, migrations, @schema_migration, @internal_metadata, 1).migrate
    assert_equal 0, ActiveRecord::Migration.message_count
    ActiveRecord::Migrator.new(:down, migrations, @schema_migration, @internal_metadata, 0).migrate
    assert_equal 0, ActiveRecord::Migration.message_count
  end

  def test_target_version_zero_should_run_only_once
    calls, migrations = sensors(3)

    # migrate up to 1
    ActiveRecord::Migrator.new(:up, migrations, @schema_migration, @internal_metadata, 1).migrate
    assert_equal [[:up, 1]], calls
    calls.clear

    # migrate down to 0
    ActiveRecord::Migrator.new(:down, migrations, @schema_migration, @internal_metadata, 0).migrate
    assert_equal [[:down, 1]], calls
    calls.clear

    # migrate down to 0 again
    ActiveRecord::Migrator.new(:down, migrations, @schema_migration, @internal_metadata, 0).migrate
    assert_equal [], calls
  end

  def test_migrator_going_down_due_to_version_target
    schema_migration = ActiveRecord::Base.connection_pool.schema_migration
    calls, migrator = migrator_class(3)
    migrator = migrator.new("valid", schema_migration)

    migrator.up(1)
    assert_equal [[:up, 1]], calls
    calls.clear

    migrator.migrate(0)
    assert_equal [[:down, 1]], calls
    calls.clear

    migrator.migrate
    assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
  end

  def test_migrator_output_when_running_multiple_migrations
    schema_migration = ActiveRecord::Base.connection_pool.schema_migration
    _, migrator = migrator_class(3)
    migrator = migrator.new("valid", schema_migration)

    result = migrator.migrate
    assert_equal(3, result.count)

    # Nothing migrated from duplicate run
    result = migrator.migrate
    assert_equal(0, result.count)

    result = migrator.rollback
    assert_equal(1, result.count)
  end

  def test_migrator_output_when_running_single_migration
    schema_migration = ActiveRecord::Base.connection_pool.schema_migration
    _, migrator = migrator_class(1)
    migrator = migrator.new("valid", schema_migration)

    result = migrator.run(:up, 1)

    assert_equal("1", result)
  end

  def test_migrator_rollback
    schema_migration = ActiveRecord::Base.connection_pool.schema_migration
    _, migrator = migrator_class(3)
    migrator = migrator.new("valid", schema_migration)

    migrator.migrate
    assert_equal(3, migrator.current_version)

    migrator.rollback
    assert_equal(2, migrator.current_version)

    migrator.rollback
    assert_equal(1, migrator.current_version)

    migrator.rollback
    assert_equal(0, migrator.current_version)

    migrator.rollback
    assert_equal(0, migrator.current_version)
  end

  def test_migrator_db_has_no_schema_migrations_table
    schema_migration = ActiveRecord::Base.connection_pool.schema_migration
    _, migrator = migrator_class(3)
    migrator = migrator.new("valid", schema_migration)

    @schema_migration.drop_table
    assert_not_predicate @schema_migration, :table_exists?
    migrator.migrate(1)
    assert_predicate @schema_migration, :table_exists?
  end

  def test_migrator_forward
    schema_migration = ActiveRecord::Base.connection_pool.schema_migration
    _, migrator = migrator_class(3)
    migrator = migrator.new("/valid", schema_migration)
    migrator.migrate(1)
    assert_equal(1, migrator.current_version)

    migrator.forward(2)
    assert_equal(3, migrator.current_version)

    migrator.forward
    assert_equal(3, migrator.current_version)
  end

  def test_only_loads_pending_migrations
    # migrate up to 1
    @schema_migration.create_version("1")

    schema_migration = ActiveRecord::Base.connection_pool.schema_migration
    calls, migrator = migrator_class(3)
    migrator = migrator.new("valid", schema_migration)
    migrator.migrate

    assert_equal [[:up, 2], [:up, 3]], calls
  end

  def test_get_all_versions
    schema_migration = ActiveRecord::Base.connection_pool.schema_migration
    _, migrator = migrator_class(3)
    migrator = migrator.new("valid", schema_migration)

    migrator.migrate
    assert_equal([1, 2, 3], migrator.get_all_versions)

    migrator.rollback
    assert_equal([1, 2], migrator.get_all_versions)

    migrator.rollback
    assert_equal([1], migrator.get_all_versions)

    migrator.rollback
    assert_equal([], migrator.get_all_versions)
  end

  private
    def m(name, version)
      x = Sensor.new name, version
      x.extend(Module.new {
        define_method(:up) { yield(:up, x); super() }
        define_method(:down) { yield(:down, x); super() }
      }) if block_given?
    end

    def sensors(count)
      calls = []
      migrations = count.times.map { |i|
        m(nil, i + 1) { |c, migration|
          calls << [c, migration.version]
        }
      }
      [calls, migrations]
    end

    def migrator_class(count)
      calls, migrations = sensors(count)

      migrator = Class.new(ActiveRecord::MigrationContext) {
        define_method(:migrations) { |*|
          migrations
        }
      }
      [calls, migrator]
    end
end
