require 'unit_spec_helper'

describe Shoulda::Matchers::ActiveModel::ValidateExclusionOfMatcher, type: :model do
  context 'an attribute which must be excluded from a range' do
    context 'passing multiple attributes' do
      it 'accepts when every attribute has the validation' do
        model = define_model 'Example', attr1: :integer, attr2: :integer do
          validates_exclusion_of :attr1, :attr2, in: 2..5
        end

        expect(model.new).
          to validate_exclusion_of(:attr1, :attr2).in_range(2..5)
      end

      it 'rejects when one attribute has the validation and one does not' do
        model = define_model 'Example', attr1: :integer, attr2: :integer do
          validates_exclusion_of :attr1, in: 2..5
        end

        assertion = lambda do
          expect(model.new).
            to validate_exclusion_of(:attr1, :attr2).in_range(2..5)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr2 lies outside the range ‹2› to
‹5›, but this could not be proved.
  After setting :attr2 to ‹2›, the matcher expected the Example to be
  invalid, but it was valid instead.
        MESSAGE

        expect(&assertion).to fail_with_message(message)
      end

      it 'rejects when no attribute has the validation' do
        model = define_model 'Example', attr1: :integer, attr2: :integer

        assertion = lambda do
          expect(model.new).
            to validate_exclusion_of(:attr1, :attr2).in_range(2..5)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr1 lies outside the range ‹2› to
‹5› and validate that :attr2 lies outside the range ‹2› to ‹5›, but this
could not be proved.
  After setting :attr1 to ‹2›, the matcher expected the Example to be
  invalid, but it was valid instead.
  After setting :attr2 to ‹2›, the matcher expected the Example to be
  invalid, but it was valid instead.
        MESSAGE

        expect(&assertion).to fail_with_message(message)
      end
    end

    it 'accepts ensuring the correct range' do
      expect(validating_exclusion(in: 2..5)).
        to validate_exclusion_of(:attr).in_range(2..5)
    end

    it 'rejects if the given range spills past the top of the range in the validation' do
      expect(validating_exclusion(in: 2..5)).
        not_to validate_exclusion_of(:attr).in_range(2..6)
    end

    it 'rejects if the given range falls short of the top of the range in the validation' do
      expect(validating_exclusion(in: 2..5)).
        not_to validate_exclusion_of(:attr).in_range(2..4)
    end

    it 'rejects if the given range spills past the bottom of the range in the validation' do
      expect(validating_exclusion(in: 2..5)).
        not_to validate_exclusion_of(:attr).in_range(1..5)
    end

    it 'rejects if the given range falls short of the bottom of the range in the validation' do
      expect(validating_exclusion(in: 2..5)).
        not_to validate_exclusion_of(:attr).in_range(3..5)
    end

    it 'does not override the default message with a blank' do
      expect(validating_exclusion(in: 2..5)).
        to validate_exclusion_of(:attr).in_range(2..5).with_message(nil)
    end

    it_supports(
      'ignoring_interference_by_writer',
      tests: {
        reject_if_qualified_but_changing_value_interferes: {
          model_name: 'Example',
          attribute_name: :attr,
          changing_values_with: :next_value,
          expected_message: <<-MESSAGE.strip,
Expected Example to validate that :attr lies outside the range ‹2› to
‹5›, but this could not be proved.
  After setting :attr to ‹1› -- which was read back as ‹2› -- the
  matcher expected the Example to be valid, but it was invalid instead,
  producing these validation errors:

  * attr: ["is reserved"]

  As indicated in the message above, :attr seems to be changing certain
  values as they are set, and this could have something to do with why
  this test is failing. If you've overridden the writer method for this
  attribute, then you may need to change it to make this test pass, or
  do something else entirely.
          MESSAGE
        },
      },
      model_creator: :active_model,
    ) do
      def validation_matcher_scenario_args
        super.deep_merge(validation_options: { in: 2..5 })
      end

      def configure_validation_matcher(matcher)
        matcher.in_range(2..5)
      end
    end

    it 'fails when used in the negative' do
      assertion = lambda do
        expect(validating_exclusion(in: 2..5)).
          not_to validate_exclusion_of(:attr).in_range(2..5)
      end

      message = <<-MESSAGE
Expected Example not to validate that :attr lies outside the range ‹2›
to ‹5›, but this could not be proved.
  After setting :attr to ‹6›, the matcher expected the Example to be
  invalid, but it was valid instead.
      MESSAGE

      expect(&assertion).to fail_with_message(message)
    end
  end

  context 'an attribute which must be excluded from a range with excluded end' do
    it 'accepts ensuring the correct range' do
      expect(validating_exclusion(in: 2...5)).
        to validate_exclusion_of(:attr).in_range(2...5)
    end

    it 'rejects ensuring excluded value' do
      expect(validating_exclusion(in: 2...5)).
        not_to validate_exclusion_of(:attr).in_range(2...4)
    end
  end

  context 'an attribute with a custom validation message' do
    it 'accepts ensuring the correct range' do
      expect(validating_exclusion(in: 2..4, message: 'not good')).
        to validate_exclusion_of(:attr).in_range(2..4).with_message(/not good/)
    end

    it 'accepts ensuring the correct range with an interpolated variable in the message' do
      matcher = validating_exclusion(in: 2..4, message: '%{value} is not good')
      expect(matcher).
        to validate_exclusion_of(:attr).
        in_range(2..4).
        with_message(/^[234] is not good$/)
    end
  end

  context 'an attribute with custom range validations' do
    it 'accepts ensuring the correct range and messages' do
      model = custom_validation do
        if attr >= 2 && attr <= 5
          errors.add(:attr, 'should be out of this range')
        end
      end

      expect(model).to validate_exclusion_of(:attr).in_range(2..5).
        with_message(/should be out of this range/)

      model = custom_validation do
        if attr >= 2 && attr <= 4
          errors.add(:attr, 'should be out of this range')
        end
      end

      expect(model).to validate_exclusion_of(:attr).in_range(2...5).
        with_message(/should be out of this range/)
    end

    it 'has correct description' do
      matcher = validate_exclusion_of(:attr).in_range(1..10)

      expect(matcher.description).to eq(
        'validate that :attr lies outside the range ‹1› to ‹10›',
      )
    end
  end

  context 'an attribute which must be excluded from an array' do
    it 'accepts with correct array' do
      expect(validating_exclusion(in: %w(one two))).
        to validate_exclusion_of(:attr).in_array(%w(one two))
    end

    it 'rejects when only part of array matches' do
      expect(validating_exclusion(in: %w(one two))).
        not_to validate_exclusion_of(:attr).in_array(%w(one wrong_value))
    end

    it 'rejects when array does not match at all' do
      expect(validating_exclusion(in: %w(one two))).
        not_to validate_exclusion_of(:attr).in_array(%w(cat dog))
    end

    context 'when there is one value' do
      it 'has correct description' do
        expect(validate_exclusion_of(:attr).in_array([true]).description).
          to eq 'validate that :attr is not ‹true›'
      end
    end

    context 'when there are two values' do
      it 'has correct description' do
        matcher = validate_exclusion_of(:attr).in_array([true, 'dog'])

        expect(matcher.description).to eq(
          'validate that :attr is neither ‹true› nor ‹"dog"›',
        )
      end
    end

    context 'when there are three or more values' do
      it 'has correct description' do
        matcher = validate_exclusion_of(:attr).in_array([true, 'dog', 'cat'])

        expect(matcher.description).to eq(
          'validate that :attr is neither ‹true›, ‹"dog"›, nor ‹"cat"›',
        )
      end
    end

    it_supports(
      'ignoring_interference_by_writer',
      tests: {
        reject_if_qualified_but_changing_value_interferes: {
          model_name: 'Example',
          attribute_name: :attr,
          changing_values_with: :next_value,
          expected_message: <<-MESSAGE.strip,
Expected Example to validate that :attr is neither ‹"one"› nor ‹"two"›,
but this could not be proved.
  After setting :attr to ‹"one"› -- which was read back as ‹"onf"› --
  the matcher expected the Example to be invalid, but it was valid
  instead.

  As indicated in the message above, :attr seems to be changing certain
  values as they are set, and this could have something to do with why
  this test is failing. If you've overridden the writer method for this
  attribute, then you may need to change it to make this test pass, or
  do something else entirely.
          MESSAGE
        },
      },
      model_creator: :active_model,
    ) do
      def validation_matcher_scenario_args
        super.deep_merge(validation_options: { in: ['one', 'two'] })
      end

      def configure_validation_matcher(matcher)
        matcher.in_array(['one', 'two'])
      end
    end

    it 'fails when used in the negative' do
      assertion = lambda do
        expect(validating_exclusion(in: %w(one two))).
          not_to validate_exclusion_of(:attr).in_array(%w(one two))
      end

      message = <<-MESSAGE
Expected Example not to validate that :attr is neither ‹"one"› nor
‹"two"›, but this could not be proved.
  After setting :attr to ‹"two"›, the matcher expected the Example to be
  valid, but it was invalid instead, producing these validation errors:

  * attr: ["is reserved"]
      MESSAGE

      expect(&assertion).to fail_with_message(message)
    end

    def define_model_validating_exclusion(options)
      options = options.dup
      column_type = options.delete(:column_type) { :string }
      super options.merge!(column_type:)
    end
  end

  def define_model_validating_exclusion(options)
    options = options.dup
    attribute_name = options.delete(:attribute_name) { :attr }
    column_type = options.delete(:column_type) { :integer }

    define_model(:example, attribute_name => column_type) do |model|
      model.validates_exclusion_of(attribute_name, options)
    end
  end

  def validating_exclusion(options)
    define_model_validating_exclusion(options).new
  end

  alias_method :build_record_validating_exclusion, :validating_exclusion

  def validation_matcher_scenario_args
    super.deep_merge(matcher_name: :validate_exclusion_of)
  end
end
