# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::Database::SharedModel, feature_category: :database do
  shared_examples 'shared model using the correct connection' do
    it 'overrides the connection for the duration of the block', :aggregate_failures do
      expect_original_connection_around do
        described_class.using_connection(new_connection) do
          expect(described_class.connection).to be(new_connection)
        end
      end
    end
  end

  describe 'using an external connection' do
    let!(:original_connection) { described_class.connection }
    let(:new_connection) { double('connection') }

    it_behaves_like 'shared model using the correct connection'

    it 'does not affect connections in other threads', :aggregate_failures do
      expect_original_connection_around do
        described_class.using_connection(new_connection) do
          expect(described_class.connection).to be(new_connection)

          Thread.new do
            expect(described_class.connection).not_to be(new_connection)
          end.join
        end
      end
    end

    it 'raises an error if the connection does not include shared gitlab_schema' do
      allow(Gitlab::Database)
        .to receive(:gitlab_schemas_for_connection)
        .with(new_connection)
        .and_return([:gitlab_main])

      expect_original_connection_around do
        expect do
          described_class.using_connection(new_connection) {}
        end.to raise_error(/Cannot set `SharedModel` to connection/)
      end
    end

    context 'with connection including gitlab_shared_org' do
      before do
        allow(Gitlab::Database).to receive(:gitlab_schemas_for_connection)
          .with(new_connection)
          .and_return([:gitlab_shared_org])
      end

      it_behaves_like 'shared model using the correct connection'
    end

    context 'with connection including gitlab_shared_cell_local' do
      before do
        allow(Gitlab::Database).to receive(:gitlab_schemas_for_connection)
          .with(new_connection)
          .and_return([:gitlab_shared_cell_local])
      end

      it_behaves_like 'shared model using the correct connection'
    end

    context 'when multiple connection overrides are nested', :aggregate_failures do
      let(:second_connection) { double('connection') }

      it 'allows the nesting with the same connection object' do
        expect_original_connection_around do
          described_class.using_connection(new_connection) do
            expect(described_class.connection).to be(new_connection)

            described_class.using_connection(new_connection) do
              expect(described_class.connection).to be(new_connection)
            end

            expect(described_class.connection).to be(new_connection)
          end
        end
      end

      it 'raises an error if the connection is changed' do
        expect_original_connection_around do
          described_class.using_connection(new_connection) do
            expect(described_class.connection).to be(new_connection)

            expect do
              described_class.using_connection(second_connection) {}
            end.to raise_error(/Cannot change connection for Gitlab::Database::SharedModel/)

            expect(described_class.connection).to be(new_connection)
          end
        end
      end
    end

    context 'when the block raises an error', :aggregate_failures do
      it 're-raises the error, removing the overridden connection' do
        expect_original_connection_around do
          expect do
            described_class.using_connection(new_connection) do
              expect(described_class.connection).to be(new_connection)

              raise 'here comes an error!'
            end
          end.to raise_error(RuntimeError, 'here comes an error!')
        end
      end
    end
  end

  describe '#connection_db_config' do
    let!(:original_connection) { shared_model_class.connection }
    let!(:original_connection_db_config) { shared_model_class.connection_db_config }
    let(:shared_model) { shared_model_class.new }
    let(:shared_model_class) do
      Class.new(described_class) do
        self.table_name = 'postgres_async_indexes'
      end
    end

    it 'returns the class connection_db_config' do
      expect(shared_model.connection_db_config).to eq(described_class.connection_db_config)
    end

    context 'when switching the class connection' do
      before do
        skip_if_multiple_databases_not_setup
      end

      let(:new_base_model) { Ci::ApplicationRecord }
      let(:new_connection) { new_base_model.connection }

      it 'returns the db_config of the used connection when using load balancing' do
        expect_original_connection_around do
          described_class.using_connection(new_connection) do
            expect(shared_model.connection_db_config).to eq(new_base_model.connection_db_config)
          end
        end

        # it restores the connection_db_config afterwards
        expect(shared_model.connection_db_config).to eq(original_connection_db_config)
      end
    end
  end

  def expect_original_connection_around
    # For safety, ensure our original connection is distinct from our double
    # This should be the case, but in case of something leaking we should verify
    expect(original_connection).not_to be(new_connection)
    expect(described_class.connection).to be(original_connection)

    yield

    expect(described_class.connection).to be(original_connection)
  end
end
