require 'spec_helper'
Dir[File.dirname(__FILE__) + "/../models/callbacks/*.rb"].sort.each { |f| require File.expand_path(f) }

shared_examples 'an implemented callback that accepts error' do
  context 'with callback defined' do
    it "should run error_callback if an exception is raised and always return false" do
      aasm_model.class.send(:define_method, callback_name) do |e|
        @data = [e]
      end

      allow(aasm_model).to receive(:before_enter).and_raise(e = StandardError.new)

      expect(aasm_model.safe_close!).to be false
      expect(aasm_model.data).to eql [e]
    end

    it "should run error_callback without parameters if callback does not support any" do
      aasm_model.class.send(:define_method, callback_name) do |e|
        @data = []
      end

      allow(aasm_model).to receive(:before_enter).and_raise(e = StandardError.new)

      aasm_model.safe_close!('arg1', 'arg2')
      expect(aasm_model.data).to eql []
    end

    it "should run error_callback with parameters if callback supports them" do
      aasm_model.class.send(:define_method, callback_name) do |e, arg1, arg2|
        @data = [arg1, arg2]
      end

      allow(aasm_model).to receive(:before_enter).and_raise(e = StandardError.new)

      aasm_model.safe_close!('arg1', 'arg2')
      expect(aasm_model.data).to eql ['arg1', 'arg2']
    end
  end
end

shared_examples 'an implemented callback' do
  context 'with callback defined' do
    it 'should run callback without parameters if callback does not support any' do
      aasm_model.class.send(:define_method, callback_name) do
        @data = ['callback-was-called']
      end

      aasm_model.safe_close!
      expect(aasm_model.data).to eql ['callback-was-called']
    end

    it 'should run callback with parameters if callback supports them' do
      aasm_model.class.send(:define_method, callback_name) do |arg1, arg2|
        @data = [arg1, arg2]
      end

      aasm_model.safe_close!('arg1', 'arg2')
      expect(aasm_model.data).to eql ['arg1', 'arg2']
    end
  end
end

describe 'callbacks for the new DSL' do

  it "be called in order" do
    show_debug_log = false

    callback = Callbacks::Basic.new(:log => show_debug_log)
    callback.aasm.current_state

    unless show_debug_log
      expect(callback).to receive(:before_all_events).once.ordered
      expect(callback).to receive(:before_event).once.ordered
      expect(callback).to receive(:event_guard).once.ordered.and_return(true)
      expect(callback).to receive(:transition_guard).once.ordered.and_return(true)
      expect(callback).to receive(:before_exit_open).once.ordered                   # these should be before the state changes
      expect(callback).to receive(:exit_open).once.ordered
      # expect(callback).to receive(:event_guard).once.ordered.and_return(true)
      # expect(callback).to receive(:transition_guard).once.ordered.and_return(true)
      expect(callback).to receive(:after_all_transitions).once.ordered
      expect(callback).to receive(:after_transition).once.ordered
      expect(callback).to receive(:before_enter_closed).once.ordered
      expect(callback).to receive(:enter_closed).once.ordered
      expect(callback).to receive(:aasm_write_state).once.ordered.and_return(true)  # this is when the state changes
      expect(callback).to receive(:after_exit_open).once.ordered                    # these should be after the state changes
      expect(callback).to receive(:after_enter_closed).once.ordered
      expect(callback).to receive(:after_event).once.ordered
      expect(callback).to receive(:after_all_events).once.ordered
      expect(callback).to receive(:ensure_event).once.ordered
      expect(callback).to receive(:ensure_on_all_events).once.ordered
    end

    callback.close!
  end

  it "works fine after reload" do
    show_debug_log = false

    callback = Callbacks::Basic.new(:log => show_debug_log)
    callback.aasm.current_state

    # reload the class
    Callbacks.send(:remove_const, :Basic)
    load 'models/callbacks/basic.rb'

    unless show_debug_log
      expect(callback).to receive(:before_event).once.ordered
      expect(callback).to receive(:event_guard).once.ordered.and_return(true)
      expect(callback).to receive(:transition_guard).once.ordered.and_return(true)
      expect(callback).to receive(:before_exit_open).once.ordered                    # these should be before the state changes
      expect(callback).to receive(:exit_open).once.ordered
      # expect(callback).to receive(:event_guard).once.ordered.and_return(true)
      # expect(callback).to receive(:transition_guard).once.ordered.and_return(true)
      expect(callback).to receive(:after_all_transitions).once.ordered
      expect(callback).to receive(:after_transition).once.ordered
      expect(callback).to receive(:before_enter_closed).once.ordered
      expect(callback).to receive(:enter_closed).once.ordered
      expect(callback).to receive(:aasm_write_state).once.ordered.and_return(true)   # this is when the state changes
      expect(callback).to receive(:event_before_success).once.ordered
      expect(callback).to receive(:success_transition).once.ordered.and_return(true) # these should be after the state changes
      expect(callback).to receive(:after_exit_open).once.ordered
      expect(callback).to receive(:after_enter_closed).once.ordered
      expect(callback).to receive(:after_event).once.ordered
    end

    callback.close!
  end

  it 'does not run callbacks if firing an unknown event' do
    show_debug_log = false

    callback = Callbacks::Basic.new(:log => show_debug_log)

    expect(callback).to_not receive(:before_all_events).ordered
    expect(callback).to_not receive(:before_event).ordered
    expect(callback).to_not receive(:event_guard).ordered
    expect(callback).to_not receive(:transition_guard)
    expect(callback).to_not receive(:before_exit_open)
    expect(callback).to_not receive(:exit_open)
    expect(callback).to_not receive(:after_all_transitions)
    expect(callback).to_not receive(:after_transition)
    expect(callback).to_not receive(:before_enter_closed)
    expect(callback).to_not receive(:enter_closed)
    expect(callback).to_not receive(:aasm_write_state)
    expect(callback).to_not receive(:event_before_success)
    expect(callback).to_not receive(:success_transition)
    expect(callback).to_not receive(:after_exit_open)
    expect(callback).to_not receive(:after_enter_closed)
    expect(callback).to_not receive(:after_event)
    expect(callback).to_not receive(:after_all_events)
    expect(callback).to_not receive(:ensure_event).ordered
    expect(callback).to_not receive(:ensure_on_all_events).ordered

    expect {
      callback.aasm.fire(:unknown)
    }.to raise_error(AASM::UndefinedEvent, "Event :unknown doesn't exist")

    expect {
      callback.aasm.fire!(:unknown)
    }.to raise_error(AASM::UndefinedEvent, "Event :unknown! doesn't exist")
  end

  it "does not run any state callback if the event guard fails" do
    callback = Callbacks::Basic.new(:log => false)
    callback.aasm.current_state

    expect(callback).to receive(:before_all_events).once.ordered
    expect(callback).to receive(:before_event).once.ordered
    expect(callback).to receive(:event_guard).once.ordered.and_return(false)
    expect(callback).to_not receive(:transition_guard)
    expect(callback).to_not receive(:before_exit_open)
    expect(callback).to_not receive(:exit_open)
    expect(callback).to_not receive(:after_all_transitions)
    expect(callback).to_not receive(:after_transition)
    expect(callback).to_not receive(:before_enter_closed)
    expect(callback).to_not receive(:enter_closed)
    expect(callback).to_not receive(:aasm_write_state)
    expect(callback).to_not receive(:event_before_success)
    expect(callback).to_not receive(:success_transition)
    expect(callback).to_not receive(:after_exit_open)
    expect(callback).to_not receive(:after_enter_closed)
    expect(callback).to_not receive(:after_event)
    expect(callback).to_not receive(:after_all_events)
    expect(callback).to receive(:ensure_event).once.ordered
    expect(callback).to receive(:ensure_on_all_events).once.ordered

    expect {
      callback.close!
    }.to raise_error(AASM::InvalidTransition)
  end

  it "handles private callback methods as well" do
    show_debug_log = false

    callback = Callbacks::PrivateMethod.new(:log => show_debug_log)
    callback.aasm.current_state

    expect {
      callback.close!
    }.to_not raise_error
  end

  context "if the transition guard fails" do
    it "does not run any state callback if guard is defined inline" do
      show_debug_log = false
      callback = Callbacks::Basic.new(:log => show_debug_log, :fail_transition_guard => true)
      callback.aasm.current_state

      unless show_debug_log
        expect(callback).to receive(:before_all_events).once.ordered
        expect(callback).to receive(:before_event).once.ordered
        expect(callback).to receive(:event_guard).once.ordered.and_return(true)
        expect(callback).to receive(:transition_guard).once.ordered.and_return(false)
        expect(callback).to_not receive(:before_exit_open)
        expect(callback).to_not receive(:exit_open)
        expect(callback).to_not receive(:after_all_transitions)
        expect(callback).to_not receive(:after_transition)
        expect(callback).to_not receive(:before_enter_closed)
        expect(callback).to_not receive(:enter_closed)
        expect(callback).to_not receive(:aasm_write_state)
        expect(callback).to_not receive(:event_before_success)
        expect(callback).to_not receive(:success_transition)
        expect(callback).to_not receive(:after_exit_open)
        expect(callback).to_not receive(:after_enter_closed)
        expect(callback).to_not receive(:after_event)
        expect(callback).to_not receive(:after_all_events)
        expect(callback).to receive(:ensure_event).once.ordered
        expect(callback).to receive(:ensure_on_all_events).once.ordered
      end

      expect {
        callback.close!
      }.to raise_error(AASM::InvalidTransition)
    end

    it "does not propagate failures to next attempt of same transition" do
      callback = Callbacks::Basic.new(:log => false, :fail_transition_guard => true)

      expect {
        callback.close!
      }.to raise_error(AASM::InvalidTransition, "Event 'close' cannot transition from 'open'. Failed callback(s): [:transition_guard].")

      expect {
        callback.close!
      }.to raise_error(AASM::InvalidTransition, "Event 'close' cannot transition from 'open'. Failed callback(s): [:transition_guard].")
    end

    it "does not propagate failures to next attempt of same event when no transition is applicable" do
      callback = Callbacks::Basic.new(:log => false, :fail_transition_guard => true)

      expect {
        callback.close!
      }.to raise_error(AASM::InvalidTransition, "Event 'close' cannot transition from 'open'. Failed callback(s): [:transition_guard].")

      callback.aasm.current_state = :closed

      expect {
        callback.close!
      }.to raise_error(AASM::InvalidTransition, "Event 'close' cannot transition from 'closed'.")
    end

    it "does not run transition_guard twice for multiple permitted transitions" do
      show_debug_log = false
      callback = Callbacks::MultipleTransitionsTransitionGuard.new(:log => show_debug_log, :fail_transition_guard => true)
      callback.aasm.current_state

      unless show_debug_log
        expect(callback).to receive(:before).once.ordered
        expect(callback).to receive(:event_guard).once.ordered.and_return(true)
        expect(callback).to receive(:transition_guard).once.ordered.and_return(false)
        expect(callback).to receive(:event_guard).once.ordered.and_return(true)
        expect(callback).to receive(:before_exit_open).once.ordered
        expect(callback).to receive(:exit_open).once.ordered
        expect(callback).to receive(:aasm_write_state).once.ordered.and_return(true)  # this is when the state changes
        expect(callback).to receive(:after_exit_open).once.ordered
        expect(callback).to receive(:after).once.ordered

        expect(callback).to_not receive(:transitioning)
        expect(callback).to_not receive(:event_before_success)
        expect(callback).to_not receive(:success_transition)
        expect(callback).to_not receive(:before_enter_closed)
        expect(callback).to_not receive(:enter_closed)
        expect(callback).to_not receive(:after_enter_closed)
      end

      callback.close!
      expect(callback.aasm.current_state).to eql :failed
    end

    it "does not run any state callback if guard is defined with block" do
      callback = Callbacks::GuardWithinBlock.new #(:log => true, :fail_transition_guard => true)
      callback.aasm.current_state

      expect(callback).to receive(:before).once.ordered
      expect(callback).to receive(:event_guard).once.ordered.and_return(true)
      expect(callback).to receive(:transition_guard).once.ordered.and_return(false)
      expect(callback).to_not receive(:before_exit_open)
      expect(callback).to_not receive(:exit_open)
      expect(callback).to_not receive(:transitioning)
      expect(callback).to_not receive(:before_enter_closed)
      expect(callback).to_not receive(:enter_closed)
      expect(callback).to_not receive(:aasm_write_state)
      expect(callback).to_not receive(:event_before_success)
      expect(callback).to_not receive(:success_transition)
      expect(callback).to_not receive(:after_exit_open)
      expect(callback).to_not receive(:after_enter_closed)
      expect(callback).to_not receive(:after)

      expect {
        callback.close!
      }.to raise_error(AASM::InvalidTransition)
    end
  end

  it "should properly pass arguments" do
    cb = Callbacks::WithArgs.new(:log => false)
    cb.aasm.current_state

    cb.reset_data
    cb.close!(:arg1, :arg2)
    expect(cb.data).to eql 'before(:arg1,:arg2) before_exit_open(:arg1,:arg2) transition_proc(:arg1,:arg2) before_enter_closed(:arg1,:arg2) aasm_write_state transition_success(:arg1,:arg2) after_exit_open(:arg1,:arg2) after_enter_closed(:arg1,:arg2) after(:arg1,:arg2)'
  end

  it "should call the callbacks given the to-state as argument" do
    cb = Callbacks::WithStateArg.new
    expect(cb).to receive(:before_method).with(:arg1).once.ordered
    expect(cb).to receive(:transition_method).never
    expect(cb).to receive(:success_method).never
    expect(cb).to receive(:transition_method2).with(:arg1).once.ordered
    expect(cb).to receive(:success_method2).with(:arg1).once.ordered
    expect(cb).to receive(:after_method).with(:arg1).once.ordered
    cb.close!(:out_to_lunch, :arg1)

    cb = Callbacks::WithStateArg.new
    some_object = double('some object')
    expect(cb).to receive(:before_method).with(some_object).once.ordered
    expect(cb).to receive(:transition_method2).with(some_object).once.ordered
    expect(cb).to receive(:success_method2).with(some_object).once.ordered
    expect(cb).to receive(:after_method).with(some_object).once.ordered
    cb.close!(:out_to_lunch, some_object)
  end

  it "should call the proper methods just with arguments" do
    cb = Callbacks::WithStateArg.new
    expect(cb).to receive(:before_method).with(:arg1).once.ordered
    expect(cb).to receive(:transition_method).with(:arg1).once.ordered
    expect(cb).to receive(:transition_method).never
    expect(cb).to receive(:before_success_method).with(:arg1).once.ordered
    expect(cb).to receive(:success_method).with(:arg1).once.ordered
    expect(cb).to receive(:success_method3).with(:arg1).once.ordered
    expect(cb).to receive(:success_method).never
    expect(cb).to receive(:after_method).with(:arg1).once.ordered
    cb.close!(:arg1)

    cb = Callbacks::WithStateArg.new
    some_object = double('some object')
    expect(cb).to receive(:before_method).with(some_object).once.ordered
    expect(cb).to receive(:transition_method).with(some_object).once.ordered
    expect(cb).to receive(:transition_method).never
    expect(cb).to receive(:before_success_method).with(some_object).once.ordered
    expect(cb).to receive(:success_method).with(some_object).once.ordered
    expect(cb).to receive(:success_method3).with(some_object).once.ordered
    expect(cb).to receive(:success_method).never
    expect(cb).to receive(:after_method).with(some_object).once.ordered
    cb.close!(some_object)
  end
end

describe 'event callbacks' do
  describe "with an error callback defined" do
    before do
      class Foo
        # this hack is needed to allow testing of parameters, since RSpec
        # destroys a method's arity when mocked
        attr_accessor :data

        aasm do
          event :safe_close, :success => :success_callback, :error => :error_callback do
            transitions :to => :closed, :from => [:open], :success => :transition_success_callback
          end
        end
      end

      @foo = Foo.new
    end

    it_behaves_like 'an implemented callback that accepts error' do
      let(:aasm_model) { @foo }
      let(:callback_name) { :error_callback }
    end

    it "should raise NoMethodError if exception is raised and error_callback is declared but not defined" do
      allow(@foo).to receive(:before_enter).and_raise(StandardError)
      expect{@foo.safe_close!}.to raise_error(NoMethodError)
    end

    it "should propagate an error if no error callback is declared" do
      allow(@foo).to receive(:before_enter).and_raise("Cannot enter safe")
      expect{@foo.close!}.to raise_error(StandardError, "Cannot enter safe")
    end
  end

  describe 'with an ensure callback defined' do
    before do
      class Foo
        # this hack is needed to allow testing of parameters, since RSpec
        # destroys a method's arity when mocked
        attr_accessor :data

        aasm do
          event :safe_close, :success => :success_callback, :ensure => :ensure_callback do
            transitions :to => :closed, :from => [:open]
          end
        end
      end

      @foo = Foo.new
    end

    it_behaves_like 'an implemented callback' do
      let(:aasm_model) { @foo }
      let(:callback_name) { :ensure_callback }
    end

    it "should raise NoMethodError if ensure_callback is declared but not defined" do
      expect{@foo.safe_close!}.to raise_error(NoMethodError)
    end

    it "should not raise any error if no ensure_callback is declared" do
      expect{@foo.close!}.to_not raise_error
    end
  end

  describe "with aasm_event_fired defined" do
    before do
      @foo = Foo.new
      def @foo.aasm_event_fired(event, from, to); end
    end

    it 'should call it for successful bang fire' do
      expect(@foo).to receive(:aasm_event_fired).with(:close, :open, :closed)
      @foo.close!
    end

    it 'should call it for successful non-bang fire' do
      expect(@foo).to receive(:aasm_event_fired)
      @foo.close
    end

    it 'should not call it for failing bang fire' do
      allow(@foo.aasm).to receive(:set_current_state_with_persistence).and_return(false)
      expect(@foo).not_to receive(:aasm_event_fired)
      @foo.close!
    end
  end

  describe "with aasm_event_failed defined" do
    before do
      @foo = Foo.new
      def @foo.aasm_event_failed(event, from); end
    end

    it 'should call it when transition failed for bang fire' do
      expect(@foo).to receive(:aasm_event_failed).with(:null, :open)
      expect {@foo.null!}.to raise_error(AASM::InvalidTransition)
    end

    it 'should call it when transition failed for non-bang fire' do
      expect(@foo).to receive(:aasm_event_failed).with(:null, :open)
      expect {@foo.null}.to raise_error(AASM::InvalidTransition)
    end

    it 'should not call it if persist fails for bang fire' do
      allow(@foo.aasm).to receive(:set_current_state_with_persistence).and_return(false)
      expect(@foo).to receive(:aasm_event_failed)
      @foo.close!
    end
  end
end

describe 'global error_on_all_events_callback callbacks' do
  describe "with an error_on_all_events" do
    before do
      class FooGlobal
        # this hack is needed to allow testing of parameters, since RSpec
        # destroys a method's arity when mocked
        attr_accessor :data

        aasm do
          error_on_all_events  :error_on_all_events_callback

          event :safe_close do
            transitions :to => :closed, :from => [:open]
          end
        end
      end

      @foo = FooGlobal.new
    end

    it_behaves_like 'an implemented callback that accepts error' do
      let(:aasm_model) { @foo }
      let(:callback_name) { :error_on_all_events_callback }
    end

    it "should raise NoMethodError if exception is raised and error_callback is declared but not defined" do
      allow(@foo).to receive(:before_enter).and_raise(StandardError)
      expect{@foo.safe_close!}.to raise_error(NoMethodError)
    end

    it "should raise NoMethodError if no error callback is declared" do
      allow(@foo).to receive(:before_enter).and_raise("Cannot enter safe")
      expect{@foo.close!}.to raise_error(NoMethodError)
    end
  end
end

describe 'global ensure_on_all_events_callback callbacks' do
  describe "with an ensure_on_all_events" do
    before do
      class FooGlobal
        # this hack is needed to allow testing of parameters, since RSpec
        # destroys a method's arity when mocked
        attr_accessor :data

        aasm do
          ensure_on_all_events  :ensure_on_all_events_callback

          event :safe_close do
            transitions :to => :closed, :from => [:open]
          end
        end
      end

      @foo = FooGlobal.new
    end

    it_behaves_like 'an implemented callback' do
      let(:aasm_model) { @foo }
      let(:callback_name) { :ensure_on_all_events_callback }
    end

    it "should raise NoMethodError if ensure_on_all_events callback is declared but not defined" do
      expect{@foo.safe_close!}.to raise_error(NoMethodError)
    end

    it "should raise NoMethodError if no ensure_on_all_events callback is declared" do
      expect{@foo.close!}.to raise_error(NoMethodError)
    end
  end
end
