module Shoulda
  module Matchers
    module ActiveModel
      # The `validate_length_of` matcher tests usage of the
      # `validates_length_of` matcher. Note that this matcher is intended to be
      # used against string columns and associations and not integer columns.
      #
      # #### Qualifiers
      #
      # Use `on` if your validation applies only under a certain context.
      #
      #     class User
      #       include ActiveModel::Model
      #       attr_accessor :password
      #
      #       validates_length_of :password, minimum: 10, on: :create
      #     end
      #
      #     # RSpec
      #     RSpec.describe User, type: :model do
      #       it do
      #         should validate_length_of(:password).
      #           is_at_least(10).
      #           on(:create)
      #       end
      #     end
      #
      #     # Minitest (Shoulda)
      #     class UserTest < ActiveSupport::TestCase
      #       should validate_length_of(:password).
      #         is_at_least(10).
      #         on(:create)
      #     end
      #
      # ##### is_at_least
      #
      # Use `is_at_least` to test usage of the `:minimum` option. This asserts
      # that the attribute can take a string which is equal to or longer than
      # the given length and cannot take a string which is shorter. This qualifier
      # also works for associations.
      #
      #     class User
      #       include ActiveModel::Model
      #       attr_accessor :bio
      #
      #       validates_length_of :bio, minimum: 15
      #     end
      #
      #     # RSpec
      #
      #     RSpec.describe User, type: :model do
      #       it { should validate_length_of(:bio).is_at_least(15) }
      #     end
      #
      #     # Minitest (Shoulda)
      #
      #     class UserTest < ActiveSupport::TestCase
      #       should validate_length_of(:bio).is_at_least(15)
      #     end
      #
      # ##### is_at_most
      #
      # Use `is_at_most` to test usage of the `:maximum` option. This asserts
      # that the attribute can take a string which is equal to or shorter than
      # the given length and cannot take a string which is longer. This qualifier
      # also works for associations.
      #
      #     class User
      #       include ActiveModel::Model
      #       attr_accessor :status_update
      #
      #       validates_length_of :status_update, maximum: 140
      #     end
      #
      #     # RSpec
      #     RSpec.describe User, type: :model do
      #       it { should validate_length_of(:status_update).is_at_most(140) }
      #     end
      #
      #     # Minitest (Shoulda)
      #     class UserTest < ActiveSupport::TestCase
      #       should validate_length_of(:status_update).is_at_most(140)
      #     end
      #
      # ##### is_equal_to
      #
      # Use `is_equal_to` to test usage of the `:is` option. This asserts that
      # the attribute can take a string which is exactly equal to the given
      # length and cannot take a string which is shorter or longer. This qualifier
      # also works for associations.
      #
      #     class User
      #       include ActiveModel::Model
      #       attr_accessor :favorite_superhero
      #
      #       validates_length_of :favorite_superhero, is: 6
      #     end
      #
      #     # RSpec
      #     RSpec.describe User, type: :model do
      #       it { should validate_length_of(:favorite_superhero).is_equal_to(6) }
      #     end
      #
      #     # Minitest (Shoulda)
      #     class UserTest < ActiveSupport::TestCase
      #       should validate_length_of(:favorite_superhero).is_equal_to(6)
      #     end
      #
      # ##### is_at_least + is_at_most
      #
      # Use `is_at_least` and `is_at_most` together to test usage of the `:in`
      # option. This qualifies also works for associations.
      #
      #     class User
      #       include ActiveModel::Model
      #       attr_accessor :password
      #
      #       validates_length_of :password, in: 5..30
      #     end
      #
      #     # RSpec
      #     RSpec.describe User, type: :model do
      #       it do
      #         should validate_length_of(:password).
      #           is_at_least(5).is_at_most(30)
      #       end
      #     end
      #
      #     # Minitest (Shoulda)
      #     class UserTest < ActiveSupport::TestCase
      #       should validate_length_of(:password).
      #         is_at_least(5).is_at_most(30)
      #     end
      #
      # ##### with_message
      #
      # Use `with_message` if you are using a custom validation message.
      #
      #     class User
      #       include ActiveModel::Model
      #       attr_accessor :password
      #
      #       validates_length_of :password,
      #         minimum: 10,
      #         message: "Password isn't long enough"
      #     end
      #
      #     # RSpec
      #     RSpec.describe User, type: :model do
      #       it do
      #         should validate_length_of(:password).
      #           is_at_least(10).
      #           with_message("Password isn't long enough")
      #       end
      #     end
      #
      #     # Minitest (Shoulda)
      #     class UserTest < ActiveSupport::TestCase
      #       should validate_length_of(:password).
      #         is_at_least(10).
      #         with_message("Password isn't long enough")
      #     end
      #
      # ##### with_short_message
      #
      # Use `with_short_message` if you are using a custom "too short" message.
      #
      #     class User
      #       include ActiveModel::Model
      #       attr_accessor :secret_key
      #
      #       validates_length_of :secret_key,
      #         in: 15..100,
      #         too_short: 'Secret key must be more than 15 characters'
      #     end
      #
      #     # RSpec
      #     RSpec.describe User, type: :model do
      #       it do
      #         should validate_length_of(:secret_key).
      #           is_at_least(15).
      #           with_short_message('Secret key must be more than 15 characters')
      #       end
      #     end
      #
      #     # Minitest (Shoulda)
      #     class UserTest < ActiveSupport::TestCase
      #       should validate_length_of(:secret_key).
      #         is_at_least(15).
      #         with_short_message('Secret key must be more than 15 characters')
      #     end
      #
      # ##### with_long_message
      #
      # Use `with_long_message` if you are using a custom "too long" message.
      #
      #     class User
      #       include ActiveModel::Model
      #       attr_accessor :secret_key
      #
      #       validates_length_of :secret_key,
      #         in: 15..100,
      #         too_long: 'Secret key must be less than 100 characters'
      #     end
      #
      #     # RSpec
      #     RSpec.describe User, type: :model do
      #       it do
      #         should validate_length_of(:secret_key).
      #           is_at_most(100).
      #           with_long_message('Secret key must be less than 100 characters')
      #       end
      #     end
      #
      #     # Minitest (Shoulda)
      #     class UserTest < ActiveSupport::TestCase
      #       should validate_length_of(:secret_key).
      #         is_at_most(100).
      #         with_long_message('Secret key must be less than 100 characters')
      #     end
      #
      # ##### allow_nil
      #
      # Use `allow_nil` to assert that the attribute allows nil.
      #
      #     class User
      #       include ActiveModel::Model
      #       attr_accessor :bio
      #
      #       validates_length_of :bio, minimum: 15, allow_nil: true
      #     end
      #
      #     # RSpec
      #     describe User do
      #       it { should validate_length_of(:bio).is_at_least(15).allow_nil }
      #     end
      #
      #     # Minitest (Shoulda)
      #     class UserTest < ActiveSupport::TestCase
      #       should validate_length_of(:bio).is_at_least(15).allow_nil
      #     end
      #
      # @return [ValidateLengthOfMatcher]
      #
      # ##### allow_blank
      #
      # Use `allow_blank` to assert that the attribute allows blank.
      #
      #     class User
      #       include ActiveModel::Model
      #       attr_accessor :bio
      #
      #       validates_length_of :bio, minimum: 15, allow_blank: true
      #     end
      #
      #     # RSpec
      #     describe User do
      #       it { should validate_length_of(:bio).is_at_least(15).allow_blank }
      #     end
      #
      #     # Minitest (Shoulda)
      #     class UserTest < ActiveSupport::TestCase
      #       should validate_length_of(:bio).is_at_least(15).allow_blank
      #     end
      #
      # ##### as_array
      #
      # Use `as_array` if you have an ActiveModel model and the attribute being validated
      # is designed to store an array. (This is not necessary if you have an ActiveRecord
      # model with an array column, as the matcher will detect this automatically.)
      #
      #     class User
      #       include ActiveModel::Model
      #       attribute :arr, array: true
      #
      #       validates_length_of :arr, minimum: 15
      #     end
      #
      #     # RSpec
      #     describe User do
      #       it { should validate_length_of(:arr).as_array.is_at_least(15) }
      #     end
      #
      #     # Minitest (Shoulda)
      #     class UserTest < ActiveSupport::TestCase
      #       should validate_length_of(:arr).as_array.is_at_least(15)
      #     end
      #
      def validate_length_of(attr)
        ValidateLengthOfMatcher.new(attr)
      end

      # @private
      class ValidateLengthOfMatcher < ValidationMatcher
        include Helpers

        def initialize(attribute)
          super(attribute)
          @options = {}
          @short_message = nil
          @long_message = nil
        end

        def as_array
          @options[:array] = true
          self
        end

        def is_at_least(length)
          @options[:minimum] = length
          @short_message ||= :too_short
          self
        end

        def is_at_most(length)
          @options[:maximum] = length
          @long_message ||= :too_long
          self
        end

        def is_equal_to(length)
          @options[:minimum] = length
          @options[:maximum] = length
          @short_message ||= :wrong_length
          @long_message ||= :wrong_length
          self
        end

        def with_message(message)
          if message
            @expects_custom_validation_message = true
            @short_message = message
            @long_message = message
          end

          self
        end

        def with_short_message(message)
          if message
            @expects_custom_validation_message = true
            @short_message = message
          end

          self
        end

        def with_long_message(message)
          if message
            @expects_custom_validation_message = true
            @long_message = message
          end

          self
        end

        def allow_nil
          @options[:allow_nil] = true
          self
        end

        def simple_description
          description = "validate that the length of :#{@attribute}"

          if @options.key?(:minimum) && @options.key?(:maximum)
            if @options[:minimum] == @options[:maximum]
              description << " is #{@options[:minimum]}"
            else
              description << " is between #{@options[:minimum]}"
              description << " and #{@options[:maximum]}"
            end
          elsif @options.key?(:minimum)
            description << " is at least #{@options[:minimum]}"
          elsif @options.key?(:maximum)
            description << " is at most #{@options[:maximum]}"
          end

          description
        end

        def matches?(subject)
          super(subject)

          lower_bound_matches? &&
            upper_bound_matches? &&
            allow_nil_matches? &&
            allow_blank_matches?
        end

        def does_not_match?(subject)
          super(subject)

          lower_bound_does_not_match? ||
            upper_bound_does_not_match? ||
            allow_nil_does_not_match? ||
            allow_blank_does_not_match?
        end

        private

        def expects_to_allow_nil?
          @options[:allow_nil]
        end

        def lower_bound_matches?
          disallows_lower_length? && allows_minimum_length?
        end

        def lower_bound_does_not_match?
          allows_lower_length? || disallows_minimum_length?
        end

        def upper_bound_matches?
          disallows_higher_length? && allows_maximum_length?
        end

        def upper_bound_does_not_match?
          allows_higher_length? || disallows_maximum_length?
        end

        def allows_lower_length?
          @options.key?(:minimum) &&
            @options[:minimum] > 0 &&
            allows_length_of?(
              @options[:minimum] - 1,
              translated_short_message,
            )
        end

        def disallows_lower_length?
          !@options.key?(:minimum) ||
            @options[:minimum] == 0 ||
            (@options[:minimum] == 1 && expects_to_allow_blank?) ||
            disallows_length_of?(
              @options[:minimum] - 1,
              translated_short_message,
            )
        end

        def allows_higher_length?
          @options.key?(:maximum) &&
            allows_length_of?(
              @options[:maximum] + 1,
              translated_long_message,
            )
        end

        def disallows_higher_length?
          !@options.key?(:maximum) ||
            disallows_length_of?(
              @options[:maximum] + 1,
              translated_long_message,
            )
        end

        def allows_minimum_length?
          !@options.key?(:minimum) ||
            allows_length_of?(@options[:minimum], translated_short_message)
        end

        def disallows_minimum_length?
          @options.key?(:minimum) &&
            disallows_length_of?(@options[:minimum], translated_short_message)
        end

        def allows_maximum_length?
          !@options.key?(:maximum) ||
            allows_length_of?(@options[:maximum], translated_long_message)
        end

        def disallows_maximum_length?
          @options.key?(:maximum) &&
            disallows_length_of?(@options[:maximum], translated_long_message)
        end

        def allow_nil_matches?
          !expects_to_allow_nil? || allows_value_of(nil)
        end

        def allow_nil_does_not_match?
          expects_to_allow_nil? && disallows_value_of(nil)
        end

        def allows_length_of?(length, message)
          allows_value_of(value_of_length(length), message)
        end

        def disallows_length_of?(length, message)
          disallows_value_of(value_of_length(length), message)
        end

        def value_of_length(length)
          if array_column?
            ['x'] * length
          elsif collection_association?
            Array.new(length) { association_reflection.klass.new }
          else
            'x' * length
          end
        end

        def array_column?
          @options[:array] || super
        end

        def collection_association?
          association? && [:has_many, :has_and_belongs_to_many].include?(
            association_reflection.macro,
          )
        end

        def association?
          association_reflection.present?
        end

        def association_reflection
          model.try(:reflect_on_association, @attribute)
        end

        def translated_short_message
          @_translated_short_message ||=
            if @short_message.is_a?(Symbol)
              default_error_message(
                @short_message,
                model_name: @subject.class.to_s.underscore,
                instance: @subject,
                attribute: @attribute,
                count: @options[:minimum],
              )
            else
              @short_message
            end
        end

        def translated_long_message
          @_translated_long_message ||=
            if @long_message.is_a?(Symbol)
              default_error_message(
                @long_message,
                model_name: @subject.class.to_s.underscore,
                instance: @subject,
                attribute: @attribute,
                count: @options[:maximum],
              )
            else
              @long_message
            end
        end
      end
    end
  end
end
