module Shoulda
  module Matchers
    module ActiveModel
      # @private
      class ValidationMatcher
        include Qualifiers::IgnoringInterferenceByWriter

        def initialize(attribute)
          super
          @attribute = attribute
          @expects_strict = false
          @subject = nil
          @last_submatcher_run = nil
          @expected_message = nil
          @expects_custom_validation_message = false
        end

        def description
          ValidationMatcher::BuildDescription.call(self, simple_description)
        end

        def on(context)
          @context = context
          self
        end

        def allow_blank
          options[:allow_blank] = true
          self
        end

        def strict
          @expects_strict = true
          self
        end

        def expects_strict?
          @expects_strict
        end

        def with_message(expected_message)
          if expected_message
            @expects_custom_validation_message = true
            @expected_message = expected_message
          end

          self
        end

        def expects_custom_validation_message?
          @expects_custom_validation_message
        end

        def matches?(subject)
          @subject = subject
          false
        end

        def does_not_match?(subject)
          @subject = subject
          true
        end

        def failure_message
          overall_failure_message.dup.tap do |message|
            if failure_reason.present?
              message << "\n"
              message << Shoulda::Matchers.word_wrap(
                failure_reason,
                indent: 2,
              )
            end
          end
        end

        def failure_message_when_negated
          overall_failure_message_when_negated.dup.tap do |message|
            if failure_reason.present?
              message << "\n"
              message << Shoulda::Matchers.word_wrap(
                failure_reason,
                indent: 2,
              )
            end
          end
        end

        protected

        attr_reader :attribute, :context, :subject, :last_submatcher_run

        def model
          subject.class
        end

        def allows_value_of(value, message = nil, &block)
          matcher = allow_value_matcher(value, message, &block)
          run_allow_or_disallow_matcher(matcher)
        end

        def disallows_value_of(value, message = nil, &block)
          matcher = disallow_value_matcher(value, message, &block)
          run_allow_or_disallow_matcher(matcher)
        end

        def allow_value_matcher(value, message = nil, &block)
          build_allow_or_disallow_value_matcher(
            matcher_class: AllowValueMatcher,
            value: value,
            message: message,
            &block
          )
        end

        def disallow_value_matcher(value, message = nil, &block)
          build_allow_or_disallow_value_matcher(
            matcher_class: DisallowValueMatcher,
            value: value,
            message: message,
            &block
          )
        end

        def allow_blank_matches?
          !expects_to_allow_blank? ||
            blank_values.all? { |value| allows_value_of(value) }
        end

        def allow_blank_does_not_match?
          expects_to_allow_blank? &&
            blank_values.all? { |value| disallows_value_of(value) }
        end

        private

        attr_reader :options

        def overall_failure_message
          Shoulda::Matchers.word_wrap(
            "Expected #{model.name} to #{description}, but this could not be "\
            'proved.',
          )
        end

        def overall_failure_message_when_negated
          Shoulda::Matchers.word_wrap(
            "Expected #{model.name} not to #{description}, but this could "\
            'not be proved.',
          )
        end

        def failure_reason
          last_submatcher_run.try(:failure_message)
        end

        def failure_reason_when_negated
          last_submatcher_run.try(:failure_message_when_negated)
        end

        def build_allow_or_disallow_value_matcher(args)
          matcher_class = args.fetch(:matcher_class)
          value = args.fetch(:value)
          message = args[:message]

          matcher = matcher_class.new(value).
            for(attribute).
            with_message(message).
            on(context).
            strict(expects_strict?).
            ignoring_interference_by_writer(ignore_interference_by_writer)

          yield matcher if block_given?

          matcher
        end

        def run_allow_or_disallow_matcher(matcher)
          @last_submatcher_run = matcher
          matcher.matches?(subject)
        end

        def expects_to_allow_blank?
          options[:allow_blank]
        end

        def blank_values
          ['', ' ', "\n", "\r", "\t", "\f"]
        end

        def array_column?
          @subject.class.respond_to?(:columns_hash) &&
            @subject.class.columns_hash[@attribute.to_s].respond_to?(:array) &&
            @subject.class.columns_hash[@attribute.to_s].array
        end
      end
    end
  end
end
