# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
  include ProjectForksHelper
  include StubRequests
  include Ci::SourcePipelineHelpers

  let_it_be(:user) { create(:user, :public_email) }
  let_it_be(:namespace) { create_default(:namespace).freeze }
  let_it_be(:project) { create_default(:project, :repository).freeze }

  it 'paginates 15 pipeleines per page' do
    expect(described_class.default_per_page).to eq(15)
  end

  it_behaves_like 'having unique enum values'

  it { is_expected.to belong_to(:project) }
  it { is_expected.to belong_to(:user) }
  it { is_expected.to belong_to(:auto_canceled_by) }
  it { is_expected.to belong_to(:pipeline_schedule) }
  it { is_expected.to belong_to(:merge_request) }
  it { is_expected.to belong_to(:external_pull_request) }

  it { is_expected.to have_many(:statuses) }
  it { is_expected.to have_many(:trigger_requests) }
  it { is_expected.to have_many(:variables) }
  it { is_expected.to have_many(:builds) }
  it { is_expected.to have_many(:bridges) }
  it { is_expected.to have_many(:job_artifacts).through(:builds) }
  it { is_expected.to have_many(:auto_canceled_pipelines) }
  it { is_expected.to have_many(:auto_canceled_jobs) }
  it { is_expected.to have_many(:sourced_pipelines) }
  it { is_expected.to have_many(:triggered_pipelines) }
  it { is_expected.to have_many(:pipeline_artifacts) }

  it { is_expected.to have_one(:chat_data) }
  it { is_expected.to have_one(:source_pipeline) }
  it { is_expected.to have_one(:triggered_by_pipeline) }
  it { is_expected.to have_one(:source_job) }
  it { is_expected.to have_one(:pipeline_config) }

  it { is_expected.to respond_to :git_author_name }
  it { is_expected.to respond_to :git_author_email }
  it { is_expected.to respond_to :git_author_full_text }
  it { is_expected.to respond_to :short_sha }
  it { is_expected.to delegate_method(:full_path).to(:project).with_prefix }

  describe 'validations' do
    it { is_expected.to validate_presence_of(:sha) }
    it { is_expected.to validate_presence_of(:status) }
  end

  describe 'associations' do
    let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }

    it 'has a bidirectional relationship with projects' do
      expect(described_class.reflect_on_association(:project).has_inverse?).to eq(:all_pipelines)
      expect(Project.reflect_on_association(:all_pipelines).has_inverse?).to eq(:project)
      expect(Project.reflect_on_association(:ci_pipelines).has_inverse?).to eq(:project)
    end

    describe '#latest_builds' do
      it 'has a one to many relationship with its latest builds' do
        _old_build = create(:ci_build, :retried, pipeline: pipeline)
        latest_build = create(:ci_build, :expired, pipeline: pipeline)

        expect(pipeline.latest_builds).to contain_exactly(latest_build)
      end
    end

    describe '#downloadable_artifacts' do
      let_it_be(:build) { create(:ci_build, pipeline: pipeline) }
      let_it_be(:downloadable_artifact) { create(:ci_job_artifact, :codequality, job: build) }
      let_it_be(:expired_artifact) { create(:ci_job_artifact, :junit, :expired, job: build) }
      let_it_be(:undownloadable_artifact) { create(:ci_job_artifact, :trace, job: build) }

      context 'when artifacts are locked' do
        it 'returns downloadable artifacts including locked artifacts' do
          expect(pipeline.downloadable_artifacts).to contain_exactly(downloadable_artifact, expired_artifact)
        end
      end

      context 'when artifacts are unlocked' do
        it 'returns only downloadable artifacts not expired' do
          expired_artifact.job.pipeline.unlocked!

          expect(pipeline.reload.downloadable_artifacts).to contain_exactly(downloadable_artifact)
        end
      end
    end
  end

  describe '#set_status' do
    let(:pipeline) { build(:ci_empty_pipeline, :created) }

    where(:from_status, :to_status) do
      from_status_names = described_class.state_machines[:status].states.map(&:name)
      to_status_names = from_status_names - [:created] # we never want to transition into created

      from_status_names.product(to_status_names)
    end

    with_them do
      it do
        pipeline.status = from_status.to_s

        if from_status != to_status
          expect(pipeline.set_status(to_status.to_s))
            .to eq(true)
        else
          expect(pipeline.set_status(to_status.to_s))
            .to eq(false), "loopback transitions are not allowed"
        end
      end
    end
  end

  describe '.processables' do
    let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }

    before do
      create(:ci_build, name: 'build', pipeline: pipeline)
      create(:ci_bridge, name: 'bridge', pipeline: pipeline)
      create(:commit_status, name: 'commit status', pipeline: pipeline)
      create(:generic_commit_status, name: 'generic status', pipeline: pipeline)
    end

    it 'has an association with processable CI/CD entities' do
      pipeline.processables.pluck('name').yield_self do |processables|
        expect(processables).to match_array %w[build bridge]
      end
    end

    it 'makes it possible to append a new processable' do
      pipeline.processables << build(:ci_bridge)

      pipeline.save!

      expect(pipeline.processables.reload.count).to eq 3
    end
  end

  describe '.for_iid' do
    subject { described_class.for_iid(iid) }

    let(:iid) { '1234' }
    let!(:pipeline) { create(:ci_pipeline, iid: '1234') }

    it 'returns the pipeline' do
      is_expected.to contain_exactly(pipeline)
    end
  end

  describe '.for_sha' do
    subject { described_class.for_sha(sha) }

    let(:sha) { 'abc' }

    let_it_be(:pipeline) { create(:ci_pipeline, sha: 'abc') }

    it 'returns the pipeline' do
      is_expected.to contain_exactly(pipeline)
    end

    context 'when argument is array' do
      let(:sha) { %w[abc def] }
      let!(:pipeline_2) { create(:ci_pipeline, sha: 'def') }

      it 'returns the pipelines' do
        is_expected.to contain_exactly(pipeline, pipeline_2)
      end
    end

    context 'when sha is empty' do
      let(:sha) { nil }

      it 'does not return anything' do
        is_expected.to be_empty
      end
    end
  end

  describe '.for_source_sha' do
    subject { described_class.for_source_sha(source_sha) }

    let(:source_sha) { 'abc' }

    let_it_be(:pipeline) { create(:ci_pipeline, source_sha: 'abc') }

    it 'returns the pipeline' do
      is_expected.to contain_exactly(pipeline)
    end

    context 'when argument is array' do
      let(:source_sha) { %w[abc def] }
      let!(:pipeline_2) { create(:ci_pipeline, source_sha: 'def') }

      it 'returns the pipelines' do
        is_expected.to contain_exactly(pipeline, pipeline_2)
      end
    end

    context 'when source_sha is empty' do
      let(:source_sha) { nil }

      it 'does not return anything' do
        is_expected.to be_empty
      end
    end
  end

  describe '.for_sha_or_source_sha' do
    subject { described_class.for_sha_or_source_sha(sha) }

    let(:sha) { 'abc' }

    context 'when sha is matched' do
      let!(:pipeline) { create(:ci_pipeline, sha: sha) }

      it 'returns the pipeline' do
        is_expected.to contain_exactly(pipeline)
      end
    end

    context 'when source sha is matched' do
      let!(:pipeline) { create(:ci_pipeline, source_sha: sha) }

      it 'returns the pipeline' do
        is_expected.to contain_exactly(pipeline)
      end
    end

    context 'when both sha and source sha are not matched' do
      let!(:pipeline) { create(:ci_pipeline, sha: 'bcd', source_sha: 'bcd') }

      it 'does not return anything' do
        is_expected.to be_empty
      end
    end
  end

  describe '.for_branch' do
    subject { described_class.for_branch(branch) }

    let(:branch) { 'master' }

    let_it_be(:pipeline) { create(:ci_pipeline, ref: 'master') }

    it 'returns the pipeline' do
      is_expected.to contain_exactly(pipeline)
    end

    context 'with tag pipeline' do
      let(:branch) { 'v1.0' }
      let!(:pipeline) { create(:ci_pipeline, ref: 'v1.0', tag: true) }

      it 'returns nothing' do
        is_expected.to be_empty
      end
    end
  end

  describe '.with_pipeline_source' do
    subject { described_class.with_pipeline_source(source) }

    let(:source) { 'web' }

    let_it_be(:push_pipeline)   { create(:ci_pipeline, source: :push) }
    let_it_be(:web_pipeline)    { create(:ci_pipeline, source: :web) }
    let_it_be(:api_pipeline)    { create(:ci_pipeline, source: :api) }

    it 'contains pipelines created due to specified source' do
      expect(subject).to contain_exactly(web_pipeline)
    end
  end

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

    let(:push_pipeline)   { build(:ci_pipeline, source: :push) }
    let(:web_pipeline)    { build(:ci_pipeline, source: :web) }
    let(:api_pipeline)    { build(:ci_pipeline, source: :api) }
    let(:webide_pipeline) { build(:ci_pipeline, source: :webide) }
    let(:child_pipeline)  { build(:ci_pipeline, source: :parent_pipeline) }
    let(:pipelines) { [push_pipeline, web_pipeline, api_pipeline, webide_pipeline, child_pipeline] }

    it 'contains pipelines having CI only sources' do
      pipelines.map(&:save!)

      expect(subject).to contain_exactly(push_pipeline, web_pipeline, api_pipeline)
    end

    it 'filters on expected sources' do
      expect(::Enums::Ci::Pipeline.ci_sources.keys).to contain_exactly(
        *%i[unknown push web trigger schedule api external pipeline chat
            merge_request_event external_pull_request_event])
    end
  end

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

    let_it_be(:push_pipeline)   { create(:ci_pipeline, source: :push) }
    let_it_be(:web_pipeline)    { create(:ci_pipeline, source: :web) }
    let_it_be(:api_pipeline)    { create(:ci_pipeline, source: :api) }
    let_it_be(:webide_pipeline) { create(:ci_pipeline, source: :webide) }
    let_it_be(:child_pipeline)  { create(:ci_pipeline, source: :parent_pipeline) }
    let_it_be(:merge_request_pipeline) { create(:ci_pipeline, :detached_merge_request_pipeline) }

    it 'contains pipelines having CI only sources' do
      expect(subject).to contain_exactly(push_pipeline, web_pipeline, api_pipeline)
    end

    it 'filters on expected sources' do
      expect(::Enums::Ci::Pipeline.ci_branch_sources.keys).to contain_exactly(
        *%i[unknown push web trigger schedule api external pipeline chat
            external_pull_request_event])
    end
  end

  describe '.outside_pipeline_family' do
    subject(:outside_pipeline_family) { described_class.outside_pipeline_family(upstream_pipeline) }

    let(:upstream_pipeline) { create(:ci_pipeline, project: project) }
    let(:child_pipeline) { create(:ci_pipeline, project: project) }

    let!(:other_pipeline) { create(:ci_pipeline, project: project) }

    before do
      create(:ci_sources_pipeline,
             source_job: create(:ci_build, pipeline: upstream_pipeline),
             source_project: project,
             pipeline: child_pipeline,
             project: project)
    end

    it 'only returns pipelines outside pipeline family' do
      expect(outside_pipeline_family).to contain_exactly(other_pipeline)
    end
  end

  describe '.before_pipeline' do
    subject(:before_pipeline) { described_class.before_pipeline(child_pipeline) }

    let!(:older_other_pipeline) { create(:ci_pipeline, project: project) }

    let!(:upstream_pipeline) { create(:ci_pipeline, project: project) }
    let!(:child_pipeline) { create(:ci_pipeline, child_of: upstream_pipeline) }

    let!(:other_pipeline) { create(:ci_pipeline, project: project) }

    before do
      create(:ci_sources_pipeline,
             source_job: create(:ci_build, pipeline: upstream_pipeline),
             source_project: project,
             pipeline: child_pipeline,
             project: project)
    end

    it 'only returns older pipelines outside pipeline family' do
      expect(before_pipeline).to contain_exactly(older_other_pipeline)
    end
  end

  describe '#merge_request?' do
    let(:pipeline) { create(:ci_pipeline, merge_request: merge_request) }
    let(:merge_request) { create(:merge_request) }

    it 'returns true' do
      expect(pipeline).to be_merge_request
    end

    context 'when merge request is nil' do
      let(:merge_request) { nil }

      it 'returns false' do
        expect(pipeline).not_to be_merge_request
      end
    end
  end

  describe '#detached_merge_request_pipeline?' do
    subject { pipeline.detached_merge_request_pipeline? }

    let!(:pipeline) do
      create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, target_sha: target_sha)
    end

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

    it { is_expected.to be_truthy }

    context 'when target sha exists' do
      let(:target_sha) { merge_request.target_branch_sha }

      it { is_expected.to be_falsy }
    end
  end

  describe '#merged_result_pipeline?' do
    subject { pipeline.merged_result_pipeline? }

    let!(:pipeline) do
      create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, target_sha: target_sha)
    end

    let(:merge_request) { create(:merge_request) }
    let(:target_sha) { merge_request.target_branch_sha }

    it { is_expected.to be_truthy }

    context 'when target sha is empty' do
      let(:target_sha) { nil }

      it { is_expected.to be_falsy }
    end
  end

  describe '#merge_request_ref?' do
    subject { pipeline.merge_request_ref? }

    let(:pipeline) { build(:ci_empty_pipeline, :created) }

    it 'calls MergeRequest#merge_request_ref?' do
      expect(MergeRequest).to receive(:merge_request_ref?).with(pipeline.ref)

      subject
    end
  end

  describe '#merge_request_event_type' do
    subject { pipeline.merge_request_event_type }

    let(:pipeline) { merge_request.all_pipelines.last }

    context 'when pipeline is merge request pipeline' do
      let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) }

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

    context 'when pipeline is detached merge request pipeline' do
      let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }

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

  describe '#legacy_detached_merge_request_pipeline?' do
    subject { pipeline.legacy_detached_merge_request_pipeline? }

    let_it_be(:merge_request) { create(:merge_request) }

    let(:ref) { 'feature' }
    let(:target_sha) { nil }

    let(:pipeline) do
      build(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: ref, target_sha: target_sha)
    end

    it { is_expected.to be_truthy }

    context 'when pipeline ref is a merge request ref' do
      let(:ref) { 'refs/merge-requests/1/head' }

      it { is_expected.to be_falsy }
    end

    context 'when target sha is set' do
      let(:target_sha) { 'target-sha' }

      it { is_expected.to be_falsy }
    end
  end

  describe '#matches_sha_or_source_sha?' do
    subject { pipeline.matches_sha_or_source_sha?(sample_sha) }

    let(:sample_sha) { Digest::SHA1.hexdigest(SecureRandom.hex) }

    context 'when sha matches' do
      let(:pipeline) { build(:ci_pipeline, sha: sample_sha) }

      it { is_expected.to be_truthy }
    end

    context 'when source_sha matches' do
      let(:pipeline) { build(:ci_pipeline, source_sha: sample_sha) }

      it { is_expected.to be_truthy }
    end

    context 'when both sha and source_sha do not matche' do
      let(:pipeline) { build(:ci_pipeline, sha: 'test', source_sha: 'test') }

      it { is_expected.to be_falsy }
    end
  end

  describe '#source_ref' do
    subject { pipeline.source_ref }

    let(:pipeline) { create(:ci_pipeline, ref: 'feature') }

    it 'returns source ref' do
      is_expected.to eq('feature')
    end

    context 'when the pipeline is a detached merge request pipeline' do
      let(:merge_request) { create(:merge_request) }

      let(:pipeline) do
        create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: merge_request.ref_path)
      end

      it 'returns source ref' do
        is_expected.to eq(merge_request.source_branch)
      end
    end
  end

  describe '#source_ref_slug' do
    subject { pipeline.source_ref_slug }

    let(:pipeline) { create(:ci_pipeline, ref: 'feature') }

    it 'slugifies with the source ref' do
      expect(Gitlab::Utils).to receive(:slugify).with('feature')

      subject
    end

    context 'when the pipeline is a detached merge request pipeline' do
      let(:merge_request) { create(:merge_request) }

      let(:pipeline) do
        create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: merge_request.ref_path)
      end

      it 'slugifies with the source ref of the merge request' do
        expect(Gitlab::Utils).to receive(:slugify).with(merge_request.source_branch)

        subject
      end
    end
  end

  describe '.with_reports' do
    context 'when pipeline has a test report' do
      subject { described_class.with_reports(Ci::JobArtifact.test_reports) }

      let!(:pipeline_with_report) { create(:ci_pipeline, :with_test_reports) }

      it 'selects the pipeline' do
        is_expected.to eq([pipeline_with_report])
      end
    end

    context 'when pipeline has a coverage report' do
      subject { described_class.with_reports(Ci::JobArtifact.coverage_reports) }

      let!(:pipeline_with_report) { create(:ci_pipeline, :with_coverage_reports) }

      it 'selects the pipeline' do
        is_expected.to eq([pipeline_with_report])
      end
    end

    context 'when pipeline has an accessibility report' do
      subject { described_class.with_reports(Ci::JobArtifact.accessibility_reports) }

      let(:pipeline_with_report) { create(:ci_pipeline, :with_accessibility_reports) }

      it 'selects the pipeline' do
        is_expected.to eq([pipeline_with_report])
      end
    end

    context 'when pipeline has a codequality report' do
      subject { described_class.with_reports(Ci::JobArtifact.codequality_reports) }

      let(:pipeline_with_report) { create(:ci_pipeline, :with_codequality_reports) }

      it 'selects the pipeline' do
        is_expected.to eq([pipeline_with_report])
      end
    end

    context 'when pipeline has a terraform report' do
      it 'selects the pipeline' do
        pipeline_with_report = create(:ci_pipeline, :with_terraform_reports)

        expect(described_class.with_reports(Ci::JobArtifact.terraform_reports)).to eq(
          [pipeline_with_report]
        )
      end
    end

    context 'when pipeline does not have metrics reports' do
      subject { described_class.with_reports(Ci::JobArtifact.test_reports) }

      let!(:pipeline_without_report) { create(:ci_empty_pipeline) }

      it 'does not select the pipeline' do
        is_expected.to be_empty
      end
    end
  end

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

    context 'when there is a merge request pipeline' do
      let!(:pipeline) { create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request) }
      let(:merge_request) { create(:merge_request) }

      it 'returns merge request pipeline first' do
        expect(subject).to eq([pipeline])
      end
    end

    context 'when there are no merge request pipelines' do
      let!(:pipeline) { create(:ci_pipeline, source: :push) }

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

  describe 'modules' do
    it_behaves_like 'AtomicInternalId', validate_presence: false do
      let(:internal_id_attribute) { :iid }
      let(:instance) { build(:ci_pipeline) }
      let(:scope) { :project }
      let(:scope_attrs) { { project: instance.project } }
      let(:usage) { :ci_pipelines }
    end
  end

  describe '#source' do
    context 'when creating new pipeline' do
      let(:pipeline) do
        build(:ci_empty_pipeline, :created, project: project, source: nil)
      end

      it "prevents from creating an object" do
        expect(pipeline).not_to be_valid
      end
    end

    context 'when updating existing pipeline' do
      let(:pipeline) { create(:ci_empty_pipeline, :created) }

      before do
        pipeline.update_attribute(:source, nil)
      end

      it 'object is valid' do
        expect(pipeline).to be_valid
      end
    end
  end

  describe '#block' do
    let(:pipeline) { create(:ci_empty_pipeline, :created) }

    it 'changes pipeline status to manual' do
      expect(pipeline.block).to be true
      expect(pipeline.reload).to be_manual
      expect(pipeline.reload).to be_blocked
    end
  end

  describe '#delay' do
    subject { pipeline.delay }

    let(:pipeline) { build(:ci_pipeline, :created) }

    it 'changes pipeline status to schedule' do
      subject

      expect(pipeline).to be_scheduled
    end
  end

  describe '#valid_commit_sha' do
    let(:pipeline) { build_stubbed(:ci_empty_pipeline, :created, project: project) }

    context 'commit.sha can not start with 00000000' do
      before do
        pipeline.sha = '0' * 40
        pipeline.valid_commit_sha
      end

      it('commit errors should not be empty') { expect(pipeline.errors).not_to be_empty }
    end
  end

  describe '#short_sha' do
    subject { pipeline.short_sha }

    let(:pipeline) { build_stubbed(:ci_empty_pipeline, :created) }

    it 'has 8 items' do
      expect(subject.size).to eq(8)
    end
    it { expect(pipeline.sha).to start_with(subject) }
  end

  describe '#retried' do
    subject { pipeline.retried }

    let(:pipeline) { create(:ci_empty_pipeline, :created, project: project) }
    let!(:build1) { create(:ci_build, pipeline: pipeline, name: 'deploy', retried: true) }

    before do
      create(:ci_build, pipeline: pipeline, name: 'deploy')
    end

    it 'returns old builds' do
      is_expected.to contain_exactly(build1)
    end
  end

  describe '#coverage' do
    let_it_be_with_reload(:pipeline) { create(:ci_empty_pipeline) }

    context 'with multiple pipelines' do
      before_all do
        create(:ci_build, name: "rspec", coverage: 30, pipeline: pipeline)
        create(:ci_build, name: "rubocop", coverage: 40, pipeline: pipeline)
      end

      it "calculates average when there are two builds with coverage" do
        expect(pipeline.coverage).to eq("35.00")
      end

      it "calculates average when there are two builds with coverage and one with nil" do
        create(:ci_build, pipeline: pipeline)

        expect(pipeline.coverage).to eq("35.00")
      end

      it "calculates average when there are two builds with coverage and one is retried" do
        create(:ci_build, name: "rubocop", coverage: 30, pipeline: pipeline, retried: true)

        expect(pipeline.coverage).to eq("35.00")
      end
    end

    context 'when there is one build without coverage' do
      it "calculates average to nil" do
        create(:ci_build, pipeline: pipeline)

        expect(pipeline.coverage).to be_nil
      end
    end
  end

  describe '#update_builds_coverage' do
    let_it_be(:pipeline) { create(:ci_empty_pipeline) }

    context 'builds with coverage_regex defined' do
      let!(:build_1) { create(:ci_build, :success, :trace_with_coverage, trace_coverage: 60.0, pipeline: pipeline) }
      let!(:build_2) { create(:ci_build, :success, :trace_with_coverage, trace_coverage: 80.0, pipeline: pipeline) }

      it 'updates the coverage value of each build from the trace' do
        pipeline.update_builds_coverage

        expect(build_1.reload.coverage).to eq(60.0)
        expect(build_2.reload.coverage).to eq(80.0)
      end
    end

    context 'builds without coverage_regex defined' do
      let!(:build) { create(:ci_build, :success, :trace_with_coverage, coverage_regex: nil, trace_coverage: 60.0, pipeline: pipeline) }

      it 'does not update the coverage value of each build from the trace' do
        pipeline.update_builds_coverage

        expect(build.reload.coverage).to eq(nil)
      end
    end

    context 'builds with coverage values already present' do
      let!(:build) { create(:ci_build, :success, :trace_with_coverage, trace_coverage: 60.0, coverage: 10.0, pipeline: pipeline) }

      it 'does not update the coverage value of each build from the trace' do
        pipeline.update_builds_coverage

        expect(build.reload.coverage).to eq(10.0)
      end
    end
  end

  describe '#retryable?' do
    subject { pipeline.retryable? }

    let_it_be(:pipeline) { create(:ci_empty_pipeline, :created, project: project) }

    context 'no failed builds' do
      before do
        create_build('rspec', 'success')
      end

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

      context 'one canceled job' do
        before do
          create_build('rubocop', 'canceled')
        end

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

    context 'with failed builds' do
      before do
        create_build('rspec', 'running')
        create_build('rubocop', 'failed')
      end

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

    def create_build(name, status)
      create(:ci_build, name: name, status: status, pipeline: pipeline)
    end
  end

  describe '#persisted_variables' do
    context 'when pipeline is not persisted yet' do
      subject { build(:ci_pipeline).persisted_variables }

      it 'does not contain some variables' do
        keys = subject.map { |variable| variable[:key] }

        expect(keys).not_to include 'CI_PIPELINE_ID'
      end
    end

    context 'when pipeline is persisted' do
      subject { build_stubbed(:ci_pipeline).persisted_variables }

      it 'does contains persisted variables' do
        keys = subject.map { |variable| variable[:key] }

        expect(keys).to eq %w[CI_PIPELINE_ID CI_PIPELINE_URL]
      end
    end
  end

  describe '#predefined_variables' do
    subject { pipeline.predefined_variables }

    let(:pipeline) { build(:ci_empty_pipeline, :created) }

    it 'includes all predefined variables in a valid order' do
      keys = subject.map { |variable| variable[:key] }

      expect(keys).to eq %w[
        CI_PIPELINE_IID
        CI_PIPELINE_SOURCE
        CI_PIPELINE_CREATED_AT
        CI_COMMIT_SHA
        CI_COMMIT_SHORT_SHA
        CI_COMMIT_BEFORE_SHA
        CI_COMMIT_REF_NAME
        CI_COMMIT_REF_SLUG
        CI_COMMIT_BRANCH
        CI_COMMIT_MESSAGE
        CI_COMMIT_TITLE
        CI_COMMIT_DESCRIPTION
        CI_COMMIT_REF_PROTECTED
        CI_COMMIT_TIMESTAMP
        CI_COMMIT_AUTHOR
        CI_BUILD_REF
        CI_BUILD_BEFORE_SHA
        CI_BUILD_REF_NAME
        CI_BUILD_REF_SLUG
      ]
    end

    context 'when merge request is present' do
      let_it_be(:assignees) { create_list(:user, 2) }
      let_it_be(:milestone) { create(:milestone, project: project) }
      let_it_be(:labels) { create_list(:label, 2) }

      let(:merge_request) do
        create(:merge_request, :simple,
               source_project: project,
               target_project: project,
               assignees: assignees,
               milestone: milestone,
               labels: labels)
      end

      context 'when pipeline for merge request is created' do
        let(:pipeline) do
          create(:ci_pipeline, :detached_merge_request_pipeline,
            ci_ref_presence: false,
            user: user,
            merge_request: merge_request)
        end

        before do
          project.add_developer(user)
        end

        it 'exposes merge request pipeline variables' do
          expect(subject.to_hash)
            .to include(
              'CI_MERGE_REQUEST_ID' => merge_request.id.to_s,
              'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s,
              'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s,
              'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s,
              'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path,
              'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url,
              'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s,
              'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => '',
              'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s,
              'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path,
              'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url,
              'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s,
              'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => '',
              'CI_MERGE_REQUEST_TITLE' => merge_request.title,
              'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
              'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
              'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','),
              'CI_MERGE_REQUEST_EVENT_TYPE' => 'detached',
              'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true))
        end

        it 'exposes diff variables' do
          expect(subject.to_hash)
            .to include(
              'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s,
              'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha)
        end

        context 'without assignee' do
          let(:assignees) { [] }

          it 'does not expose assignee variable' do
            expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES')
          end
        end

        context 'without milestone' do
          let(:milestone) { nil }

          it 'does not expose milestone variable' do
            expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_MILESTONE')
          end
        end

        context 'without labels' do
          let(:labels) { [] }

          it 'does not expose labels variable' do
            expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_LABELS')
          end
        end
      end

      context 'when pipeline on branch is created' do
        let(:pipeline) do
          create(:ci_pipeline, project: project, user: user, ref: 'feature')
        end

        context 'when a merge request is created' do
          before do
            merge_request
          end

          context 'when user has access to project' do
            before do
              project.add_developer(user)
            end

            it 'merge request references are returned matching the pipeline' do
              expect(subject.to_hash).to include(
                'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true))
            end
          end

          context 'when user does not have access to project' do
            it 'CI_OPEN_MERGE_REQUESTS is not returned' do
              expect(subject.to_hash).not_to have_key('CI_OPEN_MERGE_REQUESTS')
            end
          end
        end

        context 'when no a merge request is created' do
          it 'CI_OPEN_MERGE_REQUESTS is not returned' do
            expect(subject.to_hash).not_to have_key('CI_OPEN_MERGE_REQUESTS')
          end
        end
      end

      context 'with merged results' do
        let(:pipeline) do
          create(:ci_pipeline, :merged_result_pipeline, merge_request: merge_request)
        end

        it 'exposes merge request pipeline variables' do
          expect(subject.to_hash)
            .to include(
              'CI_MERGE_REQUEST_ID' => merge_request.id.to_s,
              'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s,
              'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s,
              'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s,
              'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path,
              'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url,
              'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s,
              'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => merge_request.target_branch_sha,
              'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s,
              'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path,
              'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url,
              'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s,
              'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => merge_request.source_branch_sha,
              'CI_MERGE_REQUEST_TITLE' => merge_request.title,
              'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
              'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
              'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','),
              'CI_MERGE_REQUEST_EVENT_TYPE' => 'merged_result')
        end

        it 'exposes diff variables' do
          expect(subject.to_hash)
            .to include(
              'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s,
              'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha)
        end
      end
    end

    context 'when source is external pull request' do
      let(:pipeline) do
        create(:ci_pipeline, source: :external_pull_request_event, external_pull_request: pull_request)
      end

      let(:pull_request) { create(:external_pull_request, project: project) }

      it 'exposes external pull request pipeline variables' do
        expect(subject.to_hash)
          .to include(
            'CI_EXTERNAL_PULL_REQUEST_IID' => pull_request.pull_request_iid.to_s,
            'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY' => pull_request.source_repository,
            'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY' => pull_request.target_repository,
            'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA' => pull_request.source_sha,
            'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA' => pull_request.target_sha,
            'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME' => pull_request.source_branch,
            'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME' => pull_request.target_branch
          )
      end
    end

    describe 'variable CI_KUBERNETES_ACTIVE' do
      context 'when pipeline.has_kubernetes_active? is true' do
        before do
          allow(pipeline).to receive(:has_kubernetes_active?).and_return(true)
        end

        it "is included with value 'true'" do
          expect(subject.to_hash).to include('CI_KUBERNETES_ACTIVE' => 'true')
        end
      end

      context 'when pipeline.has_kubernetes_active? is false' do
        before do
          allow(pipeline).to receive(:has_kubernetes_active?).and_return(false)
        end

        it 'is not included' do
          expect(subject.to_hash).not_to have_key('CI_KUBERNETES_ACTIVE')
        end
      end
    end
  end

  describe '#protected_ref?' do
    let(:pipeline) { build(:ci_empty_pipeline, :created) }

    it 'delegates method to project' do
      expect(pipeline).not_to be_protected_ref
    end
  end

  describe '#legacy_trigger' do
    let(:trigger_request) { build(:ci_trigger_request) }
    let(:pipeline) { build(:ci_empty_pipeline, :created, trigger_requests: [trigger_request]) }

    it 'returns first trigger request' do
      expect(pipeline.legacy_trigger).to eq trigger_request
    end
  end

  describe '#auto_canceled?' do
    subject { pipeline.auto_canceled? }

    let(:pipeline) { build(:ci_empty_pipeline, :created) }

    context 'when it is canceled' do
      before do
        pipeline.cancel
      end

      context 'when there is auto_canceled_by' do
        before do
          pipeline.auto_canceled_by = create(:ci_empty_pipeline)
        end

        it 'is auto canceled' do
          is_expected.to be_truthy
        end
      end

      context 'when there is no auto_canceled_by' do
        it 'is not auto canceled' do
          is_expected.to be_falsey
        end
      end

      context 'when it is retried and canceled manually' do
        before do
          pipeline.enqueue
          pipeline.cancel
        end

        it 'is not auto canceled' do
          is_expected.to be_falsey
        end
      end
    end
  end

  describe 'pipeline stages' do
    let(:pipeline) { build(:ci_empty_pipeline, :created) }

    describe 'legacy stages' do
      before do
        create(:commit_status, pipeline: pipeline,
                               stage: 'build',
                               name: 'linux',
                               stage_idx: 0,
                               status: 'success')

        create(:commit_status, pipeline: pipeline,
                               stage: 'build',
                               name: 'mac',
                               stage_idx: 0,
                               status: 'failed')

        create(:commit_status, pipeline: pipeline,
                               stage: 'deploy',
                               name: 'staging',
                               stage_idx: 2,
                               status: 'running')

        create(:commit_status, pipeline: pipeline,
                               stage: 'test',
                               name: 'rspec',
                               stage_idx: 1,
                               status: 'success')
      end

      describe '#legacy_stages' do
        using RSpec::Parameterized::TableSyntax

        subject { pipeline.legacy_stages }

        context 'stages list' do
          it 'returns ordered list of stages' do
            expect(subject.map(&:name)).to eq(%w[build test deploy])
          end
        end

        context 'stages with statuses' do
          let(:statuses) do
            subject.map { |stage| [stage.name, stage.status] }
          end

          it 'returns list of stages with correct statuses' do
            expect(statuses).to eq([%w(build failed),
                                    %w(test success),
                                    %w(deploy running)])
          end

          context 'when commit status is retried' do
            let!(:old_commit_status) do
              create(:commit_status, pipeline: pipeline,
                                     stage: 'build',
                                     name: 'mac',
                                     stage_idx: 0,
                                     status: 'success')
            end

            context 'when FF ci_remove_update_retried_from_process_pipeline is disabled' do
              before do
                stub_feature_flags(ci_remove_update_retried_from_process_pipeline: false)

                Ci::ProcessPipelineService
                  .new(pipeline)
                  .execute
              end

              it 'ignores the previous state' do
                expect(statuses).to eq([%w(build success),
                                        %w(test success),
                                        %w(deploy running)])
              end
            end
          end
        end

        context 'when there is a stage with warnings' do
          before do
            create(:commit_status, pipeline: pipeline,
                                  stage: 'deploy',
                                  name: 'prod:2',
                                  stage_idx: 2,
                                  status: 'failed',
                                  allow_failure: true)
          end

          it 'populates stage with correct number of warnings' do
            deploy_stage = pipeline.legacy_stages.third

            expect(deploy_stage).not_to receive(:statuses)
            expect(deploy_stage).to have_warnings
          end
        end
      end

      describe '#stages_count' do
        it 'returns a valid number of stages' do
          expect(pipeline.stages_count).to eq(3)
        end
      end

      describe '#stages_names' do
        it 'returns a valid names of stages' do
          expect(pipeline.stages_names).to eq(%w(build test deploy))
        end
      end
    end

    describe '#legacy_stage' do
      subject { pipeline.legacy_stage('test') }

      let(:pipeline) { build(:ci_empty_pipeline, :created) }

      context 'with status in stage' do
        before do
          create(:commit_status, pipeline: pipeline, stage: 'test')
        end

        it { expect(subject).to be_a Ci::LegacyStage }
        it { expect(subject.name).to eq 'test' }
        it { expect(subject.statuses).not_to be_empty }
      end

      context 'without status in stage' do
        before do
          create(:commit_status, pipeline: pipeline, stage: 'build')
        end

        it 'return stage object' do
          is_expected.to be_nil
        end
      end
    end

    describe '#stages' do
      let(:pipeline) { build(:ci_empty_pipeline, :created) }

      before do
        create(:ci_stage_entity, project: project,
                                 pipeline: pipeline,
                                 position: 4,
                                 name: 'deploy')

        create(:ci_build, project: project,
                          pipeline: pipeline,
                          stage: 'test',
                          stage_idx: 3,
                          name: 'test')

        create(:ci_build, project: project,
                          pipeline: pipeline,
                          stage: 'build',
                          stage_idx: 2,
                          name: 'build')

        create(:ci_stage_entity, project: project,
                                 pipeline: pipeline,
                                 position: 1,
                                 name: 'sanity')

        create(:ci_stage_entity, project: project,
                                 pipeline: pipeline,
                                 position: 5,
                                 name: 'cleanup')
      end

      subject { pipeline.stages }

      context 'when pipelines is not complete' do
        it 'returns stages in valid order' do
          expect(subject).to all(be_a Ci::Stage)
          expect(subject.map(&:name))
            .to eq %w[sanity build test deploy cleanup]
        end
      end

      context 'when pipeline is complete' do
        before do
          pipeline.succeed!
        end

        it 'returns stages in valid order' do
          expect(subject).to all(be_a Ci::Stage)
          expect(subject.map(&:name))
            .to eq %w[sanity build test deploy cleanup]
        end
      end
    end
  end

  describe 'state machine' do
    let_it_be_with_reload(:pipeline) { create(:ci_empty_pipeline, :created) }

    let(:current) { Time.current.change(usec: 0) }
    let(:build) { create_build('build1', queued_at: 0) }
    let(:build_b) { create_build('build2', queued_at: 0) }
    let(:build_c) { create_build('build3', queued_at: 0) }

    %w[succeed! drop! cancel! skip!].each do |action|
      context "when the pipeline recieved #{action} event" do
        it 'deletes a persistent ref' do
          expect(pipeline.persistent_ref).to receive(:delete).once

          pipeline.public_send(action)
        end
      end
    end

    describe 'synching status to Jira' do
      let(:worker) { ::JiraConnect::SyncBuildsWorker }

      %i[prepare! run! skip! drop! succeed! cancel! block! delay!].each do |event|
        context "when we call pipeline.#{event}" do
          it 'triggers a Jira synch worker' do
            expect(worker).to receive(:perform_async).with(pipeline.id, Integer)

            pipeline.send(event)
          end
        end
      end
    end

    describe '#duration', :sidekiq_inline do
      context 'when multiple builds are finished' do
        before do
          travel_to(current + 30) do
            build.run!
            build.reload.success!
            build_b.run!
            build_c.run!
          end

          travel_to(current + 40) do
            build_b.reload.drop!
          end

          travel_to(current + 70) do
            build_c.reload.success!
          end
        end

        it 'matches sum of builds duration' do
          pipeline.reload

          expect(pipeline.duration).to eq(40)
        end
      end

      context 'when pipeline becomes blocked' do
        let!(:build) { create_build('build:1') }
        let!(:action) { create_build('manual:action', :manual) }

        before do
          travel_to(current + 1.minute) do
            build.run!
          end

          travel_to(current + 5.minutes) do
            build.reload.success!
          end
        end

        it 'recalculates pipeline duration' do
          pipeline.reload

          expect(pipeline).to be_manual
          expect(pipeline.duration).to eq 4.minutes
        end
      end
    end

    describe '#started_at' do
      let(:pipeline) { create(:ci_empty_pipeline, status: from_status) }

      %i[created preparing pending].each do |status|
        context "from #{status}" do
          let(:from_status) { status }

          it 'updates on transitioning to running' do
            pipeline.run

            expect(pipeline.started_at).not_to be_nil
          end
        end
      end

      context 'from created' do
        let(:from_status) { :created }

        it 'does not update on transitioning to success' do
          pipeline.succeed

          expect(pipeline.started_at).to be_nil
        end
      end
    end

    describe '#finished_at' do
      it 'updates on transitioning to success', :sidekiq_might_not_need_inline do
        build.success

        expect(pipeline.reload.finished_at).not_to be_nil
      end

      it 'does not update on transitioning to running' do
        build.run

        expect(pipeline.reload.finished_at).to be_nil
      end
    end

    describe 'merge request metrics' do
      let(:pipeline) { create(:ci_empty_pipeline, status: from_status) }

      before do
        expect(PipelineMetricsWorker).to receive(:perform_async).with(pipeline.id)
      end

      context 'when transitioning to running' do
        %i[created preparing pending].each do |status|
          context "from #{status}" do
            let(:from_status) { status }

            it 'schedules metrics workers' do
              pipeline.run
            end
          end
        end
      end

      context 'when transitioning to success' do
        let(:from_status) { 'created' }

        it 'schedules metrics workers' do
          pipeline.succeed
        end
      end
    end

    describe 'merge on success' do
      let(:pipeline) { create(:ci_empty_pipeline, status: from_status) }

      %i[created preparing pending running].each do |status|
        context "from #{status}" do
          let(:from_status) { status }

          it 'schedules daily build group report results worker' do
            expect(Ci::DailyBuildGroupReportResultsWorker).to receive(:perform_in).with(10.minutes, pipeline.id)

            pipeline.succeed
          end
        end
      end
    end

    describe 'pipeline caching' do
      it 'performs ExpirePipelinesCacheWorker' do
        expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id)

        pipeline.cancel
      end
    end

    describe '#dangling?' do
      it 'returns true if pipeline comes from any dangling sources' do
        pipeline.source = Enums::Ci::Pipeline.dangling_sources.each_key.first

        expect(pipeline).to be_dangling
      end

      it 'returns true if pipeline comes from any CI sources' do
        pipeline.source = Enums::Ci::Pipeline.ci_sources.each_key.first

        expect(pipeline).not_to be_dangling
      end
    end

    describe 'auto merge' do
      context 'when auto merge is enabled' do
        let_it_be_with_reload(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
        let_it_be_with_reload(:pipeline) do
          create(:ci_pipeline, :running, project: merge_request.source_project,
                                        ref: merge_request.source_branch,
                                        sha: merge_request.diff_head_sha)
        end

        before_all do
          merge_request.update_head_pipeline
        end

        %w[succeed! drop! cancel! skip!].each do |action|
          context "when the pipeline recieved #{action} event" do
            it 'performs AutoMergeProcessWorker' do
              expect(AutoMergeProcessWorker).to receive(:perform_async).with(merge_request.id)

              pipeline.public_send(action)
            end
          end
        end
      end

      context 'when auto merge is not enabled in the merge request' do
        let(:merge_request) { create(:merge_request) }

        it 'performs AutoMergeProcessWorker' do
          expect(AutoMergeProcessWorker).not_to receive(:perform_async)

          pipeline.succeed!
        end
      end
    end

    describe 'auto devops pipeline metrics' do
      using RSpec::Parameterized::TableSyntax

      let(:pipeline) { create(:ci_empty_pipeline, config_source: config_source) }
      let(:config_source) { :auto_devops_source }

      where(:action, :status) do
        :succeed | 'success'
        :drop    | 'failed'
        :skip    | 'skipped'
        :cancel  | 'canceled'
      end

      with_them do
        context "when pipeline receives action '#{params[:action]}'" do
          subject { pipeline.public_send(action) }

          it { expect { subject }.to change { auto_devops_pipelines_completed_total(status) }.by(1) }

          context 'when not auto_devops_source?' do
            let(:config_source) { :repository_source }

            it { expect { subject }.not_to change { auto_devops_pipelines_completed_total(status) } }
          end
        end
      end

      def auto_devops_pipelines_completed_total(status)
        Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines').get(status: status)
      end
    end

    describe 'bridge triggered pipeline' do
      shared_examples 'upstream downstream pipeline' do
        let!(:source_pipeline) { create(:ci_sources_pipeline, pipeline: downstream_pipeline, source_job: bridge) }
        let!(:job) { downstream_pipeline.builds.first }

        context 'when source bridge is dependent on pipeline status' do
          let!(:bridge) { create(:ci_bridge, :strategy_depend, pipeline: upstream_pipeline) }

          it 'schedules the pipeline bridge worker' do
            expect(::Ci::PipelineBridgeStatusWorker).to receive(:perform_async).with(downstream_pipeline.id)

            downstream_pipeline.succeed!
          end

          context 'when the downstream pipeline first fails then retries and succeeds' do
            it 'makes the upstream pipeline successful' do
              Sidekiq::Testing.inline! { job.drop! }

              expect(downstream_pipeline.reload).to be_failed
              expect(upstream_pipeline.reload).to be_failed

              Sidekiq::Testing.inline! do
                new_job = Ci::Build.retry(job, project.users.first)

                expect(downstream_pipeline.reload).to be_running
                expect(upstream_pipeline.reload).to be_running

                new_job.success!
              end

              expect(downstream_pipeline.reload).to be_success
              expect(upstream_pipeline.reload).to be_success
            end
          end

          context 'when the downstream pipeline first succeeds then retries and fails' do
            it 'makes the upstream pipeline failed' do
              Sidekiq::Testing.inline! { job.success! }

              expect(downstream_pipeline.reload).to be_success
              expect(upstream_pipeline.reload).to be_success

              Sidekiq::Testing.inline! do
                new_job = Ci::Build.retry(job, project.users.first)

                expect(downstream_pipeline.reload).to be_running
                expect(upstream_pipeline.reload).to be_running

                new_job.drop!
              end

              expect(downstream_pipeline.reload).to be_failed
              expect(upstream_pipeline.reload).to be_failed
            end
          end

          context 'when the upstream pipeline has another dependent upstream pipeline' do
            let!(:upstream_of_upstream_pipeline) { create(:ci_pipeline) }

            before do
              upstream_bridge = create(:ci_bridge, :strategy_depend, pipeline: upstream_of_upstream_pipeline)
              create(:ci_sources_pipeline, pipeline: upstream_pipeline,
                                           source_job: upstream_bridge)
            end

            context 'when the downstream pipeline first fails then retries and succeeds' do
              it 'makes upstream pipelines successful' do
                Sidekiq::Testing.inline! { job.drop! }

                expect(downstream_pipeline.reload).to be_failed
                expect(upstream_pipeline.reload).to be_failed
                expect(upstream_of_upstream_pipeline.reload).to be_failed

                Sidekiq::Testing.inline! do
                  new_job = Ci::Build.retry(job, project.users.first)

                  expect(downstream_pipeline.reload).to be_running
                  expect(upstream_pipeline.reload).to be_running
                  expect(upstream_of_upstream_pipeline.reload).to be_running

                  new_job.success!
                end

                expect(downstream_pipeline.reload).to be_success
                expect(upstream_pipeline.reload).to be_success
                expect(upstream_of_upstream_pipeline.reload).to be_success
              end
            end
          end
        end

        context 'when source bridge is not dependent on pipeline status' do
          let!(:bridge) { create(:ci_bridge, pipeline: upstream_pipeline) }

          it 'does not schedule the pipeline bridge worker' do
            expect(::Ci::PipelineBridgeStatusWorker).not_to receive(:perform_async)

            downstream_pipeline.succeed!
          end
        end
      end

      context 'multi-project pipelines' do
        let!(:downstream_project) { create(:project, :repository) }
        let!(:upstream_pipeline) { create(:ci_pipeline) }
        let!(:downstream_pipeline) { create(:ci_pipeline, :with_job, project: downstream_project) }

        it_behaves_like 'upstream downstream pipeline'
      end

      context 'parent-child pipelines' do
        let!(:upstream_pipeline) { create(:ci_pipeline) }
        let!(:downstream_pipeline) { create(:ci_pipeline, :with_job) }

        it_behaves_like 'upstream downstream pipeline'
      end
    end

    def create_build(name, *traits, queued_at: current, started_from: 0, **opts)
      create(:ci_build, *traits,
             name: name,
             pipeline: pipeline,
             queued_at: queued_at,
             started_at: queued_at + started_from,
             **opts)
    end
  end

  describe '#branch?' do
    subject { pipeline.branch? }

    let(:pipeline) { build(:ci_empty_pipeline, :created) }

    context 'when ref is not a tag' do
      before do
        pipeline.tag = false
      end

      it 'return true' do
        is_expected.to be_truthy
      end

      context 'when pipeline is merge request' do
        let(:pipeline) { build(:ci_pipeline, merge_request: merge_request) }

        let(:merge_request) do
          create(:merge_request, :simple,
                 source_project: project,
                 target_project: project)
        end

        it 'returns false' do
          is_expected.to be_falsey
        end
      end
    end

    context 'when ref is a tag' do
      before do
        pipeline.tag = true
      end

      it 'return false' do
        is_expected.to be_falsey
      end
    end
  end

  describe '#git_ref' do
    subject { pipeline.send(:git_ref) }

    context 'when ref is branch' do
      let(:pipeline) { create(:ci_pipeline, tag: false) }

      it 'returns branch ref' do
        is_expected.to eq(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.ref.to_s)
      end
    end

    context 'when ref is tag' do
      let(:pipeline) { create(:ci_pipeline, tag: true) }

      it 'returns branch ref' do
        is_expected.to eq(Gitlab::Git::TAG_REF_PREFIX + pipeline.ref.to_s)
      end
    end

    context 'when ref is merge request' do
      let(:pipeline) do
        create(:ci_pipeline,
               source: :merge_request_event,
               merge_request: merge_request)
      end

      let(:merge_request) do
        create(:merge_request,
               source_project: project,
               source_branch: 'feature',
               target_project: project,
               target_branch: 'master')
      end

      it 'returns branch ref' do
        is_expected.to eq(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.ref.to_s)
      end
    end
  end

  describe 'ref_exists?' do
    context 'when repository exists' do
      using RSpec::Parameterized::TableSyntax

      let_it_be(:pipeline, refind: true) { create(:ci_empty_pipeline) }

      where(:tag, :ref, :result) do
        false | 'master'              | true
        false | 'non-existent-branch' | false
        true  | 'v1.1.0'              | true
        true  | 'non-existent-tag'    | false
      end

      with_them do
        before do
          pipeline.update!(tag: tag, ref: ref)
        end

        it "correctly detects ref" do
          expect(pipeline.ref_exists?).to be result
        end
      end
    end

    context 'when repository does not exist' do
      let(:pipeline) { build(:ci_empty_pipeline, ref: 'master', project: build(:project)) }

      it 'always returns false' do
        expect(pipeline.ref_exists?).to eq false
      end
    end
  end

  context 'with non-empty project' do
    let(:pipeline) do
      create(:ci_pipeline,
             ref: project.default_branch,
             sha: project.commit.sha)
    end

    describe '#lazy_ref_commit' do
      let(:another) do
        create(:ci_pipeline,
               ref: 'feature',
               sha: project.commit('feature').sha)
      end

      let(:unicode) do
        create(:ci_pipeline,
               ref: 'ΓΌ/unicode/multi-byte')
      end

      it 'returns the latest commit for a ref lazily' do
        expect(project.repository)
          .to receive(:list_commits_by_ref_name).once
          .and_call_original

        pipeline.lazy_ref_commit
        another.lazy_ref_commit
        unicode.lazy_ref_commit

        expect(pipeline.lazy_ref_commit.id).to eq pipeline.sha
        expect(another.lazy_ref_commit.id).to eq another.sha
        expect(unicode.lazy_ref_commit).to be_nil
      end
    end

    describe '#latest?' do
      context 'with latest sha' do
        it 'returns true' do
          expect(pipeline).to be_latest
        end
      end

      context 'with a branch name as the ref' do
        it 'looks up a commit for a branch' do
          expect(pipeline.ref).to eq 'master'
          expect(pipeline).to be_latest
        end
      end

      context 'with a tag name as a ref' do
        it 'looks up a commit for a tag' do
          expect(project.repository.branch_names).not_to include 'v1.0.0'

          pipeline.update!(sha: project.commit('v1.0.0').sha, ref: 'v1.0.0', tag: true)

          expect(pipeline).to be_tag
          expect(pipeline).to be_latest
        end
      end

      context 'with not latest sha' do
        before do
          pipeline.update!(sha: project.commit("#{project.default_branch}~1").sha)
        end

        it 'returns false' do
          expect(pipeline).not_to be_latest
        end
      end
    end
  end

  describe '#manual_actions' do
    subject { pipeline.manual_actions }

    let(:pipeline) { create(:ci_empty_pipeline, :created) }

    it 'when none defined' do
      is_expected.to be_empty
    end

    context 'when action defined' do
      let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') }

      it 'returns one action' do
        is_expected.to contain_exactly(manual)
      end

      context 'there are multiple of the same name' do
        let!(:manual2) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') }

        before do
          manual.update!(retried: true)
        end

        it 'returns latest one' do
          is_expected.to contain_exactly(manual2)
        end
      end
    end
  end

  describe '#branch_updated?' do
    let(:pipeline) { create(:ci_empty_pipeline, :created) }

    context 'when pipeline has before SHA' do
      before do
        pipeline.update!(before_sha: 'a1b2c3d4')
      end

      it 'runs on a branch update push' do
        expect(pipeline.before_sha).not_to be Gitlab::Git::BLANK_SHA
        expect(pipeline.branch_updated?).to be true
      end
    end

    context 'when pipeline does not have before SHA' do
      before do
        pipeline.update!(before_sha: Gitlab::Git::BLANK_SHA)
      end

      it 'does not run on a branch updating push' do
        expect(pipeline.branch_updated?).to be false
      end
    end
  end

  describe '#modified_paths' do
    let(:pipeline) { create(:ci_empty_pipeline, :created) }

    context 'when old and new revisions are set' do
      before do
        pipeline.update!(before_sha: '1234abcd', sha: '2345bcde')
      end

      it 'fetches stats for changes between commits' do
        expect(project.repository)
          .to receive(:diff_stats).with('1234abcd', '2345bcde')
          .and_call_original

        pipeline.modified_paths
      end
    end

    context 'when either old or new revision is missing' do
      before do
        pipeline.update!(before_sha: Gitlab::Git::BLANK_SHA)
      end

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

    context 'when source is merge request' do
      let(:pipeline) do
        create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request)
      end

      let(:merge_request) do
        create(:merge_request, :simple,
               source_project: project,
               target_project: project)
      end

      it 'returns merge request modified paths' do
        expect(pipeline.modified_paths).to match(merge_request.modified_paths)
      end
    end

    context 'when source is an external pull request' do
      let(:pipeline) do
        create(:ci_pipeline, source: :external_pull_request_event, external_pull_request: external_pull_request)
      end

      let(:external_pull_request) do
        create(:external_pull_request, project: project, target_sha: '281d3a7', source_sha: '498214d')
      end

      it 'returns external pull request modified paths' do
        expect(pipeline.modified_paths).to match(external_pull_request.modified_paths)
      end

      context 'when the FF ci_modified_paths_of_external_prs is disabled' do
        before do
          stub_feature_flags(ci_modified_paths_of_external_prs: false)
        end

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

  describe '#all_worktree_paths' do
    let(:files) { { 'main.go' => '', 'mocks/mocks.go' => '' } }
    let(:project) { create(:project, :custom_repo, files: files) }
    let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) }

    it 'returns all file paths cached' do
      expect(project.repository).to receive(:ls_files).with(pipeline.sha).once.and_call_original
      expect(pipeline.all_worktree_paths).to eq(files.keys)
      expect(pipeline.all_worktree_paths).to eq(files.keys)
    end
  end

  describe '#top_level_worktree_paths' do
    let(:files) { { 'main.go' => '', 'mocks/mocks.go' => '' } }
    let(:project) { create(:project, :custom_repo, files: files) }
    let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) }

    it 'returns top-level file paths cached' do
      expect(project.repository).to receive(:tree).with(pipeline.sha).once.and_call_original
      expect(pipeline.top_level_worktree_paths).to eq(['main.go'])
      expect(pipeline.top_level_worktree_paths).to eq(['main.go'])
    end
  end

  describe '#has_kubernetes_active?' do
    let(:pipeline) { create(:ci_empty_pipeline, :created, project: project) }

    context 'when kubernetes is active' do
      context 'when user configured kubernetes from CI/CD > Clusters' do
        let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
        let(:project) { cluster.project }

        it 'returns true' do
          expect(pipeline).to have_kubernetes_active
        end
      end
    end

    context 'when kubernetes is not active' do
      it 'returns false' do
        expect(pipeline).not_to have_kubernetes_active
      end
    end
  end

  describe '#has_warnings?' do
    subject { pipeline.has_warnings? }

    let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }

    context 'build which is allowed to fail fails' do
      before do
        create :ci_build, :success, pipeline: pipeline, name: 'rspec'
        create :ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop'
      end

      it 'returns true' do
        is_expected.to be_truthy
      end
    end

    context 'build which is allowed to fail succeeds' do
      before do
        create :ci_build, :success, pipeline: pipeline, name: 'rspec'
        create :ci_build, :allowed_to_fail, :success, pipeline: pipeline, name: 'rubocop'
      end

      it 'returns false' do
        is_expected.to be_falsey
      end
    end

    context 'build is retried and succeeds' do
      before do
        create :ci_build, :success, pipeline: pipeline, name: 'rubocop'
        create :ci_build, :failed, pipeline: pipeline, name: 'rspec'
        create :ci_build, :success, pipeline: pipeline, name: 'rspec'
      end

      it 'returns false' do
        is_expected.to be_falsey
      end
    end

    context 'bridge which is allowed to fail fails' do
      before do
        create :ci_bridge, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop'
      end

      it 'returns true' do
        is_expected.to be_truthy
      end
    end

    context 'bridge which is allowed to fail is successful' do
      before do
        create :ci_bridge, :allowed_to_fail, :success, pipeline: pipeline, name: 'rubocop'
      end

      it 'returns false' do
        is_expected.to be_falsey
      end
    end
  end

  describe '#number_of_warnings' do
    let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }

    it 'returns the number of warnings' do
      create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop')
      create(:ci_bridge, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop')

      expect(pipeline.number_of_warnings).to eq(2)
    end

    it 'supports eager loading of the number of warnings' do
      pipeline2 = create(:ci_empty_pipeline, :created)

      create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop')
      create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline2, name: 'rubocop')

      pipelines = project.ci_pipelines.to_a

      pipelines.each(&:number_of_warnings)

      # To run the queries we need to actually use the lazy objects, which we do
      # by just sending "to_i" to them.
      amount = ActiveRecord::QueryRecorder
        .new { pipelines.each { |p| p.number_of_warnings.to_i } }
        .count

      expect(amount).to eq(1)
    end
  end

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

    subject { pipeline.needs_processing? }

    let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }

    where(:processed, :result) do
      nil   | true
      false | true
      true  | false
    end

    with_them do
      let(:build) do
        create(:ci_build, :success, pipeline: pipeline, name: 'rubocop')
      end

      before do
        build.update_column(:processed, processed)
      end

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

  context 'with outdated pipelines' do
    before_all do
      create_pipeline(:canceled, 'ref', 'A')
      create_pipeline(:success, 'ref', 'A')
      create_pipeline(:failed, 'ref', 'B')
      create_pipeline(:skipped, 'feature', 'C')
    end

    def create_pipeline(status, ref, sha)
      create(
        :ci_empty_pipeline,
        status: status,
        ref: ref,
        sha: sha
      )
    end

    describe '.newest_first' do
      it 'returns the pipelines from new to old' do
        expect(described_class.newest_first.pluck(:status))
          .to eq(%w[skipped failed success canceled])
      end

      it 'searches limited backlog' do
        expect(described_class.newest_first(limit: 1).pluck(:status))
          .to eq(%w[skipped])
      end
    end

    describe '.latest_status' do
      context 'when no ref is specified' do
        it 'returns the status of the latest pipeline' do
          expect(described_class.latest_status).to eq('skipped')
        end
      end

      context 'when ref is specified' do
        it 'returns the status of the latest pipeline for the given ref' do
          expect(described_class.latest_status('ref')).to eq('failed')
        end
      end
    end

    describe '.latest_successful_for_ref' do
      let!(:latest_successful_pipeline) do
        create_pipeline(:success, 'ref', 'D')
      end

      it 'returns the latest successful pipeline' do
        expect(described_class.latest_successful_for_ref('ref'))
          .to eq(latest_successful_pipeline)
      end
    end

    describe '.latest_running_for_ref' do
      let!(:latest_running_pipeline) do
        create_pipeline(:running, 'ref', 'D')
      end

      it 'returns the latest running pipeline' do
        expect(described_class.latest_running_for_ref('ref'))
          .to eq(latest_running_pipeline)
      end
    end

    describe '.latest_failed_for_ref' do
      let!(:latest_failed_pipeline) do
        create_pipeline(:failed, 'ref', 'D')
      end

      it 'returns the latest failed pipeline' do
        expect(described_class.latest_failed_for_ref('ref'))
          .to eq(latest_failed_pipeline)
      end
    end

    describe '.latest_successful_for_sha' do
      let!(:latest_successful_pipeline) do
        create_pipeline(:success, 'ref', 'awesomesha')
      end

      it 'returns the latest successful pipeline' do
        expect(described_class.latest_successful_for_sha('awesomesha'))
          .to eq(latest_successful_pipeline)
      end
    end

    describe '.latest_successful_for_refs' do
      subject(:latest_successful_for_refs) { described_class.latest_successful_for_refs(refs) }

      context 'when refs are specified' do
        let(:refs) { %w(first_ref second_ref third_ref) }

        before do
          create(:ci_empty_pipeline, id: 1001, status: :success, ref: 'first_ref', sha: 'sha')
          create(:ci_empty_pipeline, id: 1002, status: :success, ref: 'second_ref', sha: 'sha')
        end

        let!(:latest_successful_pipeline_for_first_ref) do
          create(:ci_empty_pipeline, id: 2001, status: :success, ref: 'first_ref', sha: 'sha')
        end

        let!(:latest_successful_pipeline_for_second_ref) do
          create(:ci_empty_pipeline, id: 2002, status: :success, ref: 'second_ref', sha: 'sha')
        end

        it 'returns the latest successful pipeline for both refs' do
          expect(latest_successful_for_refs).to eq({
            'first_ref' => latest_successful_pipeline_for_first_ref,
            'second_ref' => latest_successful_pipeline_for_second_ref
          })
        end
      end

      context 'when no refs are specified' do
        let(:refs) { [] }

        it 'returns an empty relation whenno refs are specified' do
          expect(latest_successful_for_refs).to be_empty
        end
      end
    end
  end

  describe '.latest_pipeline_per_commit' do
    let!(:commit_123_ref_master) do
      create(
        :ci_empty_pipeline,
        status: 'success',
        ref: 'master',
        sha: '123'
      )
    end

    let!(:commit_123_ref_develop) do
      create(
        :ci_empty_pipeline,
        status: 'success',
        ref: 'develop',
        sha: '123'
      )
    end

    let!(:commit_456_ref_test) do
      create(
        :ci_empty_pipeline,
        status: 'success',
        ref: 'test',
        sha: '456'
      )
    end

    context 'without a ref' do
      it 'returns a Hash containing the latest pipeline per commit for all refs' do
        result = described_class.latest_pipeline_per_commit(%w[123 456])

        expect(result).to match(
          '123' => commit_123_ref_develop,
          '456' => commit_456_ref_test
        )
      end

      it 'only includes the latest pipeline of the given commit SHAs' do
        result = described_class.latest_pipeline_per_commit(%w[123])

        expect(result).to match(
          '123' => commit_123_ref_develop
        )
      end

      context 'when there are two pipelines for a ref and SHA' do
        let!(:commit_123_ref_master_latest) do
          create(
            :ci_empty_pipeline,
            status: 'failed',
            ref: 'master',
            sha: '123',
            project: project
          )
        end

        it 'returns the latest pipeline' do
          result = described_class.latest_pipeline_per_commit(%w[123])

          expect(result).to match(
            '123' => commit_123_ref_master_latest
          )
        end
      end
    end

    context 'with a ref' do
      it 'only includes the pipelines for the given ref' do
        result = described_class.latest_pipeline_per_commit(%w[123 456], 'master')

        expect(result).to match(
          '123' => commit_123_ref_master
        )
      end
    end

    context 'when method is scoped' do
      let!(:commit_123_ref_master_parent_pipeline) do
        create(
          :ci_pipeline,
          sha: '123',
          ref: 'master',
          project: project
        )
      end

      let!(:commit_123_ref_master_child_pipeline) do
        create(
          :ci_pipeline,
          sha: '123',
          ref: 'master',
          project: project,
          child_of: commit_123_ref_master_parent_pipeline
        )
      end

      it 'returns the latest pipeline after applying the scope' do
        result = described_class.ci_sources.latest_pipeline_per_commit(%w[123], 'master')

        expect(result).to match(
          '123' => commit_123_ref_master_parent_pipeline
        )
      end
    end
  end

  describe '.latest_successful_ids_per_project' do
    let(:projects) { create_list(:project, 2) }
    let!(:pipeline1) { create(:ci_pipeline, :success, project: projects[0]) }
    let!(:pipeline2) { create(:ci_pipeline, :success, project: projects[0]) }
    let!(:pipeline3) { create(:ci_pipeline, :failed, project: projects[0]) }
    let!(:pipeline4) { create(:ci_pipeline, :success, project: projects[1]) }

    it 'returns expected pipeline ids' do
      expect(described_class.latest_successful_ids_per_project)
        .to contain_exactly(pipeline2, pipeline4)
    end
  end

  describe '.last_finished_for_ref_id' do
    let(:branch) { project.default_branch }
    let(:ref) { project.ci_refs.take }
    let(:dangling_source) { Enums::Ci::Pipeline.sources[:ondemand_dast_scan] }
    let!(:pipeline1) { create(:ci_pipeline, :success, project: project, ref: branch) }
    let!(:pipeline2) { create(:ci_pipeline, :success, project: project, ref: branch) }
    let!(:pipeline3) { create(:ci_pipeline, :failed, project: project, ref: branch) }
    let!(:pipeline4) { create(:ci_pipeline, :success, project: project, ref: branch) }
    let!(:pipeline5) { create(:ci_pipeline, :success, project: project, ref: branch, source: dangling_source) }

    it 'returns the expected pipeline' do
      result = described_class.last_finished_for_ref_id(ref.id)
      expect(result).to eq(pipeline4)
    end
  end

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

    it { is_expected.to be_an(Array) }
  end

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

    it { is_expected.to be_an(Array) }
    it { is_expected.not_to include('created', 'waiting_for_resource', 'preparing', 'pending') }
  end

  describe '#status', :sidekiq_inline do
    subject { pipeline.reload.status }

    let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }

    let(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') }

    context 'on waiting for resource' do
      before do
        allow(build).to receive(:with_resource_group?) { true }
        allow(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async)

        build.enqueue
      end

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

    context 'on prepare' do
      before do
        # Prevent skipping directly to 'pending'
        allow(build).to receive(:prerequisites).and_return([double])
        allow(Ci::BuildPrepareWorker).to receive(:perform_async)

        build.enqueue
      end

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

    context 'on queuing' do
      before do
        build.enqueue
      end

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

    context 'on run' do
      before do
        build.enqueue
        build.reload.run
      end

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

    context 'on drop' do
      before do
        build.drop
      end

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

    context 'on success' do
      before do
        build.success
      end

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

    context 'on cancel' do
      before do
        build.cancel
      end

      context 'when build is pending' do
        let(:build) do
          create(:ci_build, :pending, pipeline: pipeline)
        end

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

    context 'on failure and build retry' do
      before do
        stub_not_protect_default_branch

        build.drop
        project.add_developer(user)

        Ci::Build.retry(build, user)
      end

      # We are changing a state: created > failed > running
      # Instead of: created > failed > pending
      # Since the pipeline already run, so it should not be pending anymore

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

  describe '#detailed_status' do
    subject { pipeline.detailed_status(user) }

    let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }

    context 'when pipeline is created' do
      let(:pipeline) { create(:ci_pipeline, :created) }

      it 'returns detailed status for created pipeline' do
        expect(subject.text).to eq s_('CiStatusText|created')
      end
    end

    context 'when pipeline is pending' do
      let(:pipeline) { create(:ci_pipeline, status: :pending) }

      it 'returns detailed status for pending pipeline' do
        expect(subject.text).to eq s_('CiStatusText|pending')
      end
    end

    context 'when pipeline is running' do
      let(:pipeline) { create(:ci_pipeline, status: :running) }

      it 'returns detailed status for running pipeline' do
        expect(subject.text).to eq s_('CiStatus|running')
      end
    end

    context 'when pipeline is successful' do
      let(:pipeline) { create(:ci_pipeline, status: :success) }

      it 'returns detailed status for successful pipeline' do
        expect(subject.text).to eq s_('CiStatusText|passed')
      end
    end

    context 'when pipeline is failed' do
      let(:pipeline) { create(:ci_pipeline, status: :failed) }

      it 'returns detailed status for failed pipeline' do
        expect(subject.text).to eq s_('CiStatusText|failed')
      end
    end

    context 'when pipeline is canceled' do
      let(:pipeline) { create(:ci_pipeline, status: :canceled) }

      it 'returns detailed status for canceled pipeline' do
        expect(subject.text).to eq s_('CiStatusText|canceled')
      end
    end

    context 'when pipeline is skipped' do
      let(:pipeline) { create(:ci_pipeline, status: :skipped) }

      it 'returns detailed status for skipped pipeline' do
        expect(subject.text).to eq s_('CiStatusText|skipped')
      end
    end

    context 'when pipeline is blocked' do
      let(:pipeline) { create(:ci_pipeline, status: :manual) }

      it 'returns detailed status for blocked pipeline' do
        expect(subject.text).to eq s_('CiStatusText|blocked')
      end
    end

    context 'when pipeline is successful but with warnings' do
      let(:pipeline) { create(:ci_pipeline, status: :success) }

      before do
        create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline)
      end

      it 'retruns detailed status for successful pipeline with warnings' do
        expect(subject.label).to eq(s_('CiStatusLabel|passed with warnings'))
      end
    end
  end

  describe '#cancelable?' do
    let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }

    %i[created running pending].each do |status0|
      context "when there is a build #{status0}" do
        before do
          create(:ci_build, status0, pipeline: pipeline)
        end

        it 'is cancelable' do
          expect(pipeline.cancelable?).to be_truthy
        end
      end

      context "when there is an external job #{status0}" do
        before do
          create(:generic_commit_status, status0, pipeline: pipeline)
        end

        it 'is cancelable' do
          expect(pipeline.cancelable?).to be_truthy
        end
      end

      %i[success failed canceled].each do |status1|
        context "when there are generic_commit_status jobs for #{status0} and #{status1}" do
          before do
            create(:generic_commit_status, status0, pipeline: pipeline)
            create(:generic_commit_status, status1, pipeline: pipeline)
          end

          it 'is cancelable' do
            expect(pipeline.cancelable?).to be_truthy
          end
        end

        context "when there are generic_commit_status and ci_build jobs for #{status0} and #{status1}" do
          before do
            create(:generic_commit_status, status0, pipeline: pipeline)
            create(:ci_build, status1, pipeline: pipeline)
          end

          it 'is cancelable' do
            expect(pipeline.cancelable?).to be_truthy
          end
        end

        context "when there are ci_build jobs for #{status0} and #{status1}" do
          before do
            create(:ci_build, status0, pipeline: pipeline)
            create(:ci_build, status1, pipeline: pipeline)
          end

          it 'is cancelable' do
            expect(pipeline.cancelable?).to be_truthy
          end
        end
      end
    end

    %i[success failed canceled].each do |status|
      context "when there is a build #{status}" do
        before do
          create(:ci_build, status, pipeline: pipeline)
        end

        it 'is not cancelable' do
          expect(pipeline.cancelable?).to be_falsey
        end
      end

      context "when there is an external job #{status}" do
        before do
          create(:generic_commit_status, status, pipeline: pipeline)
        end

        it 'is not cancelable' do
          expect(pipeline.cancelable?).to be_falsey
        end
      end
    end

    context 'when there is a manual action present in the pipeline' do
      before do
        create(:ci_build, :manual, pipeline: pipeline)
      end

      it 'is not cancelable' do
        expect(pipeline).not_to be_cancelable
      end
    end
  end

  describe '#cancel_running' do
    subject(:latest_status) { pipeline.statuses.pluck(:status) }

    let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }

    context 'when there is a running external job and a regular job' do
      before do
        create(:ci_build, :running, pipeline: pipeline)
        create(:generic_commit_status, :running, pipeline: pipeline)

        pipeline.cancel_running
      end

      it 'cancels both jobs' do
        expect(latest_status).to contain_exactly('canceled', 'canceled')
      end
    end

    context 'when jobs are in different stages' do
      before do
        create(:ci_build, :running, stage_idx: 0, pipeline: pipeline)
        create(:ci_build, :running, stage_idx: 1, pipeline: pipeline)

        pipeline.cancel_running
      end

      it 'cancels both jobs' do
        expect(latest_status).to contain_exactly('canceled', 'canceled')
      end
    end

    context 'when there are created builds present in the pipeline' do
      before do
        create(:ci_build, :running, stage_idx: 0, pipeline: pipeline)
        create(:ci_build, :created, stage_idx: 1, pipeline: pipeline)

        pipeline.cancel_running
      end

      it 'cancels created builds' do
        expect(latest_status).to eq %w(canceled canceled)
      end
    end

    context 'preloading relations' do
      let(:pipeline1) { create(:ci_empty_pipeline, :created) }
      let(:pipeline2) { create(:ci_empty_pipeline, :created) }

      before do
        create(:ci_build, :pending, pipeline: pipeline1)
        create(:generic_commit_status, :pending, pipeline: pipeline1)

        create(:ci_build, :pending, pipeline: pipeline2)
        create(:ci_build, :pending, pipeline: pipeline2)
        create(:generic_commit_status, :pending, pipeline: pipeline2)
        create(:generic_commit_status, :pending, pipeline: pipeline2)
        create(:generic_commit_status, :pending, pipeline: pipeline2)
      end

      it 'preloads relations for each build to avoid N+1 queries' do
        control1 = ActiveRecord::QueryRecorder.new do
          pipeline1.cancel_running
        end

        control2 = ActiveRecord::QueryRecorder.new do
          pipeline2.cancel_running
        end

        extra_update_queries = 4 # transition ... => :canceled, queue pop
        extra_generic_commit_status_validation_queries = 2 # name_uniqueness_across_types

        expect(control2.count).to eq(control1.count + extra_update_queries + extra_generic_commit_status_validation_queries)
      end
    end

    context 'when the first try cannot get an exclusive lock' do
      let(:retries) { 1 }

      subject(:cancel_running) { pipeline.cancel_running(retries: retries) }

      before do
        build = create(:ci_build, :running, pipeline: pipeline)

        allow(pipeline.cancelable_statuses).to receive(:find_in_batches).and_yield([build])

        call_count = 0
        allow(build).to receive(:cancel).and_wrap_original do |original, *args|
          call_count >= retries ? raise(ActiveRecord::StaleObjectError) : original.call(*args)

          call_count += 1
        end
      end

      it 'retries again and cancels the build' do
        cancel_running

        expect(latest_status).to contain_exactly('canceled')
      end

      context 'when the retries parameter is 0' do
        let(:retries) { 0 }

        it 'raises error' do
          expect do
            cancel_running
          end.to raise_error(ActiveRecord::StaleObjectError)
        end
      end
    end
  end

  describe '#retry_failed' do
    subject(:latest_status) { pipeline.latest_statuses.pluck(:status) }

    let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }

    before do
      stub_not_protect_default_branch

      project.add_developer(user)
    end

    context 'when there is a failed build and failed external status' do
      before do
        create(:ci_build, :failed, name: 'build', pipeline: pipeline)
        create(:generic_commit_status, :failed, name: 'jenkins', pipeline: pipeline)

        pipeline.retry_failed(user)
      end

      it 'retries only build' do
        expect(latest_status).to contain_exactly('pending', 'failed')
      end
    end

    context 'when builds are in different stages' do
      before do
        create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline)
        create(:ci_build, :failed, name: 'jenkins', stage_idx: 1, pipeline: pipeline)

        pipeline.retry_failed(user)
      end

      it 'retries both builds' do
        expect(latest_status).to contain_exactly('pending', 'created')
      end
    end

    context 'when there are canceled and failed' do
      before do
        create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline)
        create(:ci_build, :canceled, name: 'jenkins', stage_idx: 1, pipeline: pipeline)

        pipeline.retry_failed(user)
      end

      it 'retries both builds' do
        expect(latest_status).to contain_exactly('pending', 'created')
      end
    end
  end

  describe '#execute_hooks' do
    let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }

    let!(:build_a) { create_build('a', 0) }
    let!(:build_b) { create_build('b', 0) }

    let!(:hook) do
      create(:project_hook, pipeline_events: enabled)
    end

    before do
      WebHookWorker.drain
    end

    context 'with pipeline hooks enabled' do
      let(:enabled) { true }

      before do
        stub_full_request(hook.url, method: :post)
      end

      context 'with multiple builds', :sidekiq_inline do
        context 'when build is queued' do
          before do
            build_a.reload.enqueue
            build_b.reload.enqueue
          end

          it 'receives a pending event once' do
            expect(WebMock).to have_requested_pipeline_hook('pending').once
          end

          it 'builds hook data once' do
            create(:pipelines_email_integration)

            expect(Gitlab::DataBuilder::Pipeline).to receive(:build).once.and_call_original

            pipeline.execute_hooks
          end
        end

        context 'when build is run' do
          before do
            build_a.reload.enqueue
            build_a.reload.run!
            build_b.reload.enqueue
            build_b.reload.run!
          end

          it 'receives a running event once' do
            expect(WebMock).to have_requested_pipeline_hook('running').once
          end
        end

        context 'when all builds succeed' do
          before do
            build_a.success

            # We have to reload build_b as this is in next stage and it gets triggered by PipelineProcessWorker
            build_b.reload.success
          end

          it 'receives a success event once' do
            expect(WebMock).to have_requested_pipeline_hook('success').once
          end
        end

        context 'when stage one failed' do
          let!(:build_b) { create_build('b', 1) }

          before do
            build_a.drop
          end

          it 'receives a failed event once' do
            expect(WebMock).to have_requested_pipeline_hook('failed').once
          end
        end

        def have_requested_pipeline_hook(status)
          have_requested(:post, stubbed_hostname(hook.url)).with do |req|
            json_body = Gitlab::Json.parse(req.body)
            json_body['object_attributes']['status'] == status &&
              json_body['builds'].length == 2
          end
        end
      end
    end

    context 'with pipeline hooks disabled' do
      let(:enabled) { false }

      before do
        build_a.enqueue
        build_b.enqueue
      end

      it 'did not execute pipeline_hook after touched' do
        expect(WebMock).not_to have_requested(:post, hook.url)
      end

      it 'does not build hook data' do
        expect(Gitlab::DataBuilder::Pipeline).not_to receive(:build)

        pipeline.execute_hooks
      end
    end

    def create_build(name, stage_idx)
      create(:ci_build,
             :created,
             pipeline: pipeline,
             name: name,
             stage: "stage:#{stage_idx}",
             stage_idx: stage_idx)
    end
  end

  describe "#merge_requests_as_head_pipeline" do
    let_it_be_with_reload(:pipeline) { create(:ci_empty_pipeline, status: 'created', ref: 'master', sha: 'a288a022a53a5a944fae87bcec6efc87b7061808') }

    it "returns merge requests whose `diff_head_sha` matches the pipeline's SHA" do
      allow_next_instance_of(MergeRequest) do |instance|
        allow(instance).to receive(:diff_head_sha) { 'a288a022a53a5a944fae87bcec6efc87b7061808' }
      end
      merge_request = create(:merge_request, source_project: project, head_pipeline: pipeline, source_branch: pipeline.ref)

      expect(pipeline.merge_requests_as_head_pipeline).to eq([merge_request])
    end

    it "doesn't return merge requests whose source branch doesn't match the pipeline's ref" do
      create(:merge_request, :simple, source_project: project)

      expect(pipeline.merge_requests_as_head_pipeline).to be_empty
    end

    it "doesn't return merge requests whose `diff_head_sha` doesn't match the pipeline's SHA" do
      create(:merge_request, source_project: project, source_branch: pipeline.ref)
      allow_next_instance_of(MergeRequest) do |instance|
        allow(instance).to receive(:diff_head_sha) { '97de212e80737a608d939f648d959671fb0a0142b' }
      end

      expect(pipeline.merge_requests_as_head_pipeline).to be_empty
    end
  end

  describe '#all_merge_requests' do
    let_it_be_with_reload(:project) { create(:project) }
    let_it_be(:pipeline) { create(:ci_empty_pipeline, :created, project: project) }

    shared_examples 'a method that returns all merge requests for a given pipeline' do
      let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: pipeline_project, ref: 'master') }

      it 'returns all merge requests having the same source branch and the pipeline sha' do
        merge_request = create(:merge_request, source_project: pipeline_project, target_project: project, source_branch: pipeline.ref)

        create(:merge_request_diff, merge_request: merge_request).tap do |diff|
          create(:merge_request_diff_commit, merge_request_diff: diff, sha: pipeline.sha)
        end

        expect(pipeline.all_merge_requests).to eq([merge_request])
      end

      it "doesn't return merge requests having the same source branch without the pipeline sha" do
        merge_request = create(:merge_request, source_project: pipeline_project, target_project: project, source_branch: pipeline.ref)
        create(:merge_request_diff, merge_request: merge_request).tap do |diff|
          create(:merge_request_diff_commit, merge_request_diff: diff, sha: 'unrelated')
        end

        expect(pipeline.all_merge_requests).to be_empty
      end

      it "doesn't return merge requests having a different source branch" do
        create(:merge_request, source_project: pipeline_project, target_project: project, source_branch: 'feature', target_branch: 'master')

        expect(pipeline.all_merge_requests).to be_empty
      end

      context 'when there is a merge request pipeline' do
        let(:source_branch) { 'feature' }
        let(:target_branch) { 'master' }

        let!(:pipeline) do
          create(:ci_pipeline,
                 source: :merge_request_event,
                 project: pipeline_project,
                 ref: source_branch,
                 merge_request: merge_request)
        end

        let(:merge_request) do
          create(:merge_request,
                 source_project: pipeline_project,
                 source_branch: source_branch,
                 target_project: project,
                 target_branch: target_branch)
        end

        it 'returns an associated merge request' do
          expect(pipeline.all_merge_requests).to eq([merge_request])
        end

        context 'when there is another merge request pipeline that targets a different branch' do
          let(:target_branch_2) { 'merge-test' }

          let!(:pipeline_2) do
            create(:ci_pipeline,
                   source: :merge_request_event,
                   project: pipeline_project,
                   ref: source_branch,
                   merge_request: merge_request_2)
          end

          let(:merge_request_2) do
            create(:merge_request,
                   source_project: pipeline_project,
                   source_branch: source_branch,
                   target_project: project,
                   target_branch: target_branch_2)
          end

          it 'does not return an associated merge request' do
            expect(pipeline.all_merge_requests).not_to include(merge_request_2)
          end
        end
      end
    end

    it_behaves_like 'a method that returns all merge requests for a given pipeline' do
      let(:pipeline_project) { project }
    end

    context 'for a fork' do
      let(:fork) { fork_project(project) }

      it_behaves_like 'a method that returns all merge requests for a given pipeline' do
        let(:pipeline_project) { fork }
      end
    end
  end

  describe '#related_merge_requests' do
    let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') }
    let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'stable') }
    let(:branch_pipeline) { create(:ci_pipeline, ref: 'feature') }
    let(:merge_pipeline) { create(:ci_pipeline, :detached_merge_request_pipeline, merge_request: merge_request) }

    context 'for a branch pipeline' do
      subject { branch_pipeline.related_merge_requests }

      it 'when no merge request is created' do
        is_expected.to be_empty
      end

      it 'when another merge requests are created' do
        merge_request
        other_merge_request

        is_expected.to contain_exactly(merge_request, other_merge_request)
      end
    end

    context 'for a merge pipeline' do
      subject { merge_pipeline.related_merge_requests }

      it 'when only merge pipeline is created' do
        merge_pipeline

        is_expected.to contain_exactly(merge_request)
      end

      it 'when a merge request is created' do
        merge_pipeline
        other_merge_request

        is_expected.to contain_exactly(merge_request, other_merge_request)
      end
    end
  end

  describe '#open_merge_requests_refs' do
    let!(:pipeline) { create(:ci_pipeline, user: user, ref: 'feature') }
    let!(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') }

    subject { pipeline.open_merge_requests_refs }

    context 'when user is a developer' do
      before do
        project.add_developer(user)
      end

      it 'returns open merge requests' do
        is_expected.to eq([merge_request.to_reference(full: true)])
      end

      it 'does not return closed merge requests' do
        merge_request.close!

        is_expected.to be_empty
      end

      context 'limits amount of returned merge requests' do
        let!(:other_merge_requests) do
          Array.new(4) do |idx|
            create(:merge_request, source_project: project, source_branch: 'feature', target_branch: "master-#{idx}")
          end
        end

        let(:other_merge_requests_refs) do
          other_merge_requests.map { |mr| mr.to_reference(full: true) }
        end

        it 'returns only last 4 in a reverse order' do
          is_expected.to eq(other_merge_requests_refs.reverse)
        end
      end
    end

    context 'when user does not have permissions' do
      it 'does not return any merge requests' do
        is_expected.to be_empty
      end
    end
  end

  describe '#same_family_pipeline_ids' do
    subject { pipeline.same_family_pipeline_ids.map(&:id) }

    let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }

    context 'when pipeline is not child nor parent' do
      it 'returns just the pipeline id' do
        expect(subject).to contain_exactly(pipeline.id)
      end
    end

    context 'when pipeline is child' do
      let(:parent) { create(:ci_pipeline) }
      let!(:pipeline) { create(:ci_pipeline, child_of: parent) }
      let!(:sibling) { create(:ci_pipeline, child_of: parent) }

      it 'returns parent sibling and self ids' do
        expect(subject).to contain_exactly(parent.id, pipeline.id, sibling.id)
      end
    end

    context 'when pipeline is parent' do
      let!(:child) { create(:ci_pipeline, child_of: pipeline) }

      it 'returns self and child ids' do
        expect(subject).to contain_exactly(pipeline.id, child.id)
      end
    end

    context 'when pipeline is a child of a child pipeline' do
      let(:ancestor) { create(:ci_pipeline) }
      let!(:parent) { create(:ci_pipeline, child_of: ancestor) }
      let!(:pipeline) { create(:ci_pipeline, child_of: parent) }
      let!(:cousin_parent) { create(:ci_pipeline, child_of: ancestor) }
      let!(:cousin) { create(:ci_pipeline, child_of: cousin_parent) }

      it 'returns all family ids' do
        expect(subject).to contain_exactly(
          ancestor.id, parent.id, cousin_parent.id, cousin.id, pipeline.id
        )
      end
    end

    context 'when pipeline is a triggered pipeline' do
      let!(:upstream) { create(:ci_pipeline, project: create(:project), upstream_of: pipeline)}

      it 'returns self id' do
        expect(subject).to contain_exactly(pipeline.id)
      end
    end
  end

  describe '#environments_in_self_and_descendants' do
    subject { pipeline.environments_in_self_and_descendants }

    context 'when pipeline is not child nor parent' do
      let_it_be(:pipeline) { create(:ci_pipeline, :created) }
      let_it_be(:build) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) }

      it 'returns just the pipeline environment' do
        expect(subject).to contain_exactly(build.deployment.environment)
      end
    end

    context 'when pipeline is in extended family' do
      let_it_be(:parent) { create(:ci_pipeline) }
      let_it_be(:parent_build) { create(:ci_build, :with_deployment, environment: 'staging', pipeline: parent) }

      let_it_be(:pipeline) { create(:ci_pipeline, child_of: parent) }
      let_it_be(:build) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) }

      let_it_be(:child) { create(:ci_pipeline, child_of: pipeline) }
      let_it_be(:child_build) { create(:ci_build, :with_deployment, environment: 'canary', pipeline: child) }

      let_it_be(:grandchild) { create(:ci_pipeline, child_of: child) }
      let_it_be(:grandchild_build) { create(:ci_build, :with_deployment, environment: 'test', pipeline: grandchild) }

      let_it_be(:sibling) { create(:ci_pipeline, child_of: parent) }
      let_it_be(:sibling_build) { create(:ci_build, :with_deployment, environment: 'review', pipeline: sibling) }

      it 'returns its own environment and from all descendants' do
        expected_environments = [
          build.deployment.environment,
          child_build.deployment.environment,
          grandchild_build.deployment.environment
        ]
        expect(subject).to match_array(expected_environments)
      end

      it 'does not return parent environment' do
        expect(subject).not_to include(parent_build.deployment.environment)
      end

      it 'does not return sibling environment' do
        expect(subject).not_to include(sibling_build.deployment.environment)
      end
    end

    context 'when each pipeline has multiple environments' do
      let_it_be(:pipeline) { create(:ci_pipeline, :created) }
      let_it_be(:build1) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) }
      let_it_be(:build2) { create(:ci_build, :with_deployment, environment: 'staging', pipeline: pipeline) }

      let_it_be(:child) { create(:ci_pipeline, child_of: pipeline) }
      let_it_be(:child_build1) { create(:ci_build, :with_deployment, environment: 'canary', pipeline: child) }
      let_it_be(:child_build2) { create(:ci_build, :with_deployment, environment: 'test', pipeline: child) }

      it 'returns all related environments' do
        expected_environments = [
          build1.deployment.environment,
          build2.deployment.environment,
          child_build1.deployment.environment,
          child_build2.deployment.environment
        ]
        expect(subject).to match_array(expected_environments)
      end
    end

    context 'when pipeline has no environment' do
      let_it_be(:pipeline) { create(:ci_pipeline, :created) }

      it 'returns empty' do
        expect(subject).to be_empty
      end
    end
  end

  describe '#root_ancestor' do
    subject { pipeline.root_ancestor }

    let_it_be(:pipeline) { create(:ci_pipeline) }

    context 'when pipeline is child of child pipeline' do
      let!(:root_ancestor) { create(:ci_pipeline) }
      let!(:parent_pipeline) { create(:ci_pipeline, child_of: root_ancestor) }
      let!(:pipeline) { create(:ci_pipeline, child_of: parent_pipeline) }

      it 'returns the root ancestor' do
        expect(subject).to eq(root_ancestor)
      end
    end

    context 'when pipeline is root ancestor' do
      let!(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) }

      it 'returns itself' do
        expect(subject).to eq(pipeline)
      end
    end

    context 'when pipeline is standalone' do
      it 'returns itself' do
        expect(subject).to eq(pipeline)
      end
    end

    context 'when pipeline is multi-project downstream pipeline' do
      let!(:upstream_pipeline) do
        create(:ci_pipeline, project: create(:project), upstream_of: pipeline)
      end

      it 'ignores cross project ancestors' do
        expect(subject).to eq(pipeline)
      end
    end
  end

  describe '#stuck?' do
    let(:pipeline) { create(:ci_empty_pipeline, :created) }

    before do
      create(:ci_build, :pending, pipeline: pipeline)
    end

    context 'when pipeline is stuck' do
      it 'is stuck' do
        expect(pipeline).to be_stuck
      end
    end

    context 'when pipeline is not stuck' do
      before do
        create(:ci_runner, :instance, :online)
      end

      it 'is not stuck' do
        expect(pipeline).not_to be_stuck
      end
    end
  end

  describe '#add_error_message' do
    let(:pipeline) { build_stubbed(:ci_pipeline) }

    it 'adds a new pipeline error message' do
      pipeline.add_error_message('The error message')

      expect(pipeline.messages.map(&:content)).to contain_exactly('The error message')
    end
  end

  describe '#has_yaml_errors?' do
    let(:pipeline) { build_stubbed(:ci_pipeline) }

    context 'when yaml_errors is set' do
      before do
        pipeline.yaml_errors = 'File not found'
      end

      it 'returns true if yaml_errors is set' do
        expect(pipeline).to have_yaml_errors
        expect(pipeline.yaml_errors).to include('File not foun')
      end
    end

    it 'returns false if yaml_errors is not set' do
      expect(pipeline).not_to have_yaml_errors
    end
  end

  describe 'notifications when pipeline success or failed' do
    let(:namespace) { create(:namespace) }
    let(:project) { create(:project, :repository, namespace: namespace) }

    let(:pipeline) do
      create(:ci_pipeline,
             project: project,
             sha: project.commit('master').sha,
             user: project.owner)
    end

    before do
      project.add_developer(pipeline.user)

      pipeline.user.global_notification_setting
        .update!(level: 'custom', failed_pipeline: true, success_pipeline: true)

      perform_enqueued_jobs do
        pipeline.enqueue
        pipeline.run
      end
    end

    shared_examples 'sending a notification' do
      it 'sends an email', :sidekiq_might_not_need_inline do
        should_only_email(pipeline.user, kind: :bcc)
      end
    end

    shared_examples 'not sending any notification' do
      it 'does not send any email' do
        should_not_email_anyone
      end
    end

    context 'with success pipeline' do
      it_behaves_like 'sending a notification' do
        before do
          perform_enqueued_jobs do
            pipeline.succeed
          end
        end
      end

      it 'enqueues PipelineNotificationWorker' do
        expect(PipelineNotificationWorker)
          .to receive(:perform_async).with(pipeline.id, ref_status: :success)

        pipeline.succeed
      end

      context 'when pipeline is not the latest' do
        before do
          create(:ci_pipeline, :success, ci_ref: pipeline.ci_ref)
        end

        it 'does not pass ref_status' do
          expect(PipelineNotificationWorker)
            .to receive(:perform_async).with(pipeline.id, ref_status: nil)

          pipeline.succeed!
        end
      end
    end

    context 'with failed pipeline' do
      it_behaves_like 'sending a notification' do
        before do
          perform_enqueued_jobs do
            create(:ci_build, :failed, pipeline: pipeline)
            create(:generic_commit_status, :failed, pipeline: pipeline)

            pipeline.drop
          end
        end
      end

      it 'enqueues PipelineNotificationWorker' do
        expect(PipelineNotificationWorker)
          .to receive(:perform_async).with(pipeline.id, ref_status: :failed)

        pipeline.drop
      end
    end

    context 'with skipped pipeline' do
      before do
        perform_enqueued_jobs do
          pipeline.skip
        end
      end

      it_behaves_like 'not sending any notification'
    end

    context 'with cancelled pipeline' do
      before do
        perform_enqueued_jobs do
          pipeline.cancel
        end
      end

      it_behaves_like 'not sending any notification'
    end
  end

  describe 'updates ci_ref when pipeline finished' do
    context 'when ci_ref exists' do
      let!(:pipeline) { create(:ci_pipeline, :running) }

      it 'updates the ci_ref' do
        expect(pipeline.ci_ref)
          .to receive(:update_status_by!).with(pipeline).and_call_original

        pipeline.succeed!
      end
    end

    context 'when ci_ref does not exist' do
      let!(:pipeline) { create(:ci_pipeline, :running, ci_ref_presence: false) }

      it 'does not raise an exception' do
        expect { pipeline.succeed! }.not_to raise_error
      end
    end
  end

  describe '#ensure_ci_ref!' do
    subject { pipeline.ensure_ci_ref! }

    context 'when ci_ref does not exist yet' do
      let!(:pipeline) { create(:ci_pipeline, ci_ref_presence: false) }

      it 'creates a new ci_ref and assigns it' do
        expect { subject }.to change { Ci::Ref.count }.by(1)

        expect(pipeline.ci_ref).to be_present
      end
    end

    context 'when ci_ref already exists' do
      let!(:pipeline) { create(:ci_pipeline) }

      it 'fetches a new ci_ref and assigns it' do
        expect { subject }.not_to change { Ci::Ref.count }

        expect(pipeline.ci_ref).to be_present
      end
    end
  end

  describe '#builds_in_self_and_descendants' do
    subject(:builds) { pipeline.builds_in_self_and_descendants }

    let(:pipeline) { create(:ci_pipeline) }
    let!(:build) { create(:ci_build, pipeline: pipeline) }

    context 'when pipeline is standalone' do
      it 'returns the list of builds' do
        expect(builds).to contain_exactly(build)
      end
    end

    context 'when pipeline is parent of another pipeline' do
      let(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) }
      let!(:child_build) { create(:ci_build, pipeline: child_pipeline) }

      it 'returns the list of builds' do
        expect(builds).to contain_exactly(build, child_build)
      end
    end

    context 'when pipeline is parent of another parent pipeline' do
      let(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) }
      let!(:child_build) { create(:ci_build, pipeline: child_pipeline) }
      let(:child_of_child_pipeline) { create(:ci_pipeline, child_of: child_pipeline) }
      let!(:child_of_child_build) { create(:ci_build, pipeline: child_of_child_pipeline) }

      it 'returns the list of builds' do
        expect(builds).to contain_exactly(build, child_build, child_of_child_build)
      end
    end
  end

  describe '#build_with_artifacts_in_self_and_descendants' do
    let_it_be(:pipeline) { create(:ci_pipeline) }

    let!(:build) { create(:ci_build, name: 'test', pipeline: pipeline) }
    let(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) }
    let!(:child_build) { create(:ci_build, :artifacts, name: 'test', pipeline: child_pipeline) }

    it 'returns the build with a given name, having artifacts' do
      expect(pipeline.build_with_artifacts_in_self_and_descendants('test')).to eq(child_build)
    end

    context 'when same job name is present in both parent and child pipeline' do
      let!(:build) { create(:ci_build, :artifacts, name: 'test', pipeline: pipeline) }

      it 'returns the job in the parent pipeline' do
        expect(pipeline.build_with_artifacts_in_self_and_descendants('test')).to eq(build)
      end
    end
  end

  describe '#find_job_with_archive_artifacts' do
    let(:pipeline) { create(:ci_pipeline) }
    let!(:old_job) { create(:ci_build, name: 'rspec', retried: true, pipeline: pipeline) }
    let!(:job_without_artifacts) { create(:ci_build, name: 'rspec', pipeline: pipeline) }
    let!(:expected_job) { create(:ci_build, :artifacts, name: 'rspec', pipeline: pipeline ) }
    let!(:different_job) { create(:ci_build, name: 'deploy', pipeline: pipeline) }

    subject { pipeline.find_job_with_archive_artifacts('rspec') }

    it 'finds the expected job' do
      expect(subject).to eq(expected_job)
    end
  end

  describe '#latest_builds_with_artifacts' do
    let(:pipeline) { create(:ci_pipeline) }
    let!(:fresh_build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
    let!(:stale_build) { create(:ci_build, :success, :expired, :artifacts, pipeline: pipeline) }

    it 'returns an Array' do
      expect(pipeline.latest_builds_with_artifacts).to be_an_instance_of(Array)
    end

    it 'returns the latest builds with non-expired artifacts' do
      expect(pipeline.latest_builds_with_artifacts).to contain_exactly(fresh_build)
    end

    it 'does not return builds with expired artifacts' do
      expect(pipeline.latest_builds_with_artifacts).not_to include(stale_build)
    end

    it 'memoizes the returned relation' do
      query_count = ActiveRecord::QueryRecorder
        .new { 2.times { pipeline.latest_builds_with_artifacts.to_a } }
        .count

      expect(query_count).to eq(1)
    end
  end

  describe '#batch_lookup_report_artifact_for_file_type' do
    context 'with code quality report artifact' do
      let(:pipeline) { create(:ci_pipeline, :with_codequality_reports) }

      it "returns the code quality artifact" do
        expect(pipeline.batch_lookup_report_artifact_for_file_type(:codequality)).to eq(pipeline.job_artifacts.sample)
      end
    end
  end

  describe '#latest_report_builds' do
    let_it_be(:pipeline) { create(:ci_pipeline, project: project) }

    it 'returns build with test artifacts' do
      test_build = create(:ci_build, :test_reports, pipeline: pipeline)
      coverage_build = create(:ci_build, :coverage_reports, pipeline: pipeline)
      create(:ci_build, :artifacts, pipeline: pipeline, project: project)

      expect(pipeline.latest_report_builds).to contain_exactly(test_build, coverage_build)
    end

    it 'filters builds by scope' do
      test_build = create(:ci_build, :test_reports, pipeline: pipeline)
      create(:ci_build, :coverage_reports, pipeline: pipeline)

      expect(pipeline.latest_report_builds(Ci::JobArtifact.test_reports)).to contain_exactly(test_build)
    end

    it 'only returns not retried builds' do
      test_build = create(:ci_build, :test_reports, pipeline: pipeline)
      create(:ci_build, :test_reports, :retried, pipeline: pipeline)

      expect(pipeline.latest_report_builds).to contain_exactly(test_build)
    end
  end

  describe '#has_reports?' do
    subject { pipeline.has_reports?(Ci::JobArtifact.test_reports) }

    context 'when pipeline has builds with test reports' do
      before do
        create(:ci_build, :test_reports, pipeline: pipeline)
      end

      context 'when pipeline status is running' do
        let(:pipeline) { create(:ci_pipeline, :running) }

        it { is_expected.to be_falsey }
      end

      context 'when pipeline status is success' do
        let(:pipeline) { create(:ci_pipeline, :success) }

        it { is_expected.to be_truthy }
      end
    end

    context 'when pipeline does not have builds with test reports' do
      before do
        create(:ci_build, :artifacts, pipeline: pipeline)
      end

      let(:pipeline) { create(:ci_pipeline, :success) }

      it { is_expected.to be_falsey }
    end

    context 'when retried build has test reports' do
      before do
        create(:ci_build, :retried, :test_reports, pipeline: pipeline)
      end

      let(:pipeline) { create(:ci_pipeline, :success) }

      it { is_expected.to be_falsey }
    end
  end

  describe '#has_coverage_reports?' do
    subject { pipeline.has_coverage_reports? }

    context 'when pipeline has a code coverage artifact' do
      let(:pipeline) { create(:ci_pipeline, :with_coverage_report_artifact, :running) }

      it { expect(subject).to be_truthy }
    end

    context 'when pipeline does not have a code coverage artifact' do
      let(:pipeline) { create(:ci_pipeline, :success) }

      it { expect(subject).to be_falsey }
    end
  end

  describe '#can_generate_coverage_reports?' do
    subject { pipeline.can_generate_coverage_reports? }

    context 'when pipeline has builds with coverage reports' do
      before do
        create(:ci_build, :coverage_reports, pipeline: pipeline)
      end

      context 'when pipeline status is running' do
        let(:pipeline) { create(:ci_pipeline, :running) }

        it { expect(subject).to be_falsey }
      end

      context 'when pipeline status is success' do
        let(:pipeline) { create(:ci_pipeline, :success) }

        it { expect(subject).to be_truthy }
      end
    end

    context 'when pipeline does not have builds with coverage reports' do
      before do
        create(:ci_build, :artifacts, pipeline: pipeline)
      end

      let(:pipeline) { create(:ci_pipeline, :success) }

      it { expect(subject).to be_falsey }
    end
  end

  describe '#has_codequality_mr_diff_report?' do
    subject { pipeline.has_codequality_mr_diff_report? }

    context 'when pipeline has a codequality mr diff report' do
      let(:pipeline) { create(:ci_pipeline, :with_codequality_mr_diff_report, :running) }

      it { expect(subject).to be_truthy }
    end

    context 'when pipeline does not have a codequality mr diff report' do
      let(:pipeline) { create(:ci_pipeline, :success) }

      it { expect(subject).to be_falsey }
    end
  end

  describe '#can_generate_codequality_reports?' do
    subject { pipeline.can_generate_codequality_reports? }

    context 'when pipeline has builds with codequality reports' do
      before do
        create(:ci_build, :codequality_reports, pipeline: pipeline)
      end

      context 'when pipeline status is running' do
        let(:pipeline) { create(:ci_pipeline, :running) }

        it { expect(subject).to be_falsey }
      end

      context 'when pipeline status is success' do
        let(:pipeline) { create(:ci_pipeline, :success) }

        it 'can generate a codequality report' do
          expect(subject).to be_truthy
        end
      end
    end

    context 'when pipeline does not have builds with codequality reports' do
      before do
        create(:ci_build, :artifacts, pipeline: pipeline)
      end

      let(:pipeline) { create(:ci_pipeline, :success) }

      it { expect(subject).to be_falsey }
    end
  end

  describe '#test_report_summary' do
    subject { pipeline.test_report_summary }

    let(:pipeline) { create(:ci_pipeline, :success) }

    context 'when pipeline has multiple builds with report results' do
      before do
        create(:ci_build, :success, :report_results, name: 'rspec', pipeline: pipeline)
        create(:ci_build, :success, :report_results, name: 'java', pipeline: pipeline)
      end

      it 'returns test report summary with collected data' do
        expect(subject.total).to include(time: 0.84, count: 4, success: 0, failed: 0, skipped: 0, error: 4)
      end
    end

    context 'when pipeline does not have any builds with report results' do
      it 'returns empty test report summary' do
        expect(subject.total).to include(time: 0, count: 0, success: 0, failed: 0, skipped: 0, error: 0)
      end
    end
  end

  describe '#test_reports' do
    subject { pipeline.test_reports }

    let_it_be(:pipeline) { create(:ci_pipeline) }

    context 'when pipeline has multiple builds with test reports' do
      let!(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline) }
      let!(:build_java) { create(:ci_build, :success, name: 'java', pipeline: pipeline) }

      before do
        create(:ci_job_artifact, :junit, job: build_rspec)
        create(:ci_job_artifact, :junit_with_ant, job: build_java)
      end

      it 'returns test reports with collected data' do
        expect(subject.total_count).to be(7)
        expect(subject.success_count).to be(5)
        expect(subject.failed_count).to be(2)
      end

      context 'when builds are retried' do
        let!(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline) }
        let!(:build_java) { create(:ci_build, :retried, :success, name: 'java', pipeline: pipeline) }

        it 'does not take retried builds into account' do
          expect(subject.total_count).to be(0)
          expect(subject.success_count).to be(0)
          expect(subject.failed_count).to be(0)
        end
      end
    end

    context 'when pipeline does not have any builds with test reports' do
      it 'returns empty test reports' do
        expect(subject.total_count).to be(0)
      end
    end
  end

  describe '#accessibility_reports' do
    subject { pipeline.accessibility_reports }

    let_it_be(:pipeline) { create(:ci_pipeline) }

    context 'when pipeline has multiple builds with accessibility reports' do
      let(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline) }
      let(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline) }

      before do
        create(:ci_job_artifact, :accessibility, job: build_rspec)
        create(:ci_job_artifact, :accessibility_without_errors, job: build_golang)
      end

      it 'returns accessibility report with collected data' do
        expect(subject.urls.keys).to match_array([
          "https://pa11y.org/",
          "https://about.gitlab.com/"
        ])
      end

      context 'when builds are retried' do
        let(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline) }
        let(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline) }

        it 'returns empty urls for accessibility reports' do
          expect(subject.urls).to be_empty
        end
      end
    end

    context 'when pipeline does not have any builds with accessibility reports' do
      it 'returns empty urls for accessibility reports' do
        expect(subject.urls).to be_empty
      end
    end
  end

  describe '#coverage_reports' do
    subject { pipeline.coverage_reports }

    let_it_be(:pipeline) { create(:ci_pipeline) }

    context 'when pipeline has multiple builds with coverage reports' do
      let!(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline) }
      let!(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline) }

      before do
        create(:ci_job_artifact, :cobertura, job: build_rspec)
        create(:ci_job_artifact, :coverage_gocov_xml, job: build_golang)
      end

      it 'returns coverage reports with collected data' do
        expect(subject.files.keys).to match_array([
          "auth/token.go",
          "auth/rpccredentials.go",
          "app/controllers/abuse_reports_controller.rb"
        ])
      end

      it 'does not execute N+1 queries' do
        single_build_pipeline = create(:ci_empty_pipeline, :created)
        single_rspec = create(:ci_build, :success, name: 'rspec', pipeline: single_build_pipeline)
        create(:ci_job_artifact, :cobertura, job: single_rspec, project: project)

        control = ActiveRecord::QueryRecorder.new { single_build_pipeline.coverage_reports }

        expect { subject }.not_to exceed_query_limit(control)
      end

      context 'when builds are retried' do
        let!(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline) }
        let!(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline) }

        it 'does not take retried builds into account' do
          expect(subject.files).to eql({})
        end
      end
    end

    context 'when pipeline does not have any builds with coverage reports' do
      it 'returns empty coverage reports' do
        expect(subject.files).to eql({})
      end
    end
  end

  describe '#codequality_reports' do
    subject(:codequality_reports) { pipeline.codequality_reports }

    let_it_be(:pipeline) { create(:ci_pipeline) }

    context 'when pipeline has multiple builds with codequality reports' do
      let(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline) }
      let(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline) }

      before do
        create(:ci_job_artifact, :codequality, job: build_rspec)
        create(:ci_job_artifact, :codequality_without_errors, job: build_golang)
      end

      it 'returns codequality report with collected data' do
        expect(codequality_reports.degradations_count).to eq(3)
      end

      context 'when builds are retried' do
        let(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline) }
        let(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline) }

        it 'returns a codequality reports without degradations' do
          expect(codequality_reports.degradations).to be_empty
        end
      end
    end

    context 'when pipeline does not have any builds with codequality reports' do
      it 'returns codequality reports without degradations' do
        expect(codequality_reports.degradations).to be_empty
      end
    end
  end

  describe '#uses_needs?' do
    let_it_be(:pipeline) { create(:ci_pipeline) }

    context 'when the scheduling type is `dag`' do
      it 'returns true' do
        create(:ci_build, pipeline: pipeline, scheduling_type: :dag)

        expect(pipeline.uses_needs?).to eq(true)
      end
    end

    context 'when the scheduling type is nil or stage' do
      it 'returns false' do
        create(:ci_build, pipeline: pipeline, scheduling_type: :stage)

        expect(pipeline.uses_needs?).to eq(false)
      end
    end
  end

  describe '#total_size' do
    let(:pipeline) { create(:ci_pipeline) }
    let!(:build_job1) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
    let!(:build_job2) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
    let!(:test_job_failed_and_retried) { create(:ci_build, :failed, :retried, pipeline: pipeline, stage_idx: 1) }
    let!(:second_test_job) { create(:ci_build, pipeline: pipeline, stage_idx: 1) }
    let!(:deploy_job) { create(:ci_build, pipeline: pipeline, stage_idx: 2) }

    it 'returns all jobs (including failed and retried)' do
      expect(pipeline.total_size).to eq(5)
    end
  end

  describe '#status' do
    context 'when transitioning to failed' do
      context 'when pipeline has autodevops as source' do
        let(:pipeline) { create(:ci_pipeline, :running, :auto_devops_source) }

        it 'calls autodevops disable service' do
          expect(AutoDevops::DisableWorker).to receive(:perform_async).with(pipeline.id)

          pipeline.drop
        end
      end

      context 'when pipeline has other source' do
        let(:pipeline) { create(:ci_pipeline, :running, :repository_source) }

        it 'does not call auto devops disable service' do
          expect(AutoDevops::DisableWorker).not_to receive(:perform_async)

          pipeline.drop
        end
      end

      context 'with failure_reason' do
        let(:pipeline) { create(:ci_pipeline, :running) }
        let(:failure_reason) { 'config_error' }
        let(:counter) { Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') }

        it 'increments the counter with the failure_reason' do
          expect { pipeline.drop!(failure_reason) }.to change { counter.get(reason: failure_reason) }.by(1)
        end
      end
    end
  end

  describe '#default_branch?' do
    subject { pipeline.default_branch? }

    context 'when pipeline ref is the default branch of the project' do
      let(:pipeline) do
        build(:ci_empty_pipeline, :created, project: project, ref: project.default_branch)
      end

      it "returns true" do
        expect(subject).to be_truthy
      end
    end

    context 'when pipeline ref is not the default branch of the project' do
      let(:pipeline) do
        build(:ci_empty_pipeline, :created, project: project, ref: 'another_branch')
      end

      it "returns false" do
        expect(subject).to be_falsey
      end
    end
  end

  describe '#find_stage_by_name' do
    let_it_be(:pipeline) { create(:ci_pipeline) }

    let(:stage_name) { 'test' }

    let(:stage) do
      create(:ci_stage_entity,
             pipeline: pipeline,
             project: pipeline.project,
             name: 'test')
    end

    before do
      create_list(:ci_build, 2, pipeline: pipeline, stage: stage.name)
    end

    subject { pipeline.find_stage_by_name!(stage_name) }

    context 'when stage exists' do
      it { is_expected.to eq(stage) }
    end

    context 'when stage does not exist' do
      let(:stage_name) { 'build' }

      it 'raises an ActiveRecord exception' do
        expect do
          subject
        end.to raise_exception(ActiveRecord::RecordNotFound)
      end
    end
  end

  describe '#full_error_messages' do
    subject { pipeline.full_error_messages }

    before do
      pipeline.valid?
    end

    context 'when pipeline has errors' do
      let(:pipeline) { build(:ci_pipeline, sha: nil, ref: nil) }

      it 'returns the full error messages' do
        is_expected.to eq("Sha can't be blank and Ref can't be blank")
      end
    end

    context 'when pipeline does not have errors' do
      let(:pipeline) { build(:ci_pipeline) }

      it 'returns empty string' do
        is_expected.to be_empty
      end
    end
  end

  describe '#created_successfully?' do
    subject { pipeline.created_successfully? }

    context 'when pipeline is not persisted' do
      let(:pipeline) { build(:ci_pipeline) }

      it { is_expected.to be_falsey }
    end

    context 'when pipeline is persisted' do
      context 'when pipeline has failure reasons' do
        let(:pipeline) { create(:ci_pipeline, failure_reason: :config_error) }

        it { is_expected.to be_falsey }
      end

      context 'when pipeline has no failure reasons' do
        let(:pipeline) { create(:ci_pipeline, failure_reason: nil) }

        it { is_expected.to be_truthy }
      end
    end
  end

  describe '#parent_pipeline' do
    let_it_be_with_reload(:pipeline) { create(:ci_pipeline) }

    context 'when pipeline is triggered by a pipeline from the same project' do
      let_it_be(:upstream_pipeline) { create(:ci_pipeline) }
      let_it_be(:pipeline) { create(:ci_pipeline, child_of: upstream_pipeline) }

      it 'returns the parent pipeline' do
        expect(pipeline.parent_pipeline).to eq(upstream_pipeline)
      end

      it 'is child' do
        expect(pipeline).to be_child
      end
    end

    context 'when pipeline is triggered by a pipeline from another project' do
      let(:pipeline) { create(:ci_pipeline) }
      let!(:upstream_pipeline) { create(:ci_pipeline, project: create(:project), upstream_of: pipeline) }

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

      it 'is not child' do
        expect(pipeline).not_to be_child
      end
    end

    context 'when pipeline is not triggered by a pipeline' do
      let_it_be(:pipeline) { create(:ci_pipeline) }

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

      it 'is not child' do
        expect(pipeline).not_to be_child
      end
    end
  end

  describe '#child_pipelines' do
    let_it_be(:project) { create(:project) }
    let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) }

    context 'when pipeline triggered other pipelines on same project' do
      let(:downstream_pipeline) { create(:ci_pipeline, project: pipeline.project) }

      before do
        create(:ci_sources_pipeline,
          source_pipeline: pipeline,
          source_project: pipeline.project,
          pipeline: downstream_pipeline,
          project: pipeline.project)
      end

      it 'returns the child pipelines' do
        expect(pipeline.child_pipelines).to eq [downstream_pipeline]
      end

      it 'is parent' do
        expect(pipeline).to be_parent
      end
    end

    context 'when pipeline triggered other pipelines on another project' do
      let(:downstream_pipeline) { create(:ci_pipeline) }

      before do
        create(:ci_sources_pipeline,
          source_pipeline: pipeline,
          source_project: pipeline.project,
          pipeline: downstream_pipeline,
          project: downstream_pipeline.project)
      end

      it 'returns empty array' do
        expect(pipeline.child_pipelines).to be_empty
      end

      it 'is not parent' do
        expect(pipeline).not_to be_parent
      end
    end

    context 'when pipeline did not trigger any pipelines' do
      it 'returns empty array' do
        expect(pipeline.child_pipelines).to be_empty
      end

      it 'is not parent' do
        expect(pipeline).not_to be_parent
      end
    end
  end

  describe 'upstream status interactions' do
    let_it_be_with_reload(:pipeline) { create(:ci_pipeline, :created) }

    context 'when a pipeline has an upstream status' do
      context 'when an upstream status is a bridge' do
        let(:bridge) { create(:ci_bridge, status: :pending) }

        before do
          create(:ci_sources_pipeline, pipeline: pipeline, source_job: bridge)
        end

        describe '#bridge_triggered?' do
          it 'is a pipeline triggered by a bridge' do
            expect(pipeline).to be_bridge_triggered
          end
        end

        describe '#source_job' do
          it 'has a correct source job' do
            expect(pipeline.source_job).to eq bridge
          end
        end

        describe '#source_bridge' do
          it 'has a correct bridge source' do
            expect(pipeline.source_bridge).to eq bridge
          end
        end
      end

      context 'when an upstream status is a build' do
        let(:build) { create(:ci_build) }

        before do
          create(:ci_sources_pipeline, pipeline: pipeline, source_job: build)
        end

        describe '#bridge_triggered?' do
          it 'is a pipeline that has not been triggered by a bridge' do
            expect(pipeline).not_to be_bridge_triggered
          end
        end

        describe '#source_job' do
          it 'has a correct source job' do
            expect(pipeline.source_job).to eq build
          end
        end

        describe '#source_bridge' do
          it 'does not have a bridge source' do
            expect(pipeline.source_bridge).to be_nil
          end
        end
      end
    end
  end

  describe '#source_ref_path' do
    subject { pipeline.source_ref_path }

    let(:pipeline) { create(:ci_pipeline, :created) }

    context 'when pipeline is for a branch' do
      it { is_expected.to eq(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.source_ref.to_s) }
    end

    context 'when pipeline is for a merge request' do
      let(:merge_request) { create(:merge_request, source_project: project) }
      let(:pipeline) { create(:ci_pipeline, project: project, head_pipeline_of: merge_request) }

      it { is_expected.to eq(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.source_ref.to_s) }
    end

    context 'when pipeline is for a tag' do
      let(:pipeline) { create(:ci_pipeline, tag: true) }

      it { is_expected.to eq(Gitlab::Git::TAG_REF_PREFIX + pipeline.source_ref.to_s) }
    end
  end

  describe '#builds_with_coverage' do
    let_it_be(:pipeline) { create(:ci_pipeline, :created) }

    it 'returns builds with coverage only' do
      rspec = create(:ci_build, name: 'rspec', coverage: 97.1, pipeline: pipeline)
      jest  = create(:ci_build, name: 'jest', coverage: 94.1, pipeline: pipeline)
      karma = create(:ci_build, name: 'karma', coverage: nil, pipeline: pipeline)

      builds = pipeline.builds_with_coverage

      expect(builds).to include(rspec, jest)
      expect(builds).not_to include(karma)
    end

    it 'returns only latest builds' do
      obsolete = create(:ci_build, name: "jest", coverage: 10.12, pipeline: pipeline, retried: true)
      retried  = create(:ci_build, name: "jest", coverage: 20.11, pipeline: pipeline)

      builds = pipeline.builds_with_coverage

      expect(builds).to include(retried)
      expect(builds).not_to include(obsolete)
    end
  end

  describe '#self_and_upstreams' do
    subject(:self_and_upstreams) { pipeline.self_and_upstreams }

    let_it_be(:pipeline) { create(:ci_pipeline, :created) }

    context 'when pipeline is not child nor parent' do
      it 'returns just the pipeline itself' do
        expect(self_and_upstreams).to contain_exactly(pipeline)
      end
    end

    context 'when pipeline is child' do
      let(:parent) { create(:ci_pipeline) }
      let(:sibling) { create(:ci_pipeline) }

      before do
        create_source_pipeline(parent, pipeline)
        create_source_pipeline(parent, sibling)
      end

      it 'returns parent and self' do
        expect(self_and_upstreams).to contain_exactly(parent, pipeline)
      end
    end

    context 'when pipeline is parent' do
      let(:child) { create(:ci_pipeline) }

      before do
        create_source_pipeline(pipeline, child)
      end

      it 'returns self' do
        expect(self_and_upstreams).to contain_exactly(pipeline)
      end
    end

    context 'when pipeline is a child of a child pipeline' do
      let_it_be(:pipeline) { create(:ci_pipeline, :created) }

      let(:ancestor) { create(:ci_pipeline) }
      let(:parent) { create(:ci_pipeline) }

      before do
        create_source_pipeline(ancestor, parent)
        create_source_pipeline(parent, pipeline)
      end

      it 'returns self, parent and ancestor' do
        expect(self_and_upstreams).to contain_exactly(ancestor, parent, pipeline)
      end
    end

    context 'when pipeline is a triggered pipeline from a different project' do
      let_it_be(:pipeline) { create(:ci_pipeline, :created) }

      let(:upstream) { create(:ci_pipeline, project: create(:project)) }

      before do
        create_source_pipeline(upstream, pipeline)
      end

      it 'returns upstream and self' do
        expect(self_and_upstreams).to contain_exactly(pipeline, upstream)
      end
    end
  end

  describe '#self_and_ancestors' do
    subject(:self_and_ancestors) { pipeline.self_and_ancestors }

    context 'when pipeline is child' do
      let(:pipeline) { create(:ci_pipeline, :created) }
      let(:parent) { create(:ci_pipeline) }
      let(:sibling) { create(:ci_pipeline) }

      before do
        create_source_pipeline(parent, pipeline)
        create_source_pipeline(parent, sibling)
      end

      it 'returns parent and self' do
        expect(self_and_ancestors).to contain_exactly(parent, pipeline)
      end
    end

    context 'when pipeline is a triggered pipeline from a different project' do
      let_it_be(:pipeline) { create(:ci_pipeline, :created) }

      let(:upstream) { create(:ci_pipeline, project: create(:project)) }

      before do
        create_source_pipeline(upstream, pipeline)
      end

      it 'returns only self' do
        expect(self_and_ancestors).to contain_exactly(pipeline)
      end
    end
  end

  describe '#reset_source_bridge!' do
    let(:pipeline) { create(:ci_pipeline, :created, project: project) }

    subject(:reset_bridge) { pipeline.reset_source_bridge!(project.owner) }

    # This whole block will be removed by https://gitlab.com/gitlab-org/gitlab/-/issues/329194
    # It contains some duplicate checks.
    context 'when the FF ci_reset_bridge_with_subsequent_jobs is disabled' do
      before do
        stub_feature_flags(ci_reset_bridge_with_subsequent_jobs: false)
      end

      context 'when the pipeline is a child pipeline and the bridge is depended' do
        let!(:parent_pipeline) { create(:ci_pipeline) }
        let!(:bridge) { create_bridge(parent_pipeline, pipeline, true) }

        it 'marks source bridge as pending' do
          reset_bridge

          expect(bridge.reload).to be_pending
        end

        context 'when the parent pipeline has subsequent jobs after the bridge' do
          let!(:after_bridge_job) { create(:ci_build, :skipped, pipeline: parent_pipeline, stage_idx: bridge.stage_idx + 1) }

          it 'does not touch subsequent jobs of the bridge' do
            reset_bridge

            expect(after_bridge_job.reload).to be_skipped
          end
        end

        context 'when the parent pipeline has a dependent upstream pipeline' do
          let(:upstream_pipeline) { create(:ci_pipeline, project: create(:project)) }
          let!(:upstream_bridge) { create_bridge(upstream_pipeline, parent_pipeline, true) }

          let(:upstream_upstream_pipeline) { create(:ci_pipeline, project: create(:project)) }
          let!(:upstream_upstream_bridge) { create_bridge(upstream_upstream_pipeline, upstream_pipeline, true) }

          it 'marks all source bridges as pending' do
            reset_bridge

            expect(bridge.reload).to be_pending
            expect(upstream_bridge.reload).to be_pending
            expect(upstream_upstream_bridge.reload).to be_pending
          end
        end
      end
    end

    context 'when the pipeline is a child pipeline and the bridge is depended' do
      let!(:parent_pipeline) { create(:ci_pipeline) }
      let!(:bridge) { create_bridge(parent_pipeline, pipeline, true) }

      it 'marks source bridge as pending' do
        reset_bridge

        expect(bridge.reload).to be_pending
      end

      context 'when the parent pipeline has subsequent jobs after the bridge' do
        let!(:after_bridge_job) { create(:ci_build, :skipped, pipeline: parent_pipeline, stage_idx: bridge.stage_idx + 1) }

        it 'marks subsequent jobs of the bridge as processable' do
          reset_bridge

          expect(after_bridge_job.reload).to be_created
        end
      end

      context 'when the parent pipeline has a dependent upstream pipeline' do
        let!(:upstream_bridge) do
          create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true)
        end

        it 'marks all source bridges as pending' do
          reset_bridge

          expect(bridge.reload).to be_pending
          expect(upstream_bridge.reload).to be_pending
        end
      end
    end

    context 'when the pipeline is a child pipeline and the bridge is not depended' do
      let!(:parent_pipeline) { create(:ci_pipeline) }
      let!(:bridge) { create_bridge(parent_pipeline, pipeline, false) }

      it 'does not touch source bridge' do
        reset_bridge

        expect(bridge.reload).to be_success
      end

      context 'when the parent pipeline has a dependent upstream pipeline' do
        let!(:upstream_bridge) do
          create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true)
        end

        it 'does not touch any source bridge' do
          reset_bridge

          expect(bridge.reload).to be_success
          expect(upstream_bridge.reload).to be_success
        end
      end
    end

    private

    def create_bridge(upstream, downstream, depend = false)
      options = depend ? { trigger: { strategy: 'depend' } } : {}

      bridge = create(:ci_bridge, pipeline: upstream, status: 'success', options: options)
      create(:ci_sources_pipeline, pipeline: downstream, source_job: bridge)

      bridge
    end
  end

  describe 'test failure history processing' do
    let(:pipeline) { build(:ci_pipeline, :created) }

    it 'performs the service asynchronously when the pipeline is completed' do
      service = double

      expect(Ci::TestFailureHistoryService).to receive(:new).with(pipeline).and_return(service)
      expect(service).to receive_message_chain(:async, :perform_if_needed)

      pipeline.succeed!
    end
  end

  describe '#latest_test_report_builds' do
    let_it_be(:pipeline) { create(:ci_pipeline, :created) }

    it 'returns pipeline builds with test report artifacts' do
      test_build = create(:ci_build, :test_reports, pipeline: pipeline)
      create(:ci_build, :artifacts, pipeline: pipeline, project: project)

      expect(pipeline.latest_test_report_builds).to contain_exactly(test_build)
    end

    it 'preloads project on each build to avoid N+1 queries' do
      create(:ci_build, :test_reports, pipeline: pipeline)

      control_count = ActiveRecord::QueryRecorder.new do
        pipeline.latest_test_report_builds.map(&:project).map(&:full_path)
      end

      multi_build_pipeline = create(:ci_empty_pipeline, :created)
      create(:ci_build, :test_reports, pipeline: multi_build_pipeline, project: project)
      create(:ci_build, :test_reports, pipeline: multi_build_pipeline, project: project)

      expect { multi_build_pipeline.latest_test_report_builds.map(&:project).map(&:full_path) }
        .not_to exceed_query_limit(control_count)
    end
  end

  describe '#builds_with_failed_tests' do
    let_it_be(:pipeline) { create(:ci_pipeline, :created) }

    it 'returns pipeline builds with test report artifacts' do
      failed_build = create(:ci_build, :failed, :test_reports, pipeline: pipeline)
      create(:ci_build, :success, :test_reports, pipeline: pipeline)

      expect(pipeline.builds_with_failed_tests).to contain_exactly(failed_build)
    end

    it 'supports limiting the number of builds to fetch' do
      create(:ci_build, :failed, :test_reports, pipeline: pipeline)
      create(:ci_build, :failed, :test_reports, pipeline: pipeline)

      expect(pipeline.builds_with_failed_tests(limit: 1).count).to eq(1)
    end

    it 'preloads project on each build to avoid N+1 queries' do
      create(:ci_build, :failed, :test_reports, pipeline: pipeline)

      control_count = ActiveRecord::QueryRecorder.new do
        pipeline.builds_with_failed_tests.map(&:project).map(&:full_path)
      end

      multi_build_pipeline = create(:ci_empty_pipeline, :created)
      create(:ci_build, :failed, :test_reports, pipeline: multi_build_pipeline)
      create(:ci_build, :failed, :test_reports, pipeline: multi_build_pipeline)

      expect { multi_build_pipeline.builds_with_failed_tests.map(&:project).map(&:full_path) }
        .not_to exceed_query_limit(control_count)
    end
  end

  describe '#build_matchers' do
    let_it_be(:user) { create(:user) }
    let_it_be(:pipeline) { create(:ci_pipeline, user: user) }
    let_it_be(:builds) { create_list(:ci_build, 2, pipeline: pipeline, project: pipeline.project, user: user) }

    let(:project) { pipeline.project }

    subject(:matchers) { pipeline.build_matchers }

    it 'returns build matchers' do
      expect(matchers.size).to eq(1)
      expect(matchers).to all be_a(Gitlab::Ci::Matching::BuildMatcher)
      expect(matchers.first.build_ids).to match_array(builds.map(&:id))
    end

    context 'with retried builds' do
      let(:retried_build) { builds.first }

      before do
        stub_not_protect_default_branch
        project.add_developer(user)

        retried_build.cancel!
        ::Ci::Build.retry(retried_build, user)
      end

      it 'does not include retried builds' do
        expect(matchers.size).to eq(1)
        expect(matchers.first.build_ids).not_to include(retried_build.id)
      end
    end
  end
end