# frozen_string_literal: true

require 'spec_helper'

describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
  let(:identifier) { build(:vulnerabilities_identifier) }

  let(:base_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '123', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:critical]) }
  let(:base_report) { build(:ci_reports_security_aggregated_reports, occurrences: [base_vulnerability])}

  let(:head_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '123', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:critical]) }
  let(:head_report) { build(:ci_reports_security_aggregated_reports, occurrences: [head_vulnerability])}

  before do
    allow(base_vulnerability).to receive(:location).and_return({})
    allow(head_vulnerability).to receive(:location).and_return({})
  end

  subject { described_class.new(base_report, head_report) }

  describe '#base_report_out_of_date' do
    context 'no base report' do
      let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [], occurrences: [])}

      it 'is not out of date' do
        expect(subject.base_report_out_of_date).to be false
      end
    end

    context 'base report older than one week' do
      let(:report) { build(:ci_reports_security_report, created_at: 1.week.ago - 60.seconds) }
      let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [report])}

      it 'is not out of date' do
        expect(subject.base_report_out_of_date).to be true
      end
    end

    context 'base report less than one week old' do
      let(:report) { build(:ci_reports_security_report, created_at: 1.week.ago + 60.seconds) }
      let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [report])}

      it 'is not out of date' do
        expect(subject.base_report_out_of_date).to be false
      end
    end
  end

  describe '#existing' do
    context 'with existing reports' do
      it 'points to source tree' do
        expect(subject.existing).to eq([head_vulnerability])
      end

      context 'when comparing reports with different fingerprints' do
        let(:base_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'A') }
        let(:head_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'B') }

        it 'does not find any overlap' do
          expect(subject.existing).to eq([])
        end
      end

      context 'new vulnerabilities' do
        let(:vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:medium]) }
        let(:low_vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:low]) }
        let(:base_report) { build(:ci_reports_security_aggregated_reports, occurrences: [base_vulnerability, vuln])}
        let(:head_report) { build(:ci_reports_security_aggregated_reports, occurrences: [head_vulnerability, vuln, low_vuln])}

        it 'does not change order' do
          expect(subject.existing).to eq([head_vulnerability, vuln])
        end
      end
    end
  end

  describe '#added' do
    let(:vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:critical]) }
    let(:low_vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:low]) }

    context 'with new vulnerability' do
      let(:head_report) { build(:ci_reports_security_aggregated_reports, occurrences: [head_vulnerability, vuln])}

      it 'points to source tree' do
        expect(subject.added).to eq([vuln])
      end
    end

    context 'when comparing reports with different fingerprints' do
      let(:base_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'A') }
      let(:head_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'B') }
      let(:head_report) { build(:ci_reports_security_aggregated_reports, occurrences: [head_vulnerability, vuln])}

      it 'does not find any overlap' do
        expect(subject.added).to eq([head_vulnerability, vuln])
      end
    end

    context 'order' do
      let(:head_report) { build(:ci_reports_security_aggregated_reports, occurrences: [head_vulnerability, vuln, low_vuln])}

      it 'does not change' do
        expect(subject.added).to eq([vuln, low_vuln])
      end
    end
  end

  describe '#fixed' do
    let(:vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888') }
    let(:medium_vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:medium]) }

    context 'with fixed vulnerability' do
      let(:base_report) { build(:ci_reports_security_aggregated_reports, occurrences: [base_vulnerability, vuln])}

      it 'points to base tree' do
        expect(subject.fixed).to eq([vuln])
      end
    end

    context 'when comparing reports with different fingerprints' do
      let(:base_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'A') }
      let(:head_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'B') }
      let(:base_report) { build(:ci_reports_security_aggregated_reports, occurrences: [base_vulnerability, vuln])}

      it 'does not find any overlap' do
        expect(subject.fixed).to eq([base_vulnerability, vuln])
      end
    end

    context 'order' do
      let(:base_report) { build(:ci_reports_security_aggregated_reports, occurrences: [vuln, medium_vuln, base_vulnerability])}

      it 'does not change' do
        expect(subject.fixed).to eq([vuln, medium_vuln])
      end
    end
  end

  describe 'with empty vulnerabilities' do
    let(:empty_report) { build(:ci_reports_security_aggregated_reports, reports: [], occurrences: [])}

    it 'returns empty array when reports are not present' do
      comparer = described_class.new(empty_report, empty_report)

      expect(comparer.existing).to eq([])
      expect(comparer.fixed).to eq([])
      expect(comparer.added).to eq([])
    end

    it 'returns added vulnerability when base is empty and head is not empty' do
      comparer = described_class.new(empty_report, head_report)

      expect(comparer.existing).to eq([])
      expect(comparer.fixed).to eq([])
      expect(comparer.added).to eq([head_vulnerability])
    end

    it 'returns fixed vulnerability when head is empty and base is not empty' do
      comparer = described_class.new(base_report, empty_report)

      expect(comparer.existing).to eq([])
      expect(comparer.fixed).to eq([base_vulnerability])
      expect(comparer.added).to eq([])
    end
  end
end
