# frozen_string_literal: true

require 'spec_helper'

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

  let_it_be(:project) { create(:project, :repository) }

  let(:vulnerability_params) { { project: project, report_type: :sast, identifiers: [identifier], confidence: ::Enums::Vulnerability.confidence_levels[:high], severity: ::Enums::Vulnerability.severity_levels[:critical] } }
  let(:base_vulnerability) { build(:vulnerabilities_finding, location_fingerprint: '123', **vulnerability_params) }
  let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability])}

  let(:head_vulnerability) { build(:vulnerabilities_finding, location_fingerprint: base_vulnerability.location_fingerprint, uuid: base_vulnerability.uuid, **vulnerability_params) }
  let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability])}

  shared_context 'comparing reports' do
    let(:vul_params) { { project: project, report_type: :sast, identifiers: [identifier] } }
    let(:base_vulnerability) { build(:vulnerabilities_finding, location_fingerprint: 'A', **vul_params) }
    let(:head_vulnerability) { build(:vulnerabilities_finding, location_fingerprint: 'B', **vul_params) }
    let(:head_vul_findings) { [head_vulnerability, vuln] }
  end

  subject { described_class.new(project, base_report, head_report) }

  where(vulnerability_finding_tracking_signatures_enabled: [true, false])

  with_them do
    before do
      allow(base_vulnerability).to receive(:location).and_return({})
      allow(head_vulnerability).to receive(:location).and_return({})
      stub_feature_flags(vulnerability_finding_tracking_signatures: vulnerability_finding_tracking_signatures_enabled)
      stub_licensed_features(vulnerability_finding_signatures: vulnerability_finding_tracking_signatures_enabled)
    end

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

        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 '#added' do
      let(:vul_params) { { project: project, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: ::Enums::Vulnerability.confidence_levels[:high] } }
      let(:vuln) { build(:vulnerabilities_finding, severity: Enums::Vulnerability.severity_levels[:critical], **vul_params) }
      let(:low_vuln) { build(:vulnerabilities_finding, severity: Enums::Vulnerability.severity_levels[:low], **vul_params) }

      context 'with new vulnerability' do
        let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [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
        include_context 'comparing reports'

        let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: head_vul_findings)}

        it 'does not find any overlap' do
          expect(subject.added).to eq(head_vul_findings)
        end
      end

      context 'order' do
        let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [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(:vul_params) { { project: project, report_type: :sast, identifiers: [identifier], location_fingerprint: '888' } }
      let(:vuln) { build(:vulnerabilities_finding, **vul_params ) }
      let(:medium_vuln) { build(:vulnerabilities_finding, confidence: ::Enums::Vulnerability.confidence_levels[:high], severity: Enums::Vulnerability.severity_levels[:medium], uuid: vuln.uuid, **vul_params) }

      context 'with fixed vulnerability' do
        let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [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
        include_context 'comparing reports'

        let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability, vuln])}

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

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

        it 'does not change' do
          expect(subject.fixed).to eq(vul_findings)
        end
      end
    end

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

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

        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(project, empty_report, head_report)

        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(project, base_report, empty_report)

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