module RSpec
  module Mocks
    module Matchers
      class Receive
        def initialize(message, block)
          @message                 = message
          @block                   = block
          @recorded_customizations = []

          # MRI, JRuby and RBX report the caller inconsistently; MRI
          # reports an extra "in `new'" line in the backtrace that the
          # others do not include. The safest way to find the right
          # line is to search for the first line BEFORE rspec/mocks/syntax.rb.
          @backtrace_line          = caller.find do |line|
            !line.split(':').first.end_with?('rspec/mocks/syntax.rb')
          end
        end

        def setup_expectation(subject, &block)
          setup_mock_proxy_method_substitute(subject, :add_message_expectation, block)
        end
        alias matches? setup_expectation

        def setup_negative_expectation(subject, &block)
          # ensure `never` goes first for cases like `never.and_return(5)`,
          # where `and_return` is meant to raise an error
          @recorded_customizations.unshift Customization.new(:never, [], nil)

          setup_expectation(subject, &block)
        end
        alias does_not_match? setup_negative_expectation

        def setup_allowance(subject, &block)
          setup_mock_proxy_method_substitute(subject, :add_stub, block)
        end

        def setup_any_instance_expectation(subject, &block)
          setup_any_instance_method_substitute(subject, :should_receive, block)
        end

        def setup_any_instance_negative_expectation(subject, &block)
          setup_any_instance_method_substitute(subject, :should_not_receive, block)
        end

        def setup_any_instance_allowance(subject, &block)
          setup_any_instance_method_substitute(subject, :stub, block)
        end

        MessageExpectation.public_instance_methods(false).each do |method|
          next if method_defined?(method)

          class_eval(<<-RUBY)
            def #{method}(*args, &block)
              @recorded_customizations << Customization.new(#{method.inspect}, args, block)
              self
            end
          RUBY
        end

      private

        def setup_mock_proxy_method_substitute(subject, method, block)
          proxy = ::RSpec::Mocks.proxy_for(subject)
          setup_method_substitute(proxy, method, block, @backtrace_line)
        end

        def setup_any_instance_method_substitute(subject, method, block)
          any_instance_recorder = ::RSpec::Mocks.any_instance_recorder_for(subject)
          setup_method_substitute(any_instance_recorder, method, block)
        end

        def setup_method_substitute(host, method, block, *args)
          args << @message.to_sym
          expectation = host.__send__(method, *args, &(@block || block))

          @recorded_customizations.each do |customization|
            customization.playback_onto(expectation)
          end
        end

        class Customization
          def initialize(method_name, args, block)
            @method_name = method_name
            @args        = args
            @block       = block
          end

          def playback_onto(expectation)
            expectation.__send__(@method_name, *@args, &@block)
          end
        end
      end
    end
  end
end

