# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::BackgroundMigration::MigrateRemediationsForVulnerabilityFindings, feature_category: :vulnerability_management do
  let(:vulnerability_occurrences) { table(:vulnerability_occurrences, database: :sec) }
  let(:vulnerability_findings_remediations) { table(:vulnerability_findings_remediations, database: :sec) }
  let(:vulnerability_remediations) { table(:vulnerability_remediations, database: :sec) }
  let(:vulnerability_scanners) { table(:vulnerability_scanners, database: :sec) }
  let(:remediation_hash) { { summary: 'summary', diff: "ZGlmZiAtLWdp" } }
  let(:organization1) { table(:organizations).create!(name: 'test', path: 'test') }
  let(:namespace1) do
    table(:namespaces).create!(name: 'namespace 1', path: 'namespace1',
      organization_id: organization1.id)
  end

  let(:project1) do
    table(:projects).create!(namespace_id: namespace1.id, project_namespace_id: namespace1.id,
      organization_id: organization1.id)
  end

  let(:user) do
    table(:users).create!(email: 'test1@example.com', projects_limit: 5, organization_id: organization1.id)
  end

  let(:scanner1) do
    vulnerability_scanners.create!(project_id: project1.id, external_id: 'test 1', name: 'test scanner 1')
  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: SecApplicationRecord.connection
    )
  end

  subject(:perform_migration) { migration.perform }

  context 'without the presence of remediation key' do
    before do
      create_finding!(project1.id, scanner1.id, { other_keys: 'test' })
    end

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

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

  context 'with remediation equals to an array of nil element' do
    before do
      create_finding!(project1.id, scanner1.id, { remediations: [nil] })
    end

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

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

  context 'with remediation with empty string as the diff key' do
    let!(:finding) do
      create_finding!(project1.id, scanner1.id, { remediations: [{ summary: 'summary', diff: '' }] })
    end

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

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

  context 'with remediation equals to an array of duplicated elements' do
    let!(:finding) do
      create_finding!(project1.id, scanner1.id, { remediations: [remediation_hash, remediation_hash] })
    end

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

      expect { perform_migration }.to change { vulnerability_remediations.count }.by(1)
      expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding.id).length).to eq(1)
    end
  end

  context 'with existing remediations within raw_metadata' do
    let!(:finding1) { create_finding!(project1.id, scanner1.id, { remediations: [remediation_hash] }) }
    let!(:finding2) { create_finding!(project1.id, scanner1.id, { remediations: [remediation_hash] }) }

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

      expect { perform_migration }.to change { vulnerability_remediations.count }.by(1)
      expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding1.id).length).to eq(1)
      expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding2.id).length).to eq(1)
    end

    context 'when create throws exception other than ActiveRecord::RecordNotUnique' do
      before do
        allow(migration).to receive(:create_finding_remediations).and_raise(StandardError)
      end

      it 'rolls back all related transactions' do
        expect(Gitlab::AppLogger).to receive(:error).with({
          class: described_class.name, message: StandardError.to_s, model_id: finding1.id
        })
        expect(Gitlab::AppLogger).to receive(:error).with({
          class: described_class.name, message: StandardError.to_s, model_id: finding2.id
        })
        expect { perform_migration }.not_to change { vulnerability_remediations.count }
        expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding1.id).length).to eq(0)
        expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding2.id).length).to eq(0)
      end
    end
  end

  context 'with existing remediation records' do
    let!(:finding) { create_finding!(project1.id, scanner1.id, { remediations: [remediation_hash] }) }

    before do
      vulnerability_remediations.create!(project_id: project1.id, summary: remediation_hash[:summary],
        checksum: checksum(remediation_hash[:diff]), file: Tempfile.new.path)
    end

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

      expect { perform_migration }.not_to change { vulnerability_remediations.count }
      expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding.id).length).to eq(1)
    end
  end

  context 'with same raw_metadata for different projects' do
    let(:namespace2) do
      table(:namespaces).create!(name: 'namespace 2', path: 'namespace2', organization_id: organization1.id)
    end

    let(:project2) do
      table(:projects).create!(namespace_id: namespace2.id, project_namespace_id: namespace2.id,
        organization_id: organization1.id)
    end

    let(:scanner2) do
      table(:vulnerability_scanners, database: :sec).create!(project_id: project2.id, external_id: 'test 2',
        name: 'test scanner 2')
    end

    let!(:finding1) { create_finding!(project1.id, scanner1.id, { remediations: [remediation_hash] }) }
    let!(:finding2) { create_finding!(project2.id, scanner2.id, { remediations: [remediation_hash] }) }

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

      expect { perform_migration }.to change { vulnerability_remediations.count }.by(2)
      expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding1.id).length).to eq(1)
      expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding2.id).length).to eq(1)
    end
  end

  private

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

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

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

    finding.update!(vulnerability_id: vulnerability.id)

    finding
  end

  def checksum(value)
    sha = Digest::SHA256.hexdigest(value)
    Gitlab::Database::ShaAttribute.new.serialize(sha)
  end
end
