module Paperclip
  module Shoulda
    module Matchers
      # Ensures that the given instance or class validates the size of the
      # given attachment as specified.
      #
      # Examples:
      #   it { should validate_attachment_size(:avatar).
      #                 less_than(2.megabytes) }
      #   it { should validate_attachment_size(:icon).
      #                 greater_than(1024) }
      #   it { should validate_attachment_size(:icon).
      #                 in(0..100) }
      def validate_attachment_size name
        ValidateAttachmentSizeMatcher.new(name)
      end

      class ValidateAttachmentSizeMatcher
        def initialize attachment_name
          @attachment_name = attachment_name
          @low, @high = 0, (1.0/0)
        end

        def less_than size
          @high = size
          self
        end

        def greater_than size
          @low = size
          self
        end

        def in range
          @low, @high = range.first, range.last
          self
        end

        def matches? subject
          @subject = subject
          @subject = @subject.class unless Class === @subject
          lower_than_low? && higher_than_low? && lower_than_high? && higher_than_high?
        end

        def failure_message
          "Attachment #{@attachment_name} must be between #{@low} and #{@high} bytes"
        end

        def negative_failure_message
          "Attachment #{@attachment_name} cannot be between #{@low} and #{@high} bytes"
        end

        def description
          "validate the size of attachment #{@attachment_name}"
        end

        protected

        def override_method object, method, &replacement
          (class << object; self; end).class_eval do
            define_method(method, &replacement)
          end
        end

        def passes_validation_with_size(new_size)
          file = StringIO.new(".")
          override_method(file, :size){ new_size }
          override_method(file, :to_tempfile){ file }

          (subject = @subject.new).send(@attachment_name).assign(file)
          subject.valid?
          subject.errors.on(:"#{@attachment_name}_file_size").blank?
        end

        def lower_than_low?
          not passes_validation_with_size(@low - 1)
        end

        def higher_than_low?
          passes_validation_with_size(@low + 1)
        end

        def lower_than_high?
          return true if @high == (1.0/0)
          passes_validation_with_size(@high - 1)
        end

        def higher_than_high?
          return true if @high == (1.0/0)
          not passes_validation_with_size(@high + 1)
        end
      end
    end
  end
end