require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')

require 'dm-core'
require 'dm-core/version' unless defined?(DataMapper::VERSION)
require 'dm-observer'

if Gem::Version.new(DataMapper::VERSION) >= Gem::Version.new('0.10.3')
  require 'dm-migrations'
end

# Establish database connection
DataMapper.setup(:default, 'sqlite3::memory:')
DataObjects::Sqlite3.logger = DataObjects::Logger.new("#{File.dirname(__FILE__)}/../../data_mapper.log", :debug)

module DataMapperTest
  class BaseTestCase < Test::Unit::TestCase
    def default_test
    end
    
    def teardown
      super
      @resources.uniq.each {|resource| DataMapperTest.send(:remove_const, resource)} if instance_variable_defined?('@resources')
    end
    
    protected
      # Creates a new DataMapper resource (and the associated table)
      def new_resource(create_table = :foo, &block)
        base_table_name = create_table || :foo
        name = base_table_name.to_s.capitalize
        table_name = "#{base_table_name}_#{rand(1000000)}"
        
        resource = Class.new
        DataMapperTest.send(:remove_const, name) if DataMapperTest.const_defined?(name)
        DataMapperTest.const_set(name, resource)
        (@resources ||= []) << name
        
        resource.class_eval do
          include DataMapper::Resource
          
          storage_names[:default] = table_name.to_s
          
          property :id, resource.class_eval('Serial')
          property :state, String
        end
        resource.class_eval(&block) if block_given?
        resource.auto_migrate! if create_table
        resource
      end
      
      # Creates a new DataMapper observer
      def new_observer(resource, &block)
        observer = Class.new do
          include DataMapper::Observer
        end
        observer.observe(resource)
        observer.class_eval(&block) if block_given?
        observer
      end
  end
  
  class IntegrationTest < BaseTestCase
    def test_should_have_an_integration_name
      assert_equal :data_mapper, StateMachine::Integrations::DataMapper.integration_name
    end
    
    def test_should_be_available
      assert StateMachine::Integrations::DataMapper.available?
    end
    
    def test_should_match_if_class_includes_data_mapper
      assert StateMachine::Integrations::DataMapper.matches?(new_resource)
    end
    
    def test_should_not_match_if_class_does_not_include_data_mapper
      assert !StateMachine::Integrations::DataMapper.matches?(Class.new)
    end
    
    def test_should_have_defaults
      assert_equal({:action => :save, :use_transactions => false}, StateMachine::Integrations::DataMapper.defaults)
    end
    
    def test_should_not_have_a_locale_path
      assert_nil StateMachine::Integrations::DataMapper.locale_path
    end
  end
  
  class MachineWithoutDatabaseTest < BaseTestCase
    def setup
      @resource = new_resource(false) do
        # Simulate the database not being available entirely
        def self.repository
          raise DataObjects::SyntaxError
        end
      end
    end
    
    def test_should_allow_machine_creation
      assert_nothing_raised { StateMachine::Machine.new(@resource) }
    end
  end
  
  class MachineUnmigratedTest < BaseTestCase
    def setup
      @resource = new_resource(false)
    end
    
    def test_should_allow_machine_creation
      assert_nothing_raised { StateMachine::Machine.new(@resource) }
    end
  end
  
  class MachineWithoutPropertyTest < BaseTestCase
    def setup
      @resource = new_resource
      StateMachine::Machine.new(@resource, :status)
    end
    
    def test_should_define_field_with_string_type
      property = @resource.properties.detect {|p| p.name == :status}
      assert_not_nil property
      
      if Gem::Version.new(DataMapper::VERSION) >= Gem::Version.new('1.0.0')
        assert_instance_of DataMapper::Property::String, property
      else
        assert_equal String, property.type
      end
    end
  end
  
  class MachineWithPropertyTest < BaseTestCase
    def setup
      @resource = new_resource do
        property :status, Integer
      end
      StateMachine::Machine.new(@resource, :status)
    end
    
    def test_should_not_redefine_field
      property = @resource.properties.detect {|p| p.name == :status}
      assert_not_nil property
      
      if Gem::Version.new(DataMapper::VERSION) >= Gem::Version.new('1.0.0')
        assert_instance_of DataMapper::Property::Integer, property
      else
        assert_equal Integer, property.type
      end
    end
  end
  
  class MachineByDefaultTest < BaseTestCase
    def setup
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource)
    end
    
    def test_should_use_save_as_action
      assert_equal :save, @machine.action
    end
    
    def test_should_not_use_transactions
      assert_equal false, @machine.use_transactions
    end
    
    def test_should_not_have_any_before_callbacks
      assert_equal 0, @machine.callbacks[:before].size
    end
    
    def test_should_not_have_any_after_callbacks
      assert_equal 0, @machine.callbacks[:after].size
    end
  end
  
  class MachineWithStatesTest < BaseTestCase
    def setup
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource)
      @machine.state :first_gear
    end
    
    def test_should_humanize_name
      assert_equal 'first gear', @machine.state(:first_gear).human_name
    end
  end
  
  class MachineWithStaticInitialStateTest < BaseTestCase
    def setup
      @resource = new_resource(:vehicle) do
        attr_accessor :value
      end
      @machine = StateMachine::Machine.new(@resource, :initial => :parked)
    end
    
    def test_should_set_initial_state_on_created_object
      record = @resource.new
      assert_equal 'parked', record.state
    end
    
    def test_should_set_initial_state_with_nil_attributes
      @resource.class_eval do
        def attributes=(attributes)
          super(attributes || {})
        end
      end
      
      record = @resource.new(nil)
      assert_equal 'parked', record.state
    end
    
    def test_should_still_set_attributes
      record = @resource.new(:value => 1)
      assert_equal 1, record.value
    end
    
    def test_should_not_allow_initialize_blocks
      block_args = nil
      @resource.new do |*args|
        block_args = args
      end
      
      assert_nil block_args
    end
    
    def test_should_set_initial_state_before_setting_attributes
      @resource.class_eval do
        attr_accessor :state_during_setter
        
        remove_method :value=
        define_method(:value=) do |value|
          self.state_during_setter = state
        end
      end
      
      record = @resource.new(:value => 1)
      assert_equal 'parked', record.state_during_setter
    end
    
    def test_should_not_set_initial_state_after_already_initialized
      record = @resource.new(:value => 1)
      assert_equal 'parked', record.state
      
      record.state = 'idling'
      record.attributes = {}
      assert_equal 'idling', record.state
    end
    
    def test_should_persist_initial_state
      record = @resource.new
      record.save
      record.reload
      assert_equal 'parked', record.state
    end
    
    def test_should_persist_initial_state_on_dup
      record = @resource.create.dup
      record.save
      record.reload
      assert_equal 'parked', record.state
    end
    
    def test_should_use_stored_values_when_loading_from_database
      @machine.state :idling
      
      record = @resource.get(@resource.create(:state => 'idling').id)
      assert_equal 'idling', record.state
    end
    
    def test_should_use_stored_values_when_loading_from_database_with_nil_state
      @machine.state nil
      
      record = @resource.get(@resource.create(:state => nil).id)
      assert_nil record.state
    end
    
    def test_should_use_stored_values_when_loading_for_many_association
      @machine.state :idling
      
      @resource.property :owner_id, Integer
      @resource.auto_migrate!
      
      owner_resource = new_resource(:owner) do
        has n, :vehicles
      end
      
      owner = owner_resource.create
      record = @resource.new(:state => 'idling')
      record.owner_id = owner.id
      record.save
      assert_equal 'idling', owner.vehicles[0].state
    end
    
    def test_should_use_stored_values_when_loading_for_one_association
      @machine.state :idling
      
      @resource.property :owner_id, Integer
      @resource.auto_migrate!
      
      owner_resource = new_resource(:owner) do
        has 1, :vehicle
      end
      
      owner = owner_resource.create
      record = @resource.new(:state => 'idling')
      record.owner_id = owner.id
      record.save
      assert_equal 'idling', owner.vehicle.state
    end
    
    def test_should_use_stored_values_when_loading_for_belongs_to_association
      @machine.state :idling
      
      driver_resource = new_resource(:driver) do
        belongs_to :vehicle
      end
      
      record = @resource.create(:state => 'idling')
      driver = driver_resource.create(:vehicle_id => record.id)
      assert_equal 'idling', driver.vehicle.state
    end
  end
  
  class MachineWithDynamicInitialStateTest < BaseTestCase
    def setup
      @resource = new_resource do
        attr_accessor :value
      end
      @machine = StateMachine::Machine.new(@resource, :initial => lambda {|object| :parked})
      @machine.state :parked
    end
    
    def test_should_set_initial_state_on_created_object
      record = @resource.new
      assert_equal 'parked', record.state
    end
    
    def test_should_still_set_attributes
      record = @resource.new(:value => 1)
      assert_equal 1, record.value
    end
    
    def test_should_not_allow_initialize_blocks
      block_args = nil
      @resource.new do |*args|
        block_args = args
      end
      
      assert_nil block_args
    end
    
    def test_should_set_initial_state_after_setting_attributes
      @resource.class_eval do
        attr_accessor :state_during_setter
        
        remove_method :value=
        define_method(:value=) do |value|
          self.state_during_setter = state || 'nil'
        end
      end
      
      record = @resource.new(:value => 1)
      assert_equal 'nil', record.state_during_setter
    end
    
    def test_should_not_set_initial_state_after_already_initialized
      record = @resource.new(:value => 1)
      assert_equal 'parked', record.state
      
      record.state = 'idling'
      record.attributes = {}
      assert_equal 'idling', record.state
    end
    
    def test_should_persist_initial_state
      record = @resource.new
      record.save
      record.reload
      assert_equal 'parked', record.state
    end
    
    def test_should_persist_initial_state_on_dup
      record = @resource.create.dup
      record.save
      record.reload
      assert_equal 'parked', record.state
    end
    
    def test_should_use_stored_values_when_loading_from_database
      @machine.state :idling
      
      record = @resource.get(@resource.create(:state => 'idling').id)
      assert_equal 'idling', record.state
    end
    
    def test_should_use_stored_values_when_loading_from_database_with_nil_state
      @machine.state nil
      
      record = @resource.get(@resource.create(:state => nil).id)
      assert_nil record.state
    end
  end
  
  class MachineWithEventsTest < BaseTestCase
    def setup
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource)
      @machine.event :shift_up
    end
    
    def test_should_humanize_name
      assert_equal 'shift up', @machine.event(:shift_up).human_name
    end
  end
  
  class MachineWithSameColumnDefaultTest < BaseTestCase
    def setup
      @original_stderr, $stderr = $stderr, StringIO.new
      
      @resource = new_resource do
        property :status, String, :default => 'parked'
      end
      @machine = StateMachine::Machine.new(@resource, :status, :initial => :parked)
      @record = @resource.new
    end
    
    def test_should_use_machine_default
      assert_equal 'parked', @record.status
    end
    
    def test_should_not_generate_a_warning
      assert_no_match(/have defined a different default/, $stderr.string)
    end
    
    def teardown
      $stderr = @original_stderr
      super
    end
  end
  
  class MachineWithDifferentColumnDefaultTest < BaseTestCase
    def setup
      @original_stderr, $stderr = $stderr, StringIO.new
      
      @resource = new_resource do
        property :status, String, :default => 'idling'
      end
      @machine = StateMachine::Machine.new(@resource, :status, :initial => :parked)
      @record = @resource.new
    end
    
    def test_should_use_machine_default
      assert_equal 'parked', @record.status
    end
    
    def test_should_generate_a_warning
      assert_match(/Both DataMapperTest::Foo and its :status machine have defined a different default for "status". Use only one or the other for defining defaults to avoid unexpected behaviors\./, $stderr.string)
    end
    
    def teardown
      $stderr = @original_stderr
      super
    end
  end
  
  class MachineWithDifferentIntegerColumnDefaultTest < BaseTestCase
    def setup
      @original_stderr, $stderr = $stderr, StringIO.new
      
      @resource = new_resource do
        property :status, Integer, :default => 0
      end
      @machine = StateMachine::Machine.new(@resource, :status, :initial => :parked)
      @machine.state :parked, :value => 1
      @record = @resource.new
    end
    
    def test_should_use_machine_default
      assert_equal 1, @record.status
    end
    
    def test_should_generate_a_warning
      assert_match(/Both DataMapperTest::Foo and its :status machine have defined a different default for "status". Use only one or the other for defining defaults to avoid unexpected behaviors\./, $stderr.string)
    end
    
    def teardown
      $stderr = @original_stderr
      super
    end
  end
  
  class MachineWithConflictingPredicateTest < BaseTestCase
    def setup
      @resource = new_resource do
        def state?(*args)
          true
        end
      end
      
      @machine = StateMachine::Machine.new(@resource)
      @record = @resource.new
    end
    
    def test_should_not_define_attribute_predicate
      assert @record.state?
    end
  end
  
  class MachineWithConflictingStateNameTest < BaseTestCase
    def setup
      require 'stringio'
      @original_stderr, $stderr = $stderr, StringIO.new
      
      @resource = new_resource
    end
    
    def test_should_output_warning_with_same_machine_name
      @machine = StateMachine::Machine.new(@resource)
      @machine.state :state
      
      assert_match(/^Instance method "state\?" is already defined in DataMapperTest::Foo :state instance helpers, use generic helper instead.*\n$/, $stderr.string)
    end
    
    def test_should_not_output_warning_with_same_machine_attribute
      @machine = StateMachine::Machine.new(@resource, :public_state, :attribute => :state)
      @machine.state :state
      
      assert_no_match(/^Instance method "state\?" is already defined.*\n$/, $stderr.string)
    end
    
    def teardown
      $stderr = @original_stderr
      super
    end
  end
  
  class MachineWithColumnStateAttributeTest < BaseTestCase
    def setup
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource, :initial => :parked)
      @machine.other_states(:idling)
      
      @record = @resource.new
    end
    
    def test_should_not_override_the_column_reader
      @record.attribute_set(:state, 'parked')
      assert_equal 'parked', @record.state
    end
    
    def test_should_not_override_the_column_writer
      @record.state = 'parked'
      assert_equal 'parked', @record.attribute_get(:state)
    end
    
    def test_should_have_an_attribute_predicate
      assert @record.respond_to?(:state?)
    end
    
    def test_should_raise_exception_for_predicate_without_parameters
      assert_raise(ArgumentError) { @record.state? }
    end
    
    def test_should_return_false_for_predicate_if_does_not_match_current_value
      assert !@record.state?(:idling)
    end
    
    def test_should_return_true_for_predicate_if_matches_current_value
      assert @record.state?(:parked)
    end
    
    def test_should_raise_exception_for_predicate_if_invalid_state_specified
      assert_raise(IndexError) { @record.state?(:invalid) }
    end
  end
  
  class MachineWithNonColumnStateAttributeUndefinedTest < BaseTestCase
    def setup
      @resource = new_resource do
        def initialize
          # Skip attribute initialization
          @initialized_state_machines = true
          super
        end
      end
      
      @machine = StateMachine::Machine.new(@resource, :status, :initial => 'parked')
      @record = @resource.new
    end
    
    def test_should_define_a_new_property_for_the_attribute
      assert_not_nil @resource.properties[:status]
    end
    
    def test_should_define_a_reader_attribute_for_the_attribute
      assert @record.respond_to?(:status)
    end
    
    def test_should_define_a_writer_attribute_for_the_attribute
      assert @record.respond_to?(:status=)
    end
    
    def test_should_define_an_attribute_predicate
      assert @record.respond_to?(:status?)
    end
  end
  
  class MachineWithNonColumnStateAttributeDefinedTest < BaseTestCase
    def setup
      @resource = new_resource do
        attr_accessor :status
      end
      
      @machine = StateMachine::Machine.new(@resource, :status, :initial => :parked)
      @machine.other_states(:idling)
      @record = @resource.new
    end
    
    def test_should_return_false_for_predicate_if_does_not_match_current_value
      assert !@record.status?(:idling)
    end
    
    def test_should_return_true_for_predicate_if_matches_current_value
      assert @record.status?(:parked)
    end
    
    def test_should_raise_exception_for_predicate_if_invalid_state_specified
      assert_raise(IndexError) { @record.status?(:invalid) }
    end
    
    def test_should_set_initial_state_on_created_object
      assert_equal 'parked', @record.status
    end
  end
  
  class MachineWithInitializedStateTest < BaseTestCase
    def setup
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource, :initial => :parked)
      @machine.state :idling
    end
    
    def test_should_allow_nil_initial_state_when_static
      @machine.state nil
      
      record = @resource.new(:state => nil)
      assert_nil record.state
    end
    
    def test_should_allow_nil_initial_state_when_dynamic
      @machine.state nil
      
      @machine.initial_state = lambda {:parked}
      record = @resource.new(:state => nil)
      assert_nil record.state
    end
    
    def test_should_allow_different_initial_state_when_static
      record = @resource.new(:state => 'idling')
      assert_equal 'idling', record.state
    end
    
    def test_should_allow_different_initial_state_when_dynamic
      @machine.initial_state = lambda {:parked}
      record = @resource.new(:state => 'idling')
      assert_equal 'idling', record.state
    end
    
    if Gem::Version.new(DataMapper::VERSION) >= Gem::Version.new('0.9.8')
      def test_should_raise_exception_if_protected
        resource = new_resource do
          protected :state=
        end
        
        machine = StateMachine::Machine.new(resource, :initial => :parked)
        machine.state :idling
        
        assert_raise(ArgumentError) { resource.new(:state => 'idling') }
      end
    end
  end
  
  class MachineMultipleTest < BaseTestCase
    def setup
      @resource = new_resource do
        property :status, String
      end
      @state_machine = StateMachine::Machine.new(@resource, :initial => :parked)
      @status_machine = StateMachine::Machine.new(@resource, :status, :initial => :idling)
    end
    
    def test_should_should_initialize_each_state
      record = @resource.new
      assert_equal 'parked', record.state
      assert_equal 'idling', record.status
    end
  end
  
  class MachineWithLoopbackTest < BaseTestCase
    def setup
      @resource = new_resource do
        property :updated_at, DateTime
        
        # Simulate dm-timestamps
        before :update do
          return unless dirty?
          self.updated_at = DateTime.now
        end
      end
      
      @machine = StateMachine::Machine.new(@resource, :initial => :parked)
      @machine.event :park
      
      @record = @resource.create(:updated_at => Time.now - 1)
      @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
      
      @timestamp = @record.updated_at
      @transition.perform
    end
    
    def test_should_not_update_record
      assert_equal @timestamp, @record.updated_at
    end
  end
  
  class MachineWithDirtyAttributesTest < BaseTestCase
    def setup
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource, :initial => :parked)
      @machine.event :ignite
      @machine.state :idling
      
      @record = @resource.create
      
      @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
      @transition.perform(false)
    end
    
    def test_should_include_state_in_changed_attributes
      assert_equal({@resource.properties[:state] => 'idling'}, @record.dirty_attributes)
    end
    
    def test_should_track_attribute_change
      if Gem::Version.new(DataMapper::VERSION) >= Gem::Version.new('0.10.0')
        assert_equal({@resource.properties[:state] => 'parked'}, @record.original_attributes)
      else
        assert_equal({:state => 'parked'},  @record.original_values)
      end
    end
    
    def test_should_not_reset_changes_on_multiple_transitions
      transition = StateMachine::Transition.new(@record, @machine, :ignite, :idling, :idling)
      transition.perform(false)
      
      if Gem::Version.new(DataMapper::VERSION) >= Gem::Version.new('0.10.0')
        assert_equal({@resource.properties[:state] => 'parked'}, @record.original_attributes)
      else
        assert_equal({:state => 'parked'},  @record.original_values)
      end
    end
    
    def test_should_not_have_changes_when_loaded_from_database
      record = @resource.get(@record.id)
      assert record.dirty_attributes.empty?
    end
  end
  
  class MachineWithDirtyAttributesDuringLoopbackTest < BaseTestCase
    def setup
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource, :initial => :parked)
      @machine.event :park
      
      @record = @resource.create
      
      @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
      @transition.perform(false)
    end
    
    def test_should_not_include_state_in_changed_attributes
      assert_equal({}, @record.dirty_attributes)
    end
  end
  
  class MachineWithDirtyAttributesAndCustomAttributeTest < BaseTestCase
    def setup
      @resource = new_resource do
        property :status, String
      end
      @machine = StateMachine::Machine.new(@resource, :status, :initial => :parked)
      @machine.event :ignite
      @machine.state :idling
      
      @record = @resource.create
      
      @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
      @transition.perform(false)
    end
    
    def test_should_include_state_in_changed_attributes
      assert_equal({@resource.properties[:status] => 'idling'}, @record.dirty_attributes)
    end
    
    def test_should_track_attribute_change
      if Gem::Version.new(DataMapper::VERSION) >= Gem::Version.new('0.10.0')
        assert_equal({@resource.properties[:status] => 'parked'}, @record.original_attributes)
      else
        assert_equal({:status => 'parked'},  @record.original_values)
      end
    end
    
    def test_should_not_reset_changes_on_multiple_transitions
      transition = StateMachine::Transition.new(@record, @machine, :ignite, :idling, :idling)
      transition.perform(false)
      
      if Gem::Version.new(DataMapper::VERSION) >= Gem::Version.new('0.10.0')
        assert_equal({@resource.properties[:status] => 'parked'}, @record.original_attributes)
      else
        assert_equal({:status => 'parked'},  @record.original_values)
      end
    end
  end
  
  class MachineWithDirtyAttributeAndCustomAttributesDuringLoopbackTest < BaseTestCase
    def setup
      @resource = new_resource do
        property :status, String
      end
      @machine = StateMachine::Machine.new(@resource, :status, :initial => :parked)
      @machine.event :park
      
      @record = @resource.create
      
      @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
      @transition.perform(false)
    end
    
    def test_should_not_include_state_in_changed_attributes
      assert_equal({}, @record.dirty_attributes)
    end
  end
  
  class MachineWithDirtyAttributeAndStateEventsTest < BaseTestCase
    def setup
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource, :initial => :parked)
      @machine.event :ignite
      
      @record = @resource.create
      @record.state_event = 'ignite'
    end
    
    def test_should_not_include_state_in_changed_attributes
      assert_equal({}, @record.dirty_attributes)
    end
    
    def test_should_not_track_attribute_change
      if Gem::Version.new(DataMapper::VERSION) >= Gem::Version.new('0.10.0')
        assert_equal({}, @record.original_attributes)
      else
        assert_equal({},  @record.original_values)
      end
    end
  end
  
  class MachineWithoutTransactionsTest < BaseTestCase
    def setup
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource, :use_transactions => false)
    end
    
    def test_should_not_rollback_transaction_if_false
      @machine.within_transaction(@resource.new) do
        @resource.create
        false
      end
      
      assert_equal 1, @resource.all.size
    end
    
    def test_should_not_rollback_transaction_if_true
      @machine.within_transaction(@resource.new) do
        @resource.create
        true
      end
      
      assert_equal 1, @resource.all.size
    end
  end
  
  begin
    if Gem::Version.new(DataMapper::VERSION) >= Gem::Version.new('0.10.3')
      require 'dm-transactions'
    end
    
    class MachineWithTransactionsTest < BaseTestCase
      def setup
        @resource = new_resource
        @machine = StateMachine::Machine.new(@resource, :use_transactions => true)
      end
      
      def test_should_rollback_transaction_if_false
        @machine.within_transaction(@resource.new) do
          @resource.create
          false
        end
        
        assert_equal 0, @resource.all.size
      end
      
      def test_should_not_rollback_transaction_if_true
        @machine.within_transaction(@resource.new) do
          @resource.create
          true
        end
        
        assert_equal 1, @resource.all.size
      end
    end
  rescue LoadError
    $stderr.puts "Skipping DataMapper Transaction tests."
  end
  
  class MachineWithCallbacksTest < BaseTestCase
    def setup
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource)
      @machine.state :parked, :idling
      @machine.event :ignite
      
      @record = @resource.new(:state => 'parked')
      @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
    end
    
    def test_should_run_before_callbacks
      called = false
      @machine.before_transition {called = true}
      
      @transition.perform
      assert called
    end
    
    def test_should_pass_transition_to_before_callbacks_with_one_argument
      transition = nil
      @machine.before_transition {|arg| transition = arg}
      
      @transition.perform
      assert_equal @transition, transition
    end
    
    def test_should_pass_transition_to_before_callbacks_with_multiple_arguments
      callback_args = nil
      @machine.before_transition {|*args| callback_args = args}
      
      @transition.perform
      assert_equal [@transition], callback_args
    end
    
    def test_should_run_before_callbacks_within_the_context_of_the_record
      context = nil
      @machine.before_transition {context = self}
      
      @transition.perform
      assert_equal @record, context
    end
    
    def test_should_run_after_callbacks
      called = false
      @machine.after_transition {called = true}
      
      @transition.perform
      assert called
    end
    
    def test_should_pass_transition_to_after_callbacks_with_multiple_arguments
      callback_args = nil
      @machine.after_transition {|*args| callback_args = args}
      
      @transition.perform
      assert_equal [@transition], callback_args
    end
    
    def test_should_run_after_callbacks_with_the_context_of_the_record
      context = nil
      @machine.after_transition {context = self}
      
      @transition.perform
      assert_equal @record, context
    end
    
    def test_should_run_around_callbacks
      before_called = false
      after_called = false
      ensure_called = 0
      @machine.around_transition do |block|
        before_called = true
        begin
          block.call
        ensure
          ensure_called += 1
        end
        after_called = true
      end
      
      @transition.perform
      assert before_called
      assert after_called
      assert_equal ensure_called, 1
    end
    
    def test_should_run_around_callbacks_with_the_context_of_the_record
      context = nil
      @machine.around_transition {|block| context = self; block.call}
      
      @transition.perform
      assert_equal @record, context
    end
    
    def test_should_allow_symbolic_callbacks
      callback_args = nil
      
      klass = class << @record; self; end
      klass.send(:define_method, :after_ignite) do |*args|
        callback_args = args
      end
      
      @machine.before_transition(:after_ignite)
      
      @transition.perform
      assert_equal [@transition], callback_args
    end
    
    def test_should_allow_string_callbacks
      class << @record
        attr_reader :callback_result
      end
      
      @machine.before_transition('@callback_result = [1, 2, 3]')
      @transition.perform
      
      assert_equal [1, 2, 3], @record.callback_result
    end
    
    def test_should_run_in_expected_order
      # Avoid Ruby 2.0.0 stack too deep issues
      @resource.class_eval do
        def valid?(*)
          super
        end
      end
      
      expected = [
        :before_transition, :before_validation, :after_validation,
        :before_save, :before_create, :after_create, :after_save,
        :after_transition
      ]
      
      callbacks = []
      @resource.before(:valid?) { callbacks << :before_validation }
      @resource.after(:valid?) { callbacks << :after_validation }
      @resource.before(:save) { callbacks << :before_save }
      @resource.before(:create) { callbacks << :before_create }
      @resource.after(:create) { callbacks << :after_create }
      @resource.after(:save) { callbacks << :after_save }
      
      @machine.before_transition { callbacks << :before_transition }
      @machine.after_transition { callbacks << :after_transition }
      
      @transition.perform
      
      assert_equal expected, callbacks
    end
  end
  
  class MachineWithFailedBeforeCallbacksTest < BaseTestCase
    def setup
      callbacks = []
      
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource)
      @machine.state :parked, :idling
      @machine.event :ignite
      @machine.before_transition {callbacks << :before_1; throw :halt}
      @machine.before_transition {callbacks << :before_2}
      @machine.after_transition {callbacks << :after}
      @machine.around_transition {|block| callbacks << :around_before; block.call; callbacks << :around_after}
      
      @record = @resource.new(:state => 'parked')
      @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
      @result = @transition.perform
      
      @callbacks = callbacks
    end
    
    def test_should_not_be_successful
      assert !@result
    end
    
    def test_should_not_change_current_state
      assert_equal 'parked', @record.state
    end
    
    def test_should_not_run_action
      assert @record.respond_to?(:new?) ? @record.new? : @record.new_record?
    end
    
    def test_should_not_run_further_callbacks
      assert_equal [:before_1], @callbacks
    end
  end
  
  if Gem::Version.new(DataMapper::VERSION) >= Gem::Version.new('1.0.0')
    class MachineNestedActionTest < BaseTestCase
      def setup
        @callbacks = []
        
        @resource = new_resource
        @machine = StateMachine::Machine.new(@resource)
        @machine.event :ignite do
          transition :parked => :idling
        end
        
        @record = @resource.new(:state => 'parked')
      end
      
      def test_should_allow_transition_prior_to_creation_if_skipping_action
        record = @record
        @resource.before(:create) { record.ignite }
        result = @record.save
        
        assert_equal true, result
        assert_equal "idling", @record.state
        @record.reload
        assert_equal "idling", @record.state
      end
      
      def test_should_not_allow_transition_after_creation
        record = @record
        @resource.after(:create) { record.ignite(false) }
        
        result = @record.save
        
        assert_equal false, result
      end
    end
  end
  
  class MachineWithFailedActionTest < BaseTestCase
    def setup
      @resource = new_resource do
        before(:create) { throw :halt }
      end
      
      @machine = StateMachine::Machine.new(@resource)
      @machine.state :parked, :idling
      @machine.event :ignite
      
      callbacks = []
      @machine.before_transition {callbacks << :before}
      @machine.after_transition {callbacks << :after}
      @machine.after_failure {callbacks << :after_failure}
      @machine.around_transition {|block| callbacks << :around_before; block.call; callbacks << :around_after}
      
      @record = @resource.new(:state => 'parked')
      @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
      @result = @transition.perform
      
      @callbacks = callbacks
    end
    
    def test_should_not_be_successful
      assert !@result
    end
    
    def test_should_not_change_current_state
      assert_equal 'parked', @record.state
    end
    
    def test_should_not_save_record
      assert @record.respond_to?(:new?) ? @record.new? : @record.new_record?
    end
    
    def test_should_run_before_callbacks_and_after_callbacks_with_failures
      assert_equal [:before, :around_before, :after_failure], @callbacks
    end
  end
  
  class MachineWithFailedAfterCallbacksTest < BaseTestCase
    def setup
      callbacks = []
      
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource)
      @machine.state :parked, :idling
      @machine.event :ignite
      @machine.after_transition {callbacks << :after_1; throw :halt}
      @machine.after_transition {callbacks << :after_2}
      @machine.around_transition {|block| callbacks << :around_before; block.call; callbacks << :around_after}
      
      @record = @resource.new(:state => 'parked')
      @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
      @result = @transition.perform
      
      @callbacks = callbacks
    end
    
    def test_should_be_successful
      assert @result
    end
    
    def test_should_change_current_state
      assert_equal 'idling', @record.state
    end
    
    def test_should_save_record
      assert !(@record.respond_to?(:new?) ? @record.new? : @record.new_record?)
    end
    
    def test_should_not_run_further_after_callbacks
      assert_equal [:around_before, :around_after, :after_1], @callbacks
    end
  end
  
  begin
    require 'dm-validations'
    
    class MachineWithValidationsTest < BaseTestCase
      def setup
        @resource = new_resource
        @machine = StateMachine::Machine.new(@resource)
        @machine.state :parked
        
        @record = @resource.new
      end
      
      def test_should_invalidate_using_errors
        @record.state = 'parked'
        
        @machine.invalidate(@record, :state, :invalid_transition, [[:event, 'park']])
        assert_equal ['cannot transition via "park"'], @record.errors.on(:state)
      end
      
      def test_should_auto_prefix_custom_attributes_on_invalidation
        @machine.invalidate(@record, :event, :invalid)
        
        assert_equal ['is invalid'], @record.errors.on(:state_event)
      end
      
      def test_should_clear_errors_on_reset
        @record.state = 'parked'
        @record.errors.add(:state, 'is invalid')
        
        @machine.reset(@record)
        assert_nil @record.errors.on(:id)
      end
      
      def test_should_be_valid_if_state_is_known
        @record.state = 'parked'
        
        assert @record.valid?
      end
      
      def test_should_not_be_valid_if_state_is_unknown
        @record.state = 'invalid'
        
        assert !@record.valid?
        assert_equal ['is invalid'], @record.errors.on(:state)
      end
    end
    
    class MachineWithValidationsAndCustomAttributeTest < BaseTestCase
      def setup
        @resource = new_resource
        @machine = StateMachine::Machine.new(@resource, :status, :attribute => :state)
        @machine.state :parked
        
        @record = @resource.new
      end
      
      def test_should_add_validation_errors_to_custom_attribute
        @record.state = 'invalid'
        
        assert !@record.valid?
        assert_equal ['is invalid'], @record.errors.on(:state)
        
        @record.state = 'parked'
        assert @record.valid?
      end
    end
    
    class MachineErrorsTest < BaseTestCase
      def setup
        @resource = new_resource
        @machine = StateMachine::Machine.new(@resource)
        @record = @resource.new
      end
      
      def test_should_be_able_to_describe_current_errors
        @record.errors.add(:id, 'cannot be blank')
        @record.errors.add(:state, 'is invalid')
        assert_equal ['id cannot be blank', 'state is invalid'], @machine.errors_for(@record).split(', ').sort
      end
      
      def test_should_describe_as_halted_with_no_errors
        assert_equal 'Transition halted', @machine.errors_for(@record)
      end
    end
    
    class MachineWithStateDrivenValidationsTest < BaseTestCase
      def setup
        @resource = resource = new_resource do
          attr_accessor :seatbelt
        end
        
        @machine = StateMachine::Machine.new(@resource)
        @machine.state :first_gear, :second_gear do
          if resource.respond_to?(:validates_presence_of)
            validates_presence_of :seatbelt
          else
            validates_present :seatbelt
          end
        end
        @machine.other_states :parked
      end
      
      def test_should_be_valid_if_validation_fails_outside_state_scope
        record = @resource.new(:state => 'parked', :seatbelt => nil)
        assert record.valid?
      end
      
      def test_should_be_invalid_if_validation_fails_within_state_scope
        record = @resource.new(:state => 'first_gear', :seatbelt => nil)
        assert !record.valid?
      end
      
      def test_should_be_valid_if_validation_succeeds_within_state_scope
        record = @resource.new(:state => 'second_gear', :seatbelt => true)
        assert record.valid?
      end
    end
    
    # See README caveats
    if Gem::Version.new(DataMapper::VERSION) > Gem::Version.new('0.9.6')
      class MachineWithEventAttributesOnValidationTest < BaseTestCase
        def setup
          @resource = new_resource
          @machine = StateMachine::Machine.new(@resource)
          @machine.event :ignite do
            transition :parked => :idling
          end
          
          @record = @resource.new
          @record.state = 'parked'
          @record.state_event = 'ignite'
        end
        
        def test_should_fail_if_event_is_invalid
          @record.state_event = 'invalid'
          assert !@record.valid?
          assert_equal ['is invalid'], @record.errors.full_messages
        end
        
        def test_should_fail_if_event_has_no_transition
          @record.state = 'idling'
          assert !@record.valid?
          assert_equal ['cannot transition when idling'], @record.errors.full_messages
        end
        
        def test_should_be_successful_if_event_has_transition
          assert @record.valid?
        end
        
        def test_should_run_before_callbacks
          ran_callback = false
          @machine.before_transition { ran_callback = true }
          
          @record.valid?
          assert ran_callback
        end
        
        def test_should_run_around_callbacks_before_yield
          ran_callback = false
          @machine.around_transition {|block| ran_callback = true; block.call }
          
          begin
            @record.valid?
          rescue ArgumentError
            raise if StateMachine::Transition.pause_supported?
          end
          assert ran_callback
        end
        
        def test_should_persist_new_state
          @record.valid?
          assert_equal 'idling', @record.state
        end
        
        def test_should_not_run_after_callbacks
          ran_callback = false
          @machine.after_transition { ran_callback = true }
          
          @record.valid?
          assert !ran_callback
        end
        
        def test_should_not_run_after_callbacks_with_failures_disabled_if_validation_fails
          @resource.class_eval do
            attr_accessor :seatbelt
            if respond_to?(:validates_presence_of)
              validates_presence_of :seatbelt
            else
              validates_present :seatbelt
            end
          end
          
          ran_callback = false
          @machine.after_transition { ran_callback = true }
          
          @record.valid?
          assert !ran_callback
        end
        
        def test_should_run_failure_callbacks_if_validation_fails
          @resource.class_eval do
            attr_accessor :seatbelt
            if respond_to?(:validates_presence_of)
              validates_presence_of :seatbelt
            else
              validates_present :seatbelt
            end
          end
          
          ran_callback = false
          @machine.after_failure { ran_callback = true }
          
          @record.valid?
          assert ran_callback
        end
        
        def test_should_not_run_around_callbacks_after_yield
          ran_callback = [false]
          @machine.around_transition {|block| block.call; ran_callback[0] = true }
          
          begin
            @record.valid?
          rescue ArgumentError
            raise if StateMachine::Transition.pause_supported?
          end
          assert !ran_callback[0]
        end
        
        def test_should_not_run_around_callbacks_after_yield_with_failures_disabled_if_validation_fails
          @resource.class_eval do
            attr_accessor :seatbelt
            if respond_to?(:validates_presence_of)
              validates_presence_of :seatbelt
            else
              validates_present :seatbelt
            end
          end
          
          ran_callback = [false]
          @machine.around_transition {|block| block.call; ran_callback[0] = true }
          
          begin
            @record.valid?
          rescue ArgumentError
            raise if StateMachine::Transition.pause_supported?
          end
          assert !ran_callback[0]
        end
        
        def test_should_not_run_before_transitions_within_transaction
          @machine.before_transition { self.class.create; throw :halt }
          
          assert !@record.valid?
          assert_equal 1, @resource.all.size
        end
      end
      
      class MachineWithEventAttributesOnSaveTest < BaseTestCase
        def setup
          @resource = new_resource
          @machine = StateMachine::Machine.new(@resource)
          @machine.event :ignite do
            transition :parked => :idling
          end
          
          @record = @resource.new
          @record.state = 'parked'
          @record.state_event = 'ignite'
        end
        
        def test_should_fail_if_event_is_invalid
          @record.state_event = 'invalid'
          assert !@record.save
        end
        
        def test_should_fail_if_event_has_no_transition
          @record.state = 'idling'
          assert !@record.save
        end
        
        def test_should_be_successful_if_event_has_transition
          assert_equal true, @record.save
        end
        
        def test_should_run_before_callbacks
          ran_callback = false
          @machine.before_transition { ran_callback = true }
          
          @record.save
          assert ran_callback
        end
        
        def test_should_run_before_callbacks_once
          before_count = 0
          @machine.before_transition { before_count += 1 }
          
          @record.save
          assert_equal 1, before_count
        end
        
        def test_should_run_around_callbacks_before_yield
          ran_callback = false
          @machine.around_transition {|block| ran_callback = true; block.call }
          
          @record.save
          assert ran_callback
        end
        
        def test_should_run_around_callbacks_before_yield_once
          around_before_count = 0
          @machine.around_transition {|block| around_before_count += 1; block.call }
          
          @record.save
          assert_equal 1, around_before_count
        end
        
        def test_should_persist_new_state
          @record.save
          assert_equal 'idling', @record.state
        end
        
        def test_should_run_after_callbacks
          ran_callback = false
          @machine.after_transition { ran_callback = true }
          
          @record.save
          assert ran_callback
        end
        
        def test_should_not_run_after_callbacks_with_failures_disabled_if_fails
          @resource.before(:create) { throw :halt }
          
          ran_callback = false
          @machine.after_transition { ran_callback = true }
          
          @record.save
          assert !ran_callback
        end
        
        def test_should_run_failure_callbacks_if_fails
          @resource.before(:create) { throw :halt }
          
          ran_callback = false
          @machine.after_failure { ran_callback = true }
          
          @record.save
          assert ran_callback
        end
        
        def test_should_not_run_around_callbacks_with_failures_disabled_if_fails
          @resource.before(:create) { throw :halt }
          
          ran_callback = [false]
          @machine.around_transition {|block| block.call; ran_callback[0] = true }
          
          @record.save
          assert !ran_callback[0]
        end
        
        def test_should_run_around_callbacks_after_yield
          ran_callback = [false]
          @machine.around_transition {|block| block.call; ran_callback[0] = true }
          
          @record.save
          assert ran_callback[0]
        end
        
        def test_should_not_run_before_transitions_within_transaction
          @machine.before_transition { self.class.create; throw :halt }
          
          assert_equal false, @record.save
          assert_equal 1, @resource.all.size
        end
        
        def test_should_not_run_after_transitions_within_transaction
          @machine.before_transition { self.class.create; throw :halt }
          
          assert_equal false, @record.save
          assert_equal 1, @resource.all.size
        end
        
        def test_should_not_run_around_transition_within_transaction
          @machine.around_transition { self.class.create; throw :halt }
          
          assert_equal false, @record.save
          assert_equal 1, @resource.all.size
        end
        
        def test_should_allow_additional_transitions_to_new_state_in_after_transitions
          @machine.event :park do
            transition :idling => :parked
          end
          
          @machine.after_transition(:on => :ignite) { park }
          
          @record.save
          assert_equal 'parked', @record.state
          
          @record.reload
          assert_equal 'parked', @record.state
        end
        
        def test_should_allow_additional_transitions_to_previous_state_in_after_transitions
          @machine.event :shift_up do
            transition :idling => :first_gear
          end
          
          @machine.after_transition(:on => :ignite) { shift_up }
          
          @record.save
          assert_equal 'first_gear', @record.state
          
          @record.reload
          assert_equal 'first_gear', @record.state
        end
      end
    end
    
    if Gem::Version.new(DataMapper::VERSION) >= Gem::Version.new('0.10.0')
      class MachineWithEventAttributesOnSaveBangTest < BaseTestCase
        def setup
          @resource = new_resource
          @machine = StateMachine::Machine.new(@resource)
          @machine.event :ignite do
            transition :parked => :idling
          end
          
          @record = @resource.new
          @record.state = 'parked'
          @record.state_event = 'ignite'
        end
        
        def test_should_fail_if_event_is_invalid
          @record.state_event = 'invalid'
          assert !@record.save!
        end
        
        def test_should_fail_if_event_has_no_transition
          @record.state = 'idling'
          assert !@record.save!
        end
        
        def test_should_be_successful_if_event_has_transition
          assert_equal true, @record.save!
        end
        
        def test_should_run_before_callbacks
          ran_callback = false
          @machine.before_transition { ran_callback = true }
          
          @record.save!
          assert ran_callback
        end
        
        def test_should_run_before_callbacks_once
          before_count = 0
          @machine.before_transition { before_count += 1 }
          
          @record.save!
          assert_equal 1, before_count
        end
        
        def test_should_run_around_callbacks_before_yield
          ran_callback = false
          @machine.around_transition {|block| ran_callback = true; block.call }
          
          @record.save!
          assert ran_callback
        end
        
        def test_should_run_around_callbacks_before_yield_once
          around_before_count = 0
          @machine.around_transition {|block| around_before_count += 1; block.call }
          
          @record.save!
          assert_equal 1, around_before_count
        end
        
        def test_should_persist_new_state
          @record.save!
          assert_equal 'idling', @record.state
        end
        
        def test_should_run_after_callbacks
          ran_callback = false
          @machine.after_transition { ran_callback = true }
          
          @record.save!
          assert ran_callback
        end
        
        def test_should_run_around_callbacks_after_yield
          ran_callback = [false]
          @machine.around_transition {|block| block.call; ran_callback[0] = true }
          
          @record.save!
          assert ran_callback[0]
        end
      end
    end
    
    if Gem::Version.new(DataMapper::VERSION) > Gem::Version.new('0.9.6')
      class MachineWithEventAttributesOnCustomActionTest < BaseTestCase
        def setup
          @superclass = new_resource do
            def persist
              save
            end
          end
          @resource = Class.new(@superclass)
          @machine = StateMachine::Machine.new(@resource, :action => :persist)
          @machine.event :ignite do
            transition :parked => :idling
          end
          
          @record = @resource.new
          @record.state = 'parked'
          @record.state_event = 'ignite'
        end
        
        def test_should_not_transition_on_valid?
          @record.valid?
          assert_equal 'parked', @record.state
        end
        
        def test_should_not_transition_on_save
          @record.save
          assert_equal 'parked', @record.state
        end
        
        def test_should_transition_on_custom_action
          @record.persist
          assert_equal 'idling', @record.state
        end
      end
    end
  rescue LoadError
    $stderr.puts "Skipping DataMapper Validation tests."
  end
  
  class MachineWithObserversTest < BaseTestCase
    def setup
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource)
      @machine.state :parked, :idling
      @machine.event :ignite
      @record = @resource.new(:state => 'parked')
      @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
    end
    
    def test_should_provide_matcher_helpers
      matchers = []
      
      new_observer(@resource) do
        matchers = [all, any, same]
      end
      
      assert_equal [StateMachine::AllMatcher.instance, StateMachine::AllMatcher.instance, StateMachine::LoopbackMatcher.instance], matchers
    end
    
    def test_should_call_before_transition_callback_if_requirements_match
      called = false
      
      new_observer(@resource) do
        before_transition :from => :parked do
          called = true
        end
      end
      
      @transition.perform
      assert called
    end
    
    def test_should_not_call_before_transition_callback_if_requirements_do_not_match
      called = false
      
      new_observer(@resource) do
        before_transition :from => :idling do
          called = true
        end
      end
      
      @transition.perform
      assert !called
    end
    
    def test_should_pass_transition_to_before_callbacks
      callback_args = nil
      
      new_observer(@resource) do
        before_transition do |*args|
          callback_args = args
        end
      end
      
      @transition.perform
      assert_equal [@transition], callback_args
    end
    
    def test_should_call_after_transition_callback_if_requirements_match
      called = false
      
      new_observer(@resource) do
        after_transition :from => :parked do
          called = true
        end
      end
      
      @transition.perform
      assert called
    end
    
    def test_should_not_call_after_transition_callback_if_requirements_do_not_match
      called = false
      
      new_observer(@resource) do
        after_transition :from => :idling do
          called = true
        end
      end
      
      @transition.perform
      assert !called
    end
    
    def test_should_pass_transition_to_after_callbacks
      callback_args = nil
      
      new_observer(@resource) do
        after_transition do |*args|
          callback_args = args
        end
      end
      
      @transition.perform
      assert_equal [@transition], callback_args
    end
    
    def test_should_call_around_transition_callback_if_requirements_match
      called = false
      
      new_observer(@resource) do
        around_transition :from => :parked do |block|
          called = true
          block.call
        end
      end
      
      @transition.perform
      assert called
    end
    
    def test_should_not_call_around_transition_callback_if_requirements_do_not_match
      called = false
      
      new_observer(@resource) do
        around_transition :from => :idling do |block|
          called = true
          block.call
        end
      end
      
      @transition.perform
      assert !called
    end
    
    def test_should_pass_transition_to_around_callbacks
      callback_args = nil
      
      new_observer(@resource) do
        around_transition do |*args|
          block = args.pop
          callback_args = args
          block.call
        end
      end
      
      @transition.perform
      assert_equal [@transition], callback_args
    end
    
    def test_should_call_failure_callback_if_requirements_match
      @resource.before(:create) { throw :halt } 
      
      called = false
      
      new_observer(@resource) do
        after_transition_failure :on => :ignite do
          called = true
        end
      end
      
      @transition.perform
      assert called
    end
    
    def test_should_not_call_failure_callback_if_requirements_do_not_match
      @resource.before(:create) { throw :halt } 
      
      called = false
      
      new_observer(@resource) do
        after_transition_failure :on => :park do
          called = true
        end
      end
      
      @transition.perform
      assert !called
    end
    
    def test_should_pass_transition_to_failure_callbacks
      @resource.before(:create) { throw :halt } 
      
      callback_args = nil
      
      new_observer(@resource) do
        after_transition_failure do |*args|
          callback_args = args
        end
      end
      
      @transition.perform
      assert_equal [@transition], callback_args
    end
    
    def test_should_raise_exception_if_targeting_invalid_machine
      assert_raise(RUBY_VERSION < '1.9' ? IndexError : KeyError) do
        new_observer(@resource) do
          before_transition :invalid, :from => :parked do
          end
        end
      end
    end
    
    def test_should_allow_targeting_specific_machine
      @second_machine = StateMachine::Machine.new(@resource, :status, :namespace => 'alarm')
      @resource.auto_migrate!
      
      called_state = false
      called_status = false
      
      new_observer(@resource) do
        before_transition :state, :from => :parked do
          called_state = true
        end
        
        before_transition :status, :from => :parked do
          called_status = true
        end
      end
      
      @transition.perform
      
      assert called_state
      assert !called_status
    end
    
    def test_should_allow_targeting_multiple_specific_machines
      @second_machine = StateMachine::Machine.new(@resource, :status, :namespace => 'alarm')
      @second_machine.state :parked, :idling
      @second_machine.event :ignite
      @resource.auto_migrate!
      
      called_attribute = nil
      
      new_observer(@resource) do
        before_transition :state, :status, :from => :parked do |transition|
          called_attribute = transition.attribute
        end
      end
      
      @transition.perform
      assert_equal :state, called_attribute
      
      StateMachine::Transition.new(@record, @second_machine, :ignite, :parked, :idling).perform
      assert_equal :status, called_attribute
    end
  end
  
  class MachineWithMixedCallbacksTest < BaseTestCase
    def setup
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource)
      @machine.state :parked, :idling
      @machine.event :ignite
      @record = @resource.new(:state => 'parked')
      @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
      
      @notifications = notifications = []
      
      # Create callbacks
      @machine.before_transition {notifications << :callback_before_transition}
      @machine.after_transition {notifications << :callback_after_transition}
      @machine.around_transition do |block|
        notifications << :callback_around_before_transition
        block.call
        notifications << :callback_around_after_transition
      end
      
      new_observer(@resource) do
        before_transition do
          notifications << :observer_before_transition
        end
        
        after_transition do
          notifications << :observer_after_transition
        end
        
        around_transition do |block|
          notifications << :observer_around_before_transition
          block.call
          notifications << :observer_around_after_transition
        end
      end
      
      @transition.perform
    end
    
    def test_should_invoke_callbacks_in_specific_order
      expected = [
        :callback_before_transition,
        :callback_around_before_transition,
        :observer_before_transition,
        :observer_around_before_transition,
        :observer_around_after_transition,
        :callback_around_after_transition,
        :callback_after_transition,
        :observer_after_transition
      ]
      
      assert_equal expected, @notifications
    end
  end
  
  class MachineWithScopesTest < BaseTestCase
    def setup
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource)
      @machine.state :parked, :first_gear
      @machine.state :idling, :value => lambda {'idling'}
    end
    
    def test_should_create_singular_with_scope
      assert @resource.respond_to?(:with_state)
    end
    
    def test_should_only_include_records_with_state_in_singular_with_scope
      parked = @resource.create :state => 'parked'
      @resource.create :state => 'idling'
      
      assert_equal [parked], @resource.with_state(:parked)
    end
    
    def test_should_create_plural_with_scope
      assert @resource.respond_to?(:with_states)
    end
    
    def test_should_only_include_records_with_states_in_plural_with_scope
      parked = @resource.create :state => 'parked'
      idling = @resource.create :state => 'idling'
      
      assert_equal [parked, idling], @resource.with_states(:parked, :idling)
    end
    
    def test_should_allow_lookup_by_string_name
      parked = @resource.create :state => 'parked'
      idling = @resource.create :state => 'idling'
      
      assert_equal [parked, idling], @resource.with_states('parked', 'idling')
    end
    
    def test_should_create_singular_without_scope
      assert @resource.respond_to?(:without_state)
    end
    
    def test_should_only_include_records_without_state_in_singular_without_scope
      parked = @resource.create :state => 'parked'
      idling = @resource.create :state => 'idling'
      
      assert_equal [parked], @resource.without_state(:idling)
    end
    
    def test_should_create_plural_without_scope
      assert @resource.respond_to?(:without_states)
    end
    
    def test_should_only_include_records_without_states_in_plural_without_scope
      parked = @resource.create :state => 'parked'
      idling = @resource.create :state => 'idling'
      first_gear = @resource.create :state => 'first_gear'
      
      assert_equal [parked, idling], @resource.without_states(:first_gear)
    end
    
    def test_should_allow_chaining_scopes
      parked = @resource.create :state => 'parked'
      idling = @resource.create :state => 'idling'
      
      assert_equal [idling], @resource.without_state(:parked).with_state(:idling)
    end
  end
  
  class MachineWithScopesAndOwnerSubclassTest < BaseTestCase
    def setup
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource, :state)
      
      @subclass = Class.new(@resource)
      DataMapperTest.const_set('Bar', @subclass)
      @resources << 'Bar'
      @subclass.auto_migrate!
      @subclass_machine = @subclass.state_machine(:state) {}
      @subclass_machine.state :parked, :idling, :first_gear
    end
    
    def test_should_only_include_records_with_subclass_states_in_with_scope
      parked = @subclass.create :state => 'parked'
      idling = @subclass.create :state => 'idling'
      
      assert_equal [parked, idling], @subclass.with_states(:parked, :idling)
    end
    
    def test_should_only_include_records_without_subclass_states_in_without_scope
      parked = @subclass.create :state => 'parked'
      idling = @subclass.create :state => 'idling'
      first_gear = @subclass.create :state => 'first_gear'
      
      assert_equal [parked, idling], @subclass.without_states(:first_gear)
    end
  end
  
  class MachineWithComplexPluralizationScopesTest < BaseTestCase
    def setup
      @resource = new_resource
      @machine = StateMachine::Machine.new(@resource, :status)
    end
    
    def test_should_create_singular_with_scope
      assert @resource.respond_to?(:with_status)
    end
    
    def test_should_create_plural_with_scope
      assert @resource.respond_to?(:with_statuses)
    end
  end
  
  class MachineWithScopesAndJoinsTest < BaseTestCase
    def setup
      @company = new_resource(:company)
      
      @vehicle = new_resource(:vehicle) do
        property :company_id, Integer
        
        belongs_to :company
      end
      
      @company_machine = StateMachine::Machine.new(@company, :initial => :active)
      @vehicle_machine = StateMachine::Machine.new(@vehicle, :initial => :parked)
      @vehicle_machine.state :idling
      
      @ford = @company.create
      @mustang = @vehicle.create(:company => @ford)
    end
    
    def test_should_find_records_in_with_scope
      assert_equal [@mustang], @vehicle.with_states(:parked).all(Vehicle.company.state => 'active')
    end
    
    def test_should_find_records_in_without_scope
      assert_equal [@mustang], @vehicle.without_states(:idling).all(Vehicle.company.state => 'active')
    end
  end
end
