module Flapjack
  class ArgumentValidator
    attr_reader :query

    def initialize(query = {})
      @errors = []
      @query = query
    end

    def validate(args)
      args = args.dup
      validations = args.delete(:as)
      validations = [validations] unless validations.is_a?(Array)

      elements = args[:query]

      unless elements.nil?
        elements = [elements] unless elements.is_a?(Array)
        validations.each {|v| __send__(v.to_s.downcase, *elements) }
      end

      raise(ArgumentError, @errors.join('; ')) unless @errors.empty?
    end

    private

    def valid_time_str?(str)
      Time.iso8601(str)
      true
    rescue ArgumentError
      false
    end

    def time(*elements)
      elements.each do |element|
        target = @query[element]
        next if target.nil? || target.respond_to?(:iso8601) ||
          (target.is_a?(String) && valid_time_str?(target))
        @errors << "'#{target}' should be a time object or ISO " \
                   '8601-formatted string.'
      end
    end

    def boolean(*elements)
      elements.each do |element|
        target = @query[element]
        next if target.nil? || [TrueClass, FalseClass].include?(target.class)
        @errors << "'#{target}' should be 'true' or 'false'."
      end
    end

    def array_of_strings(*elements)
      elements.each do |element|
        target = @query[element]
        next if target.nil? || (target.is_a?(Array) &&
          target.all? {|t| t.is_a?(String) })
        @errors << "'#{target}' should be an Array of Strings."
      end
    end

    def required(*elements)
      elements.each do |element|
        next unless @query[element].nil?
        @errors << "'#{element}' is required."
      end
    end

    def respond_to?(name, include_private = false)
      !classify_name(name).nil? || super
    end

    def method_missing(name, *args)
      klass = classify_name(name)
      return super if klass.nil?
      elements = args
      elements.each do |element|
        next if @query[element].nil? || @query[element].is_a?(klass)
        @errors << "'#{element}' is expected to be a #{klass}"
      end
    end

    def classify_name(name)
      class_name = name.to_s.split('_').map(&:capitalize).join
      Module.const_get(class_name)
    rescue NameError
      nil
    end
  end
end