module Mongoid
  module Matchers
    class HaveFieldMatcher # :nodoc:
      def initialize(*attrs)
        @attributes = attrs.collect(&:to_s)
      end

      def localized
        @localized = true
        self
      end

      def of_type(type)
        @type = type
        self
      end

      def with_alias(field_alias)
        @field_alias = field_alias
        self
      end

      def with_default_value_of(default)
        @default = default
        self
      end

      def matches?(klass)
        @klass = klass.is_a?(Class) ? klass : klass.class
        @errors = []
        @attributes.each do |attr|
          if @klass.fields.include?(attr)
            error = ""
            if @type and @klass.fields[attr].type != @type
              error << " of type #{@klass.fields[attr].type}"
            end

            if !@default.nil?
              if @klass.fields[attr].default_val.nil?
                error << " with default not set"
              elsif @klass.fields[attr].default_val != @default
                error << " with default value of #{@klass.fields[attr].default_val}"
              end
            end

            if @field_alias and @klass.fields[attr].options[:as] != @field_alias
              error << " with alias #{@klass.fields[attr].options[:as]}"
            end

            @errors.push("field #{attr.inspect}" << error) unless error.blank?

            if @localized
              unless @klass.fields[attr].localized?
                @errors.push "is not localized #{attr.inspect}"
              end
            end

          else
            @errors.push "no field named #{attr.inspect}"
          end
        end
        @errors.empty?
      end

      def failure_message_for_should
        "Expected #{@klass.inspect} to #{description}, got #{@errors.to_sentence}"
      end

      def failure_message_for_should_not
        "Expected #{@klass.inspect} to not #{description}, got #{@klass.inspect} to #{description}"
      end

      alias :failure_message :failure_message_for_should
      alias :failure_message_when_negated :failure_message_for_should_not

      def description
        desc = "have #{@attributes.size > 1 ? 'fields' : 'field'} named #{@attributes.collect(&:inspect).to_sentence}"
        desc << " of type #{@type.inspect}" if @type
        desc << " with alias #{@field_alias}" if @field_alias
        desc << " with default value of #{@default.inspect}" if !@default.nil?
        desc
      end
    end

    def have_field(*args)
      HaveFieldMatcher.new(*args)
    end
    alias_method :have_fields, :have_field
  end
end

RSpec::Matchers.define :have_instance_method do |name|
  match do |klass|
    klass.instance_methods.include?(name.to_sym)
  end

  description do
    "have instance method #{name.to_s}"
  end
end

RSpec::Matchers.define :be_mongoid_document do
  match do |doc|
    doc.class.included_modules.include?(Mongoid::Document)
  end

  description do
    "be a Mongoid document"
  end
end

RSpec::Matchers.define :be_versioned_document do
  match do |doc|
    doc.class.included_modules.include?(Mongoid::Versioning)
  end

  description do
    "be a versioned Mongoid document"
  end
end

RSpec::Matchers.define :be_timestamped_document do
  match do |doc|
    if [*@timestamped_module].any?
      modules = [*@timestamped_module].map{|m| "Mongoid::Timestamps::#{m.to_s.classify}".constantize }
      (modules - doc.class.included_modules).empty?
    else
      doc.class.included_modules.include?(Mongoid::Timestamps) or
      doc.class.included_modules.include?(Mongoid::Timestamps::Created) or
      doc.class.included_modules.include?(Mongoid::Timestamps::Updated)
    end
  end

  chain :with do |timestamped_module|
    @timestamped_module = timestamped_module
  end

  description do
    desc = "be a timestamped Mongoid document"
    desc << " with #{@timestamped_module}" if @timestamped_module
    desc
  end
end

RSpec::Matchers.define :be_paranoid_document do
  match do |doc|
    doc.class.included_modules.include?(Mongoid::Paranoia)
  end

  description do
    "be a paranoid Mongoid document"
  end
end

RSpec::Matchers.define :be_multiparameted_document do
  match do |doc|
    doc.class.included_modules.include?(Mongoid::MultiParameterAttributes)
  end

  description do
    "be a multiparameted Mongoid document"
  end
end

RSpec::Matchers.define :be_dynamic_document do |_|
  match do |doc|
    doc.class.included_modules.include?(Mongoid::Attributes::Dynamic)
  end

  description do
    'be a Mongoid document with dynamic attributes'
  end
end
