require 'spec_helper'

describe 'Migration' do

  describe 'repository_execute' do
    before(:each) do
      class DefaultKlass
        include DataMapper::Resource
        property :required_property, String, key: true
      end

      class ContextKlass
        include DataMapper::Resource
        property :required_property, String, key: true
        def self.repository_name
          :not_default_repository
        end
      end
    end

    it 'shoud send the method to all models by default' do
      DataMapper.finalize
      DefaultKlass.should_receive(:auto_migrate!).with(:default)
      ContextKlass.should_receive(:auto_migrate!).with(:not_default_repository)
      DataMapper.auto_migrate!
    end

    it 'should send the method to only repository models if a repository_name is given' do
      DataMapper.finalize
      DefaultKlass.should_not_receive(:auto_migrate!)
      ContextKlass.should_receive(:auto_migrate!).with(:not_default_repository)
      DataMapper.auto_migrate!(:not_default_repository)
    end
  end

  supported_by :postgres, :mysql, :sqlite do
    before do
      @adapter = mock('adapter', :class => DataMapper::Spec.adapter.class)
      @repo = mock('DataMapper.repository', :adapter => @adapter)
      DataMapper.stub!(:repository).and_return(@repo)
      @m = DataMapper::Migration.new(1, :do_nothing, {}) {}
      @m.stub!(:write) # silence any output
    end

    [:position, :name, :database, :adapter].each do |meth|
      it "should respond to ##{meth}" do
        @m.should respond_to(meth)
      end
    end

    describe 'initialization' do
      it 'should set @position from the given position' do
        @m.instance_variable_get(:@position).should == 1
      end

      it 'should set @name from the given name' do
        @m.instance_variable_get(:@name).should == :do_nothing
      end

      it 'should set @options from the options hash' do
        @m.instance_variable_get(:@options).should == {}
      end

      it 'should set @repository from the default repository if no :repository option is given' do
        m = DataMapper::Migration.new(1, :do_nothing, {}) {}

        m.instance_variable_get(:@repository).should == :default
      end

      it 'should set @repository to the specified :repository option' do
        m = DataMapper::Migration.new(1, :do_nothing, :repository => :foobar) {}

        m.instance_variable_get(:@repository).should == :foobar
      end

      it 'should set @verbose from the options hash' do
        m = DataMapper::Migration.new(1, :do_nothing, :verbose => false) {}
        m.instance_variable_get(:@verbose).should be(false)
      end

      it 'should set @verbose to true by default' do
        @m.instance_variable_get(:@verbose).should be(true)
      end

      it 'should set the @up_action to nil' do
        @m.instance_variable_get(:@up_action).should be_nil
      end

      it 'should set the @down_action to nil' do
        @m.instance_variable_get(:@down_action).should be_nil
      end

      it 'should evaluate the given block'

    end

    it 'should set the @up_action when #up is called with a block' do
      action = lambda {}
      @m.up(&action)
      @m.instance_variable_get(:@up_action).should == action
    end

    it 'should set the @up_action when #up is called with a block' do
      action = lambda {}
      @m.down(&action)
      @m.instance_variable_get(:@down_action).should == action
    end

    describe 'adapter' do
      before(:each) do
        @m.instance_variable_set(:@adapter, nil)
      end

      it 'should determine the class of the adapter to be extended' do
        @adapter.should_receive(:class).and_return(DataMapper::Spec.adapter.class)

        @m.adapter
      end

      it 'should extend the adapter with the right module' do
        @adapter.should_receive(:extend).with(SQL.const_get(DataMapper::Spec.adapter_name.capitalize))

        @m.adapter
      end

      it 'should raise "Unsupported adapter" on an unknown adapter' do
        @adapter.should_receive(:class).any_number_of_times.and_return("InvalidAdapter")

        lambda { @m.adapter }.should raise_error
      end
    end

    describe 'perform_up' do
      before do
        @up_action = mock('proc', :call => true)
        @m.instance_variable_set(:@up_action, @up_action)
        @m.stub!(:needs_up?).and_return(true)
        @m.stub!(:update_migration_info)
      end

      it 'should call the action assigned to @up_action and return the result' do
        @up_action.should_receive(:call).and_return(:result)
        @m.perform_up.should == :result
      end

      it 'should output a status message with the position and name of the migration' do
        @m.should_receive(:write).with(/Performing Up Migration #1: do_nothing/)
        @m.perform_up
      end

      it 'should not run if it doesnt need to be' do
        @m.should_receive(:needs_up?).and_return(false)
        @up_action.should_not_receive(:call)
        @m.perform_up
      end

      it 'should update the migration info table' do
        @m.should_receive(:update_migration_info).with(:up)
        @m.perform_up
      end

      it 'should not update the migration info table if the migration does not need run' do
        @m.should_receive(:needs_up?).and_return(false)
        @m.should_not_receive(:update_migration_info)
        @m.perform_up
      end

    end

    describe 'perform_down' do
      before do
        @down_action = mock('proc', :call => true)
        @m.instance_variable_set(:@down_action, @down_action)
        @m.stub!(:needs_down?).and_return(true)
        @m.stub!(:update_migration_info)
      end

      it 'should call the action assigned to @down_action and return the result' do
        @down_action.should_receive(:call).and_return(:result)
        @m.perform_down.should == :result
      end

      it 'should output a status message with the position and name of the migration' do
        @m.should_receive(:write).with(/Performing Down Migration #1: do_nothing/)
        @m.perform_down
      end

      it 'should not run if it doesnt need to be' do
        @m.should_receive(:needs_down?).and_return(false)
        @down_action.should_not_receive(:call)
        @m.perform_down
      end

      it 'should update the migration info table' do
        @m.should_receive(:update_migration_info).with(:down)
        @m.perform_down
      end

      it 'should not update the migration info table if the migration does not need run' do
        @m.should_receive(:needs_down?).and_return(false)
        @m.should_not_receive(:update_migration_info)
        @m.perform_down
      end

    end

    describe 'methods used in the action blocks' do

      describe '#execute' do
        before do
          @adapter.stub!(:execute)
        end

        it 'should send the SQL it its executing to the adapter execute method' do
          @adapter.should_receive(:execute).with('SELECT SOME SQL')
          @m.execute('SELECT SOME SQL')
        end

        it 'should output the SQL it is executing' do
          @m.should_receive(:write).with(/SELECT SOME SQL/)
          @m.execute('SELECT SOME SQL')
        end
      end

      describe '#execute' do
        before do
          @adapter.stub!(:select)
        end

        it 'should send the SQL it its executing to the adapter execute method' do
          @adapter.should_receive(:select).with('SELECT SOME SQL')
          @m.select('SELECT SOME SQL')
        end

        it 'should output the SQL it is executing' do
          @m.should_receive(:write).with(/SELECT SOME SQL/)
          @m.select('SELECT SOME SQL')
        end
      end

      describe 'helpers' do
        before do
          @m.stub!(:execute) # don't actually run anything
        end

        describe '#create_table' do
          before do
            @tc = mock('TableCreator', :to_sql => 'CREATE TABLE')
            SQL::TableCreator.stub!(:new).and_return(@tc)
          end

          it 'should create a new TableCreator object' do
            SQL::TableCreator.should_receive(:new).with(@adapter, :users, {}).and_return(@tc)
            @m.create_table(:users) { }
          end

          it 'should convert the TableCreator object to an sql statement' do
            @tc.should_receive(:to_sql).and_return('CREATE TABLE')
            @m.create_table(:users) { }
          end

          it 'should execute the create table sql' do
            @m.should_receive(:execute).with('CREATE TABLE')
            @m.create_table(:users) { }
          end

        end

        describe '#drop_table' do
          it 'should quote the table name' do
            @adapter.should_receive(:quote_name).with('users')
            @m.drop_table :users
          end

          it 'should execute the DROP TABLE sql for the table' do
            @adapter.stub!(:quote_name).and_return("'users'")
            @m.should_receive(:execute).with(%{DROP TABLE 'users'})
            @m.drop_table :users
          end

        end

        describe '#modify_table' do
          before do
            @tm = mock('TableModifier', :statements => [])
            SQL::TableModifier.stub!(:new).and_return(@tm)
          end

          it 'should create a new TableModifier object' do
            SQL::TableModifier.should_receive(:new).with(@adapter, :users, {}).and_return(@tm)
            @m.modify_table(:users){ }
          end

          it 'should get the statements from the TableModifier object' do
            @tm.should_receive(:statements).and_return([])
            @m.modify_table(:users){ }
          end

          it 'should iterate over the statements and execute each one' do
            @tm.should_receive(:statements).and_return(['SELECT 1', 'SELECT 2'])
            @m.should_receive(:execute).with('SELECT 1')
            @m.should_receive(:execute).with('SELECT 2')
            @m.modify_table(:users){ }
          end

        end

        describe 'sorting' do
          it 'should order things by position' do
            m1 = DataMapper::Migration.new(1, :do_nothing){}
            m2 = DataMapper::Migration.new(2, :do_nothing_else){}

            (m1 <=> m2).should == -1
          end

          it 'should order things by name when they have the same position' do
            m1 = DataMapper::Migration.new(1, :do_nothing_a){}
            m2 = DataMapper::Migration.new(1, :do_nothing_b){}

            (m1 <=> m2).should == -1
          end

        end

        describe 'formatting output' do
          describe '#say' do
            it 'should output the message' do
              @m.should_receive(:write).with(/Paul/)
              @m.say("Paul")
            end

            it 'should indent the message with 4 spaces by default' do
              @m.should_receive(:write).with(/^\s{4}/)
              @m.say("Paul")
            end

            it 'should indext the message with a given number of spaces' do
              @m.should_receive(:write).with(/^\s{3}/)
              @m.say("Paul", 3)
            end
          end

          describe '#say_with_time' do
            before do
              @m.stub!(:say)
            end

            it 'should say the message with an indent of 2' do
              @m.should_receive(:say).with("Paul", 2)
              @m.say_with_time("Paul"){}
            end

            it 'should output the time it took' do
              @m.should_receive(:say).with(/\d+/, 2)
              @m.say_with_time("Paul"){}
            end
          end

          describe '#write' do
            before do
              # need a new migration object, because the main one had #write stubbed to silence output
              @m = DataMapper::Migration.new(1, :do_nothing) {}
            end

            it 'should puts the message' do
              @m.should_receive(:puts).with("Paul")
              @m.write("Paul")
            end

            it 'should not puts the message if @verbose is false' do
              @m.instance_variable_set(:@verbose, false)
              @m.should_not_receive(:puts)
              @m.write("Paul")
            end

          end

        end

        describe 'working with the migration_info table' do
          before do
            @adapter.stub!(:storage_exists?).and_return(true)
            # --- Please remove stubs ---
            @adapter.stub!(:quote_name).and_return { |name| "'#{name}'" }
          end

          describe '#update_migration_info' do
            it 'should add a record of the migration' do
              @m.should_receive(:execute).with(
                %Q{INSERT INTO 'migration_info' ('migration_name') VALUES ('do_nothing')}
              )
              @m.update_migration_info(:up)
            end

            it 'should remove the record of the migration' do
              @m.should_receive(:execute).with(
                %Q{DELETE FROM 'migration_info' WHERE 'migration_name' = 'do_nothing'}
              )
              @m.update_migration_info(:down)
            end

            it 'should try to create the migration_info table' do
              @m.should_receive(:create_migration_info_table_if_needed)
              @m.update_migration_info(:up)
            end
          end

          describe '#create_migration_info_table_if_needed' do
            it 'should create the migration info table' do
              @m.should_receive(:migration_info_table_exists?).and_return(false)
              @m.should_receive(:execute).with(
                %Q{CREATE TABLE 'migration_info' ('migration_name' VARCHAR(255) UNIQUE)}
              )
              @m.create_migration_info_table_if_needed
            end

            it 'should not try to create the migration info table if it already exists' do
              @m.should_receive(:migration_info_table_exists?).and_return(true)
              @m.should_not_receive(:execute)
              @m.create_migration_info_table_if_needed
            end
          end

          it 'should quote the name of the migration for use in sql' do
            @m.quoted_name.should == %{'do_nothing'}
          end

          it 'should query the adapter to see if the migration_info table exists' do
            @adapter.should_receive(:storage_exists?).with('migration_info').and_return(true)
            @m.migration_info_table_exists?.should == true
          end

          describe '#migration_record' do
            it 'should query for the migration' do
              @adapter.should_receive(:select).with(
                %Q{SELECT 'migration_name' FROM 'migration_info' WHERE 'migration_name' = 'do_nothing'}
              )
              @m.migration_record
            end

            it 'should not try to query if the table does not exist' do
              @m.stub!(:migration_info_table_exists?).and_return(false)
              @adapter.should_not_receive(:select)
              @m.migration_record
            end

          end

          describe '#needs_up?' do
            it 'should be true if there is no record' do
              @m.should_receive(:migration_record).and_return([])
              @m.needs_up?.should == true
            end

            it 'should be false if the record exists' do
              @m.should_receive(:migration_record).and_return([:not_empty])
              @m.needs_up?.should == false
            end

            it 'should be true if there is no migration_info table' do
              @m.should_receive(:migration_info_table_exists?).and_return(false)
              @m.needs_up?.should == true
            end

          end

          describe '#needs_down?' do
            it 'should be false if there is no record' do
              @m.should_receive(:migration_record).and_return([])
              @m.needs_down?.should == false
            end

            it 'should be true if the record exists' do
              @m.should_receive(:migration_record).and_return([:not_empty])
              @m.needs_down?.should == true
            end

            it 'should be false if there is no migration_info table' do
              @m.should_receive(:migration_info_table_exists?).and_return(false)
              @m.needs_down?.should == false
            end

          end

          it 'should have the adapter quote the migration_info table' do
            @adapter.should_receive(:quote_name).with('migration_info').and_return("'migration_info'")
            @m.migration_info_table.should == "'migration_info'"
          end

          it 'should have a quoted migration_name_column' do
            @adapter.should_receive(:quote_name).with('migration_name').and_return("'migration_name'")
            @m.migration_name_column.should == "'migration_name'"
          end

        end

      end

    end
  end
end
