# frozen_string_literal: true

RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true|
  describe '.has_internal_id' do
    describe 'Module inclusion' do
      subject { described_class }

      it { is_expected.to include_module(AtomicInternalId) }
    end

    describe 'Validation' do
      context 'when presence validation is required' do
        before do
          skip unless validate_presence
        end

        context 'when creating an object' do
          before do
            allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
          end

          it 'raises an error if the internal id is blank' do
            expect { instance.save! }.to raise_error(AtomicInternalId::MissingValueError)
          end
        end

        context 'when updating an object' do
          it 'raises an error if the internal id is blank' do
            instance.save!

            write_internal_id(nil)
            allow(instance).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")

            expect { instance.save! }.to raise_error(AtomicInternalId::MissingValueError)
          end
        end
      end

      context 'when presence validation is not required' do
        before do
          skip if validate_presence
        end

        context 'when creating an object' do
          before do
            allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
          end

          it 'does not raise an error if the internal id is blank' do
            expect(read_internal_id).to be_nil

            expect { instance.save! }.not_to raise_error
          end
        end

        context 'when updating an object' do
          it 'does not raise an error if the internal id is blank' do
            instance.save!

            write_internal_id(nil)
            allow(instance).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")

            expect { instance.save! }.not_to raise_error
          end
        end
      end
    end

    describe 'Creating an instance' do
      subject { instance.save! }

      it 'saves a new instance properly' do
        expect { subject }.not_to raise_error
      end
    end

    describe 'internal id generation' do
      subject { instance.save! }

      it 'calls InternalId.generate_next and sets internal id attribute' do
        iid = rand(1..1000)

        # Need to do this before evaluating instance otherwise it gets set
        # already in factory
        allow(InternalId).to receive(:generate_next).and_return(iid)

        subject
        expect(read_internal_id).to eq(iid)

        expect(InternalId).to have_received(:generate_next).with(instance, scope_attrs, usage, any_args)
      end

      it 'does not overwrite an existing internal id' do
        write_internal_id(4711)

        allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347091') do
          expect { subject }.not_to change { read_internal_id }
        end
      end

      context 'when the instance has an internal ID set' do
        let(:internal_id) { 9001 }

        it 'calls InternalId.update_last_value and sets the `last_value` to that of the instance' do
          write_internal_id(internal_id)

          expect(InternalId)
            .to receive(:track_greatest)
            .with(instance, scope_attrs, usage, internal_id, any_args)
            .and_return(internal_id)

          subject
        end
      end
    end

    describe 'unsetting the instance internal id on rollback' do
      context 'when the internal id has been changed' do
        context 'when the internal id is automatically set' do
          it 'clears it on the instance' do
            write_internal_id(nil)

            allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347091') do
              expect_iid_to_be_set_and_rollback
            end

            expect(read_internal_id).to be_nil
          end
        end

        context 'when the internal id is manually set' do
          it 'does not clear it on the instance' do
            write_internal_id(100)

            allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347091') do
              expect_iid_to_be_set_and_rollback
            end

            expect(read_internal_id).not_to be_nil
          end
        end
      end

      context 'when the internal id has not been changed' do
        it 'preserves the value on the instance' do
          instance.save!
          original_id = read_internal_id

          expect(original_id).not_to be_nil

          expect_iid_to_be_set_and_rollback

          expect(read_internal_id).to eq(original_id)
        end
      end

      def expect_iid_to_be_set_and_rollback
        instance.transaction(requires_new: true) do
          instance.save!

          expect(read_internal_id).not_to be_nil

          raise ActiveRecord::Rollback
        end
      end
    end

    describe 'supply of internal ids' do
      let(:scope_value) { scope_attrs.each_value.first }
      let(:method_name) { :"with_#{scope}_#{internal_id_attribute}_supply" }

      it 'provides a persistent supply of IID values, sensitive to the current state' do
        iid = rand(1..1000)
        write_internal_id(iid)
        instance.public_send(:"track_#{scope}_#{internal_id_attribute}!")

        # Allocate 3 IID values
        described_class.public_send(method_name, scope_value) do |supply|
          3.times { supply.next_value }
        end

        described_class.public_send(method_name, scope_value) do |supply|
          expect(supply.next_value).to eq(iid + 4)
        end
      end
    end

    describe "#reset_scope_internal_id_attribute" do
      it 'rewinds the allocated IID' do
        expect { ensure_scope_attribute! }.not_to raise_error
        expect(read_internal_id).not_to be_nil

        expect(reset_scope_attribute).to be_nil
        expect(read_internal_id).to be_nil
      end

      it 'allocates the same IID' do
        internal_id = ensure_scope_attribute!
        reset_scope_attribute
        expect(read_internal_id).to be_nil

        expect(ensure_scope_attribute!).to eq(internal_id)
      end
    end

    def ensure_scope_attribute!
      instance.public_send(:"ensure_#{scope}_#{internal_id_attribute}!")
    end

    def reset_scope_attribute
      instance.public_send(:"reset_#{scope}_#{internal_id_attribute}")
    end

    def read_internal_id
      instance.public_send(internal_id_attribute)
    end

    def write_internal_id(value)
      instance.public_send(:"#{internal_id_attribute}=", value)
    end
  end
end
