module Normalizy
  module RSpec
    def normalizy(attribute)
      Matcher.new attribute
    end

    class Matcher
      def initialize(attribute)
        @attribute = attribute
      end

      def description
        return "normalizy #{@attribute} with #{with_expected}" if @with.present?

        "normalizy #{@attribute} from #{from_value} to #{to_value}"
      end

      def failure_message
        return "expected: #{with_expected}\n     got: #{actual_value}" if @with.present?

        "expected: #{to_value}\n     got: #{actual_value}"
      end

      def failure_message_when_negated
        return "expected: value != #{with_expected}\n     got: #{actual_value}" if @with.present?

        "expected: value != #{to_value}\n     got: #{actual_value}"
      end

      def from(value)
        @from = value

        self
      end

      def matches?(subject)
        @subject = subject

        if @with.present?
          options = @subject.class.normalizy_rules[@attribute]

          return false if options.blank?

          options.each do |option|
            rules = option[:rules]

            return true if rules.is_a?(Array) && rules.include?(@with)
            return true if rules == @with
          end

          false
        else
          @subject.send "#{@attribute}=", @from

          @subject.send(@attribute) == @to
        end
      end

      def to(value)
        @to = value

        self
      end

      def with(value)
        @with = value

        self
      end

      private

      def actual_value
        return with_value if @with

        value = @subject.send(@attribute)

        value.is_a?(String) ? %("#{value}") : value
      end

      def from_value
        @from.nil? ? :nil : %("#{@from}")
      end

      def to_value
        @to.nil? ? :nil : %("#{@to}")
      end

      def with_expected
        @with
      end

      def with_value
        options = @subject.class.normalizy_rules[@attribute]

        return :nil            if options.nil?
        return %("#{options}") if options.blank?

        result = options.map do |option|
          rules = option[:rules]

          if rules.nil?
            :nil
          elsif rules.blank?
            %("#{rules}")
          else
            rules
          end
        end

        result.join ', '
      end
    end
  end
end