module RSpec
  module Mocks
    # @private
    class Proxy
      # @private
      SpecificMessage = Struct.new(:object, :message, :args) do
        def ==(expectation)
          expectation.orig_object == object && expectation.matches?(message, *args)
        end
      end

      unless defined?(Mutex)
        Support.require_rspec_support 'mutex'
        Mutex = Support::Mutex
      end

      # @private
      def ensure_implemented(*_args)
        # noop for basic proxies, see VerifyingProxy for behaviour.
      end

      # @private
      def initialize(object, order_group, options={})
        ensure_can_be_proxied!(object)

        @object = object
        @order_group = order_group
        @error_generator = ErrorGenerator.new(object)
        @messages_received = []
        @messages_received_mutex = Mutex.new
        @options = options
        @null_object = false
        @method_doubles = Hash.new { |h, k| h[k] = MethodDouble.new(@object, k, self) }
      end

      # @private
      def ensure_can_be_proxied!(object)
        return unless object.is_a?(Symbol)

        msg = "Cannot proxy frozen objects. Symbols such as #{object} cannot be mocked or stubbed."
        raise ArgumentError, msg
      end

      # @private
      attr_reader :object

      # @private
      def null_object?
        @null_object
      end

      # @private
      # Tells the object to ignore any messages that aren't explicitly set as
      # stubs or message expectations.
      def as_null_object
        @null_object = true
        @object
      end

      # @private
      def original_method_handle_for(_message)
        nil
      end

      DEFAULT_MESSAGE_EXPECTATION_OPTS = {}.freeze

      # @private
      def add_message_expectation(method_name, opts=DEFAULT_MESSAGE_EXPECTATION_OPTS, &block)
        location = opts.fetch(:expected_from) { CallerFilter.first_non_rspec_line }
        meth_double = method_double_for(method_name)

        if null_object? && !block
          meth_double.add_default_stub(@error_generator, @order_group, location, opts) do
            @object
          end
        end

        meth_double.add_expectation @error_generator, @order_group, location, opts, &block
      end

      # @private
      def add_simple_expectation(method_name, response, location)
        method_double_for(method_name).add_simple_expectation method_name, response, @error_generator, location
      end

      # @private
      def build_expectation(method_name)
        meth_double = method_double_for(method_name)

        meth_double.build_expectation(
          @error_generator,
          @order_group
        )
      end

      # @private
      def replay_received_message_on(expectation, &block)
        expected_method_name = expectation.message
        meth_double = method_double_for(expected_method_name)

        if meth_double.expectations.any?
          @error_generator.raise_expectation_on_mocked_method(expected_method_name)
        end

        unless null_object? || meth_double.stubs.any?
          @error_generator.raise_expectation_on_unstubbed_method(expected_method_name)
        end

        @messages_received_mutex.synchronize do
          @messages_received.each do |(actual_method_name, args, received_block)|
            next unless expectation.matches?(actual_method_name, *args)

            expectation.safe_invoke(nil)
            block.call(*args, &received_block) if block
          end
        end
      end

      # @private
      def check_for_unexpected_arguments(expectation)
        @messages_received_mutex.synchronize do
          return if @messages_received.empty?

          return if @messages_received.any? { |method_name, args, _| expectation.matches?(method_name, *args) }

          name_but_not_args, others = @messages_received.partition do |(method_name, args, _)|
            expectation.matches_name_but_not_args(method_name, *args)
          end

          return if name_but_not_args.empty? && !others.empty?

          expectation.raise_unexpected_message_args_error(name_but_not_args.map { |args| args[1] })
        end
      end

      # @private
      def add_stub(method_name, opts={}, &implementation)
        location = opts.fetch(:expected_from) { CallerFilter.first_non_rspec_line }
        method_double_for(method_name).add_stub @error_generator, @order_group, location, opts, &implementation
      end

      # @private
      def add_simple_stub(method_name, response)
        method_double_for(method_name).add_simple_stub method_name, response
      end

      # @private
      def remove_stub(method_name)
        method_double_for(method_name).remove_stub
      end

      # @private
      def remove_stub_if_present(method_name)
        method_double_for(method_name).remove_stub_if_present
      end

      # @private
      def verify
        @method_doubles.each_value { |d| d.verify }
      end

      # @private
      def reset
        @messages_received_mutex.synchronize do
          @messages_received.clear
        end
      end

      # @private
      def received_message?(method_name, *args, &block)
        @messages_received_mutex.synchronize do
          @messages_received.any? { |array| array == [method_name, args, block] }
        end
      end

      # @private
      def messages_arg_list
        @messages_received_mutex.synchronize do
          @messages_received.map { |_, args, _| args }
        end
      end

      # @private
      def has_negative_expectation?(message)
        method_double_for(message).expectations.find { |expectation| expectation.negative_expectation_for?(message) }
      end

      # @private
      def record_message_received(message, *args, &block)
        @order_group.invoked SpecificMessage.new(object, message, args)
        @messages_received_mutex.synchronize do
          @messages_received << [message, args, block]
        end
      end
      ruby2_keywords :record_message_received if respond_to?(:ruby2_keywords, true)

      # @private
      def message_received(message, *args, &block)
        record_message_received message, *args, &block

        expectation = find_matching_expectation(message, *args)
        stub = find_matching_method_stub(message, *args)

        if (stub && expectation && expectation.called_max_times?) || (stub && !expectation)
          expectation.increase_actual_received_count! if expectation && expectation.actual_received_count_matters?
          if (expectation = find_almost_matching_expectation(message, *args))
            expectation.advise(*args) unless expectation.expected_messages_received?
          end
          stub.invoke(nil, *args, &block)
        elsif expectation
          expectation.unadvise(messages_arg_list)
          expectation.invoke(stub, *args, &block)
        elsif (expectation = find_almost_matching_expectation(message, *args))
          expectation.advise(*args) if null_object? unless expectation.expected_messages_received?

          if null_object? || !has_negative_expectation?(message)
            expectation.raise_unexpected_message_args_error([args])
          end
        elsif (stub = find_almost_matching_stub(message, *args))
          stub.advise(*args)
          raise_missing_default_stub_error(stub, [args])
        elsif Class === @object
          @object.superclass.__send__(message, *args, &block)
        else
          @object.__send__(:method_missing, message, *args, &block)
        end
      end
      ruby2_keywords :message_received if respond_to?(:ruby2_keywords, true)

      # @private
      def raise_unexpected_message_error(method_name, args)
        @error_generator.raise_unexpected_message_error method_name, args
      end

      # @private
      def raise_missing_default_stub_error(expectation, args_for_multiple_calls)
        @error_generator.raise_missing_default_stub_error(expectation, args_for_multiple_calls)
      end

      # @private
      def visibility_for(_method_name)
        # This is the default (for test doubles). Subclasses override this.
        :public
      end

      if Support::RubyFeatures.module_prepends_supported?
        def self.prepended_modules_of(klass)
          ancestors = klass.ancestors

          # `|| 0` is necessary for Ruby 2.0, where the singleton class
          # is only in the ancestor list when there are prepended modules.
          singleton_index = ancestors.index(klass) || 0

          ancestors[0, singleton_index]
        end

        def prepended_modules_of_singleton_class
          @prepended_modules_of_singleton_class ||= RSpec::Mocks::Proxy.prepended_modules_of(@object.singleton_class)
        end
      end

      # @private
      def method_double_if_exists_for_message(message)
        method_double_for(message) if @method_doubles.key?(message.to_sym)
      end

    private

      def method_double_for(message)
        @method_doubles[message.to_sym]
      end

      def find_matching_expectation(method_name, *args)
        find_best_matching_expectation_for(method_name) do |expectation|
          expectation.matches?(method_name, *args)
        end
      end
      ruby2_keywords :find_matching_expectation if respond_to?(:ruby2_keywords, true)

      def find_almost_matching_expectation(method_name, *args)
        find_best_matching_expectation_for(method_name) do |expectation|
          expectation.matches_name_but_not_args(method_name, *args)
        end
      end
      ruby2_keywords :find_almost_matching_expectation if respond_to?(:ruby2_keywords, true)

      def find_best_matching_expectation_for(method_name)
        first_match = nil

        method_double_for(method_name).expectations.each do |expectation|
          next unless yield expectation
          return expectation unless expectation.called_max_times?
          first_match ||= expectation
        end

        first_match
      end

      def find_matching_method_stub(method_name, *args)
        method_double_for(method_name).stubs.find { |stub| stub.matches?(method_name, *args) }
      end
      ruby2_keywords :find_matching_method_stub if respond_to?(:ruby2_keywords, true)

      def find_almost_matching_stub(method_name, *args)
        method_double_for(method_name).stubs.find { |stub| stub.matches_name_but_not_args(method_name, *args) }
      end
      ruby2_keywords :find_almost_matching_stub if respond_to?(:ruby2_keywords, true)
    end

    # @private
    class TestDoubleProxy < Proxy
      def reset
        @method_doubles.clear
        object.__disallow_further_usage!
        super
      end
    end

    # @private
    class PartialDoubleProxy < Proxy
      def original_method_handle_for(message)
        if any_instance_class_recorder_observing_method?(@object.class, message)
          message = ::RSpec::Mocks.space.
            any_instance_recorder_for(@object.class).
            build_alias_method_name(message)
        end

        ::RSpec::Support.method_handle_for(@object, message)
      rescue NameError
        nil
      end

      # @private
      def add_simple_expectation(method_name, response, location)
        method_double_for(method_name).configure_method
        super
      end

      # @private
      def add_simple_stub(method_name, response)
        method_double_for(method_name).configure_method
        super
      end

      # @private
      def visibility_for(method_name)
        # We fall back to :public because by default we allow undefined methods
        # to be stubbed, and when we do so, we make them public.
        MethodReference.method_visibility_for(@object, method_name) || :public
      end

      def reset
        @method_doubles.each_value { |d| d.reset }
        super
      end

      def message_received(message, *args, &block)
        RSpec::Mocks.space.any_instance_recorders_from_ancestry_of(object).each do |subscriber|
          subscriber.notify_received_message(object, message, args, block)
        end
        super
      end
      ruby2_keywords :message_received if respond_to?(:ruby2_keywords, true)

    private

      def any_instance_class_recorder_observing_method?(klass, method_name)
        only_return_existing = true
        recorder = ::RSpec::Mocks.space.any_instance_recorder_for(klass, only_return_existing)
        return true if recorder && recorder.already_observing?(method_name)

        superklass = klass.superclass
        return false if superklass.nil?
        any_instance_class_recorder_observing_method?(superklass, method_name)
      end
    end

    # @private
    # When we mock or stub a method on a class, we have to treat it a bit different,
    # because normally singleton method definitions only affect the object on which
    # they are defined, but on classes they affect subclasses, too. As a result,
    # we need some special handling to get the original method.
    module PartialClassDoubleProxyMethods
      def initialize(source_space, *args)
        @source_space = source_space
        super(*args)
      end

      # Consider this situation:
      #
      #   class A; end
      #   class B < A; end
      #
      #   allow(A).to receive(:new)
      #   expect(B).to receive(:new).and_call_original
      #
      # When getting the original definition for `B.new`, we cannot rely purely on
      # using `B.method(:new)` before our redefinition is defined on `B`, because
      # `B.method(:new)` will return a method that will execute the stubbed version
      # of the method on `A` since singleton methods on classes are in the lookup
      # hierarchy.
      #
      # To do it properly, we need to find the original definition of `new` from `A`
      # from _before_ `A` was stubbed, and we need to rebind it to `B` so that it will
      # run with the proper `self`.
      #
      # That's what this method (together with `original_unbound_method_handle_from_ancestor_for`)
      # does.
      def original_method_handle_for(message)
        unbound_method = superclass_proxy &&
          superclass_proxy.original_unbound_method_handle_from_ancestor_for(message.to_sym)

        return super unless unbound_method
        unbound_method.bind(object)
        # :nocov:
      rescue TypeError
        if RUBY_VERSION == '1.8.7'
          # In MRI 1.8.7, a singleton method on a class cannot be rebound to its subclass
          if unbound_method && unbound_method.owner.ancestors.first != unbound_method.owner
            # This is a singleton method; we can't do anything with it
            # But we can work around this using a different implementation
            double = method_double_from_ancestor_for(message)
            return object.method(double.method_stasher.stashed_method_name)
          end
        end
        raise
        # :nocov:
      end

    protected

      def original_unbound_method_handle_from_ancestor_for(message)
        double = method_double_from_ancestor_for(message)
        double && double.original_method.unbind
      end

      def method_double_from_ancestor_for(message)
        @method_doubles.fetch(message) do
          # The fact that there is no method double for this message indicates
          # that it has not been redefined by rspec-mocks. We need to continue
          # looking up the ancestor chain.
          return superclass_proxy &&
            superclass_proxy.method_double_from_ancestor_for(message)
        end
      end

      def superclass_proxy
        return @superclass_proxy if defined?(@superclass_proxy)

        if (superclass = object.superclass)
          @superclass_proxy = @source_space.superclass_proxy_for(superclass)
        else
          @superclass_proxy = nil
        end
      end
    end

    # @private
    class PartialClassDoubleProxy < PartialDoubleProxy
      include PartialClassDoubleProxyMethods
    end

    # @private
    class ProxyForNil < PartialDoubleProxy
      def initialize(order_group)
        set_expectation_behavior
        super(nil, order_group)
      end

      attr_accessor :disallow_expectations
      attr_accessor :warn_about_expectations

      def add_message_expectation(method_name, opts={}, &block)
        warn_or_raise!(method_name)
        super
      end

      def add_stub(method_name, opts={}, &implementation)
        warn_or_raise!(method_name)
        super
      end

    private

      def set_expectation_behavior
        case RSpec::Mocks.configuration.allow_message_expectations_on_nil
        when false
          @warn_about_expectations = false
          @disallow_expectations = true
        when true
          @warn_about_expectations = false
          @disallow_expectations = false
        else
          @warn_about_expectations = true
          @disallow_expectations = false
        end
      end

      def warn_or_raise!(method_name)
        # This method intentionally swallows the message when
        # neither disallow_expectations nor warn_about_expectations
        # are set to true.
        if disallow_expectations
          raise_error(method_name)
        elsif warn_about_expectations
          warn(method_name)
        end
      end

      def warn(method_name)
        warning_msg = @error_generator.expectation_on_nil_message(method_name)
        RSpec.warning(warning_msg)
      end

      def raise_error(method_name)
        @error_generator.raise_expectation_on_nil_error(method_name)
      end
    end
  end
end
