# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::BackgroundMigration::MigrateLinksForVulnerabilityFindings,
  schema: 20241112161753,
  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(:vulnerability_occurrences) { table(:vulnerability_occurrences) }
  let(:vulnerability_finding_links) { table(:vulnerability_finding_links) }
  let(:link_hash) { { url: 'http://test.com' } }

  let(:organization) { table(:organizations).create!(name: 'organization', path: 'organization') }
  let(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace', 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) { table(:users).create!(email: 'test1@example.com', projects_limit: 5) }

  let(:scanner) do
    table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test', name: 'test scanner')
  end

  let(:stating_id) { vulnerability_occurrences.pluck(:id).min }
  let(:end_id) { vulnerability_occurrences.pluck(:id).max }

  let(:migration) do
    described_class.new(
      start_id: stating_id,
      end_id: end_id,
      batch_table: :vulnerability_occurrences,
      batch_column: :id,
      sub_batch_size: 2,
      pause_ms: 2,
      connection: ApplicationRecord.connection
    )
  end

  subject(:perform_migration) { migration.perform }

  context 'without the presence of links key' do
    before do
      create_finding!(project.id, scanner.id, { other_keys: 'test' })
    end

    it 'does not create any link' do
      expect(Gitlab::AppLogger).not_to receive(:error)

      expect { perform_migration }.not_to change { vulnerability_finding_links.count }
    end
  end

  context 'with links equals to an array of nil element' do
    before do
      create_finding!(project.id, scanner.id, { links: [nil] })
    end

    it 'does not create any link' do
      expect(Gitlab::AppLogger).not_to receive(:error)

      expect { perform_migration }.not_to change { vulnerability_finding_links.count }
    end
  end

  context 'with links equals to a string' do
    before do
      create_finding!(project.id, scanner.id, { links: "wrong format" })
    end

    it 'does not create any link' do
      expect(Gitlab::AppLogger).not_to receive(:error)

      expect { perform_migration }.not_to change { vulnerability_finding_links.count }
    end
  end

  context 'with some elements which do not contain the key url' do
    let!(:finding) do
      create_finding!(project.id, scanner.id, { links: [link_hash, "wrong format", {}] })
    end

    it 'creates links only to valid elements' do
      expect(Gitlab::AppLogger).not_to receive(:error)

      perform_migration

      expect(vulnerability_finding_links.all).to contain_exactly(have_attributes(
        url: link_hash[:url],
        vulnerability_occurrence_id: finding.id))
    end
  end

  context 'when link name is too long' do
    let!(:finding) do
      create_finding!(project.id, scanner.id, { links: [{ name: 'A' * 300, url: 'https://foo' }] })
    end

    it 'skips creation of link and logs error' do
      expect(Gitlab::AppLogger).to receive(:error).with({
        class: described_class.name,
        message: /check_55f0a95439/,
        model_id: finding.id
      })
      expect { perform_migration }.not_to change { vulnerability_finding_links.count }
    end
  end

  context 'when link url is too long' do
    let!(:finding) do
      create_finding!(project.id, scanner.id, { links: [{ url: "https://f#{'o' * 2050}" }] })
    end

    it 'skips creation of link and logs error' do
      expect(Gitlab::AppLogger).to receive(:error).with({
        class: described_class.name,
        message: /check_b7fe886df6/,
        model_id: finding.id
      })
      expect { perform_migration }.not_to change { vulnerability_finding_links.count }
    end
  end

  context 'with links equals to an array of duplicated elements' do
    let!(:finding) do
      create_finding!(project.id, scanner.id, { links: [link_hash, link_hash] })
    end

    it 'creates one new link' do
      expect(Gitlab::AppLogger).not_to receive(:error)

      perform_migration

      expect(vulnerability_finding_links.all).to contain_exactly(have_attributes(
        url: link_hash[:url],
        vulnerability_occurrence_id: finding.id))
    end
  end

  context 'with existing links within raw_metadata' do
    let!(:finding1) { create_finding!(project.id, scanner.id, { links: [link_hash] }) }
    let!(:finding2) { create_finding!(project.id, scanner.id, { links: [link_hash] }) }

    it 'creates new link for each finding' do
      expect(Gitlab::AppLogger).not_to receive(:error)

      expect { perform_migration }.to change { vulnerability_finding_links.count }.by(2)
    end
  end

  context 'when Gitlab::Json throws exception JSON::ParserError' do
    before do
      create_finding!(project.id, scanner.id, { links: [link_hash] })
      allow(Gitlab::Json).to receive(:parse).and_raise(JSON::ParserError)
    end

    it 'does not log this error nor create new records' do
      expect(Gitlab::AppLogger).not_to receive(:error)

      expect { perform_migration }.not_to change { vulnerability_finding_links.count }
    end
  end

  context 'with existing link records' do
    let!(:finding) { create_finding!(project.id, scanner.id, { links: [link_hash] }) }

    before do
      vulnerability_finding_links.create!(vulnerability_occurrence_id: finding.id, url: link_hash[:url])
    end

    it 'does not create new link' do
      expect(Gitlab::AppLogger).not_to receive(:error)

      expect { perform_migration }.not_to change { vulnerability_finding_links.count }
    end

    it 'does not raise ActiveRecord::RecordNotUnique' do
      expect { perform_migration }.not_to raise_error
    end
  end

  private

  def create_finding!(project_id, scanner_id, raw_metadata)
    identifier = table(:vulnerability_identifiers).create!(project_id: project_id, external_type: 'uuid-v5',
      external_id: 'uuid-v5', fingerprint: OpenSSL::Digest::SHA256.hexdigest(SecureRandom.uuid),
      name: 'Identifier for UUIDv5 2 2')

    finding = table(:vulnerability_occurrences).create!(
      project_id: project_id, scanner_id: scanner_id,
      primary_identifier_id: identifier.id, name: 'test', severity: 4, confidence: 4, report_type: 0,
      uuid: SecureRandom.uuid, project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" },
      location_fingerprint: 'test', metadata_version: 'test',
      raw_metadata: raw_metadata.to_json)

    vulnerability = table(:vulnerabilities).create!(project_id: project_id, author_id: user.id, title: 'test',
      severity: 4, confidence: 4, report_type: 0, finding_id: finding.id)

    finding.update!(vulnerability_id: vulnerability.id)

    finding
  end
end
