require 'doublespeak_spec_helper'

module Shoulda::Matchers::Doublespeak
  describe DoubleCollection do
    describe '#register_stub' do
      it 'calls DoubleImplementationRegistry.find correctly' do
        allow(DoubleImplementationRegistry).to receive(:find)
        double_collection = described_class.new(build_world, :klass)

        double_collection.register_stub(:a_method)

        expect(DoubleImplementationRegistry).to have_received(:find).with(:stub)
      end

      it 'calls Double.new correctly' do
        world = build_world
        allow(DoubleImplementationRegistry).
          to receive(:find).
          and_return(:implementation)
        allow(Double).to receive(:new)
        double_collection = described_class.new(world, :klass)

        double_collection.register_stub(:a_method)

        expect(Double).
          to have_received(:new).
          with(world, :klass, :a_method, :implementation)
      end

      context 'if a double has already been registered for the method' do
        it 'does not call Double.new again' do
          world = build_world
          allow(DoubleImplementationRegistry).
            to receive(:find).
            and_return(:implementation)
          allow(Double).to receive(:new)
          double_collection = described_class.new(world, :klass)

          double_collection.register_stub(:a_method)
          double_collection.register_stub(:a_method)

          expect(Double).to have_received(:new).once
        end

        it 'returns the same Double' do
          world = build_world
          allow(DoubleImplementationRegistry).
            to receive(:find).
            and_return(:implementation)
          allow(Double).to receive(:new)
          double_collection = described_class.new(world, :klass)

          double1 = double_collection.register_stub(:a_method)
          double2 = double_collection.register_stub(:a_method)

          expect(double1).to equal(double2)
        end
      end
    end

    describe '#register_proxy' do
      it 'calls DoubleImplementationRegistry.find correctly' do
        allow(DoubleImplementationRegistry).to receive(:find)
        double_collection = described_class.new(build_world, :klass)

        double_collection.register_proxy(:a_method)

        expect(DoubleImplementationRegistry).
          to have_received(:find).
          with(:proxy)
      end

      it 'calls Double.new correctly' do
        world = build_world
        allow(DoubleImplementationRegistry).
          to receive(:find).
          and_return(:implementation)
        allow(Double).to receive(:new)
        double_collection = described_class.new(world, :klass)

        double_collection.register_proxy(:a_method)

        expect(Double).
          to have_received(:new).
          with(world, :klass, :a_method, :implementation)
      end

      context 'if a double has already been registered for the method' do
        it 'does not call Double.new again' do
          world = build_world
          allow(DoubleImplementationRegistry).
            to receive(:find).
            and_return(:implementation)
          allow(Double).to receive(:new)
          double_collection = described_class.new(world, :klass)

          double_collection.register_proxy(:a_method)
          double_collection.register_proxy(:a_method)

          expect(Double).to have_received(:new).once
        end

        it 'returns the same Double' do
          world = build_world
          allow(DoubleImplementationRegistry).
            to receive(:find).
            and_return(:implementation)
          allow(Double).to receive(:new)
          double_collection = described_class.new(world, :klass)

          double1 = double_collection.register_proxy(:a_method)
          double2 = double_collection.register_proxy(:a_method)

          expect(double1).to equal(double2)
        end
      end
    end

    describe '#activate' do
      it 'replaces all registered methods with doubles' do
        klass = create_class(first_method: 1, second_method: 2)
        double_collection = described_class.new(build_world, klass)
        double_collection.register_stub(:first_method)
        double_collection.register_stub(:second_method)

        double_collection.activate

        instance = klass.new
        expect(instance.first_method).to eq nil
        expect(instance.second_method).to eq nil
      end
    end

    describe '#deactivate' do
      it 'restores the original methods that were doubled' do
        klass = create_class(first_method: 1, second_method: 2)
        double_collection = described_class.new(build_world, klass)
        double_collection.register_stub(:first_method)
        double_collection.register_stub(:second_method)

        double_collection.activate
        double_collection.deactivate

        instance = klass.new
        expect(instance.first_method).to eq 1
        expect(instance.second_method).to eq 2
      end
    end

    describe '#calls_to' do
      it 'returns all calls to the given method' do
        klass = create_class(a_method: nil)
        double_collection = described_class.new(build_world, klass)
        double_collection.register_stub(:a_method)
        double_collection.activate

        actual_calls = [
          { args: [:some, :args, :here] },
          { args: [:some, :args], block: -> { :whatever } },
        ]
        instance = klass.new
        instance.a_method(*actual_calls[0][:args])
        instance.a_method(*actual_calls[1][:args], &actual_calls[1][:block])

        calls = double_collection.calls_to(:a_method)
        expect(calls[0].args).to eq actual_calls[0][:args]
        expect(calls[1].args).to eq actual_calls[1][:args]
        expect(calls[1].block).to eq actual_calls[1][:block]
      end

      it 'returns an empty array if the method has never been doubled' do
        klass = create_class
        double_collection = described_class.new(build_world, klass)
        expect(double_collection.calls_to(:non_existent_method)).to eq []
      end
    end

    def create_class(methods = {})
      Class.new.tap do |klass|
        methods.each do |name, value|
          klass.__send__(:define_method, name) { |*_args| value }
        end
      end
    end

    def build_world
      Shoulda::Matchers::Doublespeak::World.new
    end
  end
end
