require 'support/doubled_classes'

module RSpec
  module Mocks
    RSpec.describe "Method visibility for verified doubles" do
      include_context "with isolated configuration"

      before do
        RSpec::Mocks.configuration.verify_doubled_constant_names = true
      end

      context "for an instance double (when the class is loaded)" do
        shared_examples "preserves method visibility" do |visibility|
          method_name = :"defined_#{visibility}_method"

          it "can allow a #{visibility} instance method" do
            o = instance_double('LoadedClass')
            allow(o).to receive(method_name).and_return(3)
            expect(o.send method_name).to eq(3)
          end

          it "can expect a #{visibility} instance method" do
            o = instance_double('LoadedClass')
            expect(o).to receive(method_name)
            o.send method_name
          end

          it "preserves #{visibility} visibility when allowing a #{visibility} method" do
            preserves_visibility(method_name, visibility) do
              instance_double('LoadedClass').tap do |o|
                allow(o).to receive(method_name)
              end
            end
          end

          it "preserves #{visibility} visibility when expecting a #{visibility} method" do
            preserves_visibility(method_name, visibility) do
              instance_double('LoadedClass').tap do |o|
                expect(o).to receive(method_name).at_least(:once)
                o.send(method_name) # to satisfy the expectation
              end
            end
          end

          it "preserves #{visibility} visibility on a null object" do
            preserves_visibility(method_name, visibility) do
              instance_double('LoadedClass').as_null_object
            end
          end
        end

        include_examples "preserves method visibility", :private
        include_examples "preserves method visibility", :protected
      end

      context "for a class double (when the class is loaded)" do
        shared_examples "preserves method visibility" do |visibility|
          method_name = :"defined_#{visibility}_class_method"

          it "can allow a #{visibility} instance method" do
            o = class_double('LoadedClass')
            allow(o).to receive(method_name).and_return(3)
            expect(o.send method_name).to eq(3)
          end

          it "can expect a #{visibility} instance method" do
            o = class_double('LoadedClass')
            expect(o).to receive(method_name)
            o.send method_name
          end

          it "preserves #{visibility} visibility when allowing a #{visibility} method" do
            preserves_visibility(method_name, visibility) do
              class_double('LoadedClass').tap do |o|
                allow(o).to receive(method_name)
              end
            end
          end

          it "preserves #{visibility} visibility when expecting a #{visibility} method" do
            preserves_visibility(method_name, visibility) do
              class_double('LoadedClass').tap do |o|
                expect(o).to receive(method_name).at_least(:once)
                o.send(method_name) # to satisfy the expectation
              end
            end
          end

          it "preserves #{visibility} visibility on a null object" do
            preserves_visibility(method_name, visibility) do
              class_double('LoadedClass').as_null_object
            end
          end
        end

        include_examples "preserves method visibility", :private
        include_examples "preserves method visibility", :protected
      end

      def preserves_visibility(method_name, visibility)
        double = yield

        expect {
          # send bypasses visibility, so we use eval instead.
          eval("double.#{method_name}")
        }.to raise_error(NoMethodError, a_message_indicating_visibility_violation(method_name, visibility))

        expect { double.send(method_name) }.not_to raise_error
        expect { double.__send__(method_name) }.not_to raise_error

        unless double.null_object?
          # Null object doubles use `method_missing` and so the singleton class
          # doesn't know what methods are defined.
          singleton_class = class << double; self; end
          expect(singleton_class.send("#{visibility}_method_defined?", method_name)).to be true
        end
      end

      RSpec::Matchers.define :a_message_indicating_visibility_violation do |method_name, visibility|
        match do |msg|
          # This should NOT Be just `msg.match(visibility)` because the method being called
          # has the visibility name in it. We want to ensure it's a message that ruby is
          # stating is of the given visibility.
          msg.match("#{visibility} ") && msg.match(method_name.to_s)
        end
      end
    end
  end
end
