# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::BackgroundMigration::BackfillIdentifierNamesOfVulnerabilityReads,
  schema: 20241112161755,
  feature_category: :vulnerability_management do
  before(:all) do
    # This migration will not work if a sec database is configured. It should be finalized and removed prior to
    # sec db rollout.
    # Consult https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171707 for more info.
    skip_if_multiple_databases_are_setup(:sec)
  end

  let(:users) { table(:users) }
  let(:scanners) { table(:vulnerability_scanners) }
  let(:vulnerabilities) { table(:vulnerabilities) }
  let(:vulnerability_reads) { table(:vulnerability_reads) }
  let(:vulnerability_findings) { table(:vulnerability_occurrences) }
  let(:vulnerability_occurrence_identifiers) { table(:vulnerability_occurrence_identifiers) }
  let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }

  let(:organization) { table(:organizations).create!(name: 'organization', path: 'organization') }
  let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user', organization_id: organization.id) }
  let(:project) do
    table(:projects).create!(
      namespace_id: namespace.id,
      project_namespace_id: namespace.id,
      organization_id: organization.id
    )
  end

  let(:user) { users.create!(username: 'john_doe', email: 'johndoe@gitlab.com', projects_limit: 10) }
  let(:scanner) { scanners.create!(project_id: project.id, external_id: 'external_id', name: 'Test Scanner') }

  shared_context 'with vulnerability data' do
    let(:identifier_1) do
      create_identifier(external_id: 'A03:2021', external_type: 'owasp', name: 'A03:2021 - Injection')
    end

    let(:identifier_2) { create_identifier(external_id: 'CVE-2021-1234', external_type: 'cve', name: 'CVE-2021-1234') }
    let(:identifier_3) { create_identifier(external_id: '79', external_type: 'cwe', name: 'CWE-79') }

    let(:finding_1) { create_finding(primary_identifier_id: identifier_1.id) }
    let(:finding_2) { create_finding(primary_identifier_id: identifier_2.id) }
    let(:finding_3) { create_finding(primary_identifier_id: identifier_3.id) }

    let(:vulnerability_1) { create_vulnerability(title: 'vulnerability 1', finding_id: finding_1.id) }
    let(:vulnerability_2) { create_vulnerability(title: 'vulnerability 2', finding_id: finding_2.id) }
    let(:vulnerability_3) { create_vulnerability(title: 'vulnerability 3', finding_id: finding_3.id) }

    let!(:vulnerability_read_1) { create_vulnerability_read(vulnerability_id: vulnerability_1.id) }
    let!(:vulnerability_read_2) { create_vulnerability_read(vulnerability_id: vulnerability_2.id) }
    let!(:vulnerability_read_3) { create_vulnerability_read(vulnerability_id: vulnerability_3.id) }

    before do
      create_vulnerability_occurrence_identifier(occurrence_id: finding_1.id, identifier_id: identifier_1.id)
      create_vulnerability_occurrence_identifier(occurrence_id: finding_2.id, identifier_id: identifier_2.id)
      create_vulnerability_occurrence_identifier(occurrence_id: finding_3.id, identifier_id: identifier_3.id)

      finding_1.update!(vulnerability_id: vulnerability_1.id)
      finding_2.update!(vulnerability_id: vulnerability_2.id)
      finding_3.update!(vulnerability_id: vulnerability_3.id)
    end
  end

  describe '#perform' do
    subject(:perform_migration) do
      described_class.new(
        start_id: vulnerability_reads.first.id,
        end_id: vulnerability_reads.last.id,
        batch_table: :vulnerability_reads,
        batch_column: :id,
        sub_batch_size: vulnerability_reads.count,
        pause_ms: 0,
        connection: ActiveRecord::Base.connection
      ).perform
    end

    context 'with vulnerability data' do
      include_context 'with vulnerability data'

      it 'updates identifier_names for vulnerability_reads' do
        expect { perform_migration }
          .to change { vulnerability_read_1.reload.identifier_names }
          .from([]).to(array_including(identifier_1.name))
          .and change { vulnerability_read_2.reload.identifier_names }
          .from([]).to(array_including(identifier_2.name))
          .and change { vulnerability_read_3.reload.identifier_names }
          .from([]).to(array_including(identifier_3.name))
      end

      it 'updates identifier_names with correct aggregation' do
        create_vulnerability_occurrence_identifier(occurrence_id: finding_1.id, identifier_id: identifier_2.id)
        create_vulnerability_occurrence_identifier(occurrence_id: finding_2.id, identifier_id: identifier_3.id)

        perform_migration

        expect(vulnerability_read_1.reload.identifier_names).to contain_exactly(identifier_1.name,
          identifier_2.name)
        expect(vulnerability_read_2.reload.identifier_names).to contain_exactly(identifier_2.name,
          identifier_3.name)
        expect(vulnerability_read_3.reload.identifier_names).to contain_exactly(identifier_3.name)
      end

      it 'sorts identifier_names' do
        create_vulnerability_occurrence_identifier(occurrence_id: finding_1.id, identifier_id: identifier_3.id)
        create_vulnerability_occurrence_identifier(occurrence_id: finding_1.id, identifier_id: identifier_2.id)

        perform_migration

        expect(vulnerability_read_1.reload.identifier_names).to eq([identifier_1.name,
          identifier_2.name, identifier_3.name])
      end
    end

    context 'with no matching identifiers' do
      include_context 'with vulnerability data' do
        before do
          vulnerability_occurrence_identifiers.delete_all
        end
      end

      it 'does not update identifier_names' do
        perform_migration

        expect(vulnerability_reads.where.not(identifier_names: []).count).to eq(0)
      end
    end
  end

  private

  def create_vulnerability(overrides = {})
    vulnerabilities.create!({
      project_id: project.id,
      author_id: user.id,
      title: 'test',
      severity: 1,
      confidence: 1,
      report_type: 1
    }.merge(overrides))
  end

  def create_vulnerability_read(overrides = {})
    vulnerability_reads.create!({
      project_id: project.id,
      vulnerability_id: 1,
      scanner_id: scanner.id,
      severity: 1,
      report_type: 1,
      state: 1,
      uuid: SecureRandom.uuid
    }.merge(overrides))
  end

  def create_finding(overrides = {})
    vulnerability_findings.create!({
      project_id: project.id,
      scanner_id: scanner.id,
      severity: 5, # medium
      report_type: 99, # generic
      primary_identifier_id: create_identifier.id,
      project_fingerprint: SecureRandom.hex(20),
      location_fingerprint: SecureRandom.hex(20),
      uuid: SecureRandom.uuid,
      name: "CVE-2018-1234",
      raw_metadata: "{}",
      metadata_version: "test:1.0"
    }.merge(overrides))
  end

  def create_identifier(overrides = {})
    vulnerability_identifiers.create!({
      project_id: project.id,
      external_id: "CVE-2018-1234",
      external_type: "CVE",
      name: "CVE-2018-1234",
      fingerprint: SecureRandom.hex(20)
    }.merge(overrides))
  end

  def create_vulnerability_occurrence_identifier(overrides = {})
    vulnerability_occurrence_identifiers.create!({
      created_at: Time.now.utc,
      updated_at: Time.now.utc,
      occurrence_id: nil,
      identifier_id: nil
    }.merge(overrides))
  end
end
