# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Vulnerability do
  let(:state_values) { { detected: 1, dismissed: 2, resolved: 3, confirmed: 4 } }
  let(:severity_values) { { info: 1, unknown: 2, low: 4, medium: 5, high: 6, critical: 7 } }

  let(:confidence_values) do
    { ignore: 1, unknown: 2, experimental: 3, low: 4, medium: 5, high: 6, confirmed: 7 }
  end

  let(:report_types) do
    { sast: 0,
      dependency_scanning: 1,
      container_scanning: 2,
      dast: 3,
      secret_detection: 4,
      coverage_fuzzing: 5,
      api_fuzzing: 6,
      cluster_image_scanning: 7 }
  end

  let_it_be(:project) { create(:project) }
  let_it_be(:user) { create(:user) }
  let_it_be(:vulnerability) { create(:vulnerability, :sast, :confirmed, :low, project: project) }
  let_it_be(:finding) { create(:vulnerabilities_finding, vulnerability: vulnerability) }

  describe 'enums' do
    it { is_expected.to define_enum_for(:state).with_values(state_values) }
    it { is_expected.to define_enum_for(:severity).with_values(severity_values).with_prefix(:severity) }
    it { is_expected.to define_enum_for(:confidence).with_values(confidence_values).with_prefix(:confidence) }
    it { is_expected.to define_enum_for(:report_type).with_values(report_types) }

    it_behaves_like 'having unique enum values'
  end

  describe 'associations' do
    subject { build(:vulnerability) }

    it { is_expected.to belong_to(:project) }
    it { is_expected.to belong_to(:milestone) }
    it { is_expected.to belong_to(:epic) }
    it { is_expected.to have_many(:findings).class_name('Vulnerabilities::Finding').inverse_of(:vulnerability) }
    it { is_expected.to have_many(:dismissed_findings).class_name('Vulnerabilities::Finding').inverse_of(:vulnerability) }
    it { is_expected.to have_many(:external_issue_links).class_name('Vulnerabilities::ExternalIssueLink').inverse_of(:vulnerability) }
    it { is_expected.to have_many(:issue_links).class_name('Vulnerabilities::IssueLink').inverse_of(:vulnerability) }
    it { is_expected.to have_many(:created_issue_links).class_name('Vulnerabilities::IssueLink').inverse_of(:vulnerability).conditions(link_type: Vulnerabilities::IssueLink.link_types['created']) }
    it { is_expected.to have_many(:related_issues).through(:issue_links).source(:issue) }
    it { is_expected.to belong_to(:author).class_name('User') }
    it { is_expected.to belong_to(:updated_by).class_name('User') }
    it { is_expected.to belong_to(:last_edited_by).class_name('User') }
    it { is_expected.to belong_to(:resolved_by).class_name('User') }
    it { is_expected.to belong_to(:dismissed_by).class_name('User') }
    it { is_expected.to belong_to(:confirmed_by).class_name('User') }

    it { is_expected.to have_one(:group).through(:project) }

    it { is_expected.to have_many(:findings).class_name('Vulnerabilities::Finding').dependent(false) }
    it { is_expected.to have_many(:notes).dependent(:delete_all) }
    it { is_expected.to have_many(:user_mentions).class_name('VulnerabilityUserMention') }
  end

  describe 'validations' do
    subject { build(:vulnerability) }

    it { is_expected.to validate_presence_of(:project) }
    it { is_expected.to validate_presence_of(:author) }
    it { is_expected.to validate_presence_of(:title) }
    it { is_expected.to validate_presence_of(:severity) }
    it { is_expected.to validate_presence_of(:confidence) }
    it { is_expected.to validate_presence_of(:report_type) }

    it { is_expected.to validate_length_of(:title).is_at_most(::Issuable::TITLE_LENGTH_MAX) }
    it { is_expected.to validate_length_of(:title_html).is_at_most(::Issuable::TITLE_HTML_LENGTH_MAX) }
    it { is_expected.to validate_length_of(:description).is_at_most(::Issuable::DESCRIPTION_LENGTH_MAX).allow_nil }
    it { is_expected.to validate_length_of(:description_html).is_at_most(::Issuable::DESCRIPTION_HTML_LENGTH_MAX).allow_nil }
  end

  describe 'text fields' do
    let_it_be(:vulnerability) { create(:vulnerability, title: '_My title_ ', description: '**Hello `world`**') }

    describe '#title_html' do
      let(:expected_title) { '_My title_' } # no markdown rendering because it's a single line field

      subject { vulnerability.title_html }

      it { is_expected.to eq(expected_title) }
    end

    describe '#description_html' do
      let(:expected_description) { '<p data-sourcepos="1:1-1:17" dir="auto"><strong>Hello <code>world</code></strong></p>' }

      subject { vulnerability.description_html }

      it { is_expected.to eq(expected_description) }
    end

    describe 'redactable fields' do
      before do
        stub_commonmark_sourcepos_disabled
      end

      it_behaves_like 'model with redactable field' do
        let(:model) { build(:vulnerability) }
        let(:field) { :description }
      end
    end
  end

  describe '.visible_to_user_and_access_level' do
    let(:project_2) { create(:project) }

    before do
      project.add_developer(user)

      create(:vulnerability, project: project_2)
    end

    subject { described_class.visible_to_user_and_access_level(user, ::Gitlab::Access::DEVELOPER) }

    it 'returns vulnerabilities visible for given user with provided access level' do
      is_expected.to contain_exactly(vulnerability)
    end
  end

  describe '.with_limit' do
    subject(:limited_vulnerabilities) { described_class.with_limit(1) }

    before do
      # There is already a vulnerability created before
      # so this will make the total of 2 vulnerabilities.
      create(:vulnerability, project: project)
    end

    it 'returns vulnerabilities limited by provided value' do
      expect(limited_vulnerabilities.count).to eq(1)
    end
  end

  describe '.autocomplete_search' do
    using RSpec::Parameterized::TableSyntax

    let_it_be(:vulnerability_1) { create(:vulnerability, title: 'Predictable pseudorandom number generator') }
    let_it_be(:vulnerability_2) { create(:vulnerability, title: 'Use of pseudorandom MD2, MD4, or MD5 hash function.') }

    subject { described_class.autocomplete_search(search) }

    where(:search, :filtered_vulnerabilities) do
      'PSEUDORANDOM'             | [:vulnerability_1, :vulnerability_2]
      'Predictable PSEUDORANDOM' | [:vulnerability_1]
      'mD2'                      | [:vulnerability_2]
    end

    with_them do
      it 'returns the vulnerabilities filtered' do
        expect(subject).to match_array(filtered_vulnerabilities.map { |name| public_send(name) })
      end
    end

    context 'when id is used in search params' do
      let(:search) { vulnerability_1.id.to_s }

      it { is_expected.to match_array([vulnerability_1]) }
    end

    context 'when query is empty' do
      let(:search) { '' }

      it { is_expected.to match_array([vulnerability, vulnerability_1, vulnerability_2]) }
    end
  end

  describe '.for_projects' do
    let(:project_2) { create(:project) }

    before do
      create(:vulnerability, project: project_2)
    end

    subject { described_class.for_projects([project.id]) }

    it 'returns vulnerabilities related to the given project IDs' do
      is_expected.to contain_exactly(vulnerability)
    end
  end

  describe '.with_report_types' do
    let!(:dast_vulnerability) { create(:vulnerability, :dast) }
    let!(:dependency_scanning_vulnerability) { create(:vulnerability, :dependency_scanning) }
    let(:sast_vulnerability) { vulnerability }
    let(:report_types) { %w[sast dast] }

    subject { described_class.with_report_types(report_types) }

    it 'returns vulnerabilities matching the given report_types' do
      is_expected.to contain_exactly(sast_vulnerability, dast_vulnerability)
    end
  end

  describe '.with_severities' do
    let!(:high_vulnerability) { create(:vulnerability, :high) }
    let!(:medium_vulnerability) { create(:vulnerability, :medium) }
    let(:low_vulnerability) { vulnerability }
    let(:severities) { %w[medium low] }

    subject { described_class.with_severities(severities) }

    it 'returns vulnerabilities matching the given severities' do
      is_expected.to contain_exactly(medium_vulnerability, low_vulnerability)
    end
  end

  describe '.with_states' do
    let!(:detected_vulnerability) { create(:vulnerability, :detected) }
    let!(:dismissed_vulnerability) { create(:vulnerability, :dismissed) }
    let(:confirmed_vulnerability) { vulnerability }
    let(:states) { %w[detected confirmed] }

    subject { described_class.with_states(states) }

    it 'returns vulnerabilities matching the given states' do
      is_expected.to contain_exactly(detected_vulnerability, confirmed_vulnerability)
    end
  end

  describe '.with_scanner_external_ids' do
    let!(:vulnerability_1) { create(:vulnerability, :with_findings) }
    let!(:vulnerability_2) { create(:vulnerability, :with_findings) }
    let(:vulnerability_3) { vulnerability }
    let(:scanner_external_ids) { [vulnerability_1.finding_scanner_external_id, vulnerability_3.finding_scanner_external_id] }

    subject { described_class.with_scanner_external_ids(scanner_external_ids) }

    it 'returns vulnerabilities matching the given scanner external IDs' do
      is_expected.to contain_exactly(vulnerability_1, vulnerability_3)
    end
  end

  describe '.order_severity_' do
    let_it_be(:low_vulnerability) { vulnerability }
    let_it_be(:critical_vulnerability) { create(:vulnerability, :critical) }
    let_it_be(:medium_vulnerability) { create(:vulnerability, :medium) }

    describe 'ascending' do
      subject { described_class.order_severity_asc }

      it { is_expected.to eq([low_vulnerability, medium_vulnerability, critical_vulnerability]) }
    end

    describe 'descending' do
      subject { described_class.order_severity_desc }

      it { is_expected.to eq([critical_vulnerability, medium_vulnerability, low_vulnerability]) }
    end
  end

  describe '.order_title_' do
    let_it_be(:vulnerability_b) { create(:vulnerability, title: 'B Vulnerability') }
    let_it_be(:vulnerability_a) { create(:vulnerability, title: 'A Vulnerability') }
    let_it_be(:vulnerability_c) { vulnerability }

    before(:all) do
      vulnerability.update_column(:title, 'C Vulnerability')
    end

    describe 'ascending' do
      subject { described_class.order_title_asc }

      it 'returns vulnerabilities ordered by title' do
        is_expected.to eq([vulnerability_a, vulnerability_b, vulnerability_c])
      end
    end

    describe 'descending' do
      subject { described_class.order_title_desc }

      it 'returns vulnerabilities ordered by title' do
        is_expected.to eq([vulnerability_c, vulnerability_b, vulnerability_a])
      end
    end
  end

  describe '.order_created_at_' do
    let_it_be(:old_vulnerability) { create(:vulnerability, created_at: 2.weeks.ago) }
    let_it_be(:very_old_vulnerability) { vulnerability }
    let_it_be(:fresh_vulnerability) { create(:vulnerability, created_at: 3.days.ago) }

    before(:all) do
      vulnerability.update_column(:created_at, 1.year.ago)
    end

    describe 'ascending' do
      subject { described_class.order_created_at_asc }

      it 'returns vulnerabilities ordered by created_at' do
        is_expected.to eq([very_old_vulnerability, old_vulnerability, fresh_vulnerability])
      end
    end

    describe 'descending' do
      subject { described_class.order_created_at_desc }

      it 'returns vulnerabilities ordered by created_at' do
        is_expected.to eq([fresh_vulnerability, old_vulnerability, very_old_vulnerability])
      end
    end
  end

  describe '.order_id_desc' do
    subject { described_class.order_id_desc }

    before do
      create_list(:vulnerability, 2)
    end

    it 'returns vulnerabilities ordered by id' do
      is_expected.to be_sorted(:id, :desc)
    end
  end

  describe '.order_report_type_' do
    let_it_be(:vulnerability_dast) { create(:vulnerability, :dast) }
    let_it_be(:vulnerability_secret_detection) { create(:vulnerability, :secret_detection) }
    let_it_be(:vulnerability_sast) { vulnerability }
    let_it_be(:vulnerability_coverage_fuzzing) { create(:vulnerability, :coverage_fuzzing) }
    let_it_be(:vulnerability_api_fuzzing) { create(:vulnerability, :api_fuzzing) }

    describe 'ascending' do
      subject { described_class.order_report_type_asc }

      it 'returns vulnerabilities ordered by report_type' do
        is_expected.to eq([vulnerability_api_fuzzing, vulnerability_coverage_fuzzing, vulnerability_dast, vulnerability_sast, vulnerability_secret_detection])
      end
    end

    describe 'descending' do
      subject { described_class.order_report_type_desc }

      it 'returns vulnerabilities ordered by report_type' do
        is_expected.to eq([vulnerability_secret_detection, vulnerability_sast, vulnerability_dast, vulnerability_coverage_fuzzing, vulnerability_api_fuzzing])
      end
    end
  end

  describe '.order_state_' do
    let_it_be(:vulnerability_confirmed) { vulnerability }
    let_it_be(:vulnerability_detected) { create(:vulnerability, :detected) }
    let_it_be(:vulnerability_dismissed) { create(:vulnerability, :dismissed) }
    let_it_be(:vulnerability_resolved) { create(:vulnerability, :resolved) }

    describe 'ascending' do
      subject { described_class.order_state_asc }

      it 'returns vulnerabilities ordered by state' do
        is_expected.to eq([vulnerability_detected, vulnerability_confirmed, vulnerability_resolved, vulnerability_dismissed])
      end
    end

    describe 'descending' do
      subject { described_class.order_state_desc }

      it 'returns vulnerabilities ordered by state' do
        is_expected.to eq([vulnerability_dismissed, vulnerability_resolved, vulnerability_confirmed, vulnerability_detected])
      end
    end
  end

  describe '.with_resolution' do
    let_it_be(:vulnerability_with_resolution) { create(:vulnerability, resolved_on_default_branch: true) }
    let_it_be(:vulnerability_without_resolution) { vulnerability }

    subject { described_class.with_resolution(with_resolution) }

    before(:all) do
      vulnerability.update!(resolved_on_default_branch: false)
    end

    context 'when no argument is provided' do
      subject { described_class.with_resolution }

      it { is_expected.to eq([vulnerability_with_resolution]) }
    end

    context 'when the argument is provided' do
      context 'when the given argument is `true`' do
        let(:with_resolution) { true }

        it { is_expected.to eq([vulnerability_with_resolution]) }
      end

      context 'when the given argument is `false`' do
        let(:with_resolution) { false }

        it { is_expected.to eq([vulnerability_without_resolution]) }
      end
    end
  end

  describe '.with_issues' do
    let_it_be(:vulnerability_with_issues) { create(:vulnerability, :with_issue_links) }
    let_it_be(:vulnerability_without_issues) { vulnerability }

    subject { described_class.with_issues(with_issues) }

    context 'when no argument is provided' do
      subject { described_class.with_issues }

      it { is_expected.to eq([vulnerability_with_issues]) }
    end

    context 'when the argument is provided' do
      context 'when the given argument is `true`' do
        let(:with_issues) { true }

        it { is_expected.to eq([vulnerability_with_issues]) }
      end

      context 'when the given argument is `false`' do
        let(:with_issues) { false }

        it { is_expected.to eq([vulnerability_without_issues]) }
      end
    end
  end

  describe '.order_by' do
    let_it_be(:low_vulnerability) { vulnerability }
    let_it_be(:critical_vulnerability) { create(:vulnerability, :critical) }
    let_it_be(:medium_vulnerability) { create(:vulnerability, :medium) }

    subject { described_class.order_by(method) }

    context 'when ordered by severity_desc' do
      let(:method) { :severity_desc }

      it { is_expected.to eq([critical_vulnerability, medium_vulnerability, low_vulnerability]) }
    end

    context 'when ordered by severity_asc' do
      let(:method) { :severity_asc }

      it { is_expected.to eq([low_vulnerability, medium_vulnerability, critical_vulnerability]) }
    end
  end

  describe '.counts_by_day_and_severity' do
    let(:current_date) { Time.zone.parse('2019-10-31') }
    let(:from_date) { Date.parse('2019-10-22') }
    let(:to_date) { Date.parse('2019-10-28') }

    let!(:vulnerability_1) { create(:vulnerability, created_at: 5.days.ago, dismissed_at: Date.current, severity: :critical) }
    let!(:vulnerability_2) { create(:vulnerability, created_at: 5.days.ago, dismissed_at: 1.day.ago, severity: :high) }
    let!(:vulnerability_3) { create(:vulnerability, created_at: 4.days.ago, resolved_at: 2.days.ago, severity: :critical) }

    subject(:counts_by_day_and_severity) { ::Vulnerability.counts_by_day_and_severity(from_date, to_date) }

    around do |example|
      travel_to(current_date) { example.run }
    end

    context 'when the vulnerability_history feature flag is disabled' do
      before do
        stub_feature_flags(vulnerability_history: false)
      end

      it 'returns an empty array' do
        expect(counts_by_day_and_severity).to be_empty
      end
    end

    context 'when the vulnerability_history feature flag is enabled' do
      before do
        stub_feature_flags(vulnerability_history: true)
      end

      context 'when there are less than 10 days between the from and to dates' do
        it 'returns the count of unresolved, undismissed vulnerabilities for each severity for each day from the start date to the end date' do
          expect(counts_by_day_and_severity.order(:day, :severity).to_json).to eq([
            { 'day' => '2019-10-26', 'severity' => 'high', 'count' => 1, 'id' => nil },
            { 'day' => '2019-10-26', 'severity' => 'critical', 'count' => 1, 'id' => nil },
            { 'day' => '2019-10-27', 'severity' => 'high', 'count' => 1, 'id' => nil },
            { 'day' => '2019-10-27', 'severity' => 'critical', 'count' => 2, 'id' => nil },
            { 'day' => '2019-10-28', 'severity' => 'high', 'count' => 1, 'id' => nil },
            { 'day' => '2019-10-28', 'severity' => 'critical', 'count' => 2, 'id' => nil }
          ].to_json)
        end
      end

      context 'when theere are more than 10 days between the from and to dates' do
        let(:from_date) { 10.days.ago.to_date }
        let(:to_date) { Date.current }

        it 'raises a TooManyDaysError' do
          expect { counts_by_day_and_severity }.to raise_error(
            ::Vulnerability::TooManyDaysError,
            'Cannot fetch counts for more than 10 days'
          )
        end
      end
    end
  end

  describe '.active_state_values' do
    let(:expected_values) { ::Vulnerability.states.values_at('detected', 'confirmed') }

    subject { described_class.active_state_values }

    it { is_expected.to match_array(expected_values) }
  end

  describe '.grouped_by_severity' do
    before do
      # There is already a vulnerability created with `low`
      # severity therefore we are not creating one with `low` severity here.
      create_list(:vulnerability, 2, :critical)
      create_list(:vulnerability, 1, :high)
      create_list(:vulnerability, 1, :medium)
      create_list(:vulnerability, 1, :info)
      create_list(:vulnerability, 1, :unknown)
    end

    subject { described_class.grouped_by_severity.count }

    it { is_expected.to eq('critical' => 2, 'high' => 1, 'info' => 1, 'low' => 1, 'medium' => 1, 'unknown' => 1) }
  end

  describe '.by_project_fingerprints' do
    let_it_be(:vulnerability_1) { vulnerability }
    let_it_be(:vulnerability_2) { create(:vulnerability, :with_findings) }

    let(:expected_vulnerabilities) { [vulnerability_1] }

    subject { described_class.by_project_fingerprints(vulnerability_1.finding.project_fingerprint) }

    it { is_expected.to match_array(expected_vulnerabilities) }
  end

  describe '.by_scanner_ids' do
    it 'returns matching vulnerabilities' do
      vulnerability1 = vulnerability
      create(:vulnerability, :with_findings)

      result = described_class.by_scanner_ids(vulnerability1.finding_scanner_id)

      expect(result).to match_array([vulnerability1])
    end
  end

  describe '.reference_prefix' do
    subject { described_class.reference_prefix }

    it { is_expected.to eq('[vulnerability:') }
  end

  describe '.reference_postfix' do
    subject { described_class.reference_postfix }

    it { is_expected.to eq(']') }
  end

  describe '.reference_pattern' do
    subject { described_class.reference_pattern }

    it { is_expected.to match('[vulnerability:123]') }
    it { is_expected.to match('[vulnerability:gitlab-foss/123]') }
    it { is_expected.to match('[vulnerability:gitlab-org/gitlab-foss/123]') }
  end

  describe '.link_reference_pattern' do
    subject { described_class.link_reference_pattern }

    it { is_expected.to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-foss/-/security/vulnerabilities/123") }
    it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-foss/security/vulnerabilities/123") }
    it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-foss/issues/123") }
    it { is_expected.not_to match("gitlab-org/gitlab-foss/milestones/123") }
  end

  describe '#to_reference' do
    let(:namespace) { build(:namespace, path: 'sample-namespace') }
    let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
    let(:vulnerability) { build(:vulnerability, id: 1, project: project) }

    context 'when nil argument' do
      it 'returns vulnerability id' do
        expect(vulnerability.to_reference).to eq '[vulnerability:1]'
      end

      it 'returns complete path to the vulnerability with full: true' do
        expect(vulnerability.to_reference(full: true)).to eq '[vulnerability:sample-namespace/sample-project/1]'
      end
    end

    context 'when argument is a project' do
      context 'when same project' do
        it 'returns vulnerability id' do
          expect(vulnerability.to_reference(project)).to eq('[vulnerability:1]')
        end

        it 'returns full reference with full: true' do
          expect(vulnerability.to_reference(project, full: true)).to eq '[vulnerability:sample-namespace/sample-project/1]'
        end
      end

      context 'when cross-project in same namespace' do
        let(:another_project) do
          build(:project, name: 'another-project', namespace: project.namespace)
        end

        it 'returns a cross-project reference' do
          expect(vulnerability.to_reference(another_project)).to eq '[vulnerability:sample-project/1]'
        end

        it 'returns full reference with full: true' do
          expect(vulnerability.to_reference(another_project, full: true)).to eq '[vulnerability:sample-namespace/sample-project/1]'
        end
      end

      context 'when cross-project in different namespace' do
        let(:another_namespace) { build(:namespace, id: non_existing_record_id, path: 'another-namespace') }
        let(:another_namespace_project) { build(:project, path: 'another-project', namespace: another_namespace) }

        it 'returns complete path to the vulnerability' do
          expect(vulnerability.to_reference(another_namespace_project)).to eq '[vulnerability:sample-namespace/sample-project/1]'
        end

        it 'returns full reference with full: true' do
          expect(vulnerability.to_reference(another_namespace_project, full: true)).to eq '[vulnerability:sample-namespace/sample-project/1]'
        end
      end
    end

    context 'when argument is a namespace' do
      context 'when same as vulnerability' do
        it 'returns path to the vulnerability with the project name' do
          expect(vulnerability.to_reference(namespace)).to eq '[vulnerability:sample-project/1]'
        end

        it 'returns full reference with full: true' do
          expect(vulnerability.to_reference(namespace, full: true)).to eq '[vulnerability:sample-namespace/sample-project/1]'
        end
      end

      context 'when different from vulnerability namespace' do
        let(:group) { build(:group, name: 'Group', path: 'sample-group') }

        it 'returns full path to the vulnerability with full: true' do
          expect(vulnerability.to_reference(group)).to eq '[vulnerability:sample-namespace/sample-project/1]'
        end

        it 'returns full path to the vulnerability with full: false' do
          expect(vulnerability.to_reference(group, full: false)).to eq '[vulnerability:sample-namespace/sample-project/1]'
        end
      end
    end
  end

  describe '#finding' do
    let_it_be(:finding_1) { finding }
    let_it_be(:finding_2) { create(:vulnerabilities_finding, vulnerability: vulnerability) }

    subject { vulnerability.finding }

    context 'with multiple findings' do
      it { is_expected.to eq(finding_1) }
    end
  end

  describe 'delegations' do
    it { is_expected.to delegate_method(:scanner_name).to(:finding).with_prefix.allow_nil }
    it { is_expected.to delegate_method(:metadata).to(:finding).with_prefix.allow_nil }
    it { is_expected.to delegate_method(:message).to(:finding).with_prefix.allow_nil }
    it { is_expected.to delegate_method(:cve_value).to(:finding).allow_nil }
    it { is_expected.to delegate_method(:cwe_value).to(:finding).allow_nil }
    it { is_expected.to delegate_method(:other_identifier_values).to(:finding).allow_nil }
    it { is_expected.to delegate_method(:default_branch).to(:project).with_prefix.allow_nil }
    it { is_expected.to delegate_method(:name).to(:project).with_prefix.allow_nil }
    it { is_expected.to delegate_method(:name).to(:group).with_prefix.allow_nil }
  end

  describe '#resource_parent' do
    subject(:resource_parent) { vulnerability.resource_parent }

    it { is_expected.to eq(project) }
  end

  describe '#discussions_rendered_on_frontend?' do
    subject(:discussions_rendered_on_frontend) { vulnerability.discussions_rendered_on_frontend? }

    it { is_expected.to be true }
  end

  describe '.parent_class' do
    subject(:parent_class) { ::Vulnerability.parent_class }

    it { is_expected.to eq(::Project) }
  end

  describe '.to_ability_name' do
    subject(:ability_name) { ::Vulnerability.to_ability_name }

    it { is_expected.to eq('vulnerability') }
  end

  describe '#note_etag_key' do
    it 'returns a correct etag key' do
      expect(vulnerability.note_etag_key).to eq(
        ::Gitlab::Routing.url_helpers.project_security_vulnerability_notes_path(project, vulnerability)
      )
    end
  end

  describe '#user_notes_count' do
    let(:expected_count) { 10 }
    let(:mock_service_class) { instance_double(Vulnerabilities::UserNotesCountService, count: expected_count) }

    subject(:user_notes_count) { vulnerability.user_notes_count }

    before do
      allow(Vulnerabilities::UserNotesCountService).to receive(:new).with(vulnerability).and_return(mock_service_class)
    end

    it 'delegates the call to Vulnerabilities::UserNotesCountService' do
      expect(user_notes_count).to eq(expected_count)
      expect(mock_service_class).to have_received(:count)
    end
  end

  describe '#after_note_changed' do
    let(:vulnerability) { build(:vulnerability) }
    let(:note) { instance_double(Note, system?: is_system_note?) }
    let(:mock_service_class) { instance_double(Vulnerabilities::UserNotesCountService, delete_cache: true) }

    subject(:after_note_changed) { vulnerability.after_note_changed(note) }

    before do
      allow(Vulnerabilities::UserNotesCountService).to receive(:new).with(vulnerability).and_return(mock_service_class)
    end

    context 'when the changed note is a system note' do
      let(:is_system_note?) { true }

      it 'does not send #delete_cache message to Vulnerabilities::UserNotesCountService' do
        after_note_changed

        expect(mock_service_class).not_to have_received(:delete_cache)
      end
    end

    context 'when the changed note is not a system note' do
      let(:is_system_note?) { false }

      it 'sends #delete_cache message to Vulnerabilities::UserNotesCountService' do
        after_note_changed

        expect(mock_service_class).to have_received(:delete_cache)
      end
    end
  end

  describe '#after_note_created' do
    subject { described_class.instance_method(:after_note_created).original_name }

    it { is_expected.to eq(:after_note_changed) }
  end

  describe '#after_note_destroyed' do
    subject { described_class.instance_method(:after_note_destroyed).original_name }

    it { is_expected.to eq(:after_note_changed) }
  end

  describe '#stat_diff' do
    subject { vulnerability.stat_diff }

    it { is_expected.to be_an_instance_of(Vulnerabilities::StatDiff) }
  end

  describe '#blob_path' do
    let_it_be(:vulnerability) { create(:vulnerability) }
    let_it_be(:pipeline) { create(:ci_pipeline) }
    let_it_be(:finding) { create(:vulnerabilities_finding, pipelines: [pipeline], vulnerability: vulnerability) }

    subject { vulnerability.blob_path }

    it 'returns project blob path' do
      expect(subject).to eq(
        "/#{vulnerability.project.namespace.path}/#{vulnerability.project.name}/-/blob/#{pipeline.sha}/#{vulnerability.finding.file}"
      )
    end
  end
end
