require 'set'

module RSpec
  module Matchers
    module DSL
      # Provides the context in which the block passed to RSpec::Matchers.define
      # will be evaluated.
      class Matcher
        include RSpec::Matchers::Extensions::InstanceEvalWithArgs
        include RSpec::Matchers::Pretty
        include RSpec::Matchers

        attr_reader :expected, :actual, :rescued_exception
        attr_accessor :matcher_execution_context

        # @api private
        def initialize(name, &declarations)
          @name         = name
          @declarations = declarations
          @actual       = nil
          @diffable     = false
          @expected_exception, @rescued_exception = nil, nil
          @match_for_should_not_block = nil
          @messages = {}
        end

        PERSISTENT_INSTANCE_VARIABLES = [
          :@name, :@declarations, :@diffable,
          :@match_block, :@match_for_should_not_block,
          :@expected_exception
        ].to_set

        # @api private
        def for_expected(*expected)
          @expected = expected
          dup.instance_eval do
            instance_variables.map {|ivar| ivar.intern}.each do |ivar|
              instance_variable_set(ivar, nil) unless (PERSISTENT_INSTANCE_VARIABLES + [:@expected]).include?(ivar)
            end
            @messages = {}
            making_declared_methods_public do
              instance_eval_with_args(*@expected, &@declarations)
            end
            self
          end
        end

        # @api private
        # Used internally by +should+ and +should_not+.
        def matches?(actual)
          @actual = actual
          if @expected_exception
            begin
              instance_eval_with_args(actual, &@match_block)
              true
            rescue @expected_exception => @rescued_exception
              false
            end
          else
            begin
              instance_eval_with_args(actual, &@match_block)
            rescue RSpec::Expectations::ExpectationNotMetError
              false
            end
          end
        end

        # Stores the block that is used to determine whether this matcher passes
        # or fails. The block should return a boolean value. When the matcher is
        # passed to `should` and the block returns `true`, then the expectation
        # passes. Similarly, when the matcher is passed to `should_not` and the
        # block returns `false`, then the expectation passes.
        #
        # Use `match_for_should` when used in conjuntion with
        # `match_for_should_not`.
        #
        # @example
        #
        #     RSpec::Matchers.define :be_even do
        #       match do |actual|
        #         actual.even?
        #       end
        #     end
        #
        #     4.should be_even     # passes
        #     3.should_not be_even # passes
        #     3.should be_even     # fails
        #     4.should_not be_even # fails
        #
        # @yield [Object] actual the actual value (or receiver of should)
        def match(&block)
          @match_block = block
        end

        alias_method :match_for_should, :match

        # Use this to define the block for a negative expectation (`should_not`)
        # when the positive and negative forms require different handling. This
        # is rarely necessary, but can be helpful, for example, when specifying
        # asynchronous processes that require different timeouts.
        #
        # @yield [Object] actual the actual value (or receiver of should)
        def match_for_should_not(&block)
          @match_for_should_not_block = block
        end

        # Use this instead of `match` when the block will raise an exception
        # rather than returning false to indicate a failure.
        #
        # @example
        #
        #     RSpec::Matchers.define :accept_as_valid do |candidate_address|
        #       match_unless_raises ValidationException do |validator|
        #         validator.validate(candidate_address)
        #       end
        #     end
        #
        #     email_validator.should accept_as_valid("person@company.com")
        def match_unless_raises(exception=Exception, &block)
          @expected_exception = exception
          match(&block)
        end

        # Customize the failure messsage to use when this matcher is invoked with
        # `should`. Only use this when the message generated by default doesn't
        # suit your needs.
        #
        # @example
        #
        #     RSpec::Matchers.define :have_strength do |expected|
        #       match { ... }
        #
        #       failure_message_for_should do |actual|
        #         "Expected strength of #{expected}, but had #{actual.strength}"
        #       end
        #     end
        #
        # @yield [Object] actual the actual object
        def failure_message_for_should(&block)
          cache_or_call_cached(:failure_message_for_should, &block)
        end

        # Customize the failure messsage to use when this matcher is invoked with
        # `should_not`. Only use this when the message generated by default
        # doesn't suit your needs.
        #
        # @example
        #
        #     RSpec::Matchers.define :have_strength do |expected|
        #       match { ... }
        #
        #       failure_message_for_should_not do |actual|
        #         "Expected not to have strength of #{expected}, but did"
        #       end
        #     end
        #
        # @yield [Object] actual the actual object
        # @yield [Object] actual the actual object
        def failure_message_for_should_not(&block)
          cache_or_call_cached(:failure_message_for_should_not, &block)
        end


        # Customize the description to use for one-liners.  Only use this when
        # the description generated by default doesn't suit your needs.
        #
        # @example
        #
        #     RSpec::Matchers.define :qualify_for do |expected|
        #       match { ... }
        #
        #       description do
        #         "qualify for #{expected}"
        #       end
        #     end
        def description(&block)
          cache_or_call_cached(:description, &block)
        end

        # Tells the matcher to diff the actual and expected values in the failure
        # message.
        def diffable
          @diffable = true
        end

        # Convenience for defining methods on this matcher to create a fluent
        # interface. The trick about fluent interfaces is that each method must
        # return self in order to chain methods together. `chain` handles that
        # for you.
        #
        # @example
        #
        #     RSpec::Matchers.define :have_errors_on do |key|
        #       chain :with do |message|
        #         @message = message
        #       end
        #
        #       match do |actual|
        #         actual.errors[key] == @message
        #       end
        #     end
        #
        #     minor.should have_errors_on(:age).with("Not old enough to participate")
        def chain(method, &block)
          define_method method do |*args|
            block.call(*args)
            self
          end
        end

        # @api private
        # Used internally by objects returns by +should+ and +should_not+.
        def diffable?
          @diffable
        end

        # @api private
        # Used internally by +should_not+
        def does_not_match?(actual)
          @actual = actual
          @match_for_should_not_block ?
            instance_eval_with_args(actual, &@match_for_should_not_block) :
            !matches?(actual)
        end

        def respond_to?(method, include_private=false)
          super || matcher_execution_context.respond_to?(method, include_private)
        end

        private

        def method_missing(method, *args, &block)
          if matcher_execution_context.respond_to?(method)
            matcher_execution_context.__send__ method, *args, &block
          else
            super(method, *args, &block)
          end
        end

        def include(*args)
          singleton_class.__send__(:include, *args)
        end

        def define_method(name, &block)
          singleton_class.__send__(:define_method, name, &block)
        end

        def making_declared_methods_public
          # Our home-grown instance_exec in ruby 1.8.6 results in any methods
          # declared in the block eval'd by instance_exec in the block to which we
          # are yielding here are scoped private. This is NOT the case for Ruby
          # 1.8.7 or 1.9.
          #
          # Also, due some crazy scoping that I don't understand, these methods
          # are actually available in the specs (something about the matcher being
          # defined in the scope of RSpec::Matchers or within an example), so not
          # doing the following will not cause specs to fail, but they *will*
          # cause features to fail and that will make users unhappy. So don't.
          orig_private_methods = private_methods
          yield
          (private_methods - orig_private_methods).each {|m| singleton_class.__send__ :public, m}
        end

        def cache_or_call_cached(key, &block)
          block ? cache(key, &block) : call_cached(key)
        end

        def cache(key, &block)
          @messages[key] = block
        end

        def call_cached(key)
          if @messages.has_key?(key)
            @messages[key].arity == 1 ? @messages[key].call(@actual) : @messages[key].call
          else
            __send__("default_#{key}")
          end
        end

        def default_description
          "#{name_to_sentence}#{expected_to_sentence}"
        end

        def default_failure_message_for_should
          "expected #{actual.inspect} to #{name_to_sentence}#{expected_to_sentence}"
        end

        def default_failure_message_for_should_not
          "expected #{actual.inspect} not to #{name_to_sentence}#{expected_to_sentence}"
        end

        unless method_defined?(:singleton_class)
          def singleton_class
            class << self; self; end
          end
        end
      end
    end
  end
end
