require 'helper'

class CreateTrafficLights < ActiveRecord::Migration[4.2]
  def self.up
    create_table(:traffic_lights, force: true) do |t|
      t.string :state
      t.string :name
      t.string :power
    end
  end
end

class CreateDifferentTrafficLights < ActiveRecord::Migration[4.2]
  def self.up
    create_table(:different_traffic_lights) do |t|
      t.string :different_state
      t.string :name
    end
  end
end

class TrafficLight < ActiveRecord::Base
  include ActiveModel::Transitions

  state_machine auto_scopes: true do
    state :off, enter: :turn_power_on

    state :red
    state :green
    state :yellow

    event :red_on do
      transitions to: :red, from: [:yellow]
    end

    event :green_on do
      transitions to: :green, from: [:red]
    end

    event :yellow_on do
      transitions to: :yellow, from: [:green]
    end

    event :reset do
      transitions to: :red, from: [:off]
    end
  end

  def turn_power_on
    fail 'the power should not have been on already' if power == 'on'
    self.power = 'on'
  end
end

class ValidatingTrafficLight < TrafficLight
  validate { |t| errors.add(:base, 'This TrafficLight will never validate after creation') unless t.new_record? }
end

class ConditionalValidatingTrafficLight < TrafficLight
  validates(:name, presence: true, if: :red?)
end

class TestActiveRecord < Test::Unit::TestCase
  def setup
    set_up_db CreateTrafficLights
    @light = TrafficLight.create!
  end

  test 'new record has the initial state set' do
    @light = TrafficLight.new
    assert_equal 'off', @light.state
  end

  test 'new active records defaults current state to the initial state' do
    assert_equal :off, @light.current_state
  end

  test 'states initial state' do
    assert @light.off?
    assert_equal :off, @light.current_state
  end

  test 'calls enter when setting the initial state' do
    @new_light = TrafficLight.new
    assert_equal 'on', @new_light.power
  end

  test 'does not call enter when loading a persisted record' do
    assert_equal 'on', @light.power
    assert_nothing_raised { TrafficLight.find(@light.id) }
  end

  test 'transition to a valid state' do
    @light.reset
    assert @light.red?
    assert_equal :red, @light.current_state

    @light.green_on
    assert @light.green?
    assert_equal :green, @light.current_state
  end

  test 'transition does not persist state' do
    @light.reset
    assert_equal :red, @light.current_state
    @light.reload
    assert_equal 'off', @light.state
  end

  test 'transition does persists state' do
    @light.reset!
    assert_equal :red, @light.current_state
    @light.reload
    assert_equal 'red', @light.state
  end

  test 'transition to an invalid state' do
    assert_raise(Transitions::InvalidTransition) { @light.yellow_on }
    assert_equal :off, @light.current_state
  end

  test 'transition with wrong state will not validate' do
    for s in @light.class.get_state_machine.states
      @light.state = s.name
      assert @light.valid?
    end
    @light.state = 'invalid_one'
    assert_false @light.valid?
  end

  test 'transition raises exception when model validation fails' do
    validating_light = ValidatingTrafficLight.create!(name: 'Foobar')
    assert_raise(ActiveRecord::RecordInvalid) do
      validating_light.reset!
    end
  end

  test 'state query method used in a validation condition' do
    validating_light = ConditionalValidatingTrafficLight.create!
    assert_raise(ActiveRecord::RecordInvalid) do
      validating_light.reset!
    end
    assert(validating_light.off?)
  end

  test 'reloading model resets current state' do
    @light.reset
    assert @light.red?
    @light.update_attribute(:state, 'green')
    assert @light.reload.green?, 'reloaded state should come from database, not instance variable'
  end

  test 'calling non-bang event updates state attribute' do
    @light.reset!
    assert @light.red?
    @light.green_on
    assert_equal 'green', @light.state
    assert_equal 'red', @light.reload.state
  end
end

if ActiveRecord::VERSION::MAJOR == 3

  class TestMassAssignmentActiveRecord < Test::Unit::TestCase
    # attr_protected unfortunately invokes a db call, so this test requires that
    # we define the class after the table already exists.
    def setup
      set_up_db CreateTrafficLights

      @light_with_protected_state = Class.new(TrafficLight) do
        attr_protected :state
      end
    end

    test 'transition does persists state when state is protected' do
      protected_light = @light_with_protected_state.create!
      protected_light.reset!
      assert_equal :red, protected_light.current_state
      protected_light.reload
      assert_equal 'red', protected_light.state
    end
  end
end

class TestNewActiveRecord < TestActiveRecord
  def setup
    set_up_db CreateTrafficLights
    @light = TrafficLight.new
  end

  test 'new active records defaults current state to the initial state' do
    assert_equal :off, @light.current_state
  end
end

class TestScopes < Test::Unit::TestCase
  test 'scope returns correct object' do
    @light = TrafficLight.create!
    assert_respond_to TrafficLight, :off
    assert_equal TrafficLight.off.first, @light
    assert TrafficLight.red.empty?
  end

  test 'scopes exist' do
    assert_respond_to TrafficLight, :off
    assert_respond_to TrafficLight, :red
    assert_respond_to TrafficLight, :green
    assert_respond_to TrafficLight, :yellow
  end

  test 'scopes are only generated if we explicitly say so' do
    assert_not_respond_to LightBulb, :off
    assert_not_respond_to LightBulb, :on
  end

  test 'scope generation raises an exception if we try to overwrite an existing method' do
    assert_raise(Transitions::InvalidMethodOverride) do
      class Light < ActiveRecord::Base
        include ActiveModel::Transitions

        state_machine auto_scopes: true do
          state :new
          state :broken
        end
      end
    end
  end
end

class DifferentTrafficLight < ActiveRecord::Base
  include ActiveModel::Transitions

  state_machine attribute_name: :different_state, auto_scopes: true do
    state :off

    state :red
    state :green
    state :yellow

    event :red_on do
      transitions to: :red, from: [:yellow]
    end

    event :green_on do
      transitions to: :green, from: [:red]
    end

    event :yellow_on do
      transitions to: :yellow, from: [:green]
    end

    event :reset do
      transitions to: :red, from: [:off]
    end
  end
end

class TestActiveRecordWithDifferentColumnName < Test::Unit::TestCase
  def setup
    set_up_db CreateDifferentTrafficLights
    @light = DifferentTrafficLight.create!
  end

  test 'new record has the initial state set' do
    @light = DifferentTrafficLight.new
    assert_equal 'off', @light.different_state
  end

  test 'states initial state' do
    assert @light.off?
    assert_equal :off, @light.current_state
  end

  test 'transition to a valid state' do
    @light.reset
    assert @light.red?
    assert_equal :red, @light.current_state

    @light.green_on
    assert @light.green?
    assert_equal :green, @light.current_state
  end

  test 'transition does not persist state' do
    @light.reset
    assert_equal :red, @light.current_state
    @light.reload
    assert_equal 'off', @light.different_state
  end

  test 'transition does persists state' do
    @light.reset!
    assert_equal :red, @light.current_state
    @light.reload
    assert_equal 'red', @light.different_state
  end

  test 'transition to an invalid state' do
    assert_raise(Transitions::InvalidTransition) { @light.yellow_on }
    assert_equal :off, @light.current_state
  end

  test 'transition with wrong state will not validate' do
    for s in @light.class.state_machine.states
      @light.different_state = s.name
      assert @light.valid?
    end
    @light.different_state = 'invalid_one'
    assert_false @light.valid?
  end

  test 'reloading model resets current state' do
    @light.reset
    assert @light.red?
    @light.update_attribute(:different_state, 'green')
    assert @light.reload.green?, 'reloaded state should come from database, not instance variable'
  end

  test 'calling non-bang event updates state attribute' do
    @light.reset!
    assert @light.red?
    @light.green_on
    assert_equal 'green', @light.different_state
    assert_equal 'red', @light.reload.different_state
  end
end
