RSpec.shared_examples_for 'Msf::ModuleManager::Cache' do

  # Wait for data to be loaded
  before(:all) do
    Msf::Modules::Metadata::Cache.instance.get_metadata
  end

  let(:parent_path) do
    parent_pathname.to_path
  end

  let(:metadata_cache) do
    Msf::Modules::Metadata::Cache.instance
  end

  let(:parent_pathname) do
    Metasploit::Framework.root.join('modules')
  end

  let(:reference_name) do
    'windows/smb/ms08_067_netapi'
  end

  let(:type) do
    'exploit'
  end

  let(:path) do
    pathname.to_path
  end

  let(:pathname) do
    parent_pathname.join(
        'exploits',
        "#{reference_name}.rb"
    )
  end

  let(:pathname_modification_time) do
    pathname.mtime
  end

  context '#cache_empty?' do
    subject(:cache_empty?) do
      module_manager.cache_empty?
    end

    before(:example) do
      module_manager.send(:module_info_by_path=, module_info_by_path)
    end

    context 'with empty' do
      let(:module_info_by_path) do
        {}
      end

      it { is_expected.to be_truthy }
    end

    context 'without empty' do
      let(:module_info_by_path) do
        {
            'path/to/module' => {}
        }
      end

      it { is_expected.to be_falsey }
    end
  end

  context '#cache_in_memory' do
    def cache_in_memory
      module_manager.cache_in_memory(
          class_or_module,
          :path => path,
          :reference_name => reference_name,
          :type => type
      )
    end

    def module_info_by_path
      module_manager.send(:module_info_by_path)
    end

    let(:class_or_module) do
      double('Class<Msf::Module> or Module', :module_parent => namespace_module)
    end

    let(:namespace_module) do
      double('Msf::Modules::Namespace', :parent_path => parent_path)
    end

    context 'with existing :path' do
      it 'should update module_info_by_path' do
        expect {
          cache_in_memory
        }.to change { module_info_by_path }
      end

      context 'module_info_by_path' do
        subject(:module_info_by_path) do
          module_manager.send(:module_info_by_path)
        end

        before(:example) do
          cache_in_memory
        end

        it 'should have entry for path' do
          expect(module_info_by_path[path]).to be_a Hash
        end

        context 'value' do
          subject(:value) do
            module_info_by_path[path]
          end

          it 'should have modification time of :path option for :modification_time' do
            expect(value[:modification_time]).to eq pathname_modification_time
          end

          it 'should have parent path from namespace module for :parent_path' do
            expect(value[:parent_path]).to eq namespace_module.parent_path
          end

          it 'should use :reference_name option' do
            expect(value[:reference_name]).to eq reference_name
          end

          it 'should use :type option' do
            expect(value[:type]).to eq type
          end
        end
      end
    end

    context 'without existing :path' do
      let(:path) do
        'non/existent/path'
      end

      it 'should not raise error' do
        expect {
          cache_in_memory
        }.to_not raise_error
      end

      it 'should not update module_info_by_path' do
        expect {
          cache_in_memory
        }.to_not change { module_info_by_path }
      end
    end
  end

  describe '#load_cached_module' do
    context 'with memory cache' do
      subject(:load_cached_module) do
        module_manager.load_cached_module(type, reference_name, cache_type: Msf::ModuleManager::Cache::MEMORY)
      end

      before(:example) do
        module_manager.send(:module_info_by_path=, module_info_by_path)
      end

      context 'with module info in cache' do
        include_context 'Metasploit::Framework::Spec::Constants cleaner'

        let(:module_info_by_path) do
          {
            'path/to/module' => {
              :parent_path => parent_path,
              :reference_name => reference_name,
              :type => type
            }
          }
        end

        it 'should enumerate loaders until if it find the one where loadable?(parent_path) is true' do
          # Only the first one gets it since it finds the module
          first_loader = module_manager.send(:loaders).first
          expect(first_loader).to receive(:loadable_module?).with(parent_path, type, reference_name, cached_metadata: nil).and_return(false)
          expect(first_loader).to_not receive(:load_module)

          second_loader = module_manager.send(:loaders).second
          expect(second_loader).to receive(:loadable_module?).with(parent_path, type, reference_name, cached_metadata: nil).and_return(true)
          expect(second_loader).to receive(:load_module).with(parent_path, type, reference_name, force: true, cached_metadata: nil).and_call_original

          load_cached_module
        end

        it 'should force load using #load_module on the loader' do
          expect_any_instance_of(Msf::Modules::Loader::Directory).to receive(
                                                                       :load_module
                                                                     ).with(
            parent_path,
            type,
            reference_name,
            :force => true,
            :cached_metadata => nil
          ).and_call_original

          load_cached_module
        end

        context 'return from load_module' do
          before(:example) do
            # Only the first one gets it since it finds the module
            loader = module_manager.send(:loaders).first
            expect(loader).to receive(:load_module).and_return(module_loaded)
          end

          context 'with false' do
            let(:module_loaded) do
              false
            end

            it { is_expected.to be_falsey }
          end

          context 'with true' do
            let(:module_loaded) do
              true
            end

            it { is_expected.to be_truthy }
          end
        end
      end

      context 'without module info in cache' do
        let(:module_info_by_path) do
          {}
        end

        it { is_expected.to be_falsey }
      end
    end

    context 'with filesystem cache' do
      subject(:load_cached_module) do
        module_manager.load_cached_module(type, reference_name, cache_type: Msf::ModuleManager::Cache::FILESYSTEM)
      end

      context 'with module info in cache' do
        include_context 'Metasploit::Framework::Spec::Constants cleaner'

        it 'should enumerate loaders until if it find the one where loadable?(parent_path) is true' do
          first_loader = module_manager.send(:loaders).first
          expect(first_loader).to receive(:loadable_module?).with(parent_path, type, reference_name, cached_metadata: instance_of(Msf::Modules::Metadata::Obj)).and_return(false)
          expect(first_loader).to_not receive(:load_module)

          second_loader = module_manager.send(:loaders).second
          expect(second_loader).to receive(:loadable_module?).with(parent_path, type, reference_name, cached_metadata: instance_of(Msf::Modules::Metadata::Obj)).and_return(true)
          expect(second_loader).to receive(:load_module).with(parent_path, type, reference_name, force: true, cached_metadata: instance_of(Msf::Modules::Metadata::Obj)).and_call_original

          load_cached_module
        end

        it 'should force load using #load_module on the loader' do
          expect_any_instance_of(Msf::Modules::Loader::Directory).to receive(
                                                                       :load_module
                                                                     ).with(
            parent_path,
            type,
            reference_name,
            :force => true,
            :cached_metadata => instance_of(Msf::Modules::Metadata::Obj)
          ).and_call_original

          load_cached_module
        end

        context 'return from load_module' do
          before(:example) do
            # Only the first one gets it since it finds the module
            loader = module_manager.send(:loaders).first
            expect(loader).to receive(:load_module).and_return(module_loaded)
          end

          context 'with false' do
            let(:module_loaded) do
              false
            end

            it { is_expected.to be_falsey }
          end

          context 'with true' do
            let(:module_loaded) do
              true
            end

            it { is_expected.to be_truthy }
          end
        end
      end

      context 'without module info in cache' do
        before(:example) do
          allow(Msf::Modules::Metadata::Cache.instance).to receive(:get_module_reference).with(anything).and_return(nil)
        end
        it { is_expected.to be_falsey }
      end
    end


  end

  context '#refresh_cache_from_module_files' do

    context 'with module argument' do
      def refresh_cache_from_module_files
        module_manager.refresh_cache_from_module_files(module_class_or_instance)
      end

      let(:module_class_or_instance) do
        Class.new(Msf::Module)
      end

      it 'should update store and then update in-memory cache from the store for the given module_class_or_instance' do
        expect(metadata_cache).to receive(:refresh_metadata_instance).with(module_class_or_instance).ordered
        expect(module_manager).to receive(:refresh_cache_from_database).ordered

        refresh_cache_from_module_files
      end
    end

    context 'without module argument' do
      def refresh_cache_from_module_files
        module_manager.refresh_cache_from_module_files
      end

      it 'should update store and then update in-memory cache from the store for all modules' do
        expect(metadata_cache).to receive(:refresh_metadata).ordered
        expect(module_manager).to receive(:refresh_cache_from_database)

        refresh_cache_from_module_files
      end
    end

  end

  context '#refresh_cache_from_database' do
    def refresh_cache_from_database
      module_manager.refresh_cache_from_database
    end

    it 'should call #module_info_by_path_from_database!' do
      expect(module_manager).to receive(:module_info_by_path_from_database!)

      refresh_cache_from_database
    end
  end


  context '#module_info_by_path' do
    it 'should have protected method module_info_by_path' do
      expect(subject.respond_to?(:module_info_by_path, true)).to be_truthy
    end
  end

  context '#module_info_by_path=' do
    it 'should have protected method module_info_by_path=' do
      expect(subject.respond_to?(:module_info_by_path=, true)).to be_truthy
    end
  end

  context '#module_info_by_path_from_database!' do
    def module_info_by_path
      module_manager.send(:module_info_by_path)
    end

    def module_info_by_path_from_database!
      module_manager.send(:module_info_by_path_from_database!)
    end

    it 'should call get metadata' do
      allow(metadata_cache).to receive(:get_metadata).and_return([])
      expect(metadata_cache).to receive(:get_metadata)

      module_info_by_path_from_database!
    end

    context 'with database cache' do
      #
      # Let!s (let + before(:each))
      #

      let!(:mdm_module_detail) do
        FactoryBot.create(:mdm_module_detail,
                           :file => path,
                           :mtype => type,
                           :mtime => pathname.mtime,
                           :refname => reference_name
        )
      end

      it 'should create cache entry for path' do
        module_info_by_path_from_database!

        expect(module_info_by_path).to have_key(path)
      end

      context 'cache entry' do
        subject(:cache_entry) do
          module_info_by_path[path]
        end

        before(:example) do
          module_info_by_path_from_database!
        end

        it { expect(subject[:modification_time]).to be_a(Time) }
        it { expect(subject[:parent_path]).to eq(parent_path) }
        it { expect(subject[:reference_name]).to eq(reference_name) }
        it { expect(subject[:type]).to eq(type) }
      end

      context 'typed module set' do
        let(:typed_module_set) do
          module_manager.module_set(type)
        end

        context 'with reference_name' do
          before(:example) do
            typed_module_set[reference_name] = double('Msf::Module')
          end

          it 'should not change reference_name value' do
            expect {
              module_info_by_path_from_database!
            }.to_not change {
              typed_module_set[reference_name]
            }
          end
        end

        context 'without reference_name' do
          it 'should set reference_name value to nil' do
            module_info_by_path_from_database!

            # have to use fetch because [] will trigger de-symbolization and
            # instantiation.
            expect(typed_module_set.fetch(reference_name)).to eq nil
          end
        end
      end
    end

  end
end
