# frozen_string_literal: true

require 'spec_helper'

# Store feature-specific specs in `ee/spec/models/merge_request instead of
# making this file longer.
#
# For instance, `ee/spec/models/merge_request/blocking_spec.rb` tests the
# "blocking MRs" feature.
RSpec.describe MergeRequest, feature_category: :code_review_workflow do
  using RSpec::Parameterized::TableSyntax
  include ReactiveCachingHelpers

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

  let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }

  describe 'associations' do
    subject { build_stubbed(:merge_request) }

    it { is_expected.to belong_to(:iteration) }
    it { is_expected.to have_many(:approvals).dependent(:delete_all) }
    it { is_expected.to have_many(:approvers).dependent(:delete_all) }
    it { is_expected.to have_many(:approver_users).through(:approvers) }
    it { is_expected.to have_many(:approver_groups).dependent(:delete_all) }
    it { is_expected.to have_many(:approved_by_users) }
    it { is_expected.to have_one(:merge_train_car) }
    it { is_expected.to have_many(:approval_rules) }
    it { is_expected.to have_many(:approval_merge_request_rule_sources).through(:approval_rules) }
    it { is_expected.to have_many(:approval_project_rules).through(:approval_merge_request_rule_sources) }
    it { is_expected.to have_many(:status_check_responses).class_name('MergeRequests::StatusCheckResponse').inverse_of(:merge_request) }
    it { is_expected.to have_many(:scan_result_policy_reads_through_violations).through(:scan_result_policy_violations).class_name('Security::ScanResultPolicyRead') }
    it { is_expected.to have_many(:scan_result_policy_reads_through_approval_rules).through(:approval_rules).class_name('Security::ScanResultPolicyRead') }
    it { is_expected.to have_many(:security_policies_through_violations).through(:scan_result_policy_violations).class_name('Security::Policy') }
    it { is_expected.to have_many(:change_requesters).through(:requested_changes) }

    describe 'policy violations' do
      let(:policy_1) { create(:scan_result_policy_read, project: project) }
      let(:policy_2) { create(:scan_result_policy_read, project: project) }
      let(:policy_3) { create(:scan_result_policy_read, project: project) }
      let(:policy_4) { create(:scan_result_policy_read, project: project) }

      let!(:running_violation_1) do
        create(:scan_result_policy_violation, :running, project: project, merge_request: merge_request,
          scan_result_policy_read: policy_1, violation_data: nil)
      end

      let!(:running_violation_2) do
        create(:scan_result_policy_violation, :running, project: project, merge_request: merge_request,
          scan_result_policy_read: policy_2, violation_data: nil)
      end

      let!(:failed_violation_1) do
        create(:scan_result_policy_violation, :failed, project: project, merge_request: merge_request,
          scan_result_policy_read: policy_3, violation_data: nil)
      end

      let!(:failed_violation_2) do
        create(:scan_result_policy_violation, :failed, project: project, merge_request: merge_request,
          scan_result_policy_read: policy_4, violation_data: nil)
      end

      describe '.running_scan_result_policy_violations' do
        it 'returns only the running violations' do
          expect(merge_request.running_scan_result_policy_violations).to contain_exactly(running_violation_1,
            running_violation_2)
        end
      end

      describe '.failed_scan_result_policy_violations' do
        it 'returns only the completed violations' do
          expect(merge_request.failed_scan_result_policy_violations).to contain_exactly(failed_violation_1,
            failed_violation_2)
        end
      end
    end

    describe 'approval_rules association' do
      describe 'applicable_post_merge_approval_rules' do
        it 'returns only the rules applicable_post_merge true or nil' do
          create(:approval_merge_request_rule, merge_request: merge_request, applicable_post_merge: false)
          rule_2 = create(:approval_merge_request_rule, merge_request: merge_request, applicable_post_merge: true)
          rule_3 = create(:approval_merge_request_rule, merge_request: merge_request, applicable_post_merge: true)

          expect(merge_request.applicable_post_merge_approval_rules).to contain_exactly(rule_2, rule_3)
        end
      end

      describe '#applicable_to_branch' do
        let!(:rule) { create(:approval_merge_request_rule, merge_request: merge_request) }
        let(:branch) { 'stable' }

        subject { merge_request.approval_rules.applicable_to_branch(branch) }

        shared_examples_for 'with applicable rules to specified branch' do
          it { is_expected.to eq([rule]) }
        end

        context 'when there are no associated source rules' do
          it_behaves_like 'with applicable rules to specified branch'
        end

        context 'when there are associated source rules' do
          let!(:source_rule) { create(:approval_project_rule, project: merge_request.target_project) }
          let!(:rule) { create(:approval_merge_request_rule, merge_request: merge_request, approval_project_rule: source_rule) }

          context 'and rule is not modified_from_project_rule' do
            before do
              rule.update!(
                name: source_rule.name,
                approvals_required: source_rule.approvals_required,
                users: source_rule.users,
                groups: source_rule.groups
              )
            end

            context 'and there are no associated protected branches to source rule' do
              it_behaves_like 'with applicable rules to specified branch'
            end

            context 'and there are associated protected branches to source rule' do
              before do
                source_rule.update!(protected_branches: protected_branches)
              end

              context 'and branch matches' do
                let(:protected_branches) { [create(:protected_branch, name: branch)] }

                it_behaves_like 'with applicable rules to specified branch'
              end

              context 'and branch does not match anything' do
                let(:protected_branches) { [create(:protected_branch, name: branch.reverse)] }

                it { is_expected.to be_empty }
              end
            end
          end

          context 'and rule is modified_from_project_rule' do
            before do
              rule.update!(name: 'Overridden Rule')
            end

            it_behaves_like 'with applicable rules to specified branch'
          end

          context 'and rule is overridden but not modified_from_project_rule' do
            let!(:rule) { create(:approval_merge_request_rule, name: 'test', merge_request: merge_request, approval_project_rule: source_rule) }

            it_behaves_like 'with applicable rules to specified branch'

            context 'and protected branches exist but branch does not match anything' do
              let(:protected_branches) { [create(:protected_branch, name: branch.reverse)] }

              before do
                source_rule.update!(protected_branches: protected_branches)
              end

              it 'does not find applicable rules' do
                expect(subject).to be_empty
              end
            end
          end
        end
      end

      describe '#set_applicable_when_copying_rules' do
        let!(:rule_2) { create(:approval_merge_request_rule, merge_request: merge_request, applicable_post_merge: nil) }
        let!(:rule_1) { create(:approval_merge_request_rule, merge_request: merge_request, applicable_post_merge: nil) }
        let(:ids) { [rule_2.id] }

        subject { merge_request.approval_rules.set_applicable_when_copying_rules(ids) }

        it 'sets all the ids to true and others to false' do
          pre_set = merge_request.approval_rules.all? { |rule| rule.applicable_post_merge.nil? }
          expect(pre_set).to eq(true)

          subject

          expect(rule_1.reload.applicable_post_merge).to eq(false)
          expect(rule_2.reload.applicable_post_merge).to eq(true)
        end
      end
    end

    describe '#merge_requests_author_approval?' do
      context 'when project lacks a target_project relation' do
        before do
          merge_request.target_project = nil
        end

        it 'returns false' do
          expect(merge_request.merge_requests_author_approval?).to be false
        end
      end

      context 'when project has a target_project relation' do
        it 'accesses the value from the target_project' do
          expect(merge_request.target_project)
            .to receive(:merge_requests_author_approval?)

          merge_request.merge_requests_author_approval?
        end

        context 'when overriden by scan result policy' do
          let(:policy) do
            create(
              :scan_result_policy_read,
              :prevent_approval_by_author,
              commits: :any,
              project: merge_request.target_project)
          end

          before do
            merge_request.target_project.update_attribute(:merge_requests_author_approval, true)

            create(
              :approval_merge_request_rule,
              :any_merge_request,
              merge_request: merge_request,
              scan_result_policy_read: policy)
            create(
              :scan_result_policy_violation,
              project: project,
              merge_request: merge_request,
              scan_result_policy_read: policy)
          end

          it 'returns false' do
            expect(merge_request.merge_requests_author_approval?).to be(false)
          end
        end
      end
    end

    describe '#merge_requests_disable_committers_approval?' do
      context 'when project lacks a target_project relation' do
        before do
          merge_request.target_project = nil
        end

        it 'returns false' do
          expect(merge_request.merge_requests_disable_committers_approval?).to be false
        end
      end

      context 'when project has a target_project relation' do
        it 'accesses the value from the target_project' do
          expect(merge_request.target_project)
            .to receive(:merge_requests_disable_committers_approval?)

          merge_request.merge_requests_disable_committers_approval?
        end

        context 'when overriden by scan result policy' do
          let(:policy) do
            create(
              :scan_result_policy_read,
              :prevent_approval_by_commit_author,
              commits: :any,
              project: merge_request.target_project)
          end

          before do
            merge_request.target_project.update_attribute(:merge_requests_disable_committers_approval, false)

            create(
              :approval_merge_request_rule,
              :any_merge_request,
              merge_request: merge_request,
              scan_result_policy_read: policy)
            create(
              :scan_result_policy_violation,
              project: project,
              merge_request: merge_request,
              scan_result_policy_read: policy)
          end

          it 'returns false' do
            expect(merge_request.merge_requests_disable_committers_approval?).to be(true)
          end
        end
      end
    end

    describe '#require_password_to_approve?' do
      subject { merge_request.require_password_to_approve? }

      let(:password_required?) { true }

      before do
        merge_request.target_project.update!(require_password_to_approve: password_required?)
      end

      context 'when target project requires password' do
        it { is_expected.to be(password_required?) }
      end

      context 'when target project does not require password' do
        let(:password_required?) { false }

        it { is_expected.to be(password_required?) }

        context 'when overridden by scan result policy' do
          let(:policy) do
            create(
              :scan_result_policy_read,
              :require_password_to_approve,
              commits: :any,
              project: merge_request.target_project)
          end

          before do
            merge_request.target_project.update_attribute(:merge_requests_disable_committers_approval, false)

            create(
              :approval_merge_request_rule,
              :any_merge_request,
              merge_request: merge_request,
              scan_result_policy_read: policy)
            create(
              :scan_result_policy_violation,
              project: project,
              merge_request: merge_request,
              scan_result_policy_read: policy)
          end

          it { is_expected.to be(true) }
        end
      end

      describe '#policy_approval_settings' do
        let(:project_approval_settings) do
          { prevent_approval_by_author: true,
            prevent_approval_by_commit_author: false,
            remove_approvals_with_new_commit: true,
            require_password_to_approve: false }
        end

        let(:policy) do
          create(:scan_result_policy_read,
            project: merge_request.target_project,
            project_approval_settings: project_approval_settings)
        end

        let(:overrides) { project_approval_settings.select { |_, v| v } }

        subject(:approval_settings) { merge_request.policy_approval_settings }

        context 'with scan finding rule' do
          let!(:approval_merge_request_rule) do
            create(:report_approver_rule,
              :scan_finding,
              merge_request: merge_request,
              scan_result_policy_read: policy)
          end

          context 'when violated' do
            before do
              create(:scan_result_policy_violation, project: project, merge_request: merge_request,
                scan_result_policy_read: approval_merge_request_rule.scan_result_policy_read)
            end

            it { is_expected.to eq(overrides) }

            context 'when scan_result_policy_violations is already loaded' do
              before do
                merge_request.scan_result_policy_violations.load
              end

              it { is_expected.to eq(overrides) }
            end
          end

          context 'when unviolated' do
            it { is_expected.to be_empty }
          end
        end

        context 'with any_merge_request rule' do
          let!(:approval_merge_request_rule) do
            create(
              :approval_merge_request_rule,
              :any_merge_request,
              merge_request: merge_request,
              scan_result_policy_read: policy,
              approvals_required: 1)
          end

          context 'when violated' do
            before do
              create(
                :scan_result_policy_violation,
                project: project,
                merge_request: merge_request,
                scan_result_policy_read: approval_merge_request_rule.scan_result_policy_read)
            end

            it { is_expected.to eq(overrides) }
          end

          context 'when unviolated' do
            it { is_expected.to be_empty }
          end

          context 'when rule is violated by other merge request' do
            let(:merge_request_2) do
              create(:merge_request, source_branch: 'test', source_project: project, target_project: project,
                state: :opened)
            end

            let(:project_approval_settings_2) do
              { prevent_approval_by_author: true,
                prevent_approval_by_commit_author: true,
                remove_approvals_with_new_commit: true,
                require_password_to_approve: true }
            end

            let(:policy_2) do
              create(:scan_result_policy_read, project: project, project_approval_settings: project_approval_settings_2)
            end

            let!(:approval_merge_request_rule_2) do
              create(:approval_merge_request_rule, :any_merge_request, merge_request: merge_request,
                scan_result_policy_read: policy_2, approvals_required: 1)
            end

            before do
              create(:scan_result_policy_violation, project: project, merge_request: merge_request,
                scan_result_policy_read: policy)

              create(:scan_result_policy_violation, project: project, merge_request: merge_request_2,
                scan_result_policy_read: policy_2)
            end

            it 'applies violations for the correct merge request' do
              is_expected.to eq(overrides)
            end
          end

          context 'with competing rules' do
            let(:other_policy) do
              create(
                :scan_result_policy_read,
                project: merge_request.target_project,
                project_approval_settings: project_approval_settings.transform_values { false },
                commits: :any)
            end

            let!(:approval_merge_request_rule) do
              create(
                :approval_merge_request_rule,
                :any_merge_request,
                merge_request: merge_request,
                scan_result_policy_read: policy,
                approvals_required: 1)
            end

            let!(:other_approval_merge_request_rule) do
              create(
                :approval_merge_request_rule,
                :any_merge_request,
                merge_request: merge_request,
                scan_result_policy_read: other_policy,
                approvals_required: 1)
            end

            before do
              [approval_merge_request_rule, other_approval_merge_request_rule].each do |rule|
                create(
                  :scan_result_policy_violation,
                  project: project,
                  merge_request: merge_request,
                  scan_result_policy_read: rule.scan_result_policy_read)
              end
            end

            it { is_expected.to eq(overrides) }
          end
        end
      end
    end
  end

  describe '#policies_overriding_approval_settings' do
    let(:project_approval_settings) do
      { prevent_approval_by_author: true,
        prevent_approval_by_commit_author: false,
        remove_approvals_with_new_commit: true,
        require_password_to_approve: false }
    end

    let(:overrides) { project_approval_settings.compact_blank }

    subject(:overriding_policies) { merge_request.policies_overriding_approval_settings }

    context 'with security policies populated' do
      let(:policy) do
        create(:security_policy, content: { approval_settings: project_approval_settings })
      end

      let(:policy_rule) do
        create(:approval_policy_rule, :any_merge_request, security_policy: policy)
      end

      context 'when violated' do
        before do
          create(:scan_result_policy_violation, project: project, merge_request: merge_request,
            approval_policy_rule: policy_rule)
        end

        it 'returns policies as keys' do
          expect(overriding_policies.keys).to contain_exactly(policy)
        end

        it 'has overrides as values' do
          expect(overriding_policies.values).to contain_exactly(overrides)
        end

        context 'when policy does not set approval_settings' do
          let(:policy) { create(:security_policy) }

          it { is_expected.to be_empty }
        end

        context 'when policy does not set overriding approval_settings' do
          let(:policy) do
            create(:security_policy, content: { approval_settings: { require_password_to_approve: false } })
          end

          it { is_expected.to be_empty }
        end

        context 'when there are multiple policies' do
          let(:other_approval_settings) { { require_password_to_approve: true } }
          let(:other_policy) do
            create(:security_policy, content: { approval_settings: other_approval_settings })
          end

          let(:other_policy_rule) do
            create(:approval_policy_rule, :any_merge_request, security_policy: other_policy)
          end

          before do
            create(:scan_result_policy_violation, project: project, merge_request: merge_request,
              approval_policy_rule: other_policy_rule)
          end

          it 'returns results by policy' do
            expect(overriding_policies.keys).to contain_exactly(policy, other_policy)
          end

          it 'returns project overrides by policy' do
            expect(overriding_policies.values).to contain_exactly(overrides, other_approval_settings)
          end

          context 'when other policies do not override approval settings' do
            let(:other_approval_settings) { { require_password_to_approve: false } }

            it 'only returns policies that override settings' do
              expect(overriding_policies.keys).to contain_exactly(policy)
            end
          end
        end
      end

      context 'when not violated' do
        it { is_expected.to be_empty }
      end
    end

    context 'when approval_policy_rule is not populated' do
      let!(:scan_result_policy_read) do
        create(:scan_result_policy_read,
          project: merge_request.target_project,
          project_approval_settings: project_approval_settings)
      end

      context 'when violated' do
        before do
          create(:scan_result_policy_violation, project: project, merge_request: merge_request,
            scan_result_policy_read: scan_result_policy_read)
        end

        it 'constructs policy object as key' do
          first_policy = overriding_policies.each_key.first
          expect(first_policy).to be_a(Security::Policy)
          expect(first_policy)
            .to have_attributes(
              name: nil,
              security_orchestration_policy_configuration_id: scan_result_policy_read.security_orchestration_policy_configuration_id,
              policy_index: scan_result_policy_read.orchestration_policy_idx
            )
        end

        it 'has overrides as values' do
          expect(overriding_policies.values).to contain_exactly(overrides)
        end

        context 'when there are multiple policies' do
          let(:other_approval_settings) { { require_password_to_approve: true } }
          let!(:other_scan_result_policy_read) do
            create(:scan_result_policy_read,
              project: merge_request.target_project,
              orchestration_policy_idx: 1,
              project_approval_settings: other_approval_settings)
          end

          before do
            create(:scan_result_policy_violation, project: project, merge_request: merge_request,
              scan_result_policy_read: other_scan_result_policy_read)
          end

          it 'returns results by policy' do
            expect(overriding_policies.keys.size).to eq(2)
            expect(overriding_policies.keys.last).to have_attributes(
              name: nil,
              security_orchestration_policy_configuration_id: other_scan_result_policy_read.security_orchestration_policy_configuration_id,
              policy_index: other_scan_result_policy_read.orchestration_policy_idx
            )
          end

          it 'returns project overrides by policy' do
            expect(overriding_policies.values).to contain_exactly(overrides, other_approval_settings)
          end

          context 'when other policies do not override approval settings' do
            let(:other_approval_settings) { { require_password_to_approve: false } }

            it 'only returns policies that override settings', :aggregate_failures do
              expect(overriding_policies.keys).to be_one
              expect(overriding_policies.values).to contain_exactly(overrides)
            end
          end
        end
      end

      context 'when not violated' do
        it { is_expected.to be_empty }
      end
    end
  end

  it_behaves_like 'an editable mentionable with EE-specific mentions' do
    subject { create(:merge_request, :simple) }

    let(:backref_text) { "merge request #{subject.to_reference}" }
    let(:set_mentionable_text) { ->(txt) { subject.description = txt } }
  end

  describe '#allow_external_status_checks?' do
    subject { merge_request.allow_external_status_checks? }

    let_it_be(:merge_request) { build_stubbed(:merge_request) }

    context 'when licensed feature `external_status_checks` is `false`' do
      before do
        stub_licensed_features(external_status_checks: false)
      end

      it { is_expected.to be false }
    end

    context 'when licensed feature `external_status_checks` is `true`' do
      before do
        stub_licensed_features(external_status_checks: true)
      end

      it { is_expected.to be true }
    end
  end

  describe '#participants' do
    subject(:participants) { merge_request.participants }

    context 'with approval rule' do
      before do
        approver = create(:approver, target: project)
        second_approver = create(:approver, target: project)

        create(:approval_merge_request_rule, merge_request: merge_request, users: [approver.user, second_approver.user])
      end

      it 'returns only the author as a participant' do
        expect(participants).to contain_exactly(merge_request.author)
      end
    end
  end

  describe '#has_denied_policies?', feature_category: :software_composition_analysis do
    let(:merge_request) { create(:ee_merge_request, :with_license_scanning_reports, source_project: project) }
    let(:apache) { build(:software_license, :apache_2_0) }

    let!(:head_pipeline) do
      create(
        :ee_ci_pipeline,
        :with_license_scanning_feature_branch,
        project: project,
        ref: merge_request.source_branch,
        sha: merge_request.diff_head_sha
      )
    end

    let!(:base_pipeline) do
      create(
        :ee_ci_pipeline,
        project: project,
        ref: merge_request.target_branch,
        sha: merge_request.diff_base_sha
      )
    end

    before do
      allow_any_instance_of(Ci::CompareSecurityReportsService)
        .to receive(:execute).with(base_pipeline, head_pipeline).and_call_original
    end

    subject { merge_request.has_denied_policies? }

    context 'without existing pipeline' do
      it { is_expected.to be_falsey }
    end

    context 'with existing pipeline' do
      before do
        stub_licensed_features(license_scanning: true)
      end

      context 'without license_scanning report' do
        let(:merge_request) { create(:ee_merge_request, :with_dependency_scanning_reports, source_project: project) }

        it { is_expected.to be_falsey }
      end

      context 'with license_scanning report' do
        context 'without denied policy' do
          it { is_expected.to be_falsey }
        end

        context 'with allowed policy' do
          let(:allowed_policy) { create(:software_license_policy, :allowed, software_license: apache) }

          before do
            project.software_license_policies << allowed_policy
            synchronous_reactive_cache(merge_request)
          end

          it { is_expected.to be_falsey }
        end

        context 'with denied policy' do
          let(:denied_policy) do
            create(:software_license_policy, :denied, software_license: apache,
              software_license_spdx_identifier: apache.spdx_identifier)
          end

          let(:merge_request) { create(:ee_merge_request, :with_cyclonedx_reports, source_project: project) }

          before do
            project.software_license_policies << denied_policy
            synchronous_reactive_cache(merge_request)

            create(:pm_package, name: "nokogiri", purl_type: "gem",
              other_licenses: [{ license_names: ["Apache-2.0"], versions: ["1.8.0"] }])
          end

          it { is_expected.to be_truthy }

          context 'with disabled licensed feature' do
            before do
              stub_licensed_features(license_scanning: false)
            end

            it { is_expected.to be_falsey }
          end

          context 'with License-Check enabled' do
            let!(:license_check) { create(:report_approver_rule, :license_scanning, merge_request: merge_request) }

            context 'when rule is not approved' do
              let(:software_license) { build(:software_license, :apache_2_0) }
              let(:merge_request) { create(:ee_merge_request, :with_cyclonedx_reports, source_project: project) }
              let(:denied_policy) do
                create(:software_license_policy, :denied, software_license: software_license,
                  software_license_spdx_identifier: software_license.spdx_identifier)
              end

              before do
                allow_any_instance_of(ApprovalWrappedRule).to receive(:approved?).and_return(false)

                create(
                  :pm_package, name: "nokogiri", purl_type: "gem",
                  other_licenses: [{ license_names: ["Apache-2.0"], versions: ["1.8.0"] }]
                )
              end

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

                it { is_expected.to be_truthy }
              end

              it { is_expected.to be_truthy }
            end

            context 'when rule is approved' do
              before do
                allow_any_instance_of(ApprovalWrappedRule).to receive(:approved?).and_return(true)
              end

              it { is_expected.to be_falsey }
            end
          end
        end
      end
    end

    context 'without license_scanning report in base pipeline' do
      before do
        stub_licensed_features(license_scanning: true)
        synchronous_reactive_cache(merge_request)
      end

      let(:merge_request) { create(:ee_merge_request, :with_cyclonedx_reports, source_project: project) }

      context 'when the base pipeline is nil' do
        let!(:base_pipeline) { nil }

        it { is_expected.to be_falsey }
      end

      context 'when the base pipeline does not have license reports' do
        it { is_expected.to be_falsey }
      end
    end
  end

  describe '#enabled_reports' do
    where(:report_type, :with_reports, :feature) do
      :sast                | [:with_sast_reports]                                       | :sast
      :container_scanning  | [:with_container_scanning_reports]                         | :container_scanning
      :dast                | [:with_dast_reports]                                       | :dast
      :dependency_scanning | [:with_dependency_scanning_reports]                        | :dependency_scanning
      :dependency_scanning | [:with_cyclonedx_reports]                                  | :dependency_scanning
      :license_scanning    | [:with_cyclonedx_reports]                                  | :license_scanning
      :coverage_fuzzing    | [:with_coverage_fuzzing_reports]                           | :coverage_fuzzing
      :secret_detection    | [:with_secret_detection_reports]                           | :secret_detection
      :api_fuzzing         | [:with_api_fuzzing_reports]                                | :api_fuzzing
    end

    with_them do
      subject { merge_request.enabled_reports[report_type] }

      before do
        stub_licensed_features({ feature => true })
      end

      context "when head pipeline has reports" do
        let(:merge_request) { create(:ee_merge_request, *with_reports, source_project: project) }

        it { is_expected.to be_truthy }
      end

      context "when head pipeline does not have reports" do
        let(:merge_request) { create(:ee_merge_request, source_project: project) }

        it { is_expected.to be_falsy }
      end
    end
  end

  describe '#approvals_before_merge' do
    where(:license_value, :db_value, :expected) do
      true  | 5   | 5
      true  | nil | nil
      false | 5   | nil
      false | nil | nil
    end

    with_them do
      let(:merge_request) { build(:merge_request, approvals_before_merge: db_value) }

      subject { merge_request.approvals_before_merge }

      before do
        stub_licensed_features(merge_request_approvers: license_value)
      end

      it { is_expected.to eq(expected) }
    end
  end

  describe '#has_security_reports?' do
    subject { merge_request.has_security_reports? }

    before do
      stub_licensed_features(dast: true)
    end

    context 'when head pipeline has security reports' do
      let(:merge_request) { create(:ee_merge_request, :with_dast_reports, source_project: project) }

      it { is_expected.to be_truthy }

      context 'when head pipeline is blocked by manual jobs' do
        before do
          merge_request.diff_head_pipeline.block!
        end

        it { is_expected.to be_truthy }
      end
    end

    context 'when head pipeline does not have security reports' do
      let(:merge_request) { create(:ee_merge_request, source_project: project) }

      it { is_expected.to be_falsey }
    end
  end

  describe '#has_dependency_scanning_reports?' do
    subject { merge_request.has_dependency_scanning_reports? }

    before do
      stub_licensed_features(container_scanning: true)
    end

    context 'when head pipeline has dependency scannning reports' do
      let(:merge_request) { create(:ee_merge_request, :with_dependency_scanning_reports, source_project: project) }

      it { is_expected.to be_truthy }

      context 'when head pipeline is blocked by manual jobs' do
        before do
          merge_request.diff_head_pipeline.block!
        end

        it { is_expected.to be_truthy }
      end

      context 'when head pipeline has cyclonedx reports' do
        let(:merge_request) { create(:ee_merge_request, :with_cyclonedx_reports, source_project: project) }

        it { is_expected.to be_truthy }
      end
    end

    context 'when head pipeline does not have dependency scanning reports' do
      let(:merge_request) { create(:ee_merge_request, source_project: project) }

      it { is_expected.to be_falsey }
    end
  end

  describe '#has_container_scanning_reports?' do
    subject { merge_request.has_container_scanning_reports? }

    before do
      stub_licensed_features(container_scanning: true)
    end

    context 'when head pipeline has container scanning reports' do
      let(:merge_request) { create(:ee_merge_request, :with_container_scanning_reports, source_project: project) }

      it { is_expected.to be_truthy }

      context 'when head pipeline is blocked by manual jobs' do
        before do
          merge_request.diff_head_pipeline.block!
        end

        it { is_expected.to be_truthy }
      end
    end

    context 'when head pipeline does not have container scanning reports' do
      let(:merge_request) { create(:ee_merge_request, source_project: project) }

      it { is_expected.to be_falsey }
    end
  end

  describe '#has_dast_reports?' do
    subject { merge_request.has_dast_reports? }

    before do
      stub_licensed_features(dast: true)
    end

    context 'when head pipeline has dast reports' do
      let(:merge_request) { create(:ee_merge_request, :with_dast_reports, source_project: project) }

      it { is_expected.to be_truthy }

      context 'when head pipeline is blocked by manual jobs' do
        before do
          merge_request.diff_head_pipeline.block!
        end

        it { is_expected.to be_truthy }
      end
    end

    context 'when pipeline ran for an older commit than the branch head' do
      let(:pipeline) { create(:ci_empty_pipeline, sha: 'notlatestsha') }
      let(:merge_request) { create(:ee_merge_request, source_project: project, head_pipeline: pipeline) }

      it { is_expected.to be_falsey }
    end

    context 'when head pipeline does not have dast reports' do
      let(:merge_request) { create(:ee_merge_request, source_project: project) }

      it { is_expected.to be_falsey }
    end
  end

  describe '#has_metrics_reports?' do
    subject { merge_request.has_metrics_reports? }

    before do
      stub_licensed_features(metrics_reports: true)
    end

    context 'when head pipeline has metrics reports' do
      let(:merge_request) { create(:ee_merge_request, :with_metrics_reports, source_project: project) }

      it { is_expected.to be_truthy }

      context 'when the head pipeline is still in progress' do
        it 'does not return any metrics reports' do
          merge_request.head_pipeline.update!(status: 'running')

          expect(subject).to be_falsey
        end
      end
    end

    context 'when head pipeline does not have metrics reports' do
      let(:merge_request) { create(:ee_merge_request, :with_head_pipeline, source_project: project) }

      before do
        merge_request.head_pipeline.update!(status: 'success')
      end

      it { is_expected.to be_falsey }

      context 'when child pipeline has metrics reports' do
        it 'returns child report' do
          create(:ee_ci_pipeline, :success, :with_metrics_report, child_of: merge_request.head_pipeline)

          expect(subject).to be_truthy
        end

        context 'when ff show_child_reports_in_mr_page is disabled' do
          before do
            stub_feature_flags(show_child_reports_in_mr_page: false)
          end

          it { is_expected.to be_falsey }
        end
      end
    end
  end

  describe '#has_coverage_fuzzing_reports?' do
    subject { merge_request.has_coverage_fuzzing_reports? }

    before do
      stub_licensed_features(coverage_fuzzing: true)
    end

    context 'when head pipeline has coverage fuzzing reports' do
      let(:merge_request) { create(:ee_merge_request, :with_coverage_fuzzing_reports, source_project: project) }

      it { is_expected.to be_truthy }

      context 'when head pipeline is blocked by manual jobs' do
        before do
          merge_request.diff_head_pipeline.block!
        end

        it { is_expected.to be_truthy }
      end
    end

    context 'when head pipeline does not have coverage fuzzing reports' do
      let(:merge_request) { create(:ee_merge_request, source_project: project) }

      it { is_expected.to be_falsey }
    end
  end

  describe '#has_api_fuzzing_reports?' do
    subject { merge_request.has_api_fuzzing_reports? }

    before do
      stub_licensed_features(api_fuzzing: true)
    end

    context 'when head pipeline has coverage fuzzing reports' do
      let(:merge_request) { create(:ee_merge_request, :with_api_fuzzing_reports, source_project: project) }

      it { is_expected.to be_truthy }

      context 'when head pipeline is blocked by manual jobs' do
        before do
          merge_request.diff_head_pipeline.block!
        end

        it { is_expected.to be_truthy }
      end
    end

    context 'when head pipeline does not have coverage fuzzing reports' do
      let(:merge_request) { create(:ee_merge_request, source_project: project) }

      it { is_expected.to be_falsey }
    end
  end

  describe '#calculate_reactive_cache with current_user' do
    let(:current_user) { project.users.take }
    let(:merge_request) { create(:merge_request, source_project: project) }

    subject { merge_request.calculate_reactive_cache(service_class_name, current_user&.id) }

    context 'when given a known service class name' do
      let(:service_class_name) { 'Ci::CompareTestReportsService' }

      it 'does not raises a NameError exception' do
        allow_any_instance_of(service_class_name.constantize).to receive(:execute).and_return(nil)

        expect { subject }.not_to raise_error
      end
    end
  end

  describe '#compare_container_scanning_reports' do
    subject { merge_request.compare_container_scanning_reports(current_user) }

    let(:current_user) { project.users.first }
    let(:merge_request) { create(:merge_request, source_project: project) }

    let!(:base_pipeline) do
      create(
        :ee_ci_pipeline,
        :with_container_scanning_report,
        project: project,
        ref: merge_request.target_branch,
        sha: merge_request.diff_base_sha
      )
    end

    before do
      merge_request.update!(head_pipeline_id: head_pipeline.id)
    end

    context 'when head pipeline has container scanning reports' do
      let!(:head_pipeline) do
        create(
          :ee_ci_pipeline,
          :with_container_scanning_report,
          project: project,
          ref: merge_request.source_branch,
          sha: merge_request.diff_head_sha
        )
      end

      context 'when reactive cache worker is parsing asynchronously' do
        it 'returns status' do
          expect(subject[:status]).to eq(:parsing)
        end
      end

      context 'when reactive cache worker is inline' do
        before do
          synchronous_reactive_cache(merge_request)
        end

        it 'returns status and data' do
          expect_any_instance_of(Ci::CompareSecurityReportsService)
              .to receive(:execute).with(base_pipeline, head_pipeline).and_call_original

          subject
        end

        context 'when cached results is not latest' do
          before do
            allow_any_instance_of(Ci::CompareSecurityReportsService)
                .to receive(:latest?).and_return(false)
          end

          it 'raises and InvalidateReactiveCache error' do
            expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
          end
        end
      end
    end
  end

  describe '#compare_secret_detection_reports' do
    subject { merge_request.compare_secret_detection_reports(current_user) }

    let(:current_user) { project.users.first }
    let(:merge_request) { create(:merge_request, source_project: project) }

    let!(:base_pipeline) do
      create(
        :ee_ci_pipeline,
        :with_secret_detection_report,
        project: project,
        ref: merge_request.target_branch,
        sha: merge_request.diff_base_sha
      )
    end

    before do
      merge_request.update!(head_pipeline_id: head_pipeline.id)
    end

    context 'when head pipeline has secret detection reports' do
      let!(:head_pipeline) do
        create(
          :ee_ci_pipeline,
          :with_secret_detection_report,
          project: project,
          ref: merge_request.source_branch,
          sha: merge_request.diff_head_sha
        )
      end

      context 'when reactive cache worker is parsing asynchronously' do
        it 'returns status' do
          expect(subject[:status]).to eq(:parsing)
        end
      end

      context 'when reactive cache worker is inline' do
        before do
          synchronous_reactive_cache(merge_request)
        end

        it 'returns status and data' do
          expect_any_instance_of(Ci::CompareSecurityReportsService)
              .to receive(:execute).with(base_pipeline, head_pipeline).and_call_original

          subject
        end

        context 'when cached results is not latest' do
          before do
            allow_any_instance_of(Ci::CompareSecurityReportsService)
                .to receive(:latest?).and_return(false)
          end

          it 'raises and InvalidateReactiveCache error' do
            expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
          end
        end
      end
    end
  end

  describe '#compare_sast_reports' do
    subject { merge_request.compare_sast_reports(current_user) }

    let(:current_user) { project.users.first }
    let(:merge_request) { create(:merge_request, source_project: project) }

    let!(:base_pipeline) do
      create(
        :ee_ci_pipeline,
        :with_sast_report,
        project: project,
        ref: merge_request.target_branch,
        sha: merge_request.diff_base_sha
      )
    end

    before do
      merge_request.update!(head_pipeline_id: head_pipeline.id)
    end

    context 'when head pipeline has sast reports' do
      let!(:head_pipeline) do
        create(
          :ee_ci_pipeline,
          :with_sast_report,
          project: project,
          ref: merge_request.source_branch,
          sha: merge_request.diff_head_sha
        )
      end

      context 'when reactive cache worker is parsing asynchronously' do
        it 'returns status' do
          expect(subject[:status]).to eq(:parsing)
        end
      end

      context 'when reactive cache worker is inline' do
        before do
          synchronous_reactive_cache(merge_request)
        end

        it 'returns status and data' do
          expect_any_instance_of(Ci::CompareSecurityReportsService)
              .to receive(:execute).with(base_pipeline, head_pipeline).and_call_original

          subject
        end

        context 'when cached results is not latest' do
          before do
            allow_any_instance_of(Ci::CompareSecurityReportsService)
                .to receive(:latest?).and_return(false)
          end

          it 'raises and InvalidateReactiveCache error' do
            expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
          end
        end
      end
    end
  end

  describe '#compare_license_scanning_reports', feature_category: :software_composition_analysis do
    subject { merge_request.compare_license_scanning_reports(current_user) }

    let(:current_user) { project.users.first }
    let(:merge_request) { create(:merge_request, source_project: project) }

    let!(:base_pipeline) do
      create(
        :ee_ci_pipeline,
        :with_cyclonedx_report,
        project: project,
        ref: merge_request.target_branch,
        sha: merge_request.diff_base_sha
      )
    end

    before do
      merge_request.update!(head_pipeline_id: head_pipeline.id)
    end

    context 'when head pipeline has cyclonedx reports' do
      let!(:head_pipeline) do
        create(
          :ee_ci_pipeline,
          :with_cyclonedx_report,
          project: project,
          ref: merge_request.source_branch,
          sha: merge_request.diff_head_sha
        )
      end

      context 'when reactive cache worker is parsing asynchronously' do
        it 'returns status' do
          expect(subject[:status]).to eq(:parsing)
        end
      end

      context 'when reactive cache worker is inline' do
        before do
          synchronous_reactive_cache(merge_request)
        end

        it 'returns status and data' do
          expect_any_instance_of(Ci::CompareLicenseScanningReportsService)
            .to receive(:execute).with(base_pipeline, head_pipeline).and_call_original

          subject
        end

        context 'cache key includes software license policies' do
          let(:apache_2_0) { build(:software_license, :apache_2_0) }
          let!(:license_1) { create(:software_license_policy, project: project, software_license: apache_2_0, software_license_spdx_identifier: apache_2_0.spdx_identifier) }
          let(:mit) { build(:software_license, :mit) }
          let!(:license_2) { create(:software_license_policy, project: project, software_license: mit, software_license_spdx_identifier: mit.spdx_identifier) }

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

            it 'returns key with license information' do
              expect_any_instance_of(Ci::CompareLicenseScanningReportsService)
                .to receive(:execute).with(base_pipeline, head_pipeline).and_call_original

              expect(subject[:key].last).to include("software_license_policies/query-")
              expect(subject[:data]['existing_licenses'].last.dig('classification', 'approval_status')).to eq('unclassified')
            end
          end

          it 'returns key with license information' do
            expect_any_instance_of(Ci::CompareLicenseScanningReportsService)
              .to receive(:execute).with(base_pipeline, head_pipeline).and_call_original

            expect(subject[:key].last).to include("software_license_policies/query-")
            expect(subject[:data]['existing_licenses'].last.dig('classification', 'approval_status')).to eq('unclassified')
          end
        end

        context 'when cached results is not latest' do
          before do
            allow_any_instance_of(Ci::CompareLicenseScanningReportsService)
              .to receive(:latest?).and_return(false)
          end

          it 'raises and InvalidateReactiveCache error' do
            expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
          end
        end
      end
    end

    context 'when head pipeline does not have cyclonedx reports' do
      let!(:head_pipeline) do
        create(
          :ci_pipeline,
          project: project,
          ref: merge_request.source_branch,
          sha: merge_request.diff_head_sha
        )
      end

      it 'returns status and error message' do
        expect(subject[:status]).to eq(:error)
        expect(subject[:status_reason]).to eq('This merge request does not have license scanning reports')
      end
    end

    context "when a cyclonedx report is produced from the head pipeline" do
      where(:pipeline_status, :build_types, :expected_status) do
        [
          [:blocked, [:container_scanning], :error],
          [:blocked, [:cyclonedx], :parsed],
          [:blocked, [:cyclonedx, :container_scanning], :parsed],
          [:blocked, [], :error],
          [:failed, [:container_scanning], :error],
          [:failed, [:cyclonedx], :parsed],
          [:failed, [:cyclonedx, :container_scanning], :parsed],
          [:failed, [], :error],
          [:running, [:container_scanning], :error],
          [:running, [:cyclonedx], :error],
          [:running, [:cyclonedx, :container_scanning], :error],
          [:running, [], :error],
          [:success, [:container_scanning], :error],
          [:success, [:cyclonedx], :parsed],
          [:success, [:cyclonedx, :container_scanning], :parsed],
          [:success, [], :error]
        ]
      end

      with_them do
        let!(:head_pipeline) { create(:ci_pipeline, pipeline_status, project: project, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, builds: builds) }
        let(:builds) { build_types.map { |build_type| create(:ee_ci_build, build_type) } }

        before do
          synchronous_reactive_cache(merge_request)
        end

        specify { expect(subject[:status]).to eq(expected_status) }
      end
    end
  end

  describe '#compare_license_scanning_reports_collapsed', feature_category: :software_composition_analysis do
    subject(:report) { merge_request.compare_license_scanning_reports_collapsed(current_user) }

    let(:current_user) { project.users.first }

    let!(:base_pipeline) do
      create(
        :ee_ci_pipeline,
        :with_license_scanning_report,
        project: project,
        ref: merge_request.target_branch,
        sha: merge_request.diff_base_sha
      )
    end

    let!(:head_pipeline) do
      create(
        :ci_pipeline,
        project: project,
        ref: merge_request.source_branch,
        sha: merge_request.diff_head_sha
      )
    end

    context 'when service can be executed' do
      before do
        merge_request.update!(head_pipeline_id: head_pipeline.id)

        allow_next_instance_of(::Gitlab::LicenseScanning::SbomScanner) do |scanner|
          allow(scanner).to receive(:results_available?).and_return(true)
        end
      end

      it 'returns compared report' do
        expect(report[:status]).to eq(:parsing)
      end
    end

    context 'when head pipeline does not have license scanning reports' do
      it 'returns status and error message' do
        expect(subject[:status]).to eq(:error)
      end
    end
  end

  describe '#compare_metrics_reports' do
    subject { merge_request.compare_metrics_reports }

    let(:merge_request) { create(:merge_request, source_project: project) }

    let!(:base_pipeline) do
      create(
        :ee_ci_pipeline,
        :with_metrics_report,
        project: project,
        ref: merge_request.target_branch,
        sha: merge_request.diff_base_sha
      )
    end

    before do
      merge_request.update!(head_pipeline_id: head_pipeline.id)
    end

    context 'when head pipeline has metrics reports' do
      let!(:head_pipeline) do
        create(
          :ee_ci_pipeline,
          :with_metrics_report,
          project: project,
          ref: merge_request.source_branch,
          sha: merge_request.diff_head_sha
        )
      end

      context 'when reactive cache worker is parsing asynchronously' do
        it 'returns status' do
          expect(subject[:status]).to eq(:parsing)
        end
      end

      context 'when reactive cache worker is inline' do
        before do
          synchronous_reactive_cache(merge_request)
        end

        it 'returns status and data' do
          expect_any_instance_of(Ci::CompareMetricsReportsService)
            .to receive(:execute).with(base_pipeline, head_pipeline).and_call_original

          subject
        end

        context 'when cached results is not latest' do
          before do
            allow_any_instance_of(Ci::CompareMetricsReportsService)
              .to receive(:latest?).and_return(false)
          end

          it 'raises and InvalidateReactiveCache error' do
            expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
          end
        end
      end
    end

    context 'when head pipeline does not have metrics reports' do
      let!(:head_pipeline) do
        create(
          :ci_pipeline,
          project: project,
          ref: merge_request.source_branch,
          sha: merge_request.diff_head_sha
        )
      end

      it 'returns status and error message' do
        expect(subject[:status]).to eq(:error)
        expect(subject[:status_reason]).to eq('This merge request does not have metrics reports')
      end
    end
  end

  describe '#compare_coverage_fuzzing_reports' do
    subject { merge_request.compare_coverage_fuzzing_reports(current_user) }

    let(:current_user) { project.users.first }
    let(:merge_request) { create(:merge_request, source_project: project) }

    let!(:base_pipeline) do
      create(
        :ee_ci_pipeline,
        :with_coverage_fuzzing_report,
        project: project,
        ref: merge_request.target_branch,
        sha: merge_request.diff_base_sha
      )
    end

    before do
      merge_request.update!(head_pipeline_id: head_pipeline.id)
    end

    context 'when head pipeline has coverage fuzzing reports' do
      let!(:head_pipeline) do
        create(
          :ee_ci_pipeline,
          :with_coverage_fuzzing_report,
          project: project,
          ref: merge_request.source_branch,
          sha: merge_request.diff_head_sha
        )
      end

      context 'when reactive cache worker is parsing asynchronously' do
        it 'returns status' do
          expect(subject[:status]).to eq(:parsing)
        end
      end

      context 'when reactive cache worker is inline' do
        before do
          synchronous_reactive_cache(merge_request)
        end

        it 'returns status and data' do
          expect_any_instance_of(Ci::CompareSecurityReportsService)
            .to receive(:execute).with(base_pipeline, head_pipeline).and_call_original

          subject
        end

        context 'when cached results is not latest' do
          before do
            allow_any_instance_of(Ci::CompareSecurityReportsService)
                .to receive(:latest?).and_return(false)
          end

          it 'raises and InvalidateReactiveCache error' do
            expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
          end
        end
      end
    end
  end

  describe '#compare_api_fuzzing_reports' do
    subject { merge_request.compare_api_fuzzing_reports(current_user) }

    let(:current_user) { project.users.first }
    let(:merge_request) { create(:merge_request, source_project: project) }

    let!(:base_pipeline) do
      create(
        :ee_ci_pipeline,
        :with_api_fuzzing_report,
        project: project,
        ref: merge_request.target_branch,
        sha: merge_request.diff_base_sha
      )
    end

    before do
      merge_request.update!(head_pipeline_id: head_pipeline.id)
    end

    context 'when head pipeline has api fuzzing reports' do
      let!(:head_pipeline) do
        create(
          :ee_ci_pipeline,
          :with_api_fuzzing_report,
          project: project,
          ref: merge_request.source_branch,
          sha: merge_request.diff_head_sha
        )
      end

      context 'when reactive cache worker is parsing asynchronously' do
        it 'returns status' do
          expect(subject[:status]).to eq(:parsing)
        end
      end

      context 'when reactive cache worker is inline' do
        before do
          synchronous_reactive_cache(merge_request)
        end

        it 'returns status and data' do
          expect_any_instance_of(Ci::CompareSecurityReportsService)
            .to receive(:execute).with(base_pipeline, head_pipeline).and_call_original

          subject
        end

        context 'when cached results is not latest' do
          before do
            allow_any_instance_of(Ci::CompareSecurityReportsService)
                .to receive(:latest?).and_return(false)
          end

          it 'raises an InvalidateReactiveCache error' do
            expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
          end
        end
      end
    end
  end

  describe '#use_merge_base_pipeline_for_comparison?' do
    let(:merge_request) { create(:merge_request, :with_codequality_reports, source_project: project) }

    subject { merge_request.use_merge_base_pipeline_for_comparison?(service_class) }

    context 'when service class is Ci::CompareMetricsReportsService' do
      let(:service_class) { ::Ci::CompareMetricsReportsService }

      it { is_expected.to eq(true) }
    end

    context 'when service class is Ci::CompareCodequalityReportsService' do
      let(:service_class) { ::Ci::CompareCodequalityReportsService }

      it { is_expected.to eq(true) }
    end

    context 'when service class is Ci::CompareSecurityReportsService' do
      let(:service_class) { ::Ci::CompareSecurityReportsService }

      it { is_expected.to eq(true) }
    end

    context 'when service class is Ci::CompareLicenseScanningReportsCollapsedService' do
      let(:service_class) { ::Ci::CompareLicenseScanningReportsCollapsedService }

      it { is_expected.to eq(true) }
    end

    context 'when service class is Ci::CompareLicenseScanningReportsService' do
      let(:service_class) { ::Ci::CompareLicenseScanningReportsService }

      it { is_expected.to eq(true) }
    end

    context 'when service class is different' do
      let(:service_class) { ::Ci::GenerateCoverageReportsService }

      it { is_expected.to eq(false) }
    end
  end

  describe '#approver_group_ids=' do
    it 'create approver_groups' do
      group = create :group
      group1 = create :group

      merge_request = create :merge_request

      merge_request.approver_group_ids = "#{group.id}, #{group1.id}"
      merge_request.save!

      expect(merge_request.approver_groups.map(&:group)).to match_array([group, group1])
    end
  end

  describe '#predefined_variables' do
    context 'when merge request has approver feature' do
      before do
        stub_licensed_features(merge_request_approvers: true)
      end

      context 'without any rules' do
        it 'includes variable CI_MERGE_REQUEST_APPROVED=true' do
          expect(merge_request.predefined_variables.to_hash).to include('CI_MERGE_REQUEST_APPROVED' => 'true')
        end

        context 'when the mr is temporarily unapproved' do
          it 'does not include variable CI_MERGE_REQUEST_APPROVED' do
            expect(merge_request.approval_state).to receive(:temporarily_unapproved?).and_return(true)

            expect(merge_request.predefined_variables.to_hash.keys).not_to include('CI_MERGE_REQUEST_APPROVED')
          end
        end
      end

      context 'with a rule' do
        let(:approver) { create(:user) }
        let!(:rule) { create(:approval_merge_request_rule, merge_request: merge_request, approvals_required: 1, users: [approver]) }

        context 'that has been approved' do
          it 'includes variable CI_MERGE_REQUEST_APPROVED=true' do
            create(:approval, merge_request: merge_request, user: approver)

            expect(merge_request.predefined_variables.to_hash).to include('CI_MERGE_REQUEST_APPROVED' => 'true')
          end
        end

        context 'that has not been approved' do
          it 'does not include variable CI_MERGE_REQUEST_APPROVED' do
            expect(merge_request.predefined_variables.to_hash.keys).not_to include('CI_MERGE_REQUEST_APPROVED')
          end
        end
      end
    end

    context 'when merge request does not have approver feature' do
      before do
        stub_licensed_features(merge_request_approvers: false)
      end

      it 'does not include variable CI_MERGE_REQUEST_APPROVED' do
        expect(merge_request.predefined_variables.to_hash.keys).not_to include('CI_MERGE_REQUEST_APPROVED')
      end
    end
  end

  describe '#mergeable?' do
    context 'with skip_approved_check option' do
      before do
        allow(subject).to receive_messages(mergeable_ci_state?: true,
          mergeable_discussions_state?: true,
          check_mergeability: nil,
          can_be_merged?: true,
          broken?: false)
      end

      where(:approved_state, :skip_approved_check, :expected_mergeable) do
        false | false | false
        false | true  | true
        true  | false | true
        true  | true  | true
      end

      with_them do
        it 'overrides mergeable?' do
          allow(merge_request).to receive(:approved?) { approved_state }

          expect(merge_request.mergeable?(skip_approved_check: skip_approved_check)).to eq(expected_mergeable)
        end
      end
    end
  end

  describe '#skipped_mergeable_checks' do
    subject { build_stubbed(:merge_request).skipped_mergeable_checks(options) }

    let(:options) { { auto_merge_strategy: auto_merge_strategy } }

    where(:auto_merge_strategy, :skip_approved_check, :skip_draft_check, :skip_blocked_check, :skip_discussions_check, :skip_external_status_check, :skip_locked_paths_check) do
      ''                                                      | false | false | false | false | false | false
      AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS       | true | true | true | true | true | true
    end

    with_them do
      it do
        is_expected.to include(skip_approved_check: skip_approved_check, skip_draft_check: skip_draft_check,
          skip_blocked_check: skip_blocked_check, skip_discussions_check: skip_discussions_check,
          skip_external_status_check: skip_external_status_check)
      end
    end
  end

  describe '#mergeable_state?' do
    subject { merge_request.mergeable_state?(**params) }

    let(:params) { {} }
    let(:project_with_approver) { create(:project, :repository) }
    let(:merge_request) { create(:merge_request, source_project: project_with_approver, target_project: project_with_approver) }

    let_it_be(:user) { create(:user) }

    context 'when using approvals' do
      before do
        merge_request.target_project.update!(approvals_before_merge: 1)
        project.add_developer(user)
      end

      context 'when not approved' do
        it 'is not mergeable' do
          is_expected.to be_falsey
        end
      end

      context 'when approved' do
        before do
          merge_request.approvals.create!(user: user)
        end

        it 'is mergeable' do
          is_expected.to be_truthy
        end
      end
    end

    context 'when validating jira associations' do
      let_it_be_with_reload(:project_jira) { create(:project, :repository, :with_jira_integration) }
      let_it_be_with_reload(:merge_request) { create(:merge_request, source_project: project_jira, target_project: project_jira) }

      before do
        allow(merge_request.project).to receive(:prevent_merge_without_jira_issue?).and_return(true)
      end

      context 'when the merge request does not reference a jira issue' do
        before do
          merge_request.update!(description: '')
        end

        it 'is not mergeable' do
          is_expected.to be_falsey
        end
      end

      context 'when the merge request references a jira issue' do
        before do
          merge_request.update!(description: 'PROJECT-1')
        end

        it 'is mergeable' do
          is_expected.to be_truthy
        end
      end
    end

    context 'when using external status checks' do
      let(:any_external_status_checks_not_passed?) { false }

      before do
        allow(merge_request).to receive(:only_allow_merge_if_all_status_checks_passed_enabled?).and_return(true)
        allow(merge_request.project).to receive(:only_allow_merge_if_all_status_checks_passed).and_return(true)
        allow(merge_request.project).to receive(:any_external_status_checks_not_passed?)
                            .and_return(any_external_status_checks_not_passed?)
        stub_licensed_features(external_status_checks: true)
      end

      context 'when external checks pass' do
        let!(:status_code) { create(:status_check_response, status: :passed, merge_request: merge_request) }

        it 'is mergeable' do
          is_expected.to be_truthy
        end
      end

      context 'when external status checks are not passed' do
        let(:any_external_status_checks_not_passed?) { true }

        let!(:status_code) { create(:status_check_response, status: :failed, merge_request: merge_request) }

        it 'is not mergeable' do
          is_expected.to be_falsey
        end

        context 'when skip external status checks is true' do
          let(:params) { { skip_external_status_check: true } }

          it 'is mergeable' do
            is_expected.to be_truthy
          end
        end
      end
    end

    context 'when blocking merge requests' do
      before do
        stub_licensed_features(blocking_merge_requests: true)
      end

      context 'when the merge request is blocked' do
        let(:merge_request) { create(:merge_request, :blocked, source_project: project, target_project: project) }

        it 'is not mergeable' do
          is_expected.to be_falsey
        end

        context 'when skip blocked check' do
          let(:params) { { skip_blocked_check: true } }

          it 'is mergeable' do
            is_expected.to be_truthy
          end
        end
      end

      context 'when merge request is not blocked' do
        it 'is mergeable' do
          is_expected.to be_truthy
        end
      end
    end
  end

  describe '#on_train?' do
    subject { merge_request.on_train? }

    context 'when the merge request is on a merge train' do
      let(:merge_request) do
        create(:merge_request, :on_train, source_project: project, target_project: project)
      end

      it { is_expected.to be_truthy }
    end

    context 'when the merge request was on a merge train' do
      let(:merge_request) do
        create(:merge_request, :on_train,
          status: MergeTrains::Car.state_machines[:status].states[:merged].value,
          source_project: project, target_project: project)
      end

      it { is_expected.to be_falsy }
    end

    context 'when the merge request is not on a merge train' do
      let(:merge_request) do
        create(:merge_request, source_project: project, target_project: project)
      end

      it { is_expected.to be_falsy }
    end
  end

  describe 'review time sorting' do
    def create_mr(metrics_data = {})
      create(:merge_request, :with_productivity_metrics, metrics_data: metrics_data)
    end

    it 'orders by first_comment_at or first_approved_at whatever is earlier' do
      mr1 = create_mr(first_comment_at: 1.day.ago)
      mr2 = create_mr(first_comment_at: 3.days.ago)
      mr3 = create_mr(first_approved_at: 5.days.ago)
      mr4 = create_mr(first_comment_at: 1.day.ago, first_approved_at: 4.days.ago)
      mr5 = create_mr(first_comment_at: nil, first_approved_at: nil)

      expect(described_class.order_review_time_desc).to match([mr3, mr4, mr2, mr1, mr5])
      expect(described_class.sort_by_attribute('review_time_desc')).to match([mr3, mr4, mr2, mr1, mr5])
    end
  end

  describe '#security_reports_up_to_date?' do
    let(:merge_request) do
      create(
        :ee_merge_request,
        source_project: project,
        source_branch: 'feature1',
        target_branch: project.default_branch
      )
    end

    before do
      create(
        :ee_ci_pipeline,
        :with_sast_report,
        project: project,
        ref: merge_request.target_branch
      )
    end

    subject { merge_request.security_reports_up_to_date? }

    context 'when the target branch security reports are up to date' do
      it { is_expected.to be true }
    end

    context 'when the target branch security reports are out of date' do
      before do
        create(:ee_ci_pipeline, :failed, project: project, ref: merge_request.target_branch)
      end

      it { is_expected.to be false }
    end
  end

  describe '#audit_details' do
    it 'equals to the title' do
      merge_request = create(:merge_request, title: 'I am a title')

      expect(merge_request.audit_details).to eq(merge_request.title)
    end
  end

  describe '#validate_reviewer_length' do
    let(:reviewer1) { create(:user) }
    let(:reviewer2) { create(:user) }
    let(:reviewer3) { create(:user) }

    subject { create(:merge_request) }

    before do
      stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 2)
    end

    it 'will not exceed the reviewer limit' do
      expect do
        subject.update!(reviewers: [reviewer1, reviewer2, reviewer3])
      end.to raise_error(ActiveRecord::RecordInvalid)
    end
  end

  describe '#sync_project_approval_rules_for_policy_configuration' do
    let_it_be(:merge_request) { create(:ee_merge_request, source_project: project) }
    let_it_be(:policy_configuration) { create(:security_orchestration_policy_configuration, project: project) }
    let_it_be(:project_approval_rule_without_configuration) { create(:approval_project_rule, project: project) }

    let_it_be(:project_approval_rule) do
      create(:approval_project_rule, :scan_finding,
        project: project,
        security_orchestration_policy_configuration: policy_configuration,
        scanners: %w[sast],
        approvals_required: 2,
        scan_result_policy_read: create(:scan_result_policy_read)
      )
    end

    subject { merge_request.sync_project_approval_rules_for_policy_configuration(policy_configuration.id) }

    it 'creates approval rules for project' do
      subject

      expect(merge_request.approval_rules.first.approval_project_rule).to eq(project_approval_rule)
    end

    it 'does not create approval rules for other configuration' do
      subject

      expect(merge_request.approval_rules.map(&:approval_project_rule)).not_to include(project_approval_rule_without_configuration)
    end

    context 'when mr approval rules already exist' do
      let_it_be(:mr_approval_rule) do
        create(:report_approver_rule, :scan_finding,
          merge_request: merge_request,
          approvals_required: 1
        )
      end

      let_it_be(:approval_rule_source) do
        create(:approval_merge_request_rule_source,
          approval_merge_request_rule: mr_approval_rule,
          approval_project_rule: project_approval_rule
        )
      end

      it 'updates approval rule' do
        subject

        expect(mr_approval_rule.reload.approvals_required).to eq(2)
      end
    end

    context 'when merge request is already merged' do
      let_it_be(:merge_request) { create(:ee_merge_request, source_project: project, state: :merged) }

      it 'does not create or update approval rule' do
        subject

        expect(merge_request.approval_rules).to be_empty
      end
    end
  end

  describe '#sync_project_approval_rules_for_approval_policy_rules' do
    let_it_be(:merge_request) { create(:ee_merge_request, source_project: project) }
    let_it_be(:security_policy) { create(:security_policy) }
    let_it_be(:approval_policy_rule) { create(:approval_policy_rule, security_policy: security_policy) }
    let_it_be(:project_approval_rule_without_policy_rule) { create(:approval_project_rule, project: project) }

    let_it_be(:project_approval_rule) do
      create(:approval_project_rule, :scan_finding,
        project: project,
        approval_policy_rule: approval_policy_rule,
        scanners: %w[sast],
        approvals_required: 2
      )
    end

    subject do
      merge_request.sync_project_approval_rules_for_approval_policy_rules(security_policy.approval_policy_rules)
    end

    it 'creates approval rules for project' do
      subject

      expect(merge_request.approval_rules.first.approval_project_rule).to eq(project_approval_rule)
    end

    it 'does not create approval rules for other configuration' do
      subject

      expect(merge_request.approval_rules.map(&:approval_project_rule)).not_to include(project_approval_rule_without_policy_rule)
    end

    context 'when mr approval rules already exist' do
      let_it_be(:mr_approval_rule) do
        create(:report_approver_rule, :scan_finding,
          merge_request: merge_request,
          approvals_required: 1
        )
      end

      let_it_be(:approval_rule_source) do
        create(:approval_merge_request_rule_source,
          approval_merge_request_rule: mr_approval_rule,
          approval_project_rule: project_approval_rule
        )
      end

      it 'updates approval rule' do
        subject

        expect(mr_approval_rule.reload.approvals_required).to eq(2)
      end
    end

    context 'when merge request is already merged' do
      let_it_be(:merge_request) { create(:ee_merge_request, source_project: project, state: :merged) }

      it 'does not create or update approval rule' do
        subject

        expect(merge_request.approval_rules).to be_empty
      end
    end
  end

  describe '#delete_approval_rules_for_policy_configuration' do
    let_it_be(:merge_request) { create(:ee_merge_request, source_project: project) }
    let_it_be(:policy_configuration) { create(:security_orchestration_policy_configuration, project: project) }
    let_it_be(:other_policy_configuration) { create(:security_orchestration_policy_configuration) }

    let_it_be(:mr_approval_rule) do
      create(:report_approver_rule, :scan_finding,
        merge_request: merge_request,
        security_orchestration_policy_configuration: policy_configuration
      )
    end

    let_it_be(:other_mr_approval_rule) do
      create(:report_approver_rule, :scan_finding,
        merge_request: merge_request,
        security_orchestration_policy_configuration: other_policy_configuration
      )
    end

    subject(:delete_approval_rules_for_policy_configuration) do
      merge_request.delete_approval_rules_for_policy_configuration(policy_configuration.id)
    end

    context 'when the merge request is not merged' do
      it 'deletes approval rules for the given policy configuration' do
        expect { delete_approval_rules_for_policy_configuration }.to change { ApprovalMergeRequestRule.count }.from(2).to(1)
      end
    end

    context 'when the merge request is merged' do
      before do
        merge_request.update!(state: 'merged')
      end

      it 'does not delete any approval rules' do
        expect { delete_approval_rules_for_policy_configuration }.not_to change { ApprovalMergeRequestRule.count }
      end
    end
  end

  describe '#reset_required_approvals' do
    subject(:execute) { merge_request.reset_required_approvals(approval_rules) }

    let_it_be(:merge_request) { create(:ee_merge_request, source_project: project) }
    let_it_be(:policy_configuration) { create(:security_orchestration_policy_configuration, project: project) }
    let_it_be(:scan_finding_project_rule) do
      create(:approval_project_rule, :scan_finding, project: project, scanners: %w[sast], approvals_required: 2,
        security_orchestration_policy_configuration: policy_configuration)
    end

    let_it_be(:license_project_rule) do
      create(:approval_project_rule, :license_scanning, project: project, approvals_required: 2,
        security_orchestration_policy_configuration: policy_configuration)
    end

    let_it_be(:other_project_rule) do
      create(:approval_project_rule, :scan_finding, project: project, scanners: %w[sast], approvals_required: 2,
        security_orchestration_policy_configuration: policy_configuration)
    end

    context 'when report_approver_rule has a project source' do
      let_it_be(:approval_rules) do
        [
          create(:report_approver_rule, :scan_finding, merge_request: merge_request, approvals_required: 0,
            approval_project_rule: scan_finding_project_rule),
          create(:report_approver_rule, :license_scanning, merge_request: merge_request, approvals_required: 0,
            approval_project_rule: license_project_rule)
        ]
      end

      let_it_be(:other_approval_rule_with_project_source) do
        create(:report_approver_rule, :scan_finding, merge_request: merge_request, approvals_required: 0,
          approval_project_rule: other_project_rule)
      end

      it 'resets the approvals_required for the provided approval_rules to match the project rules' do
        expect { execute }.to change { approval_rules.first.reload.approvals_required }.from(0).to(2)
          .and change { approval_rules.second.reload.approvals_required }.from(0).to(2)
      end

      it 'does not reset other approval rules' do
        expect { execute }.not_to change { other_approval_rule_with_project_source.reload.approvals_required }
      end

      context 'when merge request is already merged' do
        before do
          merge_request.update!(state: 'merged')
        end

        after do
          merge_request.update!(state: 'opened')
        end

        it 'does not update the approval rules' do
          expect { execute }.to not_change { approval_rules.first.reload.approvals_required }
            .and not_change { approval_rules.second.reload.approvals_required }
        end
      end
    end

    context "when report_approver_rule doesn't have a project source" do
      let_it_be(:approval_rules) do
        [create(:report_approver_rule, :scan_finding, merge_request: merge_request, approvals_required: 0)]
      end

      it "doesn't reset the approval_rules" do
        expect { execute }.not_to change { approval_rules.first.reload }
      end
    end
  end

  context 'scopes' do
    let_it_be(:merge_request) { create(:ee_merge_request) }
    let_it_be(:merge_request_with_head_pipeline) { create(:ee_merge_request, :with_metrics_reports) }

    describe '.with_head_pipeline' do
      it 'returns MRs that have a head pipeline' do
        expect(described_class.with_head_pipeline).to eq([merge_request_with_head_pipeline])
      end
    end

    describe '.not_merged' do
      let(:opened_merge_request) { create(:merge_request, :opened) }
      let(:merged_merge_request) { create(:merge_request, :merged) }
      let(:closed_merge_request) { create(:merge_request, :closed) }
      let(:locked_merge_request) { create(:merge_request, :locked) }

      it 'returns everything except the merged mr' do
        expect(described_class.not_merged).to contain_exactly(merge_request, merge_request_with_head_pipeline, opened_merge_request, closed_merge_request, locked_merge_request)
      end
    end

    describe '.with_applied_scan_result_policies' do
      let_it_be(:code_coverage_approval_rule) { create(:report_approver_rule, :code_coverage) }
      let_it_be(:scan_finding_approval_rule) { create(:report_approver_rule, :scan_finding) }
      let_it_be(:license_scanning_approval_rule) { create(:report_approver_rule, :license_scanning) }
      let_it_be(:any_merge_request_approval_rule) { create(:report_approver_rule, :any_merge_request) }

      it 'returns MRs that have applied scan result policies' do
        expect(described_class.with_applied_scan_result_policies).to eq([
          scan_finding_approval_rule.merge_request,
          license_scanning_approval_rule.merge_request,
          any_merge_request_approval_rule.merge_request
        ])
      end
    end

    describe '.for_projects_with_security_policy_project' do
      let_it_be(:security_orchestration_policy_configuration) { create(:security_orchestration_policy_configuration) }

      let_it_be(:merge_request_with_security_policy_project) do
        create(:ee_merge_request, source_project: security_orchestration_policy_configuration.project)
      end

      let_it_be(:merge_request_without_security_policy_project) { create(:ee_merge_request) }

      it 'returns MRs for projects with security policy project on target project' do
        expect(described_class.for_projects_with_security_policy_project).to eq(
          [merge_request_with_security_policy_project])
      end
    end
  end

  context 'after_commit hooks' do
    describe 'create_pending_status_check_responses' do
      subject(:create_merge_request) do
        create :merge_request, :opened, source_project: project, target_project: project
      end

      let_it_be(:status_check) { create(:external_status_check, project: project) }
      let_it_be(:another_status_check) { create(:external_status_check, project: project) }
      let(:merge_request) { described_class.last }

      context 'when project can have status checks' do
        before do
          stub_licensed_features(external_status_checks: true)
        end

        it 'creates `pending` status check responses' do
          expect(ComplianceManagement::PendingStatusCheckWorker).to receive(:perform_async).and_call_original

          create_merge_request
        end

        context 'when the the diff head SHA is empty' do
          it 'does not enqueue a status check' do
            expect(ComplianceManagement::PendingStatusCheckWorker).not_to receive(:perform_async)

            create :merge_request, :opened, source_branch: 'main', target_branch: 'foo'
          end
        end
      end

      context 'when project can not have status checks' do
        it 'does not create status check responses' do
          expect(ComplianceManagement::PendingStatusCheckWorker).not_to receive(:perform_async)

          create_merge_request
        end
      end
    end
  end

  context 'after_update hooks' do
    describe 'sync_merge_request_compliance_violation' do
      let_it_be(:merge_request) do
        create(:merge_request, source_project: project, target_project: project, state: :merged, title: 'old MR title')
      end

      let_it_be(:compliance_violation) do
        create(:compliance_violation, :approved_by_committer, severity_level: :low, merge_request: merge_request, title: 'old MR title')
      end

      it "calls sync_merge_request_compliance_violation when the MR title is updated" do
        expect(merge_request.compliance_violations.pluck(:title)).to contain_exactly('old MR title')
        expect(merge_request).to receive(:sync_merge_request_compliance_violation).and_call_original

        merge_request.update_attribute(:title, "new MR title")

        expect(merge_request.compliance_violations.pluck(:title)).to contain_exactly('new MR title')
      end

      it "does not call sync_merge_request_compliance_violation when the MR title is not updated" do
        expect(merge_request).not_to receive(:sync_merge_request_compliance_violation)

        merge_request.update_attribute(:milestone, Milestone.last)
      end
    end
  end

  describe '#merge_train' do
    subject { merge_request.merge_train }

    before do
      allow(project).to receive(:merge_trains_enabled?).and_return(setting)
    end

    context 'with MergeTrains disabled' do
      let(:setting) { false }

      it { is_expected.to be_nil }
    end

    context 'with MergeTrains enabled' do
      let(:setting) { true }

      it { is_expected.to be_a(MergeTrains::Train) }
    end
  end

  describe '#should_be_rebased?' do
    subject { merge_request.should_be_rebased? }

    shared_examples 'ff car on train' do
      context 'when MR is on an up-to-date fast-forward merge train' do
        before do
          car = create(:merge_train_car, merge_request: merge_request)

          merge_request.update!(
            merge_params: merge_request.merge_params.merge(
              'train_ref' => { 'commit_sha' => car.pipeline.sha }
            )
          )
        end

        it { is_expected.to eq false }
      end
    end

    context 'when MR source branch needs to be rebased to be merged' do
      before do
        stub_foss_conditions_met
      end

      context 'when the project is using ff trains' do
        before do
          allow(MergeTrains::Train).to receive(:project_using_ff?).and_return(true)
        end

        it { is_expected.to eq false }

        it_behaves_like 'ff car on train'
      end

      context 'when project not using ff trains' do
        before do
          allow(MergeTrains::Train).to receive(:project_using_ff?).and_return(false)
        end

        it { is_expected.to eq true }

        it_behaves_like 'ff car on train'
      end
    end

    def stub_foss_conditions_met
      allow(project).to receive(:ff_merge_must_be_possible?).and_return(true)
      allow(merge_request).to receive(:ff_merge_possible?).and_return(false)
    end
  end

  describe '#comparison_base_pipeline' do
    let_it_be(:project) { create(:project, :public, :repository) }
    let_it_be_with_refind(:merge_request) do
      create(:merge_request, :with_merge_request_pipeline, source_project: project)
    end

    let_it_be(:base_pipeline) do
      create(:ci_pipeline,
        :with_test_reports,
        project: project,
        ref: merge_request.target_branch,
        sha: merge_request.diff_base_sha
      )
    end

    subject(:pipeline) { merge_request.comparison_base_pipeline(service_class) }

    context 'when the service class is not `Ci::CompareSecurityReportsService`' do
      let(:service_class) { ::Ci::CompareCodequalityReportsService }

      before do
        allow(merge_request).to receive_messages(merge_base_pipeline: nil, base_pipeline: nil)
      end

      it 'follows the normal execution order' do
        pipeline

        expect(merge_request).to have_received(:merge_base_pipeline)
        expect(merge_request).to have_received(:base_pipeline)
      end
    end

    context 'when the service class is `Ci::CompareSecurityReportsService`' do
      let(:service_class) { ::Ci::CompareSecurityReportsService }

      before do
        merge_request.update_head_pipeline
      end

      context 'when there are merge base pipelines' do
        let_it_be(:old_merge_base_pipeline) do
          create(:ci_pipeline,
            :success,
            project: project,
            ref: merge_request.target_branch,
            sha: merge_request.target_branch_sha
          )
        end

        let_it_be(:most_recent_merge_base_pipeline) do
          create(:ci_pipeline,
            :success,
            project: project,
            ref: merge_request.target_branch,
            sha: merge_request.target_branch_sha
          )
        end

        context 'when all pipelines have security reports' do
          before do
            build_1 = build(:ci_build,
              :sast_report,
              pipeline: old_merge_base_pipeline,
              project: project)

            build_2 = build(:ci_build,
              :sast_report,
              pipeline: most_recent_merge_base_pipeline,
              project: project)

            old_merge_base_pipeline.builds << build_1
            most_recent_merge_base_pipeline.builds << build_2
          end

          it 'returns the most recent merge base pipeline' do
            expect(pipeline).to eq(most_recent_merge_base_pipeline)
          end
        end

        context 'when the most recent pipeline does not have security reports' do
          before do
            build = build(:ci_build,
              :sast_report,
              pipeline: old_merge_base_pipeline,
              project: project)

            old_merge_base_pipeline.builds << build
          end

          it 'returns the latest merge base pipeline with security reports' do
            expect(pipeline).to eq(old_merge_base_pipeline)
          end
        end
      end

      context 'when there is no merge base pipeline' do
        let_it_be(:old_base_pipeline) do
          create(:ci_pipeline,
            :success,
            project: project,
            ref: merge_request.target_branch,
            sha: merge_request.diff_base_sha
          )
        end

        let_it_be(:most_recent_base_pipeline) do
          create(:ci_pipeline,
            :success,
            project: project,
            ref: merge_request.target_branch,
            sha: merge_request.diff_base_sha
          )
        end

        context 'when all pipelines have security reports' do
          before do
            build_1 = build(:ci_build,
              :sast_report,
              pipeline: old_base_pipeline,
              project: project)

            build_2 = build(:ci_build,
              :sast_report,
              pipeline: most_recent_base_pipeline,
              project: project)

            old_base_pipeline.builds << build_1
            most_recent_base_pipeline.builds << build_2
          end

          it 'returns the most recent base pipeline' do
            expect(pipeline).to eq(most_recent_base_pipeline)
          end
        end

        context 'when the most recent pipeline does not have security reports' do
          before do
            build = build(:ci_build,
              :sast_report,
              pipeline: old_base_pipeline,
              project: project)

            old_base_pipeline.builds << build
          end

          it 'returns the latest base pipeline with security reports' do
            expect(pipeline).to eq(old_base_pipeline)
          end
        end
      end
    end
  end

  describe '#latest_comparison_pipeline_with_sbom_reports' do
    let_it_be(:project) { create(:project, :public, :repository) }
    let_it_be_with_refind(:merge_request) do
      create(:merge_request, :with_merge_request_pipeline, source_project: project)
    end

    let_it_be(:base_pipeline) do
      create(
        :ci_pipeline,
        :with_test_reports,
        project: project,
        ref: merge_request.target_branch,
        sha: merge_request.diff_base_sha)
    end

    subject(:pipeline) { merge_request.latest_comparison_pipeline_with_sbom_reports }

    before do
      merge_request.update_head_pipeline
    end

    context 'when there are merge base pipelines' do
      let_it_be(:old_merge_base_pipeline) do
        create(
          :ee_ci_pipeline,
          :with_cyclonedx_report,
          project: project,
          ref: merge_request.target_branch,
          sha: merge_request.target_branch_sha)
      end

      let_it_be(:most_recent_merge_base_pipeline) do
        create(
          :ee_ci_pipeline,
          :with_cyclonedx_report,
          project: project,
          ref: merge_request.target_branch,
          sha: merge_request.target_branch_sha)
      end

      context 'when all pipelines have SBOM artifacts' do
        it 'returns the most recent merge base pipeline' do
          expect(pipeline).to eq(most_recent_merge_base_pipeline)
        end
      end

      context 'when the most recent pipeline does not have an SBOM artifact' do
        before do
          most_recent_merge_base_pipeline.job_artifacts.cyclonedx.delete_all
        end

        it 'returns the latest merge base pipeline with an SBOM artifact' do
          expect(pipeline).to eq(old_merge_base_pipeline)
        end
      end
    end

    context 'when there is no merge base pipeline' do
      let_it_be(:old_base_pipeline) do
        create(
          :ee_ci_pipeline,
          :with_cyclonedx_report,
          project: project,
          ref: merge_request.target_branch,
          sha: merge_request.diff_base_sha)
      end

      let_it_be(:most_recent_base_pipeline) do
        create(
          :ee_ci_pipeline,
          :with_cyclonedx_report,
          project: project,
          ref: merge_request.target_branch,
          sha: merge_request.diff_base_sha)
      end

      context 'when all pipelines have SBOM artifacts' do
        it 'returns the most recent base pipeline' do
          expect(pipeline).to eq(most_recent_base_pipeline)
        end
      end

      context 'when the most recent pipeline does not have an SBOM artifact' do
        before do
          most_recent_base_pipeline.job_artifacts.cyclonedx.delete_all
        end

        it 'returns the latest base pipeline with an SBOM artifact' do
          expect(pipeline).to eq(old_base_pipeline)
        end
      end
    end
  end

  describe '#diff_head_pipeline?' do
    let_it_be_with_refind(:merge_request) do
      create(:merge_request, :with_merge_request_pipeline, source_project: project)
    end

    subject(:diff_head_pipeline) { merge_request.diff_head_pipeline?(pipeline) }

    context 'when the pipeline is the head pipeline' do
      let_it_be(:pipeline) do
        create(:ee_ci_pipeline,
          project: project,
          ref: merge_request.source_branch,
          sha: merge_request.diff_head_sha
        )
      end

      it { is_expected.to be_truthy }
    end

    context 'when the pipeline is merged_results_pipeline' do
      let_it_be(:pipeline) do
        create(:ci_pipeline, :merged_result_pipeline, :success, project: project, merge_request: merge_request)
      end

      it { is_expected.to be_truthy }
    end

    context 'when the pipeline is base_pipeline' do
      let_it_be(:pipeline) do
        create(:ee_ci_pipeline,
          project: project,
          ref: merge_request.source_branch,
          sha: merge_request.diff_base_sha
        )
      end

      it { is_expected.to be_falsey }
    end
  end

  describe '#latest_scan_finding_comparison_pipeline' do
    let_it_be(:project) { create(:project, :public, :repository) }
    let_it_be_with_refind(:merge_request) do
      create(:merge_request, :with_merge_request_pipeline, source_project: project)
    end

    let_it_be(:base_pipeline) do
      create(
        :ee_ci_pipeline,
        :with_dependency_scanning_report,
        project: project,
        ref: merge_request.target_branch,
        sha: merge_request.diff_base_sha)
    end

    subject(:pipeline) { merge_request.latest_scan_finding_comparison_pipeline }

    before do
      merge_request.update_head_pipeline
    end

    context 'when there are merge base pipelines' do
      let_it_be(:old_merge_base_pipeline) do
        create(
          :ee_ci_pipeline,
          :success,
          :with_dependency_scanning_report,
          project: project,
          ref: merge_request.target_branch,
          sha: merge_request.target_branch_sha)
      end

      let_it_be(:most_recent_merge_base_pipeline) do
        create(
          :ee_ci_pipeline,
          :success,
          :with_dependency_scanning_report,
          project: project,
          ref: merge_request.target_branch,
          sha: merge_request.target_branch_sha)
      end

      context 'when all pipelines have security reports' do
        it 'returns the most recent merge base pipeline' do
          expect(pipeline).to eq(most_recent_merge_base_pipeline)
        end
      end

      context 'when the most recent pipeline does not have security reports' do
        before do
          most_recent_merge_base_pipeline.job_artifacts.security_reports.delete_all
        end

        context 'when other merge base pipeline has security reports' do
          it 'returns the latest merge base pipeline with security reports' do
            expect(pipeline).to eq(old_merge_base_pipeline)
          end
        end

        context 'when no other merge base pipeline has security reports' do
          before do
            old_merge_base_pipeline.job_artifacts.security_reports.delete_all
          end

          context 'when base pipeline has security reports' do
            it 'returns the base pipeline' do
              expect(pipeline).to eq(base_pipeline)
            end
          end

          context 'when no base pipeline has security reports' do
            before do
              base_pipeline.job_artifacts.security_reports.delete_all

              most_recent_merge_base_pipeline.update!(status: :manual)
            end

            it 'returns nil' do
              expect(pipeline).to be_nil
            end
          end
        end
      end
    end

    context 'when there is no merge base pipeline' do
      let_it_be_with_refind(:old_base_pipeline) do
        create(
          :ee_ci_pipeline,
          :with_dependency_scanning_report,
          project: project,
          ref: merge_request.target_branch,
          sha: merge_request.diff_base_sha)
      end

      let_it_be_with_refind(:most_recent_base_pipeline) do
        create(
          :ee_ci_pipeline,
          :with_dependency_scanning_report,
          project: project,
          ref: merge_request.target_branch,
          sha: merge_request.diff_base_sha)
      end

      context 'when all pipelines have security reports' do
        it 'returns the most recent base pipeline' do
          expect(pipeline).to eq(most_recent_base_pipeline)
        end
      end

      context 'when the most recent pipeline does not have security reports' do
        before do
          most_recent_base_pipeline.job_artifacts.security_reports.delete_all
        end

        it 'returns the latest base pipeline with security reports' do
          expect(pipeline).to eq(old_base_pipeline)
        end

        context 'when no base pipeline has security reports' do
          before do
            base_pipeline.job_artifacts.security_reports.delete_all
            old_base_pipeline.job_artifacts.security_reports.delete_all
            most_recent_base_pipeline.job_artifacts.security_reports.delete_all
          end

          it 'returns nil' do
            expect(pipeline).to be_nil
          end
        end

        context 'when no base pipeline has completed' do
          before do
            base_pipeline.update!(status: :waiting_for_resource)
            old_base_pipeline.update!(status: :waiting_for_resource)
            most_recent_base_pipeline.update!(status: :waiting_for_resource)
          end

          context 'when comparison prior to pipeline completion is disabled' do
            it 'returns nil' do
              expect(pipeline).to be_nil
            end
          end
        end
      end

      context 'when there are multiple diff start pipelines' do
        let!(:old_diff_start_pipeline) do
          create(
            :ee_ci_pipeline,
            :with_cyclonedx_report,
            project: project,
            ref: merge_request.target_branch,
            sha: merge_request.diff_start_sha,
            created_at: 1.hour.ago
          )
        end

        let!(:most_recent_diff_start_pipeline) do
          create(
            :ee_ci_pipeline,
            :with_cyclonedx_report,
            project: project,
            ref: merge_request.target_branch,
            sha: merge_request.diff_start_sha,
            created_at: Time.current
          )
        end

        let_it_be(:third_diff_start_pipeline) do
          create(
            :ee_ci_pipeline,
            :with_dependency_scanning_report,
            project: project,
            ref: merge_request.target_branch,
            sha: merge_request.diff_start_sha
          )
        end

        context 'when all pipelines have security reports' do
          before do
            allow(old_diff_start_pipeline).to receive(:has_sbom_reports?).and_return(true)
            allow(most_recent_diff_start_pipeline).to receive(:has_sbom_reports?).and_return(true)
          end

          it 'returns the most recent pipeline within the limit' do
            expect(pipeline).to eq(most_recent_diff_start_pipeline)
          end
        end

        context 'when the most recent pipeline does not have security reports' do
          before do
            # Ensure most recent pipeline has no SBOM reports
            most_recent_diff_start_pipeline.job_artifacts.cyclonedx.delete_all

            # Explicitly set has_sbom_reports? behavior
            allow(most_recent_diff_start_pipeline).to receive(:has_sbom_reports?).and_return(false)
            allow(old_diff_start_pipeline).to receive(:has_sbom_reports?).and_return(true)
          end

          it 'returns the latest diff start pipeline with security reports' do
            expect(pipeline).to eq(old_diff_start_pipeline)
          end
        end

        context 'with child pipelines' do
          let!(:child_pipeline) do
            create(
              :ee_ci_pipeline,
              :with_cyclonedx_report,
              project: project,
              ref: merge_request.target_branch
            )
          end

          before do
            create(:ci_sources_pipeline, source_pipeline: most_recent_diff_start_pipeline, pipeline: child_pipeline)

            # Ensure parent pipeline doesn't have SBOM reports directly
            most_recent_diff_start_pipeline.job_artifacts.cyclonedx.delete_all

            # Mock the has_sbom_reports? method to consider child pipeline reports
            allow(most_recent_diff_start_pipeline).to receive(:has_sbom_reports?).and_return(true)
          end

          it 'considers child pipeline reports' do
            expect(pipeline).to eq(most_recent_diff_start_pipeline)
          end
        end
      end
    end

    context 'when there are no merge base and diff start pipelines' do
      before do
        base_pipeline.job_artifacts.security_reports.delete_all
      end

      it 'returns nil' do
        expect(pipeline).to be_nil
      end
    end
  end

  describe '#find_pipeline_with_dependency_scanning_reports' do
    let_it_be(:merge_request) { create(:merge_request) }
    let_it_be(:pipeline_with_ds) { create(:ee_ci_pipeline, :with_dependency_scanning_feature_branch) }
    let_it_be(:pipeline_without_ds) { create(:ee_ci_pipeline) }

    it 'returns the pipeline with dependency_scanning reports' do
      pipelines = [pipeline_without_ds, pipeline_with_ds]

      result = merge_request.find_pipeline_with_dependency_scanning_reports(pipelines)

      expect(result).to eq(pipeline_with_ds)
    end

    it 'returns nil if no pipeline has dependency_scanning reports' do
      pipelines = [pipeline_without_ds]

      result = merge_request.find_pipeline_with_dependency_scanning_reports(pipelines)

      expect(result).to be_nil
    end

    it 'returns the first pipeline with dependency_scanning reports if multiple exist' do
      another_pipeline_with_ds = create(:ee_ci_pipeline, :with_dependency_scanning_feature_branch)
      pipelines = [pipeline_without_ds, pipeline_with_ds, another_pipeline_with_ds]

      result = merge_request.find_pipeline_with_dependency_scanning_reports(pipelines)

      expect(result).to eq(pipeline_with_ds)
    end
  end

  describe '#blocking_merge_requests_feature_available?' do
    let(:merge_request) { build_stubbed(:merge_request) }

    subject(:result) { merge_request.blocking_merge_requests_feature_available? }

    before do
      stub_licensed_features(blocking_merge_requests: blocking_merge_requests_enabled)
    end

    context 'when blocking_merge_requests feature is enabled' do
      let(:blocking_merge_requests_enabled) { true }

      it { is_expected.to eq(true) }
    end

    context 'when blocking_merge_requests feature is disabled' do
      let(:blocking_merge_requests_enabled) { false }

      it { is_expected.to eq(false) }
    end
  end

  describe '#license_scanning_feature_available?' do
    let(:merge_request) { build_stubbed(:merge_request) }

    subject(:result) { merge_request.license_scanning_feature_available? }

    before do
      stub_licensed_features(license_scanning: license_scanning_enabled)
    end

    context 'when license_scanning feature is enabled' do
      let(:license_scanning_enabled) { true }

      it { is_expected.to eq(true) }
    end

    context 'when license_scanning feature is disabled' do
      let(:license_scanning_enabled) { false }

      it { is_expected.to eq(false) }
    end
  end

  describe '#notify_approvers' do
    let_it_be(:merge_request) { create(:merge_request) }
    let_it_be(:approvers) { create_list(:user, 2) }
    let_it_be(:approval_rule) { create(:approval_merge_request_rule, merge_request: merge_request, users: approvers) }

    it 'calls NotificationService.added_as_approver' do
      expect_any_instance_of(EE::NotificationService).to receive(:added_as_approver)
        .with(merge_request.wrapped_approval_rules.flat_map(&:approvers), merge_request)

      merge_request.notify_approvers
    end
  end

  describe '#has_changes_requested?' do
    it { expect(merge_request.has_changes_requested?).to be_falsey }

    describe 'when merge request has changes requested' do
      before do
        create(:merge_request_requested_changes, merge_request: merge_request, project: merge_request.project, user: create(:user))
      end

      it { expect(merge_request.has_changes_requested?).to be_truthy }
    end
  end

  describe '#create_requested_changes' do
    let_it_be(:user) { create(:user) }

    it 'creates a merge request requested changes' do
      expect { merge_request.create_requested_changes(user) }.to change { merge_request.requested_changes.count }.from(0).to(1)
    end
  end

  describe '#destroy_requested_changes' do
    let_it_be(:user) { create(:user) }

    before do
      create(:merge_request_requested_changes, merge_request: merge_request, project: merge_request.project, user: user)
    end

    it 'destroys a merge request requested changes' do
      expect { merge_request.destroy_requested_changes(user) }.to change { merge_request.requested_changes.count }.from(1).to(0)
    end
  end

  describe '#requested_changes_for_users' do
    let_it_be(:user) { create(:user) }
    let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
    let_it_be(:requested_changes) do
      create(:merge_request_requested_changes, merge_request: merge_request, project: merge_request.project, user: user)
    end

    before do
      create(:merge_request_requested_changes, merge_request: merge_request, project: merge_request.project, user: create(:user))
    end

    it 'returns requested changes for user IDs' do
      expect(merge_request.requested_changes_for_users([user.id])).to contain_exactly(
        requested_changes
      )
    end
  end

  describe '#ai_review_merge_request_allowed?' do
    let_it_be(:project) { create(:project) }
    let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
    let_it_be(:current_user) { create(:user, developer_of: project) }

    let(:authorizer) { instance_double(::Gitlab::Llm::FeatureAuthorizer) }

    subject(:ai_review_merge_request_allowed?) { merge_request.ai_review_merge_request_allowed?(current_user) }

    before do
      stub_licensed_features(review_merge_request: true)
      allow(::Gitlab::Llm::FeatureAuthorizer).to receive(:new).and_return(authorizer)
    end

    context 'when user does not have permission' do
      before do
        allow(project).to receive(:duo_features_enabled).and_return(false)
        allow(current_user).to receive(:allowed_to_use?)
          .with(:review_merge_request, licensed_feature: :review_merge_request).and_return(false)
      end

      it { is_expected.to eq(false) }
    end

    context "when feature is authorized" do
      before do
        allow(authorizer).to receive(:allowed?).and_return(true)
        allow(project).to receive(:duo_features_enabled).and_return(true)
        allow(current_user).to receive(:allowed_to_use?)
          .with(:review_merge_request, licensed_feature: :review_merge_request).and_return(true)
      end

      it { is_expected.to eq(true) }

      context 'when license is not set' do
        before do
          stub_licensed_features(review_merge_request: false)
          allow(current_user).to receive(:allowed_to_use?)
            .with(:review_merge_request, licensed_feature: :review_merge_request).and_return(false)
        end

        it { is_expected.to eq(false) }
      end

      context 'when user cannot create note' do
        let(:current_user) { create(:user, guest_of: project) }

        it { is_expected.to eq(false) }
      end
    end

    context "when feature is not authorized" do
      before do
        allow(authorizer).to receive(:allowed?).and_return(false)
      end

      it { is_expected.to eq(false) }
    end
  end

  describe '#temporarily_unapproved?' do
    subject(:temporarily_unapproved) { merge_request.temporarily_unapproved? }

    let(:merge_request) { create(:merge_request) }

    context 'when the MR is not temporarily unapproved' do
      it 'returns false' do
        expect(temporarily_unapproved).to eq(false)
      end
    end

    context 'when the MR is temporarily unapproved' do
      before do
        merge_request.approval_state.temporarily_unapprove!
      end

      it 'returns true' do
        expect(temporarily_unapproved).to eq(true)
      end
    end
  end

  describe '#ai_reviewable_diff_files' do
    let(:merge_request) { build_stubbed(:merge_request) }
    let(:diff_file_a) { instance_double(Gitlab::Diff::File, ai_reviewable?: true) }
    let(:diff_file_b) { instance_double(Gitlab::Diff::File, ai_reviewable?: false) }
    let(:diff_file_c) { instance_double(Gitlab::Diff::File, ai_reviewable?: true) }
    let(:diff_files) { [diff_file_a, diff_file_b, diff_file_c] }
    let(:diffs) { instance_double(Gitlab::Diff::FileCollection::MergeRequestDiff, diff_files: diff_files) }

    before do
      allow(merge_request).to receive(:diffs).and_return(diffs)
    end

    it 'returns only AI reviewable diff files' do
      expect(merge_request.ai_reviewable_diff_files).to match_array([diff_file_a, diff_file_c])
    end
  end

  describe '#squash_option' do
    let_it_be(:target_branch) { 'target_branch' }

    let(:merge_request) { build(:merge_request, project: project, target_branch: target_branch) }
    let(:project_setting) { project.project_setting }

    subject { merge_request.squash_option }

    context 'when the target branch matches a wildcard protected branch' do
      let_it_be_with_refind(:protected_branch) { create(:protected_branch, name: '*', project_id: project.id) }

      it { is_expected.to eq(project_setting) }
    end

    context 'when the target branch exact matches a protected branch' do
      let_it_be_with_refind(:protected_branch) { create(:protected_branch, name: target_branch, project_id: project.id) }

      context 'and the protected branch has a defined squash_option' do
        let_it_be(:squash_option) { create(:branch_rule_squash_option, protected_branch: protected_branch, project: project) }

        it { is_expected.to eq(squash_option) }
      end

      context 'and the protected branch does not have a squash_option' do
        it { is_expected.to eq(project_setting) }
      end
    end

    it { is_expected.to eq(project_setting) }
  end

  describe '#missing_required_squash?' do
    using RSpec::Parameterized::TableSyntax

    context 'when the target branch is not protected' do
      where(:squash, :require_squash, :expected) do
        false | true  | true
        false | false | false
        true  | true  | false
        true  | false | false
      end

      with_them do
        let(:merge_request) { build_stubbed(:merge_request, squash: squash, project: project) }

        subject { merge_request.missing_required_squash? }

        before do
          allow(project.project_setting).to receive(:squash_always?).and_return(require_squash)
        end

        it { is_expected.to eq(expected) }
      end
    end

    context 'when the target branch is protected' do
      let(:protected_branch) { create(:protected_branch, name: merge_request.target_branch, project: project) }

      where(:squash, :require_squash, :expected) do
        false | true  | true
        false | false | false
        true  | true  | false
        true  | false | false
      end

      with_them do
        let(:merge_request) { build_stubbed(:merge_request, squash: squash, project: project) }

        subject { merge_request.missing_required_squash? }

        before do
          allow(project.project_setting).to receive(:squash_always?).and_return(require_squash)
        end

        it { is_expected.to eq(expected) }
      end

      context 'when the target branch is protected and specifies a squash option' do
        let(:squash_option) { create(:branch_rule_squash_option, protected_branch: protected_branch, project: project) }

        where(:squash, :project_require_squash, :require_squash, :expected) do
          false | false | true  | true
          false | true  | false | false
          true  | false | true  | false
          true  | true  | false | false
        end

        with_them do
          let(:merge_request) { build_stubbed(:merge_request, squash: squash, project: project) }

          subject { merge_request.missing_required_squash? }

          before do
            allow(project.project_setting).to receive(:squash_always?).and_return(project_require_squash)
            allow(merge_request).to receive(:squash_option).and_return(squash_option)
            allow(squash_option).to receive(:squash_always?).and_return(require_squash)
          end

          it { is_expected.to eq(expected) }
        end
      end
    end
  end

  describe '#schedule_policy_synchronization' do
    subject(:execute) { merge_request.schedule_policy_synchronization }

    it_behaves_like 'synchronizes policies for a merge request'
  end
end
