# frozen_string_literal: true

require 'spec_helper'

describe ReleaseTools::PassingBuild do
  let(:fake_commit) { double('Commit', id: SecureRandom.hex(20)) }
  let(:target_branch) { '11-10-auto-deploy-1234' }
  let(:project) { ReleaseTools::Project::GitlabEe }
  let(:client) { ReleaseTools::GitlabClient }

  subject(:service) { described_class.new(target_branch, project, client: client) }

  before do
    # Reduce our fixture payload
    stub_const('ReleaseTools::Commits::MAX_COMMITS_TO_CHECK', 5)
  end

  shared_examples 'a passing build commit seeker' do
    let(:commits) { instance_double(ReleaseTools::Commits) }

    before do
      allow(ReleaseTools::Commits).to receive(:new).and_return(commits)
    end

    it 'correctly identify the requested operation' do
      expect(service).to receive(:passing_build_commit).with(operation)

      subject
    end

    it 'raises an error without a dev commit' do
      expect(commits).to receive(:latest_successful_on_build)
        .with({ since_last_auto_deploy: true })
        .and_return(nil)

      expect { service.for_auto_deploy_branch }
        .to raise_error(/Unable to find a passing/)
    end

    it 'returns the latest successful commit on Build' do
      expect(commits)
        .to receive(:latest_successful_on_build)
        .with({ since_last_auto_deploy: true })
        .and_return(fake_commit)

      expect(service.for_auto_deploy_branch).to eq(fake_commit)
    end

    it 'returns the latest commit when auto_deploy_tag_latest is enabled' do
      enable_feature(:auto_deploy_tag_latest)

      expect(commits).to receive(:latest)
        .and_return(fake_commit)

      expect(service.for_auto_deploy_branch).to eq(fake_commit)
    end

    context 'when the project is not GitLab' do
      let(:project) { ReleaseTools::Project::Gitaly }

      %w[preparing pending running canceled created].each do |status_name|
        it "rejects unsuccessfull commits - #{status_name}" do
          commit = build(:commit, status: status_name)

          expect(client).to receive(:commit).with(project.auto_deploy_path, ref: commit.id).and_return(commit)
          expect(commits).to receive(:latest_successful_on_build)
            .with({ since_last_auto_deploy: true })
            .and_yield(commit.id)

          # NOTE: we are stubbing Commits#latest_successful_on_build so this test
          # can only verify the block provided by PassingBuild. This is why here
          # we return a boolean
          expect(subject).to be_falsey
        end
      end

      it 'approves successfull commits' do
        commit = build(:commit, status: 'success')

        expect(client).to receive(:commit).with(project.auto_deploy_path, ref: commit.id).and_return(commit)
        expect(commits).to receive(:latest_successful_on_build)
          .with({ since_last_auto_deploy: true })
          .and_yield(commit.id)

        # NOTE: we are stubbing Commits#latest_successful_on_build so this test
        # can only verify the block provided by PassingBuild. This is why here
        # we return a boolean
        expect(subject).to be_truthy
      end
    end

    context 'when the project is GitLab' do
      let(:project) { ReleaseTools::Project::GitlabEe }

      it 'approves successful commits that have a compile-production-assets job' do
        commit = build(:commit, status: 'success')

        expect(client).to receive(:commit).with(project.auto_deploy_path, ref: commit.id).and_return(commit)
        expect(commits).to receive(:latest_successful_on_build)
          .with({ since_last_auto_deploy: true })
          .and_yield(commit.id)

        job1 = build(:job, name: 'test-job')
        job2 = build(:job, name: 'compile-production-assets')
        page = Gitlab::PaginatedResponse.new([job1, job2])

        expect(client)
          .to receive(:pipeline_jobs)
          .with(project.auto_deploy_path, commit.last_pipeline.id)
          .and_return(page)
          .once

        # NOTE: we are stubbing Commits#latest_successful_on_build so this test
        # can only verify the block provided by PassingBuild. This is why here
        # we return a boolean
        expect(subject).to be_truthy
      end

      it 'approves successful commits that have a compile-production-assets and build-assets-image job' do
        commit = build(:commit, status: 'success')

        expect(client).to receive(:commit).with(project.auto_deploy_path, ref: commit.id).and_return(commit)
        expect(commits).to receive(:latest_successful_on_build)
          .with({ since_last_auto_deploy: true })
          .and_yield(commit.id)

        job2 = build(:job, name: 'compile-production-assets')
        job1 = build(:job, name: 'build-assets-image')
        page = Gitlab::PaginatedResponse.new([job1, job2])

        expect(client)
          .to receive(:pipeline_jobs)
          .with(project.auto_deploy_path, commit.last_pipeline.id)
          .and_return(page)
          .once

        # NOTE: we are stubbing Commits#latest_successful_on_build so this test
        # can only verify the block provided by PassingBuild. This is why here
        # we return a boolean
        expect(subject).to be_truthy
      end

      it 'rejects successful commits that do not have a build-assets-image and compile-production-assets job' do
        commit = build(:commit, status: 'success')

        expect(client).to receive(:commit).with(project.auto_deploy_path, ref: commit.id).and_return(commit)
        expect(commits).to receive(:latest_successful_on_build)
          .with({ since_last_auto_deploy: true })
          .and_yield(commit.id)

        job1 = double(:job, name: 'compile-assets')
        job2 = double(:job, name: 'test-job')
        page = Gitlab::PaginatedResponse.new([job1, job2])

        expect(client)
          .to receive(:pipeline_jobs)
          .with(project.auto_deploy_path, commit.last_pipeline.id)
          .and_return(page)
          .once

        # NOTE: we are stubbing Commits#latest_successful_on_build so this test
        # can only verify the block provided by PassingBuild. This is why here
        # we return a boolean
        expect(subject).to be_falsey
      end
    end
  end

  shared_examples 'gitlab delegates to default branch' do
    let(:project) { ReleaseTools::Project::GitlabEe }

    %w[preparing pending running canceled created failed].each do |status_name|
      it "delegates the results to the default branch (success) - #{status_name}" do
        commit = build(:commit, status: status_name)

        expect(client).to receive(:commit).with(project.auto_deploy_path, ref: commit.id).and_return(commit)
        expect(commits).to receive(:latest_successful_on_build)
          .with({ since_last_auto_deploy: true })
          .and_yield(commit.id)

        pipelines = build_list(:pipeline, 2, :success)
        expect(client).to receive(:pipelines)
          .with(
            project.auto_deploy_path,
            {
              sha: commit.id,
              ref: project.default_branch,
              status: 'success'
            }
          )
          .and_return(Gitlab::PaginatedResponse.new(pipelines))

        full_pipeline_jobs = Gitlab::PaginatedResponse.new(
          [
            double('test job', name: 'test'),
            double('assets job', name: 'build-assets-image')
          ]
        )
        docs_pipeline_jobs = Gitlab::PaginatedResponse.new(
          [
            double('docs lint', name: 'lint docs')
          ]
        )
        expect(client).to receive(:pipeline_jobs)
          .with(project.auto_deploy_path, pipelines.first.id)
          .and_return(docs_pipeline_jobs)
        expect(client).to receive(:pipeline_jobs)
          .with(project.auto_deploy_path, pipelines.last.id)
          .and_return(full_pipeline_jobs)

        # NOTE: we are stubbing Commits#latest_successful_on_build so this test
        # can only verify the block provided by PassingBuild. This is why here
        # we return a boolean
        expect(subject).to be_truthy
      end

      it "delegates the results to the default branch (failure) - #{status_name}" do
        commit = build(:commit, status: status_name)

        expect(client).to receive(:commit).with(project.auto_deploy_path, ref: commit.id).and_return(commit)
        expect(commits).to receive(:latest_successful_on_build)
          .with({ since_last_auto_deploy: true })
          .and_yield(commit.id)

        pipelines = build_list(:pipeline, 2, :success)
        expect(client).to receive(:pipelines)
          .with(
            project.auto_deploy_path,
            {
              sha: commit.id,
              ref: project.default_branch,
              status: 'success'
            }
          )
          .and_return(Gitlab::PaginatedResponse.new(pipelines))

        docs_pipeline_jobs = Gitlab::PaginatedResponse.new(
          [
            double('docs lint', name: 'lint docs')
          ]
        )

        pipelines.each do |pipeline|
          expect(client).to receive(:pipeline_jobs)
            .with(project.auto_deploy_path, pipeline.id)
            .and_return(docs_pipeline_jobs)
        end

        # NOTE: we are stubbing Commits#latest_successful_on_build so this test
        # can only verify the block provided by PassingBuild. This is why here
        # we return a boolean
        expect(subject).to be_falsey
      end
    end
  end

  describe '#for_auto_deploy_branch' do
    subject { service.for_auto_deploy_branch }

    let(:operation) { :branch }

    it_behaves_like 'a passing build commit seeker' do
      it_behaves_like 'gitlab delegates to default branch'
    end
  end

  describe '#for_auto_deploy_tag' do
    subject { service.for_auto_deploy_tag }

    let(:operation) { :tag }

    it_behaves_like 'a passing build commit seeker' do
      context 'when the project is GitLab' do
        let(:project) { ReleaseTools::Project::GitlabEe }

        %w[preparing skipped pending running canceled created].each do |status_name|
          it "approves non-failed commits - #{status_name}" do
            commit = build(:commit, status: status_name)

            expect(client).to receive(:commit).with(project.auto_deploy_path, ref: commit.id).and_return(commit)
            expect(commits).to receive(:latest_successful_on_build)
              .with({ since_last_auto_deploy: true })
              .and_yield(commit.id)

            # NOTE: we are stubbing Commits#latest_successful_on_build so this test
            # can only verify the block provided by PassingBuild. This is why here
            # we return a boolean
            expect(subject).to be_truthy
          end
        end
      end
    end
  end

  describe '#next_commit' do
    let(:commits) { instance_double(ReleaseTools::Commits) }

    before do
      allow(ReleaseTools::Commits).to receive(:new).and_return(commits)
    end

    it 'calls commits.next_commit' do
      expect(service).to receive(:passing_build_commit).and_return(double(id: 'commit1'))
      expect(commits).to receive(:next_commit).with('commit1').and_return(double(id: 'commit2'))

      expect(service.next_commit.id).to eq('commit2')
    end

    it 'raises error when passing_build_commit does not exist' do
      expect(commits).to receive(:latest_successful_on_build)
        .with({ since_last_auto_deploy: true })
        .and_return(nil)

      expect { service.next_commit }.to raise_error(/Unable to find a passing/)
    end
  end

  describe '#previous_package_sha' do
    let(:commits) { instance_double(ReleaseTools::Commits) }

    before do
      allow(ReleaseTools::Commits).to receive(:new).and_return(commits)
    end

    it 'delegates to commits.find_last_auto_deploy_limit_sha' do
      expected_sha = 'abc123'

      expect(commits)
        .to receive(:find_last_auto_deploy_limit_sha)
        .and_return(expected_sha)

      expect(service.previous_package_sha).to eq(expected_sha)
    end

    it 'returns nil when no previous auto-deploy package exists' do
      expect(commits)
        .to receive(:find_last_auto_deploy_limit_sha)
        .and_return(nil)

      expect(service.previous_package_sha).to be_nil
    end
  end

  describe '#latest_successful' do
    let(:project) { ReleaseTools::Project::GitlabCe }
    let(:target_branch) { 'master' }

    it 'returns the latest successful commit' do
      VCR.use_cassette('commits/list') do
        commit = service.latest_successful

        expect(commit.id).to eq 'a5f13e591f617931434d66263418a2f26abe3abe'
      end
    end
  end

  describe '#success_for_auto_deploy_rollout?' do
    subject(:commits) { service }

    let(:project) { ReleaseTools::Project::Gitaly }
    let(:client) { double('ReleaseTools::GitlabClient') }

    it 'returns true when status is success' do
      commit = double('commit', id: 'abc', status: 'success')

      expect(client)
        .to receive(:commit)
        .with(project.auto_deploy_path, ref: commit.id)
        .and_return(commit)
        .once

      expect(client).not_to receive(:pipeline_jobs)

      expect(service.success_for_auto_deploy_rollout?(commit.id)).to be true
    end

    it 'returns false when the build is skipped' do
      commit = double('commit', id: 'abc', status: 'skipped')

      expect(client)
        .to receive(:commit)
        .with(project.auto_deploy_path, ref: commit.id)
        .and_return(commit)
        .once

      expect(client).not_to receive(:pipeline_jobs)

      expect(service.success_for_auto_deploy_rollout?(commit.id)).to be false
    end

    it 'returns false when the build has not yet finished' do
      %w[preparing pending running].each do |status_name|
        commit = build(:commit, status: status_name)

        allow(client).to receive(:commit).and_return(commit)

        expect(service).not_to receive(:success_on_default_branch?)
        expect(service.success_for_auto_deploy_rollout?(commit.id)).to be false
      end
    end

    context 'when the project is GitLab' do
      let(:project) { ReleaseTools::Project::GitlabEe }

      it 'returns true when the build has not yet finished but passed on the default branch' do
        %w[preparing pending running canceled created].each do |status_name|
          commit = build(:commit, status: status_name)

          allow(client).to receive(:commit).and_return(commit)

          expect(commits)
            .to receive(:success_on_default_branch?)
            .with(commit.id)
            .and_return(true)

          expect(service.success_for_auto_deploy_rollout?(commit)).to be true
        end
      end

      it 'also checks if the pipeline is a full pipeline when status is success' do
        commit = build(:commit, status: 'success')

        job1 = double(:job, name: 'compile-assets')
        job2 = double(:job, name: 'build-assets-image')
        page = Gitlab::PaginatedResponse.new([job1, job2])

        expect(client)
          .to receive(:commit)
          .with(project.auto_deploy_path, ref: commit.id)
          .and_return(commit)
          .once

        expect(client)
          .to receive(:pipeline_jobs)
          .with(project.auto_deploy_path, commit.last_pipeline.id)
          .and_return(page)
          .once

        expect(service.success_for_auto_deploy_rollout?(commit.id)).to be true
      end

      it 'returns false when the pipeline is not a full pipeline' do
        commit = build(:commit, status: 'success')

        job1 = double(:job, name: 'compile-assets')
        job2 = double(:job, name: 'docs lint')
        page = Gitlab::PaginatedResponse.new([job1, job2])

        expect(client)
          .to receive(:commit)
          .with(project.auto_deploy_path, ref: commit.id)
          .and_return(commit)
          .once

        expect(client)
          .to receive(:pipeline_jobs)
          .with(project.auto_deploy_path, commit.last_pipeline.id)
          .and_return(page)
          .once

        expect(service.success_for_auto_deploy_rollout?(commit.id)).to be false
      end

      it 'does check on default branch when pipeline failed' do
        commit = build(:commit, status: 'skipped')
        status = double('success_on_default_branch')

        expect(client)
          .to receive(:commit)
          .with(project.auto_deploy_path, ref: commit.id)
          .and_return(commit)
          .once

        expect(commits)
          .to receive(:success_on_default_branch?)
          .with(commit.id)
          .and_return(status)

        expect(service.success_for_auto_deploy_rollout?(commit.id)).to eq(status)
      end
    end
  end

  describe '#success_on_default_branch' do
    let(:commit) { double(:commit) }

    it 'returns true when a passing build is found' do
      pipeline = double(:pipeline, status: 'success')

      expect(client)
        .to receive(:pipelines)
        .with(
          project.auto_deploy_path,
          {
            sha: commit,
            ref: project.default_branch,
            status: 'success'
          }
        )
        .and_return(Gitlab::PaginatedResponse.new([pipeline]))

      expect(service)
        .to receive(:full_pipeline?)
        .with(pipeline)
        .and_return(true)

      expect(service.send(:success_on_default_branch?, commit)).to be(true)
    end

    it 'returns false when a the pipeline is not a full pipeline' do
      pipeline = double(:pipeline, status: 'success')

      expect(client)
        .to receive(:pipelines)
        .with(
          project.auto_deploy_path,
          {
            sha: commit,
            ref: project.default_branch,
            status: 'success'
          }
        )
        .and_return(Gitlab::PaginatedResponse.new([pipeline]))

      expect(service)
        .to receive(:full_pipeline?)
        .with(pipeline)
        .and_return(false)

      expect(service.send(:success_on_default_branch?, commit)).to be(false)
    end

    it 'returns false when the pipeline failed' do
      expect(client)
        .to receive(:pipelines)
        .with(
          project.auto_deploy_path,
          {
            sha: commit,
            ref: project.default_branch,
            status: 'success'
          }
        )
        .and_return(Gitlab::PaginatedResponse.new([]))

      expect(service.send(:success_on_default_branch?, commit)).to be(false)
    end
  end
end
