shared_examples 'a container' do
  describe 'configuration' do
    describe 'registry' do
      describe 'default' do
        it { expect(klass.config.registry).to be_a(Dry::Container::Registry) }
      end

      describe 'custom' do
        let(:custom_registry) { double('Registry') }
        let(:key) { :key }
        let(:item) { :item }
        let(:options) { {} }

        before do
          klass.configure do |config|
            config.registry = custom_registry
          end

          allow(custom_registry).to receive(:call)
        end

        after do
          # HACK: Have to reset the configuration so that it doesn't
          # interfere with other specs
          klass.configure do |config|
            config.registry = Dry::Container::Registry.new
          end
        end

        subject! { container.register(key, item, options) }

        it do
          expect(custom_registry).to have_received(:call).with(
            container._container,
            key,
            item,
            options
          )
        end
      end
    end

    describe 'resolver' do
      describe 'default' do
        it { expect(klass.config.resolver).to be_a(Dry::Container::Resolver) }
      end

      describe 'custom' do
        let(:custom_resolver) { double('Resolver') }
        let(:item) { double('Item') }
        let(:key) { :key }

        before do
          klass.configure do |config|
            config.resolver = custom_resolver
          end

          allow(custom_resolver).to receive(:call).and_return(item)
        end

        after do
          # HACK: Have to reset the configuration so that it doesn't
          # interfere with other specs
          klass.configure do |config|
            config.resolver = Dry::Container::Resolver.new
          end
        end

        subject! { container.resolve(key) }

        it { expect(custom_resolver).to have_received(:call).with(container._container, key) }
        it { is_expected.to eq(item) }
      end
    end

    describe 'namespace_separator' do
      describe 'default' do
        it { expect(klass.config.namespace_separator).to eq('.') }
      end

      describe 'custom' do
        let(:custom_registry) { double('Registry') }
        let(:key) { 'key' }
        let(:namespace_separator) { '-' }
        let(:namespace) { 'one' }

        before do
          klass.configure do |config|
            config.namespace_separator = namespace_separator
          end

          container.namespace(namespace) do
            register('key', 'item')
          end
        end

        after do
          # HACK: Have to reset the configuration so that it doesn't
          # interfere with other specs
          klass.configure do |config|
            config.namespace_separator = '.'
          end
        end

        subject! { container.resolve([namespace, key].join(namespace_separator)) }

        it { is_expected.to eq('item') }
      end
    end
  end

  context 'with default configuration' do
    describe 'registering a block' do
      context 'without options' do
        context 'without arguments' do
          it 'registers and resolves an object' do
            container.register(:item) { 'item' }

            expect(container.keys).to eq(['item'])
            expect(container.key?(:item)).to be true
            expect(container.resolve(:item)).to eq('item')
          end
        end

        context 'with arguments' do
          it 'registers and resolves a proc' do
            container.register(:item) { |item| item }

            expect(container.resolve(:item).call('item')).to eq('item')
          end

          it 'does not call a proc on resolving if one accepts an arbitrary number of keyword arguments' do
            container.register(:item) { |**kw| 'item' }

            expect(container.resolve(:item)).to be_a_kind_of Proc
            expect(container.resolve(:item).call).to eq('item')
          end
        end
      end

      context 'with option call: false' do
        it 'registers and resolves a proc' do
          container.register(:item, call: false) { 'item' }

          expect(container.keys).to eq(['item'])
          expect(container.key?(:item)).to be true
          expect(container.resolve(:item).call).to eq('item')
          expect(container[:item].call).to eq('item')
        end
      end
    end

    describe 'registering a proc' do
      context 'without options' do
        context 'without arguments' do
          it 'registers and resolves an object' do
            container.register(:item, proc { 'item' })

            expect(container.keys).to eq(['item'])
            expect(container.key?(:item)).to be true
            expect(container.resolve(:item)).to eq('item')
            expect(container[:item]).to eq('item')
          end
        end

        context 'with arguments' do
          it 'registers and resolves a proc' do
            container.register(:item, proc { |item| item })

            expect(container.keys).to eq(['item'])
            expect(container.key?(:item)).to be true
            expect(container.resolve(:item).call('item')).to eq('item')
            expect(container[:item].call('item')).to eq('item')
          end
        end
      end

      context 'with option call: false' do
        it 'registers and resolves a proc' do
          container.register(:item, proc { 'item' }, call: false)

          expect(container.keys).to eq(['item'])
          expect(container.key?(:item)).to be true
          expect(container.resolve(:item).call).to eq('item')
          expect(container[:item].call).to eq('item')
        end
      end
    end

    describe 'registering an object' do
      context 'without options' do
        it 'registers and resolves the object' do
          item = 'item'
          container.register(:item, item)

          expect(container.keys).to eq(['item'])
          expect(container.key?(:item)).to be true
          expect(container.resolve(:item)).to be(item)
          expect(container[:item]).to be(item)
        end
      end

      context 'with option call: false' do
        it 'registers and resolves an object' do
          item = -> { 'test' }
          container.register(:item, item, call: false)

          expect(container.keys).to eq(['item'])
          expect(container.key?(:item)).to be true
          expect(container.resolve(:item)).to eq(item)
          expect(container[:item]).to eq(item)
        end
      end
    end

    describe 'registering with the same key multiple times' do
      it do
        container.register(:item, proc { 'item' })

        expect { container.register(:item, proc { 'item' }) }.to raise_error(Dry::Container::Error)
      end
    end

    describe 'resolving with a key that has not been registered' do
      it do
        expect(container.key?(:item)).to be false
        expect { container.resolve(:item) }.to raise_error(Dry::Container::Error)
      end
    end

    describe 'mixing Strings and Symbols' do
      it do
        container.register(:item, 'item')
        expect(container.resolve('item')).to eql('item')
      end
    end

    describe '#merge' do
      let(:key) { :key }
      let(:other) { Dry::Container.new }

      before do
        other.register(key) { :item }
      end

      context 'without namespace argument' do
        subject! { container.merge(other) }

        it { expect(container.resolve(key)).to be(:item) }
        it { expect(container[key]).to be(:item) }
      end

      context 'with namespace argument' do
        subject! { container.merge(other, namespace: namespace) }

        context 'when namespace is nil' do
          let(:namespace) { nil }

          it { expect(container.resolve(key)).to be(:item) }
          it { expect(container[key]).to be(:item) }
        end

        context 'when namespace is not nil' do
          let(:namespace) { 'namespace' }

          it { expect(container.resolve("#{namespace}.#{key}")).to be(:item) }
          it { expect(container["#{namespace}.#{key}"]).to be(:item) }
        end
      end
    end

    describe '#key?' do
      let(:key) { :key }

      before do
        container.register(key) { :item }
      end

      subject! { container.key?(resolve_key) }

      context 'when key exists in container' do
        let(:resolve_key) { key }

        it { is_expected.to be true }
      end

      context 'when key does not exist in container' do
        let(:resolve_key) { :random }

        it { is_expected.to be false }
      end
    end

    describe '#keys' do
      let(:keys) { [:key_1, :key_2] }
      let(:expected_keys) { ['key_1', 'key_2'] }

      before do
        keys.each do |key|
          container.register(key) { :item }
        end
      end

      subject! { container.keys }

      it 'returns stringified versions of all registered keys' do
        is_expected.to match_array(expected_keys)
      end
    end

    describe '#each_key' do
      let(:keys) { [:key_1, :key_2] }
      let(:expected_keys) { ['key_1', 'key_2'] }
      let!(:yielded_keys) { [] }

      before do
        keys.each do |key|
          container.register(key) { :item }
        end
      end

      subject! do
        container.each_key { |key| yielded_keys << key }
      end

      it 'yields stringified versions of all registered keys to the block' do
        expect(yielded_keys).to match_array(expected_keys)
      end

      it 'returns the container' do
        is_expected.to eq(container)
      end
    end

    describe 'namespace' do
      context 'when block does not take arguments' do
        before do
          container.namespace('one') do
            register('two', 2)
          end
        end

        subject! { container.resolve('one.two') }

        it 'registers items under the given namespace' do
          is_expected.to eq(2)
        end
      end

      context 'when block takes arguments' do
        before do
          container.namespace('one') do |c|
            c.register('two', 2)
          end
        end

        subject! { container.resolve('one.two') }

        it 'registers items under the given namespace' do
          is_expected.to eq(2)
        end
      end

      context 'with nesting' do
        before do
          container.namespace('one') do
            namespace('two') do
              register('three', 3)
            end
          end
        end

        subject! { container.resolve('one.two.three') }

        it 'registers items under the given namespaces' do
          is_expected.to eq(3)
        end
      end
    end

    describe 'import' do
      it 'allows importing of namespaces' do
        ns = Dry::Container::Namespace.new('one') do
          register('two', 2)
        end

        container.import(ns)

        expect(container.resolve('one.two')).to eq(2)
      end

      it 'allows importing of nested namespaces' do
        ns = Dry::Container::Namespace.new('two') do
          register('three', 3)
        end

        container.namespace('one') do
          import(ns)
        end

        expect(container.resolve('one.two.three')).to eq(3)
      end
    end
  end

  describe 'stubbing' do
    before do
      container.enable_stubs!

      container.register(:item, 'item')
      container.register(:foo, 'bar')
    end

    after do
      container.unstub
    end

    it 'keys can be stubbed' do
      container.stub(:item, 'stub')
      expect(container.resolve(:item)).to eql('stub')
      expect(container[:item]).to eql('stub')
    end

    it 'only other keys remain accesible' do
      container.stub(:item, 'stub')
      expect(container.resolve(:foo)).to eql('bar')
      expect(container[:foo]).to eql('bar')
    end

    it 'keys can be reverted back to their original value' do
      container.stub(:item, 'stub')
      container.unstub(:item)

      expect(container.resolve(:item)).to eql('item')
      expect(container[:item]).to eql('item')
    end

    describe 'with block argument' do
      it 'executes the block with the given stubs' do
        expect { |b| container.stub(:item, 'stub', &b) }.to yield_control
      end

      it 'keys are stubbed only while inside the block' do
        container.stub(:item, 'stub') do
          expect(container.resolve(:item)).to eql('stub')
        end

        expect(container.resolve(:item)).to eql('item')
      end
    end
  end
end