require 'unit_spec_helper'

describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher, type: :model do
  context 'passing multiple attributes' do
    it 'accepts when every attribute has the validation' do
      model = define_model 'Example', attr1: :string, attr2: :string do
        validates_numericality_of :attr1, :attr2
      end

      expect(model.new).to validate_numericality_of(:attr1, :attr2)
    end

    it 'rejects when one attribute has the validation and one does not' do
      model = define_model 'Example', attr1: :string, attr2: :string do
        validates_numericality_of :attr1, allow_nil: true
      end

      assertion = lambda do
        expect(model.new).to validate_numericality_of(:attr1, :attr2)
      end

      message = <<-MESSAGE
Expected Example to validate that :attr2 looks like a number, but this
could not be proved.
  After setting :attr2 to ‹"abcd"›, 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: :string, attr2: :string

      assertion = lambda do
        expect(model.new).to validate_numericality_of(:attr1, :attr2)
      end

      message = <<-MESSAGE
Expected Example to validate that :attr1 looks like a number and
validate that :attr2 looks like a number, but this could not be proved.
  After setting :attr1 to ‹"abcd"›, the matcher expected the Example to
  be invalid, but it was valid instead.
  After setting :attr2 to ‹"abcd"›, the matcher expected the Example to
  be invalid, but it was valid instead.
      MESSAGE

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

  class << self
    def all_qualifiers # rubocop:disable Metrics/MethodLength
      [
        {
          category: :comparison,
          name: :is_greater_than,
          argument: 1,
          validation_name: :greater_than,
          validation_value: 1,
        },
        {
          category: :comparison,
          name: :is_greater_than_or_equal_to,
          argument: 1,
          validation_name: :greater_than_or_equal_to,
          validation_value: 1,
        },
        {
          category: :comparison,
          name: :is_less_than,
          argument: 1,
          validation_name: :less_than,
          validation_value: 1,
        },
        {
          category: :comparison,
          name: :is_less_than_or_equal_to,
          argument: 1,
          validation_name: :less_than_or_equal_to,
          validation_value: 1,
        },
        {
          category: :comparison,
          name: :is_equal_to,
          argument: 1,
          validation_name: :equal_to,
          validation_value: 1,
        },
        {
          category: :comparison,
          name: :is_other_than,
          argument: 1,
          validation_name: :other_than,
          validation_value: 1,
        },
        {
          category: :cardinality,
          name: :odd,
          validation_name: :odd,
          validation_value: true,
        },
        {
          category: :cardinality,
          name: :even,
          validation_name: :even,
          validation_value: true,
        },
        {
          name: :only_integer,
          validation_name: :only_integer,
          validation_value: true,
        },
        {
          name: :on,
          argument: :customizable,
          validation_name: :on,
          validation_value: :customizable,
        },
        {
          category: :range,
          name: :is_in,
          argument: 1..10,
          validation_name: :in,
          validation_value: 1..10,
          rails_version: 7.0,
        },
      ]
    end

    def qualifiers_under(category)
      all_available_qualifiers.select do |qualifier|
        qualifier[:category] == category
      end
    end

    def mutually_exclusive_qualifiers
      qualifiers_under(:cardinality) + qualifiers_under(:comparison) + qualifiers_under(:range)
    end

    def non_mutually_exclusive_qualifiers
      all_available_qualifiers - mutually_exclusive_qualifiers
    end

    def all_available_qualifiers
      all_qualifiers.filter do |qualifier|
        rails_version >= qualifier.fetch(:rails_version, rails_oldest_version_supported)
      end
    end

    def all_qualifier_combinations
      combinations = []

      ([nil] + mutually_exclusive_qualifiers).each do |mutually_exclusive_qualifier|
        (0..non_mutually_exclusive_qualifiers.length).each do |n|
          non_mutually_exclusive_qualifiers.combination(n) do |combination|
            super_combination = (
              [mutually_exclusive_qualifier] +
              combination
            )
            super_combination.select!(&:present?)

            if super_combination.any?
              combinations << super_combination
            end
          end
        end
      end

      combinations
    end

    def default_qualifier_arguments
      all_qualifiers.each_with_object({}) do |qualifier, hash|
        hash[qualifier[:name]] = qualifier[:argument]
      end
    end

    def default_validation_values
      all_qualifiers.each_with_object({}) do |qualifier, hash|
        hash[qualifier[:validation_name]] = qualifier[:validation_value]
      end
    end
  end

  context 'qualified with nothing' do
    context 'and validating numericality' do
      it 'accepts' do
        record = build_record_validating_numericality
        expect(record).to validate_numericality
      end

      it_supports(
        'ignoring_interference_by_writer',
        tests: {
          accept_if_qualified_but_changing_value_does_not_interfere: {
            changing_values_with: :next_value,
          },
          reject_if_qualified_but_changing_value_interferes: {
            model_name: 'Example',
            attribute_name: :attr,
            changing_values_with: :numeric_value,
            expected_message: <<-MESSAGE.strip,
Expected Example to validate that :attr looks like a number, but this
could not be proved.
  After setting :attr to ‹"abcd"› -- which was read back as ‹"1"› -- 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
          },
        },
      )

      context 'when the attribute is a virtual attribute in an ActiveRecord model' do
        it 'accepts' do
          record = build_record_validating_numericality_of_virtual_attribute
          expect(record).to validate_numericality
        end
      end

      context 'when the column is an integer column' do
        it 'accepts (and does not raise an AttributeChangedValueError)' do
          record = build_record_validating_numericality(column_type: :integer)
          expect(record).to validate_numericality
        end
      end

      context 'when the column is a float column' do
        it 'accepts (and does not raise an AttributeChangedValueError)' do
          record = build_record_validating_numericality(column_type: :float)
          expect(record).to validate_numericality
        end
      end

      context 'when the column is a decimal column' do
        it 'accepts (and does not raise an AttributeChangedValueError)' do
          record = build_record_validating_numericality(column_type: :decimal)
          expect(record).to validate_numericality
        end
      end

      if database_supports_money_columns?
        context 'when the column is a money column' do
          it 'accepts (and does not raise an AttributeChangedValueError)' do
            record = build_record_validating_numericality(column_type: :money)
            expect(record).to validate_numericality
          end
        end
      end

      context 'when used in the negative' do
        it 'fails' do
          assertion = lambda do
            record = build_record_validating_numericality
            expect(record).not_to validate_numericality
          end

          message = <<-MESSAGE
Expected Example not to validate that :attr looks like a number, but
this could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be valid, but it was invalid instead, producing these validation
  errors:

  * attr: ["is not a number"]
          MESSAGE

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

    context 'and not validating anything' do
      it 'rejects since it does not disallow non-numbers' do
        record = build_record_validating_nothing

        assertion = -> { expect(record).to validate_numericality }

        message = <<-MESSAGE
Expected Example to validate that :attr looks like a number, but this
could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be invalid, but it was valid instead.
        MESSAGE

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

  context 'qualified with allow_nil' do
    context 'and validating with allow_nil' do
      it 'accepts' do
        record = build_record_validating_numericality(allow_nil: true)
        expect(record).to validate_numericality.allow_nil
      end

      it_supports(
        'ignoring_interference_by_writer',
        tests: {
          accept_if_qualified_but_changing_value_does_not_interfere: {
            changing_values_with: :next_value_or_numeric_value,
          },
          reject_if_qualified_but_changing_value_interferes: {
            model_name: 'Example',
            attribute_name: :attr,
            changing_values_with: :next_value_or_non_numeric_value,
            expected_message: <<-MESSAGE.strip,
Expected Example to validate that :attr looks like a number as long as
it is not nil, but this could not be proved.
  In checking that Example allows :attr to be ‹nil›, after setting :attr
  to ‹nil› -- which was read back as ‹"a"› -- the matcher expected the
  Example to be valid, but it was invalid instead, producing these
  validation errors:

  * attr: ["is not a number"]

  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
          },
        },
      ) do
        def validation_matcher_scenario_args
          super.deep_merge(validation_options: { allow_nil: true })
        end

        def configure_validation_matcher(matcher)
          matcher.allow_nil
        end
      end
    end

    context 'and not validating with allow_nil' do
      it 'rejects since it tries to treat nil as a number' do
        record = build_record_validating_numericality

        assertion = lambda do
          expect(record).to validate_numericality.allow_nil
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like a number as long as
it is not nil, but this could not be proved.
  In checking that Example allows :attr to be ‹nil›, after setting :attr
  to ‹nil›, the matcher expected the Example to be valid, but it was
  invalid instead, producing these validation errors:

  * attr: ["is not a number"]
        MESSAGE

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

  context 'qualified with only_integer' do
    context 'and validating with only_integer' do
      it 'accepts' do
        record = build_record_validating_numericality(only_integer: true)
        expect(record).to validate_numericality.only_integer
      end

      it 'rejects when used in the negative' do
        record = build_record_validating_numericality(only_integer: true)

        assertion = lambda do
          expect(record).not_to validate_numericality.only_integer
        end

        expect(&assertion).to fail_with_message(<<~MESSAGE)
Expected Example not to validate that :attr looks like an integer, but
this could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be valid, but it was invalid instead, producing these validation
  errors:

  * attr: ["is not a number"]
        MESSAGE
      end

      it_supports(
        'ignoring_interference_by_writer',
        tests: {
          accept_if_qualified_but_changing_value_does_not_interfere: {
            changing_values_with: :next_value,
          },
          reject_if_qualified_but_changing_value_interferes: {
            model_name: 'Example',
            attribute_name: :attr,
            changing_values_with: :numeric_value,
            expected_message: <<-MESSAGE.strip,
Expected Example to validate that :attr looks like an integer, but this
could not be proved.
  After setting :attr to ‹"abcd"› -- which was read back as ‹"1"› -- 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
          },
        },
      ) do
        def validation_matcher_scenario_args
          super.deep_merge(validation_options: { only_integer: true })
        end

        def configure_validation_matcher(matcher)
          matcher.only_integer
        end
      end
    end

    context 'and not validating with only_integer' do
      it 'rejects since it does not disallow non-integers' do
        record = build_record_validating_numericality

        assertion = lambda do
          expect(record).to validate_numericality.only_integer
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an integer, but this
could not be proved.
  After setting :attr to ‹"0.1"›, the matcher expected the Example to be
  invalid, but it was valid instead.
        MESSAGE

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

  context 'qualified with odd' do
    context 'and validating with odd' do
      it 'accepts' do
        record = build_record_validating_numericality(odd: true)
        expect(record).to validate_numericality.odd
      end

      it 'rejects when used in the negative' do
        record = build_record_validating_numericality(odd: true)

        assertion = lambda do
          expect(record).not_to validate_numericality.odd
        end

        expect(&assertion).to fail_with_message(<<~MESSAGE)
Expected Example not to validate that :attr looks like an odd number,
but this could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be valid, but it was invalid instead, producing these validation
  errors:

  * attr: ["is not a number"]
        MESSAGE
      end

      it_supports(
        'ignoring_interference_by_writer',
        tests: {
          accept_if_qualified_but_changing_value_does_not_interfere: {
            changing_values_with: :next_next_value,
          },
          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 looks like an odd number, but
this could not be proved.
  After setting :attr to ‹"2"› -- which was read back as ‹"3"› -- 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
          },
        },
      ) do
        def validation_matcher_scenario_args
          super.deep_merge(validation_options: { odd: true })
        end

        def configure_validation_matcher(matcher)
          matcher.odd
        end
      end

      context 'when the attribute is a virtual attribute in ActiveRecord model' do
        it 'accepts' do
          record = build_record_validating_numericality_of_virtual_attribute(
            odd: true,
          )
          expect(record).to validate_numericality.odd
        end
      end

      context 'when the column is an integer column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :integer,
            odd: true,
          )

          expect(record).to validate_numericality.odd
        end
      end

      context 'when the column is a float column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :float,
            odd: true,
          )

          expect(record).to validate_numericality.odd
        end
      end

      context 'when the column is a decimal column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :decimal,
            odd: true,
          )

          expect(record).to validate_numericality.odd
        end
      end
    end

    context 'and not validating with odd' do
      it 'rejects since it does not disallow even numbers' do
        record = build_record_validating_numericality

        assertion = lambda do
          expect(record).to validate_numericality.odd
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an odd number, but
this could not be proved.
  After setting :attr 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
  end

  context 'qualified with even' do
    context 'and validating with even' do
      it 'accepts' do
        record = build_record_validating_numericality(even: true)
        expect(record).to validate_numericality.even
      end

      it 'rejects when used in the negative' do
        record = build_record_validating_numericality(even: true)

        assertion = lambda do
          expect(record).not_to validate_numericality.even
        end

        expect(&assertion).to fail_with_message(<<~MESSAGE)
Expected Example not to validate that :attr looks like an even number,
but this could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be valid, but it was invalid instead, producing these validation
  errors:

  * attr: ["is not a number"]
        MESSAGE
      end

      it_supports(
        'ignoring_interference_by_writer',
        tests: {
          accept_if_qualified_but_changing_value_does_not_interfere: {
            changing_values_with: :next_next_value,
          },
          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 looks like an even number, but
this could not be proved.
  After setting :attr to ‹"1"› -- which was read back as ‹"2"› -- 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
          },
        },
      ) do
        def validation_matcher_scenario_args
          super.deep_merge(validation_options: { even: true })
        end

        def configure_validation_matcher(matcher)
          matcher.even
        end
      end

      context 'when the attribute is a virtual attribute in an ActiveRecord model' do
        it 'accepts' do
          record = build_record_validating_numericality_of_virtual_attribute(
            even: true,
          )
          expect(record).to validate_numericality.even
        end
      end

      context 'when the column is an integer column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :integer,
            even: true,
          )

          expect(record).to validate_numericality.even
        end
      end

      context 'when the column is a float column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :float,
            even: true,
          )

          expect(record).to validate_numericality.even
        end
      end

      context 'when the column is a decimal column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :decimal,
            even: true,
          )

          expect(record).to validate_numericality.even
        end
      end
    end

    context 'and not validating with even' do
      it 'rejects since it does not disallow odd numbers' do
        record = build_record_validating_numericality

        assertion = lambda do
          expect(record).to validate_numericality.even
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an even number, but
this could not be proved.
  After setting :attr to ‹"1"›, the matcher expected the Example to be
  invalid, but it was valid instead.
        MESSAGE

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

  context 'qualified with is_less_than_or_equal_to' do
    context 'and validating with less_than_or_equal_to' do
      it 'accepts' do
        record = build_record_validating_numericality(less_than_or_equal_to: 18)
        expect(record).to validate_numericality.is_less_than_or_equal_to(18)
      end

      it 'rejects when used in the negative' do
        record = build_record_validating_numericality(less_than_or_equal_to: 18)

        assertion = lambda do
          expect(record).not_to validate_numericality.
            is_less_than_or_equal_to(18)
        end

        expect(&assertion).to fail_with_message(<<~MESSAGE)
Expected Example not to validate that :attr looks like a number less
than or equal to 18, but this could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be valid, but it was invalid instead, producing these validation
  errors:

  * attr: ["is not a number"]
        MESSAGE
      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 looks like a number less than or
equal to 18, but this could not be proved.
  After setting :attr to ‹"18"› -- which was read back as ‹"19"› -- the
  matcher expected the Example to be valid, but it was invalid instead,
  producing these validation errors:

  * attr: ["must be less than or equal to 18"]

  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
          },
        },
      ) do
        def validation_matcher_scenario_args
          super.deep_merge(
            validation_options: { less_than_or_equal_to: 18 },
          )
        end

        def configure_validation_matcher(matcher)
          matcher.is_less_than_or_equal_to(18)
        end
      end

      context 'when the attribute is a virtual attribute in an ActiveRecord model' do
        it 'accepts' do
          record = build_record_validating_numericality_of_virtual_attribute(
            less_than_or_equal_to: 18,
          )
          expect(record).to validate_numericality.is_less_than_or_equal_to(18)
        end
      end

      context 'when the column is an integer column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :integer,
            less_than_or_equal_to: 18,
          )

          expect(record).to validate_numericality.is_less_than_or_equal_to(18)
        end
      end

      context 'when the column is a float column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :float,
            less_than_or_equal_to: 18,
          )

          expect(record).to validate_numericality.is_less_than_or_equal_to(18)
        end
      end

      context 'when the column is a decimal column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :decimal,
            less_than_or_equal_to: 18,
          )

          expect(record).to validate_numericality.is_less_than_or_equal_to(18)
        end
      end
    end

    context 'and not validating with less_than_or_equal_to' do
      it 'rejects since it does not disallow numbers greater than the value' do
        record = build_record_validating_numericality

        assertion = lambda do
          expect(record).to validate_numericality.is_less_than_or_equal_to(18)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like a number less than or
equal to 18, but this could not be proved.
  After setting :attr to ‹"19"›, the matcher expected the Example to be
  invalid, but it was valid instead.
        MESSAGE

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

  context 'qualified with is_less_than' do
    context 'and validating with less_than' do
      it 'accepts' do
        record = build_record_validating_numericality(less_than: 18)
        expect(record).
          to validate_numericality.
          is_less_than(18)
      end

      it 'rejects when used in the negative' do
        record = build_record_validating_numericality(less_than: 18)

        assertion = lambda do
          expect(record).not_to validate_numericality.is_less_than(18)
        end

        expect(&assertion).to fail_with_message(<<~MESSAGE)
Expected Example not to validate that :attr looks like a number less
than 18, but this could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be valid, but it was invalid instead, producing these validation
  errors:

  * attr: ["is not a number"]
        MESSAGE
      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 looks like a number less than
18, but this could not be proved.
  After setting :attr to ‹"17"› -- which was read back as ‹"18"› -- the
  matcher expected the Example to be valid, but it was invalid instead,
  producing these validation errors:

  * attr: ["must be less than 18"]

  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
          },
        },
      ) do
        def validation_matcher_scenario_args
          super.deep_merge(validation_options: { less_than: 18 })
        end

        def configure_validation_matcher(matcher)
          matcher.is_less_than(18)
        end
      end

      context 'when the attribute is a virtual attribute in an ActiveRecord model' do
        it 'accepts' do
          record = build_record_validating_numericality_of_virtual_attribute(
            less_than: 18,
          )
          expect(record).to validate_numericality.is_less_than(18)
        end
      end

      context 'when the column is an integer column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :integer,
            less_than: 18,
          )

          expect(record).to validate_numericality.is_less_than(18)
        end
      end

      context 'when the column is a float column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :float,
            less_than: 18,
          )

          expect(record).to validate_numericality.is_less_than(18)
        end
      end

      context 'when the column is a decimal column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :decimal,
            less_than: 18,
          )

          expect(record).to validate_numericality.is_less_than(18)
        end
      end
    end

    context 'and not validating with less_than' do
      it 'rejects since it does not disallow numbers greater than or equal to the value' do
        record = build_record_validating_numericality

        assertion = lambda do
          expect(record).to validate_numericality.is_less_than(18)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like a number less than
18, but this could not be proved.
  After setting :attr to ‹"19"›, the matcher expected the Example to be
  invalid, but it was valid instead.
        MESSAGE

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

  context 'qualified with is_equal_to' do
    context 'and validating with equal_to' do
      it 'accepts' do
        record = build_record_validating_numericality(equal_to: 18)
        expect(record).to validate_numericality.is_equal_to(18)
      end

      it 'rejects when used in the negative' do
        record = build_record_validating_numericality(equal_to: 18)

        assertion = lambda do
          expect(record).not_to validate_numericality.is_equal_to(18)
        end

        expect(&assertion).to fail_with_message(<<~MESSAGE)
Expected Example not to validate that :attr looks like a number equal to
18, but this could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be valid, but it was invalid instead, producing these validation
  errors:

  * attr: ["is not a number"]
        MESSAGE
      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 looks like a number equal to 18,
but this could not be proved.
  After setting :attr to ‹"18"› -- which was read back as ‹"19"› -- the
  matcher expected the Example to be valid, but it was invalid instead,
  producing these validation errors:

  * attr: ["must be equal to 18"]

  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
          },
        },
      ) do
        def validation_matcher_scenario_args
          super.deep_merge(validation_options: { equal_to: 18 })
        end

        def configure_validation_matcher(matcher)
          matcher.is_equal_to(18)
        end
      end

      context 'when the attribute is a virtual attribute in an ActiveRecord model' do
        it 'accepts' do
          record = build_record_validating_numericality_of_virtual_attribute(
            equal_to: 18,
          )
          expect(record).to validate_numericality.is_equal_to(18)
        end
      end

      context 'when the column is an integer column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :integer,
            equal_to: 18,
          )

          expect(record).to validate_numericality.is_equal_to(18)
        end
      end

      context 'when the column is a float column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :float,
            equal_to: 18,
          )

          expect(record).to validate_numericality.is_equal_to(18)
        end
      end

      context 'when the column is a decimal column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :decimal,
            equal_to: 18,
          )

          expect(record).to validate_numericality.is_equal_to(18)
        end
      end
    end

    context 'and not validating with equal_to' do
      it 'rejects since it does not disallow numbers that are not the value' do
        record = build_record_validating_numericality

        assertion = lambda do
          expect(record).to validate_numericality.is_equal_to(18)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like a number equal to 18,
but this could not be proved.
  After setting :attr to ‹"19"›, the matcher expected the Example to be
  invalid, but it was valid instead.
        MESSAGE

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

  context 'qualified with is_other_than' do
    context 'and validating with other_than' do
      it 'accepts' do
        record = build_record_validating_numericality(other_than: 18)
        expect(record).to validate_numericality.is_other_than(18)
      end

      it 'rejects when used in the negative' do
        record = build_record_validating_numericality(other_than: 18)

        assertion = lambda do
          expect(record).not_to validate_numericality.is_other_than(18)
        end

        expect(&assertion).to fail_with_message(<<~MESSAGE)
Expected Example not to validate that :attr looks like a number other
than 18, but this could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be valid, but it was invalid instead, producing these validation
  errors:

  * attr: ["is not a number"]
        MESSAGE
      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 looks like a number other than
18, but this could not be proved.
  After setting :attr to ‹"18"› -- which was read back as ‹"19"› -- 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
          },
        },
      ) do
        def validation_matcher_scenario_args
          super.deep_merge(validation_options: { other_than: 18 })
        end

        def configure_validation_matcher(matcher)
          matcher.is_other_than(18)
        end
      end

      context 'when the attribute is a virtual attribute in an ActiveRecord model' do
        it 'accepts' do
          record = build_record_validating_numericality_of_virtual_attribute(
            other_than: 18,
          )
          expect(record).to validate_numericality.is_other_than(18)
        end
      end

      context 'when the column is an integer column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :integer,
            other_than: 18,
          )

          expect(record).to validate_numericality.is_other_than(18)
        end
      end

      context 'when the column is a float column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :float,
            other_than: 18,
          )

          expect(record).to validate_numericality.is_other_than(18)
        end
      end

      context 'when the column is a decimal column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :decimal,
            other_than: 18,
          )

          expect(record).to validate_numericality.is_other_than(18)
        end
      end
    end

    context 'and not validating with other_than' do
      it 'rejects since it does not disallow numbers that are not the value' do
        record = build_record_validating_numericality

        assertion = lambda do
          expect(record).to validate_numericality.is_other_than(18)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like a number other than
18, but this could not be proved.
  After setting :attr to ‹"18"›, the matcher expected the Example to be
  invalid, but it was valid instead.
        MESSAGE

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

  context 'qualified with is_greater_than_or_equal to' do
    context 'validating with greater_than_or_equal_to' do
      it 'accepts' do
        record = build_record_validating_numericality(
          greater_than_or_equal_to: 18,
        )
        expect(record).
          to validate_numericality.
          is_greater_than_or_equal_to(18)
      end

      it 'rejects when used in the negative' do
        record = build_record_validating_numericality(
          greater_than_or_equal_to: 18,
        )

        assertion = lambda do
          expect(record).not_to validate_numericality.
            is_greater_than_or_equal_to(18)
        end

        expect(&assertion).to fail_with_message(<<~MESSAGE)
Expected Example not to validate that :attr looks like a number greater
than or equal to 18, but this could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be valid, but it was invalid instead, producing these validation
  errors:

  * attr: ["is not a number"]
        MESSAGE
      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 looks like a number greater than
or equal to 18, but this could not be proved.
  After setting :attr to ‹"17"› -- which was read back as ‹"18"› -- 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
          },
        },
      ) do
        def validation_matcher_scenario_args
          super.deep_merge(
            validation_options: { greater_than_or_equal_to: 18 },
          )
        end

        def configure_validation_matcher(matcher)
          matcher.is_greater_than_or_equal_to(18)
        end
      end

      context 'when the attribute is a virtual attribute in an ActiveRecord model' do
        it 'accepts' do
          record = build_record_validating_numericality_of_virtual_attribute(
            greater_than_or_equal_to: 18,
          )
          expect(record).to validate_numericality.
            is_greater_than_or_equal_to(18)
        end
      end

      context 'when the column is an integer column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :integer,
            greater_than_or_equal_to: 18,
          )

          expect(record).
            to validate_numericality.
            is_greater_than_or_equal_to(18)
        end
      end

      context 'when the column is a float column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :float,
            greater_than_or_equal_to: 18,
          )

          expect(record).
            to validate_numericality.
            is_greater_than_or_equal_to(18)
        end
      end

      context 'when the column is a decimal column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :decimal,
            greater_than_or_equal_to: 18,
          )

          expect(record).
            to validate_numericality.
            is_greater_than_or_equal_to(18)
        end
      end
    end

    context 'not validating with greater_than_or_equal_to' do
      it 'rejects since it does not disallow numbers that are less than the value' do
        record = build_record_validating_numericality

        assertion = lambda do
          expect(record).to validate_numericality.
            is_greater_than_or_equal_to(18)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like a number greater than
or equal to 18, but this could not be proved.
  After setting :attr to ‹"17"›, the matcher expected the Example to be
  invalid, but it was valid instead.
        MESSAGE

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

  context 'qualified with is_greater_than' do
    context 'and validating with greater_than' do
      it 'accepts' do
        record = build_record_validating_numericality(greater_than: 18)
        expect(record).
          to validate_numericality.
          is_greater_than(18)
      end

      it 'rejects when used in the negative' do
        record = build_record_validating_numericality(greater_than: 18)

        assertion = lambda do
          expect(record).not_to validate_numericality.
            is_greater_than(18)
        end

        expect(&assertion).to fail_with_message(<<~MESSAGE)
Expected Example not to validate that :attr looks like a number greater
than 18, but this could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be valid, but it was invalid instead, producing these validation
  errors:

  * attr: ["is not a number"]
        MESSAGE
      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 looks like a number greater than
18, but this could not be proved.
  After setting :attr to ‹"18"› -- which was read back as ‹"19"› -- 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
          },
        },
      ) do
        def validation_matcher_scenario_args
          super.deep_merge(validation_options: { greater_than: 18 })
        end

        def configure_validation_matcher(matcher)
          matcher.is_greater_than(18)
        end
      end

      context 'when the attribute is a virtual attribute in an ActiveRecord model' do
        it 'accepts' do
          record = build_record_validating_numericality_of_virtual_attribute(
            greater_than: 18,
          )
          expect(record).to validate_numericality.is_greater_than(18)
        end
      end

      context 'when the column is an integer column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :integer,
            greater_than: 18,
          )

          expect(record).
            to validate_numericality.
            is_greater_than(18)
        end
      end

      context 'when the column is a float column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :float,
            greater_than: 18,
          )

          expect(record).
            to validate_numericality.
            is_greater_than(18)
        end
      end

      context 'when the column is a decimal column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :decimal,
            greater_than: 18,
          )

          expect(record).
            to validate_numericality.
            is_greater_than(18)
        end
      end
    end

    context 'and not validating with greater_than' do
      it 'rejects since it does not disallow numbers that are less than or equal to the value' do
        record = build_record_validating_numericality

        assertion = lambda do
          expect(record).to validate_numericality.is_greater_than(18)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like a number greater than
18, but this could not be proved.
  After setting :attr to ‹"18"›, the matcher expected the Example to be
  invalid, but it was valid instead.
        MESSAGE

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

  context 'qualified with with_message' do
    context 'and validating with the same message' do
      it 'accepts' do
        record = build_record_validating_numericality(message: 'custom')
        expect(record).to validate_numericality.with_message(/custom/)
      end
    end

    context 'and validating with a different message' do
      it 'rejects with the correct failure message' do
        record = build_record_validating_numericality(message: 'custom')

        assertion = lambda do
          expect(record).to validate_numericality.with_message(/wrong/)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like a number, producing a
custom validation error on failure, but this could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be invalid and to produce a validation error matching ‹/wrong/› on
  :attr. The record was indeed invalid, but it produced these validation
  errors instead:

  * attr: ["custom"]
        MESSAGE

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

    context 'and no message is provided' do
      it 'ignores the qualifier' do
        record = build_record_validating_numericality
        expect(record).to validate_numericality.with_message(nil)
      end
    end

    context 'and the validation is missing from the model' do
      it 'rejects with the correct failure message' do
        model = define_model_validating_nothing

        assertion = lambda do
          expect(model.new).to validate_numericality.with_message(/wrong/)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like a number, producing a
custom validation error on failure, but this could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be invalid, but it was valid instead.
        MESSAGE

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

  context 'qualified with strict' do
    context 'and validating strictly' do
      it 'accepts' do
        record = build_record_validating_numericality(strict: true)
        expect(record).to validate_numericality.strict
      end
    end

    context 'and not validating strictly' do
      it 'rejects since ActiveModel::StrictValidationFailed is never raised' do
        record = build_record_validating_numericality(attribute_name: :attr)

        assertion = lambda do
          expect(record).to validate_numericality_of(:attr).strict
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like a number, raising a
validation exception on failure, but this could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be invalid and to raise a validation exception, but the record
  produced validation errors instead.
        MESSAGE

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

  context 'qualified with on and validating with on' do
    it 'accepts' do
      record = build_record_validating_numericality(on: :customizable)
      expect(record).to validate_numericality.on(:customizable)
    end
  end

  context 'qualified with on but not validating with on' do
    it 'accepts since the validation never considers a context' do
      record = build_record_validating_numericality
      expect(record).to validate_numericality.on(:customizable)
    end
  end

  context 'not qualified with on but validating with on' do
    it 'rejects since the validation never runs' do
      record = build_record_validating_numericality(on: :customizable)

      assertion = lambda do
        expect(record).to validate_numericality
      end

      message = <<-MESSAGE
Expected Example to validate that :attr looks like a number, but this
could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be invalid, but it was valid instead.
      MESSAGE

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

  context 'with combinations of qualifiers together' do
    all_qualifier_combinations.each do |combination|
      if combination.size > 1
        it do
          validation_options = build_validation_options(for: combination)
          record = build_record_validating_numericality(validation_options)
          validate_numericality = self.validate_numericality
          apply_qualifiers!(for: combination, to: validate_numericality)
          expect(record).to validate_numericality
        end
      end
    end

    context 'when the qualifiers do not match the validation options' do
      specify 'such as validating even but testing that only_integer is validated' do
        record = build_record_validating_numericality(
          even: true,
          greater_than: 18,
        )

        assertion = lambda do
          expect(record).
            to validate_numericality.
            only_integer.
            is_greater_than(18)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an integer greater
than 18, but this could not be proved.
  In checking that Example disallows :attr from being a decimal number,
  after setting :attr to ‹"0.1"›, the matcher expected the Example to be
  invalid and to produce the validation error "must be an integer" on
  :attr. The record was indeed invalid, but it produced these validation
  errors instead:

  * attr: ["must be greater than 18"]
        MESSAGE

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

      specify 'such as not validating only_integer but testing that only_integer is validated' do
        record = build_record_validating_numericality(greater_than: 18)

        assertion = lambda do
          expect(record).
            to validate_numericality.
            only_integer.
            is_greater_than(18)
        end

        message = <<-MESSAGE.strip_heredoc
Expected Example to validate that :attr looks like an integer greater
than 18, but this could not be proved.
  In checking that Example disallows :attr from being a decimal number,
  after setting :attr to ‹"0.1"›, the matcher expected the Example to be
  invalid and to produce the validation error "must be an integer" on
  :attr. The record was indeed invalid, but it produced these validation
  errors instead:

  * attr: ["must be greater than 18"]
        MESSAGE

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

      specify 'such as validating greater_than_or_equal_to (+ even) but testing that greater_than is validated' do
        record = build_record_validating_numericality(
          even: true,
          greater_than_or_equal_to: 18,
        )

        assertion = lambda do
          expect(record).
            to validate_numericality.
            even.
            is_greater_than(18)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an even number
greater than 18, but this could not be proved.
  In checking that Example disallows :attr from being a number that is
  not greater than 18, after setting :attr to ‹"18"›, the matcher
  expected the Example to be invalid, but it was valid instead.
        MESSAGE

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

      specify 'such as validating odd (+ greater_than) but testing that even is validated' do
        record = build_record_validating_numericality(
          odd: true,
          greater_than: 18,
        )

        assertion = lambda do
          expect(record).
            to validate_numericality.
            even.
            is_greater_than(18)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an even number
greater than 18, but this could not be proved.
  In checking that Example disallows :attr from being an odd number,
  after setting :attr to ‹"1"›, the matcher expected the Example to be
  invalid and to produce the validation error "must be even" on :attr.
  The record was indeed invalid, but it produced these validation errors
  instead:

  * attr: ["must be greater than 18"]
        MESSAGE

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

      specify 'such as validating greater_than_or_equal_to (+ odd) but testing that is_less_than_or_equal_to is validated' do
        record = build_record_validating_numericality(
          odd: true,
          greater_than_or_equal_to: 99,
        )

        assertion = lambda do
          expect(record).
            to validate_numericality.
            odd.
            is_less_than_or_equal_to(99)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an odd number less
than or equal to 99, but this could not be proved.
  In checking that Example disallows :attr from being a number that is
  not less than or equal to 99, after setting :attr to ‹"101"›, the
  matcher expected the Example to be invalid, but it was valid instead.
        MESSAGE

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

      specify 'such as validating greater_than_or_equal_to (+ only_integer + less_than) but testing that greater_than is validated' do
        record = build_record_validating_numericality(
          only_integer: true,
          greater_than_or_equal_to: 18,
          less_than: 99,
        )

        assertion = lambda do
          expect(record).
            to validate_numericality.
            only_integer.
            is_greater_than(18).
            is_less_than(99)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an integer greater
than 18 and less than 99, but this could not be proved.
  In checking that Example disallows :attr from being a number that is
  not greater than 18, after setting :attr to ‹"18"›, the matcher
  expected the Example to be invalid, but it was valid instead.
        MESSAGE

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

    context 'when qualifiers match the validation options but the values are different' do
      specify 'such as testing greater_than (+ only_integer) with lower value' do
        record = build_record_validating_numericality(
          only_integer: true,
          greater_than: 19,
        )

        assertion = lambda do
          expect(record).
            to validate_numericality.
            only_integer.
            is_greater_than(18)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an integer greater
than 18, but this could not be proved.
  In checking that Example disallows :attr from being a number that is
  not greater than 18, after setting :attr to ‹"18"›, the matcher
  expected the Example to be invalid and to produce the validation error
  "must be greater than 18" on :attr. The record was indeed invalid, but
  it produced these validation errors instead:

  * attr: ["must be greater than 19"]
        MESSAGE

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

      specify 'such as testing greater_than (+ only_integer) with higher value' do
        record = build_record_validating_numericality(
          only_integer: true,
          greater_than: 17,
        )

        assertion = lambda do
          expect(record).
            to validate_numericality.
            only_integer.
            is_greater_than(18)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an integer greater
than 18, but this could not be proved.
  In checking that Example disallows :attr from being a number that is
  not greater than 18, after setting :attr to ‹"18"›, the matcher
  expected the Example to be invalid, but it was valid instead.
        MESSAGE

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

      specify 'such as testing greater_than (+ even) with lower value' do
        record = build_record_validating_numericality(
          even: true,
          greater_than: 20,
        )

        assertion = lambda do
          expect(record).
            to validate_numericality.
            even.
            is_greater_than(18)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an even number
greater than 18, but this could not be proved.
  In checking that Example disallows :attr from being a number that is
  not greater than 18, after setting :attr to ‹"18"›, the matcher
  expected the Example to be invalid and to produce the validation error
  "must be greater than 18" on :attr. The record was indeed invalid, but
  it produced these validation errors instead:

  * attr: ["must be greater than 20"]
        MESSAGE

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

      specify 'such as testing greater than (+ even) with higher value' do
        record = build_record_validating_numericality(
          even: true,
          greater_than: 16,
        )

        assertion = lambda do
          expect(record).
            to validate_numericality.
            even.
            is_greater_than(18)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an even number
greater than 18, but this could not be proved.
  In checking that Example disallows :attr from being a number that is
  not greater than 18, after setting :attr to ‹"18"›, the matcher
  expected the Example to be invalid, but it was valid instead.
        MESSAGE

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

      specify 'such as testing less_than_or_equal_to (+ odd) with lower value' do
        record = build_record_validating_numericality(
          odd: true,
          less_than_or_equal_to: 101,
        )

        assertion = lambda do
          expect(record).
            to validate_numericality.
            odd.
            is_less_than_or_equal_to(99)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an odd number less
than or equal to 99, but this could not be proved.
  In checking that Example disallows :attr from being a number that is
  not less than or equal to 99, after setting :attr to ‹"101"›, the
  matcher expected the Example to be invalid, but it was valid instead.
        MESSAGE

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

      specify 'such as testing less_than_or_equal_to (+ odd) with higher value' do
        record = build_record_validating_numericality(
          odd: true,
          less_than_or_equal_to: 97,
        )

        assertion = lambda do
          expect(record).
            to validate_numericality.
            odd.
            is_less_than_or_equal_to(99)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an odd number less
than or equal to 99, but this could not be proved.
  In checking that Example disallows :attr from being a number that is
  not less than or equal to 99, after setting :attr to ‹"101"›, the
  matcher expected the Example to be invalid and to produce the
  validation error "must be less than or equal to 99" on :attr. The
  record was indeed invalid, but it produced these validation errors
  instead:

  * attr: ["must be less than or equal to 97"]
        MESSAGE

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

      specify 'such as testing greater_than (+ only_integer + less_than) with lower value' do
        record = build_record_validating_numericality(
          only_integer: true,
          greater_than: 19,
          less_than: 99,
        )

        assertion = lambda do
          expect(record).
            to validate_numericality.
            only_integer.
            is_greater_than(18).
            is_less_than(99)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an integer greater
than 18 and less than 99, but this could not be proved.
  In checking that Example disallows :attr from being a number that is
  not greater than 18, after setting :attr to ‹"18"›, the matcher
  expected the Example to be invalid and to produce the validation error
  "must be greater than 18" on :attr. The record was indeed invalid, but
  it produced these validation errors instead:

  * attr: ["must be greater than 19"]
        MESSAGE

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

      specify 'such as testing less_than (+ only_integer + greater_than) with higher value' do
        record = build_record_validating_numericality(
          only_integer: true,
          greater_than: 18,
          less_than: 100,
        )

        assertion = lambda do
          expect(record).
            to validate_numericality.
            only_integer.
            is_greater_than(18).
            is_less_than(99)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like an integer greater
than 18 and less than 99, but this could not be proved.
  In checking that Example disallows :attr from being a number that is
  not less than 99, after setting :attr to ‹"100"›, the matcher expected
  the Example to be invalid and to produce the validation error "must be
  less than 99" on :attr. The record was indeed invalid, but it produced
  these validation errors instead:

  * attr: ["must be less than 100"]
        MESSAGE

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

  context 'with large numbers' do
    it do
      record = build_record_validating_numericality(greater_than: 100_000)
      expect(record).to validate_numericality.is_greater_than(100_000)
    end

    it do
      record = build_record_validating_numericality(less_than: 100_000)
      expect(record).to validate_numericality.is_less_than(100_000)
    end

    it do
      record = build_record_validating_numericality(
        greater_than_or_equal_to: 100_000,
      )
      expect(record).
        to validate_numericality.
        is_greater_than_or_equal_to(100_000)
    end

    it do
      record = build_record_validating_numericality(
        less_than_or_equal_to: 100_000,
      )
      expect(record).
        to validate_numericality.
        is_less_than_or_equal_to(100_000)
    end
  end

  context 'when the subject is stubbed' do
    it 'retains that stub while the validate_numericality is matching' do
      model = define_model :example, attr: :string do
        validates_numericality_of :attr, odd: true
        before_validation :set_attr!
        def set_attr!; self.attr = 5 end
      end

      record = model.new
      allow(record).to receive(:set_attr!)

      expect(record).to validate_numericality_of(:attr).odd
    end
  end

  context 'against an ActiveModel model' do
    it 'accepts' do
      model = define_active_model_class :example, accessors: [:attr] do
        validates_numericality_of :attr
      end

      expect(model.new).to validate_numericality_of(:attr)
    end

    it_supports(
      'ignoring_interference_by_writer',
      tests: {
        accept_if_qualified_but_changing_value_does_not_interfere: {
          changing_values_with: :next_value,
        },
        reject_if_qualified_but_changing_value_interferes: {
          model_name: 'Example',
          attribute_name: :attr,
          changing_values_with: :numeric_value,
          expected_message: <<-MESSAGE.strip,
Expected Example to validate that :attr looks like a number, but this
could not be proved.
  After setting :attr to ‹"abcd"› -- which was read back as ‹"1"› -- 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
        },
      },
    )

    def validation_matcher_scenario_args
      super.deep_merge(model_creator: :active_model)
    end
  end

  describe 'using in the negative form without numericality validations' do
    context 'when qualified with is_greater_than' do
      it 'accepts' do
        record = (define_model :example, attr: :integer).new

        expect(record).not_to validate_numericality.is_greater_than(18)
      end
    end

    context 'when qualified with is_greater_than_or_equal_to' do
      it 'accepts' do
        record = (define_model :example, attr: :integer).new

        expect(record).not_to validate_numericality.is_greater_than_or_equal_to(18)
      end
    end

    context 'when qualified with is_less_than_or_equal_to' do
      it 'accepts' do
        record = (define_model :example, attr: :integer).new

        expect(record).not_to validate_numericality.is_less_than_or_equal_to(18)
      end
    end

    context 'when qualified with is_less_than' do
      it 'accepts' do
        record = (define_model :example, attr: :integer).new

        expect(record).not_to validate_numericality.is_less_than(18)
      end
    end

    context 'when qualified with is_equal_to' do
      it 'accepts' do
        record = (define_model :example, attr: :integer).new

        expect(record).not_to validate_numericality.is_equal_to(18)
      end
    end

    context 'when qualified with is_other_than' do
      it 'accepts' do
        record = (define_model :example, attr: :integer).new

        expect(record).not_to validate_numericality.is_other_than(18)
      end
    end
  end

  describe '#description' do
    context 'qualified with nothing' do
      it 'describes that it allows numbers' do
        matcher = validate_numericality_of(:attr)
        expect(matcher.description).to eq(
          'validate that :attr looks like a number',
        )
      end
    end

    context 'qualified with only_integer' do
      it 'describes that it allows integers' do
        matcher = validate_numericality_of(:attr).only_integer
        expect(matcher.description).to eq(
          'validate that :attr looks like an integer',
        )
      end
    end

    qualifiers_under(:cardinality).each do |qualifier|
      context "qualified with #{qualifier[:name]}" do
        it "describes that it allows #{qualifier[:name]} numbers" do
          matcher = validate_numericality_of(:attr).__send__(qualifier[:name])
          expect(matcher.description).to eq(
            "validate that :attr looks like an #{qualifier[:name]} number",
          )
        end
      end
    end

    qualifiers_under(:comparison).each do |qualifier|
      comparison_phrase = qualifier[:validation_name].to_s.gsub('_', ' ')

      context "qualified with #{qualifier[:name]}" do
        it "describes that it allows numbers #{comparison_phrase} a certain value" do
          matcher = validate_numericality_of(:attr).
            __send__(qualifier[:name], 18)

          expect(matcher.description).to eq(
            "validate that :attr looks like a number #{comparison_phrase} 18",
          )
        end
      end
    end

    context 'qualified with odd + is_greater_than_or_equal_to' do
      it 'describes that it allows odd numbers greater than or equal to a certain value' do
        matcher = validate_numericality_of(:attr).
          odd.
          is_greater_than_or_equal_to(18)

        expect(matcher.description).to eq(
          'validate that :attr looks like an odd number greater than or equal to 18',
        )
      end
    end

    context 'qualified with only integer + is_greater_than + less_than_or_equal_to' do
      it 'describes that it allows integer greater than one value and less than or equal to another' do
        matcher = validate_numericality_of(:attr).
          only_integer.
          is_greater_than(18).
          is_less_than_or_equal_to(100)

        expect(matcher.description).to eq(
          'validate that :attr looks like an integer greater than 18 and less than or equal to 100',
        )
      end
    end

    context 'qualified with strict' do
      it 'describes that it relies upon a strict validation' do
        matcher = validate_numericality_of(:attr).strict
        expect(matcher.description).to eq(
          'validate that :attr looks like a number, raising a validation exception on failure',
        )
      end

      context 'and qualified with a comparison qualifier' do
        it 'places the comparison description after "strictly"' do
          matcher = validate_numericality_of(:attr).is_less_than(18).strict
          expect(matcher.description).to eq(
            'validate that :attr looks like a number less than 18, raising a validation exception on failure',
          )
        end
      end
    end
  end

  context 'qualified with in' do
    context 'validating with in' do
      it 'accepts' do
        record = build_record_validating_numericality(
          in: 1..10,
        )
        expect(record).to validate_numericality.is_in(1..10)
      end

      it 'rejects when used in the negative' do
        record = build_record_validating_numericality(
          in: 1..10,
        )

        assertion = lambda do
          expect(record).not_to validate_numericality.is_in(1..10)
        end

        expect(&assertion).to fail_with_message(<<~MESSAGE)
Expected Example not to validate that :attr looks like a number from ‹1›
to ‹10›, but this could not be proved.
  After setting :attr to ‹"abcd"›, the matcher expected the Example to
  be valid, but it was invalid instead, producing these validation
  errors:

  * attr: ["is not a number"]
        MESSAGE
      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 looks like a number from ‹1› to
‹10›, but this could not be proved.
  After setting :attr to ‹"10"› -- which was read back as ‹"11"› -- the
  matcher expected the Example to be valid, but it was invalid instead,
  producing these validation errors:

  * attr: ["must be in 1..10"]

  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
          },
        },
      ) do
        def validation_matcher_scenario_args
          super.deep_merge(
            validation_options: { in: 1..10 },
          )
        end

        def configure_validation_matcher(matcher)
          matcher.is_in(1..10)
        end
      end

      context 'when the attribute is a virtual attribute in an ActiveRecord model' do
        it 'accepts' do
          record = build_record_validating_numericality_of_virtual_attribute(
            in: 1..10,
          )
          expect(record).to validate_numericality.
            is_in(1..10)
        end
      end

      context 'when the column is an integer column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :integer,
            in: 1..10,
          )

          expect(record).
            to validate_numericality.
            is_in(1..10)
        end
      end

      context 'when the column is a float column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :float,
            in: 1..10,
          )

          expect(record).
            to validate_numericality.
            is_in(1..10)
        end
      end

      context 'when the column is a decimal column' do
        it 'accepts (and does not raise an error)' do
          record = build_record_validating_numericality(
            column_type: :decimal,
            in: 1..10,
          )

          expect(record).
            to validate_numericality.
            is_in(1..10)
        end
      end
    end

    context 'not validating with in' do
      it 'rejects since it does not disallow numbers that are not in the range specified' do
        record = build_record_validating_numericality

        assertion = lambda do
          expect(record).to validate_numericality.
            is_in(1..10)
        end

        message = <<-MESSAGE
Expected Example to validate that :attr looks like a number from ‹1› to
‹10›, but this could not be proved.
  After setting :attr to ‹"11"›, the matcher expected the Example to be
  invalid, but it was valid instead.
        MESSAGE

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

  def build_validation_options(args)
    combination = args.fetch(:for)

    combination.each_with_object({}) do |qualifier, hash|
      value = self.class.default_validation_values.fetch(qualifier[:validation_name])
      hash[qualifier[:validation_name]] = value
    end
  end

  def apply_qualifiers!(args)
    combination = args.fetch(:for)
    matcher = args.fetch(:to)

    combination.each do |qualifier|
      args = self.class.default_qualifier_arguments.fetch(qualifier[:name])
      if args
        matcher.__send__(qualifier[:name], args)
      else
        matcher.__send__(qualifier[:name])
      end
    end
  end

  def define_model_validating_numericality(options = {})
    attribute_name = options.delete(:attribute_name) { self.attribute_name }
    column_type = options.delete(:column_type) { :string }

    define_model 'Example', attribute_name => { type: column_type } do |model|
      model.validates_numericality_of(attribute_name, options)
    end
  end

  def define_model_validating_numericality_of_virtual_attribute(options = {})
    attribute_name = options.delete(:attribute_name) { self.attribute_name }

    define_model 'Example' do |model|
      model.send(:attr_accessor, attribute_name)
      model.validates_numericality_of(attribute_name, options)
    end
  end

  def build_record_validating_numericality_of_virtual_attribute(options = {})
    define_model_validating_numericality_of_virtual_attribute(options).new
  end

  def build_record_validating_numericality(options = {})
    define_model_validating_numericality(options).new
  end

  def define_model_validating_nothing
    define_model('Example', attribute_name => :string)
  end

  def build_record_validating_nothing
    define_model_validating_nothing.new
  end

  def validate_numericality
    validate_numericality_of(attribute_name)
  end

  def attribute_name
    :attr
  end

  def validation_matcher_scenario_args
    super.deep_merge(
      matcher_name: :validate_numericality_of,
      model_creator: :active_record,
    )
  end
end
