require 'spec_helper'

if defined?(ActiveRecord)

  Dir[File.dirname(__FILE__) + "/../../models/active_record/*.rb"].sort.each do |f|
    require File.expand_path(f)
  end

  load_schema

  # if you want to see the statements while running the spec enable the following line
  # require 'logger'
  # ActiveRecord::Base.logger = Logger.new(STDERR)

  describe "instance methods" do
    let(:gate) {MultipleGate.new}

    it "should respond to aasm persistence methods" do
      expect(gate).to respond_to(:aasm_read_state)
      expect(gate).to respond_to(:aasm_write_state)
      expect(gate).to respond_to(:aasm_write_state_without_persistence)
    end

    describe "aasm_column_looks_like_enum" do
      subject { lambda{ gate.send(:aasm_column_looks_like_enum, :left) } }

      let(:column_name) { "value" }
      let(:columns_hash) { Hash[column_name, column] }

      before :each do
        allow(gate.class.aasm(:left)).to receive(:attribute_name).and_return(column_name.to_sym)
        allow(gate.class).to receive(:columns_hash).and_return(columns_hash)
      end

      context "when AASM column has integer type" do
        let(:column) { double(Object, type: :integer) }

        it "returns true" do
          expect(subject.call).to be_truthy
        end
      end

      context "when AASM column has string type" do
        let(:column) { double(Object, type: :string) }

        it "returns false" do
          expect(subject.call).to be_falsey
        end
      end
    end

    describe "aasm_guess_enum_method" do
      subject { lambda{ gate.send(:aasm_guess_enum_method, :left) } }

      before :each do
        allow(gate.class.aasm(:left)).to receive(:attribute_name).and_return(:value)
      end

      it "pluralizes AASM column name" do
        expect(subject.call).to eq :values
      end
    end

    describe "aasm_enum" do
      context "when AASM enum setting contains an explicit enum method name" do
        let(:with_enum) { MultipleWithEnum.new }

        it "returns whatever value was set in AASM config" do
          expect(with_enum.send(:aasm_enum, :left)).to eq :test
        end
      end

      context "when AASM enum setting is simply set to true" do
        let(:with_true_enum) { MultipleWithTrueEnum.new }
        before :each do
          allow(MultipleWithTrueEnum.aasm(:left)).to receive(:attribute_name).and_return(:value)
        end

        it "infers enum method name from pluralized column name" do
          expect(with_true_enum.send(:aasm_enum, :left)).to eq :values
        end
      end

      context "when AASM enum setting is explicitly disabled" do
        let(:with_false_enum) { MultipleWithFalseEnum.new }

        it "returns nil" do
          expect(with_false_enum.send(:aasm_enum, :left)).to be_nil
        end
      end

      context "when AASM enum setting is not enabled" do
        before :each do
          allow(MultipleGate.aasm(:left)).to receive(:attribute_name).and_return(:value)
        end

        context "when AASM column looks like enum" do
          before :each do
            allow(gate).to receive(:aasm_column_looks_like_enum).with(:left).and_return(true)
          end

          it "infers enum method name from pluralized column name" do
            expect(gate.send(:aasm_enum, :left)).to eq :values
          end
        end

        context "when AASM column doesn't look like enum'" do
          before :each do
            allow(gate).to receive(:aasm_column_looks_like_enum)
              .and_return(false)
          end

          it "returns nil, as we're not using enum" do
            expect(gate.send(:aasm_enum, :left)).to be_nil
          end
        end
      end

      if ActiveRecord::VERSION::MAJOR >= 4 && ActiveRecord::VERSION::MINOR >= 1 # won't work with Rails <= 4.1
        # Enum are introduced from Rails 4.1, therefore enum syntax will not work on Rails <= 4.1
        context "when AASM enum setting is not enabled and aasm column not present" do

          let(:multiple_with_enum_without_column) {MultipleWithEnumWithoutColumn.new}

          it "should raise an error for transitions" do
            if ActiveRecord.gem_version >= Gem::Version.new('7.1.0')
              expect{multiple_with_enum_without_column.send(:view, :left)}.to raise_error(RuntimeError, /Undeclared attribute type for enum 'status'/)
            else
              expect{multiple_with_enum_without_column.send(:view, :left)}.to raise_error(NoMethodError, /undefined method .status./)
            end
          end
        end

      end

    end

    context "when AASM is configured to use enum" do
      let(:state_sym) { :running }
      let(:state_code) { 2 }
      let(:enum_name) { :states }
      let(:enum) { Hash[state_sym, state_code] }

      before :each do
        allow(gate).to receive(:aasm_enum).and_return(enum_name)
        allow(gate).to receive(:aasm_write_state_attribute)
        allow(gate).to receive(:write_attribute)

        allow(MultipleGate).to receive(enum_name).and_return(enum)
      end

      describe "aasm_write_state" do
        context "when AASM is configured to skip validations on save" do
          before :each do
            allow(gate).to receive(:aasm_skipping_validations).and_return(true)
          end

          it "passes state code instead of state symbol to update_all" do
            # stub_chain does not allow us to give expectations on call
            # parameters in the middle of the chain, so we need to use
            # intermediate object instead.
            obj = double(Object, update_all: 1)
            allow(MultipleGate).to receive_message_chain(:unscoped, :where)
              .and_return(obj)

            gate.aasm_write_state state_sym, :left

            expect(obj).to have_received(:update_all)
              .with(Hash[gate.class.aasm(:left).attribute_name, state_code])
          end
        end

        context "when AASM is not skipping validations" do
          it "delegates state update to the helper method" do
            # Let's pretend that validation is passed
            allow(gate).to receive(:save).and_return(true)

            gate.aasm_write_state state_sym, :left

            expect(gate).to have_received(:aasm_write_state_attribute).with(state_sym, :left)
            expect(gate).to_not have_received :write_attribute
          end
        end
      end

      describe "aasm_write_state_without_persistence" do
        it "delegates state update to the helper method" do
          gate.aasm_write_state_without_persistence state_sym, :left

          expect(gate).to have_received(:aasm_write_state_attribute).with(state_sym, :left)
          expect(gate).to_not have_received :write_attribute
        end
      end

      describe "aasm_raw_attribute_value" do
        it "converts state symbol to state code" do
          expect(gate.send(:aasm_raw_attribute_value, state_sym))
            .to eq state_code
        end
      end
    end

    context "when AASM is configured to use string field" do
      let(:state_sym) { :running }

      before :each do
        allow(gate).to receive(:aasm_enum).and_return(nil)
      end

      describe "aasm_raw_attribute_value" do
        it "converts state symbol to string" do
          expect(gate.send(:aasm_raw_attribute_value, state_sym))
            .to eq state_sym.to_s
        end
      end
    end

    describe "aasm_write_attribute helper method" do
      let(:sym) { :sym }
      let(:value) { 42 }

      before :each do
        allow(gate).to receive(:write_attribute)
        allow(gate).to receive(:aasm_raw_attribute_value).and_return(value)

        gate.send(:aasm_write_state_attribute, sym, :left)
      end

      it "generates attribute value using a helper method" do
        expect(gate).to have_received(:aasm_raw_attribute_value).with(sym, :left)
      end

      it "writes attribute to the model" do
        expect(gate).to have_received(:write_attribute).with(:aasm_state, value)
      end
    end

    it "should return the initial state when new and the aasm field is nil" do
      expect(gate.aasm(:left).current_state).to eq(:opened)
    end

    it "should return the aasm column when new and the aasm field is not nil" do
      gate.aasm_state = "closed"
      expect(gate.aasm(:left).current_state).to eq(:closed)
    end

    it "should return the aasm column when not new and the aasm.attribute_name is not nil" do
      allow(gate).to receive(:new_record?).and_return(false)
      gate.aasm_state = "state"
      expect(gate.aasm(:left).current_state).to eq(:state)
    end

    it "should allow a nil state" do
      allow(gate).to receive(:new_record?).and_return(false)
      gate.aasm_state = nil
      expect(gate.aasm(:left).current_state).to be_nil
    end

    context 'on initialization' do
      it "should initialize the aasm state" do
        expect(MultipleGate.new.aasm_state).to eql 'opened'
        expect(MultipleGate.new.aasm(:left).current_state).to eql :opened
      end

      it "should not initialize the aasm state if it has not been loaded" do
        # we have to create a gate in the database, for which we only want to
        # load the id, and not the state
        gate = MultipleGate.create!

        # then we just load the gate ids
        MultipleGate.select(:id).where(id: gate.id).first
      end
    end

  end

  if ActiveRecord::VERSION::MAJOR < 4 && ActiveRecord::VERSION::MINOR < 2 # won't work with Rails >= 4.2
  describe "direct state column access" do
    it "accepts false states" do
      f = MultipleFalseState.create!
      expect(f.aasm_state).to eql false
      expect {
        f.aasm(:left).events.map(&:name)
      }.to_not raise_error
    end
  end
  end

  describe 'subclasses' do
    it "should have the same states as its parent class" do
      expect(MultipleDerivateNewDsl.aasm(:left).states).to eq(MultipleSimpleNewDsl.aasm(:left).states)
    end

    it "should have the same events as its parent class" do
      expect(MultipleDerivateNewDsl.aasm(:left).events).to eq(MultipleSimpleNewDsl.aasm(:left).events)
    end

    it "should have the same column as its parent even for the new dsl" do
      expect(MultipleSimpleNewDsl.aasm(:left).attribute_name).to eq(:status)
      expect(MultipleDerivateNewDsl.aasm(:left).attribute_name).to eq(:status)
    end
  end

  describe "named scopes with the new DSL" do
    context "Does not already respond_to? the scope name" do
      it "should add a scope for each state" do
        expect(MultipleSimpleNewDsl).to respond_to(:unknown_scope)
        expect(MultipleSimpleNewDsl).to respond_to(:another_unknown_scope)

        expect(MultipleSimpleNewDsl.unknown_scope.is_a?(ActiveRecord::Relation)).to be_truthy
        expect(MultipleSimpleNewDsl.another_unknown_scope.is_a?(ActiveRecord::Relation)).to be_truthy
      end
    end

    context "Already respond_to? the scope name" do
      it "should not add a scope" do
        expect(MultipleSimpleNewDsl).to respond_to(:new)
        expect(MultipleSimpleNewDsl.new.class).to eq(MultipleSimpleNewDsl)
      end
    end

    it "does not create scopes if requested" do
      expect(MultipleNoScope).not_to respond_to(:pending)
    end

    context "result of scope" do
      let!(:dsl1) { MultipleSimpleNewDsl.create!(status: :new) }
      let!(:dsl2) { MultipleSimpleNewDsl.create!(status: :unknown_scope) }

      after do
        MultipleSimpleNewDsl.destroy_all
      end

      it "created scope works as where(name: :scope_name)" do
        expect(MultipleSimpleNewDsl.unknown_scope).to contain_exactly(dsl2)
      end
    end

    context "when namespeced" do
      it "add namespaced scopes" do
        expect(MultipleNamespaced).to respond_to(:car_unsold)
        expect(MultipleNamespaced).to respond_to(:car_sold)

        expect(MultipleNamespaced.car_unsold.is_a?(ActiveRecord::Relation)).to be_truthy
        expect(MultipleNamespaced.car_sold.is_a?(ActiveRecord::Relation)).to be_truthy
      end
      it "add unnamespaced scopes" do
        expect(MultipleNamespaced).to respond_to(:unsold)
        expect(MultipleNamespaced).to respond_to(:sold)

        expect(MultipleNamespaced.unsold.is_a?(ActiveRecord::Relation)).to be_truthy
        expect(MultipleNamespaced.sold.is_a?(ActiveRecord::Relation)).to be_truthy
      end
    end
  end # scopes

  describe "direct assignment" do
    it "is allowed by default" do
      obj = MultipleNoScope.create
      expect(obj.aasm_state.to_sym).to eql :pending

      obj.aasm_state = :running
      expect(obj.aasm_state.to_sym).to eql :running
    end

    it "is forbidden if configured" do
      obj = MultipleNoDirectAssignment.create
      expect(obj.aasm_state.to_sym).to eql :pending

      expect {obj.aasm_state = :running}.to raise_error(AASM::NoDirectAssignmentError)
      expect(obj.aasm_state.to_sym).to eql :pending
    end

    it 'can be turned off and on again' do
      obj = MultipleNoDirectAssignment.create
      expect(obj.aasm_state.to_sym).to eql :pending

      expect {obj.aasm_state = :running}.to raise_error(AASM::NoDirectAssignmentError)
      expect(obj.aasm_state.to_sym).to eql :pending

      # allow it temporarily
      MultipleNoDirectAssignment.aasm(:left).state_machine.config.no_direct_assignment = false
      obj.aasm_state = :running
      expect(obj.aasm_state.to_sym).to eql :running

      # and forbid it again
      MultipleNoDirectAssignment.aasm(:left).state_machine.config.no_direct_assignment = true
      expect {obj.aasm_state = :pending}.to raise_error(AASM::NoDirectAssignmentError)
      expect(obj.aasm_state.to_sym).to eql :running
    end
  end # direct assignment

  describe 'initial states' do
    it 'should support conditions' do
      expect(MultipleThief.new(:skilled => true).aasm(:left).current_state).to eq(:rich)
      expect(MultipleThief.new(:skilled => false).aasm(:left).current_state).to eq(:jailed)
    end
  end

  describe 'transitions with persistence' do

    it "should work for valid models" do
      valid_object = MultipleValidator.create(:name => 'name')
      expect(valid_object).to be_sleeping
      valid_object.status = :running
      expect(valid_object).to be_running
    end

    it 'should not store states for invalid models' do
      validator = MultipleValidator.create(:name => 'name')
      expect(validator).to be_valid
      expect(validator).to be_sleeping

      validator.name = nil
      expect(validator).not_to be_valid
      expect { validator.run! }.to raise_error(ActiveRecord::RecordInvalid)
      expect(validator).to be_sleeping

      validator.reload
      expect(validator).not_to be_running
      expect(validator).to be_sleeping

      validator.name = 'another name'
      expect(validator).to be_valid
      expect(validator.run!).to be_truthy
      expect(validator).to be_running

      validator.reload
      expect(validator).to be_running
      expect(validator).not_to be_sleeping
    end

    it 'should not store states for invalid models silently if configured' do
      validator = MultipleSilentPersistor.create(:name => 'name')
      expect(validator).to be_valid
      expect(validator).to be_sleeping

      validator.name = nil
      expect(validator).not_to be_valid
      expect(validator.run!).to be_falsey
      expect(validator).to be_sleeping

      validator.reload
      expect(validator).not_to be_running
      expect(validator).to be_sleeping

      validator.name = 'another name'
      expect(validator).to be_valid
      expect(validator.run!).to be_truthy
      expect(validator).to be_running

      validator.reload
      expect(validator).to be_running
      expect(validator).not_to be_sleeping
    end

    it 'should store states for invalid models if configured' do
      persistor = MultipleInvalidPersistor.create(:name => 'name')
      expect(persistor).to be_valid
      expect(persistor).to be_sleeping

      persistor.name = nil
      expect(persistor).not_to be_valid
      expect(persistor.run!).to be_truthy
      expect(persistor).to be_running

      persistor = MultipleInvalidPersistor.find(persistor.id)
      persistor.valid?
      expect(persistor).to be_valid
      expect(persistor).to be_running
      expect(persistor).not_to be_sleeping

      persistor.reload
      expect(persistor).to be_running
      expect(persistor).not_to be_sleeping
    end

    describe 'transactions' do
      let(:worker) { Worker.create!(:name => 'worker', :status => 'sleeping') }
      let(:transactor) { MultipleTransactor.create!(:name => 'transactor', :worker => worker) }

      it 'should rollback all changes' do
        expect(transactor).to be_sleeping
        expect(worker.status).to eq('sleeping')

        expect {transactor.run!}.to raise_error(StandardError, 'failed on purpose')
        expect(transactor).to be_running
        expect(worker.reload.status).to eq('sleeping')
      end

      context "nested transactions" do
        it "should rollback all changes in nested transaction" do
          expect(transactor).to be_sleeping
          expect(worker.status).to eq('sleeping')

          Worker.transaction do
            expect { transactor.run! }.to raise_error(StandardError, 'failed on purpose')
          end

          expect(transactor).to be_running
          expect(worker.reload.status).to eq('sleeping')
        end

        it "should only rollback changes in the main transaction not the nested one" do
          # change configuration to not require new transaction
          AASM::StateMachineStore[MultipleTransactor][:left].config.requires_new_transaction = false

          expect(transactor).to be_sleeping
          expect(worker.status).to eq('sleeping')

          Worker.transaction do
            expect { transactor.run! }.to raise_error(StandardError, 'failed on purpose')
          end

          expect(transactor).to be_running
          expect(worker.reload.status).to eq('running')
        end
      end

      describe "after_commit callback" do
        it "should fire :after_commit if transaction was successful" do
          validator = MultipleValidator.create(:name => 'name')
          expect(validator).to be_sleeping

          validator.run!
          expect(validator).to be_running
          expect(validator.name).to eq("name changed")

          validator.sleep!("sleeper")
          expect(validator).to be_sleeping
          expect(validator.name).to eq("sleeper")
        end

        it "should not fire :after_commit if transaction failed" do
          validator = MultipleValidator.create(:name => 'name')
          expect { validator.fail! }.to raise_error(StandardError, 'failed on purpose')
          expect(validator.name).to eq("name")
        end

        it "should not fire if not saving" do
          validator = MultipleValidator.create(:name => 'name')
          expect(validator).to be_sleeping
          validator.run
          expect(validator).to be_running
          expect(validator.name).to eq("name")
        end

      end

      context "when not persisting" do
        it 'should not rollback all changes' do
          expect(transactor).to be_sleeping
          expect(worker.status).to eq('sleeping')

          # Notice here we're calling "run" and not "run!" with a bang.
          expect {transactor.run}.to raise_error(StandardError, 'failed on purpose')
          expect(transactor).to be_running
          expect(worker.reload.status).to eq('running')
        end

        it 'should not create a database transaction' do
          expect(transactor.class).not_to receive(:transaction)
          expect {transactor.run}.to raise_error(StandardError, 'failed on purpose')
        end
      end
    end
  end

  describe "invalid states with persistence" do
    it "should not store states" do
      validator = MultipleValidator.create(:name => 'name')
      validator.status = 'invalid_state'
      expect(validator.save).to be_falsey
      expect {validator.save!}.to raise_error(ActiveRecord::RecordInvalid)

      validator.reload
      expect(validator).to be_sleeping
    end

    it "should store invalid states if configured" do
      persistor = MultipleInvalidPersistor.create(:name => 'name')
      persistor.status = 'invalid_state'
      expect(persistor.save).to be_truthy

      persistor.reload
      expect(persistor.status).to eq('invalid_state')
    end
  end

  describe "complex example" do
    it "works" do
      record = ComplexActiveRecordExample.new
      expect_aasm_states record, :one, :alpha

      record.save!
      expect_aasm_states record, :one, :alpha
      record.reload
      expect_aasm_states record, :one, :alpha

      record.increment!
      expect_aasm_states record, :two, :alpha
      record.reload
      expect_aasm_states record, :two, :alpha

      record.level_up!
      expect_aasm_states record, :two, :beta
      record.reload
      expect_aasm_states record, :two, :beta

      record.increment!
      expect { record.increment! }.to raise_error(AASM::InvalidTransition)
      expect_aasm_states record, :three, :beta
      record.reload
      expect_aasm_states record, :three, :beta

      record.level_up!
      expect_aasm_states record, :three, :gamma
      record.reload
      expect_aasm_states record, :three, :gamma

      record.level_down # without saving
      expect_aasm_states record, :three, :beta
      record.reload
      expect_aasm_states record, :three, :gamma

      record.level_down # without saving
      expect_aasm_states record, :three, :beta
      record.reset!
      expect_aasm_states record, :one, :beta
    end

    def expect_aasm_states(record, left_state, right_state)
      expect(record.aasm(:left).current_state).to eql left_state.to_sym
      expect(record.left).to eql left_state.to_s
      expect(record.aasm(:right).current_state).to eql right_state.to_sym
      expect(record.right).to eql right_state.to_s
    end
  end
end
