# frozen_string_literal: true

require 'spec_helper'

RSpec.describe BulkImport, type: :model, feature_category: :importers do
  using RSpec::Parameterized::TableSyntax

  let_it_be(:created_bulk_import) { create(:bulk_import, :created, updated_at: 2.hours.ago) }
  let_it_be(:started_bulk_import) { create(:bulk_import, :started, updated_at: 3.hours.ago) }
  let_it_be(:finished_bulk_import) { create(:bulk_import, :finished, updated_at: 1.hour.ago) }
  let_it_be(:failed_bulk_import) { create(:bulk_import, :failed) }
  let_it_be(:stale_created_bulk_import) { create(:bulk_import, :created, updated_at: 3.days.ago) }
  let_it_be(:stale_started_bulk_import) { create(:bulk_import, :started, updated_at: 2.days.ago) }

  describe 'associations' do
    it { is_expected.to belong_to(:user).required }
    it { is_expected.to belong_to(:organization) }
    it { is_expected.to have_one(:configuration) }
    it { is_expected.to have_many(:entities) }
  end

  describe 'validations' do
    it { is_expected.to validate_presence_of(:source_type) }
    it { is_expected.to validate_presence_of(:status) }

    it { is_expected.to define_enum_for(:source_type).with_values(%i[gitlab]) }
  end

  describe 'scopes' do
    describe '.stale' do
      subject { described_class.stale }

      it { is_expected.to contain_exactly(stale_created_bulk_import, stale_started_bulk_import) }
    end

    describe '.order_by_updated_at_and_id' do
      subject { described_class.order_by_updated_at_and_id(:desc) }

      it 'sorts by given direction' do
        is_expected.to eq([
          failed_bulk_import,
          finished_bulk_import,
          created_bulk_import,
          started_bulk_import,
          stale_started_bulk_import,
          stale_created_bulk_import
        ])
      end
    end

    describe '.with_configuration' do
      it 'includes configuration association' do
        imports = described_class.with_configuration

        expect(imports.first.association_cached?(:configuration)).to be(true)
      end
    end
  end

  describe '.all_human_statuses' do
    it 'returns all human readable entity statuses' do
      expect(described_class.all_human_statuses)
        .to contain_exactly('created', 'started', 'finished', 'failed', 'timeout', 'canceled')
    end
  end

  describe '.min_gl_version_for_project' do
    it { expect(described_class.min_gl_version_for_project_migration).to be_a(Gitlab::VersionInfo) }
    it { expect(described_class.min_gl_version_for_project_migration.to_s).to eq('14.4.0') }
  end

  describe '#completed?' do
    it { expect(described_class.new(status: -2)).to be_completed }
    it { expect(described_class.new(status: -1)).to be_completed }
    it { expect(described_class.new(status: 0)).not_to be_completed }
    it { expect(described_class.new(status: 1)).not_to be_completed }
    it { expect(described_class.new(status: 2)).to be_completed }
    it { expect(described_class.new(status: 3)).to be_completed }
  end

  describe '#source_version_info' do
    it 'returns source_version as Gitlab::VersionInfo' do
      bulk_import = build(:bulk_import, source_version: '9.13.2')

      expect(bulk_import.source_version_info).to be_a(Gitlab::VersionInfo)
      expect(bulk_import.source_version_info.to_s).to eq(bulk_import.source_version)
    end
  end

  describe '#update_has_failures' do
    let(:import) { create(:bulk_import, :started) }
    let(:entity) { create(:bulk_import_entity, bulk_import: import) }

    context 'when entity has failures' do
      it 'sets has_failures flag to true' do
        expect(import.has_failures).to eq(false)

        entity.update!(has_failures: true)
        import.fail_op!

        expect(import.has_failures).to eq(true)
      end
    end

    context 'when entity does not have failures' do
      it 'sets has_failures flag to false' do
        expect(import.has_failures).to eq(false)

        entity.update!(has_failures: false)
        import.fail_op!

        expect(import.has_failures).to eq(false)
      end
    end
  end

  describe '#supports_batched_export?' do
    context 'when source version is greater than min supported version for batched migrations' do
      it 'returns true' do
        bulk_import = build(:bulk_import, source_version: '16.2.0')

        expect(bulk_import.supports_batched_export?).to eq(true)
      end
    end

    context 'when source version is less than min supported version for batched migrations' do
      it 'returns false' do
        bulk_import = build(:bulk_import, source_version: '15.5.0')

        expect(bulk_import.supports_batched_export?).to eq(false)
      end
    end
  end

  describe 'import canceling' do
    let(:import) { create(:bulk_import, :started) }

    it 'marks import as canceled' do
      expect(import.canceled?).to eq(false)

      import.cancel!

      expect(import.canceled?).to eq(true)
    end

    context 'when import has entities' do
      it 'marks entities as canceled' do
        entity = create(:bulk_import_entity, bulk_import: import)

        expect(entity.canceled?).to eq(false)

        import.cancel!

        expect(entity.reload.canceled?).to eq(true)
      end
    end
  end

  describe 'completion notification trigger' do
    RSpec::Matchers.define :send_completion_notification do
      def supports_block_expectations?
        true
      end

      match(notify_expectation_failures: true) do |proc|
        expect(Notify).to receive(:bulk_import_complete).with(import.user.id, import.id).and_call_original

        proc.call
        true
      end

      match_when_negated(notify_expectation_failures: true) do |proc|
        expect(Notify).not_to receive(:bulk_import_complete)

        proc.call
        true
      end
    end

    subject(:import) { create(:bulk_import, :started) }

    let(:non_triggering_events) do
      import.status_paths.events - %i[finish cleanup_stale fail_op]
    end

    it { expect { import.finish! }.to send_completion_notification }
    it { expect { import.fail_op! }.to send_completion_notification }
    it { expect { import.cleanup_stale! }.to send_completion_notification }

    it "does not email after non-completing events" do
      non_triggering_events.each do |event|
        expect { import.send(:"#{event}!") }.not_to send_completion_notification
      end
    end
  end

  describe '#destination_group_roots' do
    let_it_be(:import) { create(:bulk_import, :started) }

    let_it_be(:project_namespace) { create(:group) }
    let_it_be(:project) { create(:project, namespace: project_namespace) }
    let_it_be(:root_project_entity) do
      create(:bulk_import_entity, :project_entity, project: project, bulk_import: import)
    end

    let_it_be(:top_level_group) { create(:group) }
    let_it_be(:root_group_entity) do
      create(:bulk_import_entity, :group_entity, group: top_level_group, bulk_import: import)
    end

    let_it_be(:child_group_entity) do
      create(:bulk_import_entity, parent: root_group_entity, bulk_import: import)
    end

    it 'returns the topmost group nodes of the import entity tree' do
      expect(import.destination_group_roots).to match_array([project_namespace, top_level_group])
    end
  end

  describe '#source_url' do
    it 'returns migration source url via configuration' do
      import = create(:bulk_import, :with_configuration)

      expect(import.source_url).to eq(import.configuration.url)
    end

    context 'when configuration is missing' do
      it 'returns nil' do
        import = create(:bulk_import)

        expect(import.source_url).to be_nil
      end
    end
  end

  describe '#source_equals_destination?' do
    subject(:bulk_import) do
      build_stubbed(:bulk_import,
        configuration: build_stubbed(:bulk_import_configuration, url: source_url)
      )
    end

    before do
      allow(Settings.gitlab).to receive(:host).and_return('gitlab.example')
    end

    where(:source_url, :value) do
      'https://gitlab.example' | true
      'https://gitlab.example:443' | true
      'https://gitlab.example/' | true
      'https://gitlab.example/dir' | true
      'http://gitlab.example' | true
      'https://gitlab.example2' | false
      'https://subdomain.example' | false
      'https://subdomain.gitlab.example' | false
      'http://192.168.1.1' | false
    end

    with_them do
      it { expect(bulk_import.source_equals_destination?).to eq(value) }
    end
  end

  describe '#namespaces_with_unassigned_placeholders' do
    let_it_be(:group) { create(:group) }
    let_it_be(:entity) do
      create(:bulk_import_entity, :group_entity, bulk_import: finished_bulk_import, group: group)
    end

    before do
      create_list(:import_source_user, 5, :completed, namespace: group)
    end

    context 'when all placeholders have been assigned' do
      it { expect(finished_bulk_import.namespaces_with_unassigned_placeholders).to be_empty }
    end

    context 'when some placeholders have not been assigned' do
      before do
        create(:import_source_user, :pending_reassignment, namespace: group)
      end

      it { expect(finished_bulk_import.namespaces_with_unassigned_placeholders).to include(group) }
    end
  end

  describe '#schedule_configuration_purge' do
    subject(:schedule_purge) { bulk_import.schedule_configuration_purge }

    context 'when configuration exists' do
      let_it_be(:bulk_import) { create(:bulk_import, :with_configuration) }
      let(:configuration_id) { bulk_import.configuration.id }
      let(:delay) { 24.hours }

      before do
        allow(bulk_import).to receive(:run_after_commit).and_yield
        stub_const('BulkImport::PURGE_CONFIGURATION_DELAY', delay)
      end

      it 'schedules purge worker with default delay' do
        expect(Import::BulkImports::ConfigurationPurgeWorker).to receive(:perform_in).with(delay, configuration_id)

        schedule_purge
      end
    end

    context 'when configuration does not exist' do
      let_it_be(:bulk_import) { create(:bulk_import) }

      it 'does not schedule any job' do
        expect(Import::BulkImports::ConfigurationPurgeWorker).not_to receive(:perform_in)

        schedule_purge
      end
    end
  end

  describe 'state-machine transitions for configuration purging' do
    RSpec::Matchers.define :schedule_configuration_purge do
      def supports_block_expectations?
        true
      end

      match(notify_expectation_failures: true) do |proc|
        expect_next_instance_of(described_class) do |instance|
          expect(instance).to receive(:schedule_configuration_purge).and_call_original
        end

        proc.call
        true
      end

      match_when_negated(notify_expectation_failures: true) do |proc|
        expect_next_instance_of(described_class) do |instance|
          expect(instance).not_to receive(:schedule_configuration_purge)
        end

        proc.call
        true
      end
    end

    context "when bulk import transitions to a completed state" do
      subject(:import) { create(:bulk_import, :started, :with_configuration) }

      it { expect { import.finish! }.to schedule_configuration_purge }
      it { expect { import.fail_op! }.to schedule_configuration_purge }
      it { expect { import.cleanup_stale! }.to schedule_configuration_purge }
      it { expect { import.cancel }.to schedule_configuration_purge }
    end

    context "when bulk import transitions to a non-completed state" do
      subject(:import) { create(:bulk_import, :created, :with_configuration) }

      it { expect { import.start }.not_to schedule_configuration_purge }
    end
  end
end
