# frozen_string_literal: true

require 'spec_helper'

describe ReleaseTools::GitlabClient do
  describe 'internal client delegates' do
    let(:internal_client) { instance_double(Gitlab::Client) }
    let(:internal_graphql_client) { instance_double(ReleaseTools::GraphqlAdapter) }

    before do
      allow(described_class).to receive_messages(client: internal_client, graphql_client: internal_graphql_client)
    end

    it 'delegates .job_play' do
      expect(internal_client).to receive(:job_play)

      described_class.job_play('foo', 'bar')
    end
  end

  describe '.current_user' do
    after do
      # HACK: Prevent cross-test pollution with the `.approve_merge_request` spec
      described_class.instance_variable_set(:@current_user, nil)
    end

    it 'returns the current user', vcr: { cassette_name: 'current_user' } do
      expect(described_class.current_user).not_to be_nil
    end
  end

  describe '.pipelines', vcr: { cassette_name: 'pipelines' } do
    it 'returns project pipelines' do
      response = described_class.pipelines

      expect(response.map(&:web_url)).to all(include('/pipelines/'))
    end
  end

  describe '.pipeline', vcr: { cassette_name: 'pipeline' } do
    it 'returns project pipeline' do
      pipeline_id = '55053803'
      response = described_class.pipeline(ReleaseTools::Project::GitlabCe, pipeline_id)

      expect(response.web_url).to include("/pipelines/#{pipeline_id}")
    end
  end

  describe '.pipeline_jobs', vcr: { cassette_name: 'pipeline_jobs' } do
    it 'returns pipeline jobs' do
      response = described_class.pipeline_jobs(ReleaseTools::Project::GitlabCe, '55053803')

      expect(response.map(&:web_url)).to all(include('/jobs/'))
    end
  end

  describe '.pipeline_job_by_name', vcr: { cassette_name: 'pipeline_job_by_name' } do
    it 'returns first pipeline job by name' do
      job_name = 'setup-test-env'
      response = described_class.pipeline_job_by_name(ReleaseTools::Project::GitlabCe, '55053803', job_name)

      expect(response.name).to eq(job_name)
    end
  end

  describe '.job_trace', vcr: { cassette_name: 'job_trace' } do
    it 'returns job trace' do
      response = described_class.job_trace(ReleaseTools::Project::GitlabCe, '189985934')

      expect(response).to include('mkdir -p rspec_flaky/')
    end
  end

  describe '.milestones', vcr: { cassette_name: 'merge_requests/with_milestone' } do
    it 'returns a combination of project and group milestones' do
      skip "See https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/1032"
      response = described_class.milestones

      expect(response.map(&:title)).to include('9.4', '10.4')
    end
  end

  describe '.current_milestone' do
    it 'fetches the current milestone' do
      current = build(:gitlab_response, title: '42.0')

      expect(ReleaseTools::ProductMilestone).to receive(:current)
        .and_return(current)

      expect(described_class.current_milestone).to eq(current)
    end

    it 'falls back to MissingMilestone' do
      expect(ReleaseTools::ProductMilestone).to receive(:current)
        .and_return(nil)

      expect(described_class.current_milestone).to be_a(described_class::MissingMilestone)
    end
  end

  describe '.milestone', vcr: { cassette_name: 'merge_requests/with_milestone' } do
    context 'when the milestone title is nil' do
      it 'returns a MissingMilestone' do
        skip "See https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/1032"
        milestone = described_class.milestone(title: nil)

        expect(milestone).to be_a(described_class::MissingMilestone)
        expect(milestone.id).to be_nil
      end
    end

    context 'when the milestone exists' do
      it 'returns the milestone' do
        skip "See https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/1032"
        response = described_class.milestone(title: '9.4')

        expect(response.title).to eq('9.4')
      end
    end

    context 'when the milestone does not exist' do
      it 'raises an exception' do
        skip "See https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/1032"
        expect { described_class.milestone(title: 'not-existent') }.to raise_error('Milestone not-existent not found for project gitlab-org/gitlab-foss!')
      end
    end
  end

  describe '.accept_merge_request' do
    before do
      allow(described_class).to receive(:current_user).and_return(double(id: 42))
    end

    let(:merge_request) do
      double(
        project: double(path: 'gitlab-org/gitlab-foss'),
        title: 'Upstream MR',
        iid: '12345',
        description: 'Hello world',
        labels: 'CE upstream',
        source_branch: 'feature',
        target_branch: 'master',
        milestone: nil)
    end

    let(:default_params) do
      {
        merge_when_pipeline_succeeds: true
      }
    end

    it 'accepts a merge request against master on the GitLab CE project' do
      expect(described_class.__send__(:client))
        .to receive(:accept_merge_request).with(
          ReleaseTools::Project::GitlabCe.path,
          merge_request.iid,
          default_params)

      described_class.accept_merge_request(merge_request)
    end

    context 'when passing a project' do
      it 'accepts a merge request in the given project' do
        expect(described_class.__send__(:client))
          .to receive(:accept_merge_request).with(
            ReleaseTools::Project::GitlabEe.path,
            merge_request.iid,
            default_params)

        described_class
          .accept_merge_request(merge_request, ReleaseTools::Project::GitlabEe)
      end
    end
  end

  describe '.cancel_merge_when_pipeline_succeeds' do
    it 'cancels the merge when pipeline succeeds' do
      merge_request = build(:merge_request)
      response = double(:response)

      allow(described_class.client)
        .to receive(:post)
        .with("/projects/#{merge_request.project_id}/merge_requests/#{merge_request.iid}/cancel_merge_when_pipeline_succeeds")
        .and_return(response)

      expect(described_class.cancel_merge_when_pipeline_succeeds(merge_request)).to eq(response)
    end
  end

  describe '.create_merge_request' do
    before do
      allow(described_class).to receive_messages(
        current_user: double(id: current_user_id),
        current_milestone: double(id: 1)
      )
    end

    let(:current_user_id) { 42 }

    let(:merge_request) do
      double(
        project: double(path: 'gitlab-org/gitlab-foss'),
        title: 'Upstream MR',
        description: 'Hello world',
        labels: 'CE upstream',
        source_branch: 'feature',
        target_branch: 'master',
        assignee_ids: nil,
        milestone: nil)
    end

    let(:default_params) do
      {
        description: merge_request.description,
        assignee_ids: [current_user_id],
        labels: merge_request.labels,
        source_branch: merge_request.source_branch,
        target_branch: 'master',
        milestone_id: 1,
        remove_source_branch: true
      }
    end

    it 'creates a merge request against master on the GitLab CE project' do
      expect(described_class.__send__(:client))
        .to receive(:create_merge_request).with(
          ReleaseTools::Project::GitlabCe.path,
          merge_request.title,
          default_params)

      described_class.create_merge_request(merge_request)
    end

    context 'when passing a project' do
      it 'creates a merge request in the given project' do
        expect(described_class.__send__(:client))
          .to receive(:create_merge_request).with(
            ReleaseTools::Project::GitlabEe.path,
            merge_request.title,
            default_params)

        described_class.create_merge_request(merge_request, ReleaseTools::Project::GitlabEe)
      end
    end

    context 'when merge request has a target branch' do
      before do
        allow(merge_request).to receive(:target_branch).and_return('stable')
      end

      it 'creates a merge request against the given target branch' do
        expect(described_class.__send__(:client))
          .to receive(:create_merge_request).with(
            ReleaseTools::Project::GitlabEe.path,
            merge_request.title,
            default_params.merge(target_branch: 'stable'))

        described_class.create_merge_request(merge_request, ReleaseTools::Project::GitlabEe)
      end
    end

    context 'with milestone', vcr: { cassette_name: 'merge_requests/with_milestone' } do
      it 'sets milestone id' do
        skip "See https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/1032"
        allow(merge_request).to receive(:milestone).and_return('9.4')

        response = described_class.create_merge_request(merge_request)

        expect(response.milestone.title).to eq '9.4'
      end
    end

    context 'when assignee_ids is nil' do
      it 'defaults to current_user as assignee' do
        expect(described_class.__send__(:client))
          .to receive(:create_merge_request)
          .with(anything, anything, default_params)

        described_class.create_merge_request(merge_request)
      end
    end

    context 'when assignee_ids is not nil' do
      before do
        assignee_ids = [1, 2]
        allow(merge_request).to receive(:assignee_ids).and_return(assignee_ids)

        default_params[:assignee_ids] = assignee_ids
      end

      it 'adds assignee_ids to params' do
        expect(described_class.__send__(:client))
          .to receive(:create_merge_request)
          .with(anything, anything, default_params)

        described_class.create_merge_request(merge_request)
      end
    end
  end

  describe '.find_issue' do
    context 'when issue is open' do
      it 'finds issues by title', vcr: { cassette_name: 'issues/release-8-7' } do
        version = double(milestone_name: '8.7')
        issue = double(title: 'Release 8.7', labels: 'Release', version: version)

        expect(described_class.find_issue(issue)).not_to be_nil
      end
    end

    context 'when issue is closed' do
      it 'finds issues by title', vcr: { cassette_name: 'issues/regressions-8-5' } do
        version = double(milestone_name: '8.5')
        issue = double(title: '8.5 Regressions', labels: 'Release', state_filter: nil, version: version)

        expect(described_class.find_issue(issue)).not_to be_nil
      end
    end

    context 'when issue version is nil' do
      let(:issue) do
        build(:issue,
              title: 'Release 8.5.7',
              labels: ['devops::release'],
              state: 'opened',
              version: nil,
              state_filter: nil
             )
      end

      before do
        allow(described_class).to receive(:issues).and_return([issue])
      end

      it 'finds issues by title', vcr: { cassette_name: 'issues/nil-version' } do
        expect(described_class.find_issue(issue)).not_to be_nil
      end

      it 'exclude milestone from search', vcr: { cassette_name: 'issues/nil-version' } do
        expect(described_class).to receive(:issues).with(anything, hash_excluding(:milestone)).and_call_original
        described_class.find_issue(issue)
      end
    end

    context 'when issue cannot be found' do
      it 'does not find non-matching issues', vcr: { cassette_name: 'issues/release-7-14' } do
        version = double(milestone_name: '7.14')
        issue = double(title: 'Release 7.14', labels: 'Release', version: version)

        expect(described_class.find_issue(issue)).to be_nil
      end
    end
  end

  context 'issue links' do
    let(:internal_client) { instance_double(Gitlab::Client) }
    let(:issue_project) { ReleaseTools::Project::GitlabCe }

    let(:issue) do
      instance_double(
        ReleaseTools::PatchRelease::Issue,
        project: issue_project,
        iid: 1
      )
    end

    before do
      allow(described_class).to receive(:client).and_return(internal_client)
    end

    describe '.link_issues' do
      let(:target) do
        instance_double(
          ReleaseTools::MonthlyIssue,
          project: target_project,
          iid: 2
        )
      end

      let(:target_project) { ReleaseTools::Project::GitlabEe }

      it 'links an issue to a target' do
        allow(internal_client).to receive(:url_encode)
          .with('gitlab-org/gitlab-foss')
          .and_return('gitlab-org%2Fgitlab-foss')

        expect(internal_client).to receive(:post).with(
          '/projects/gitlab-org%2Fgitlab-foss/issues/1/links',
          { query: { target_project_id: 'gitlab-org/gitlab', target_issue_iid: 2 } }
        )

        described_class.link_issues(issue, target)
      end
    end

    describe '.delete_issue_link' do
      it 'deletes an issue link' do
        issue_link = 111

        expect(internal_client).to receive(:delete_issue_link).with(
          issue.project.path,
          issue.iid,
          issue_link
        )

        described_class.delete_issue_link(issue, issue_link)
      end
    end
  end

  describe '.find_branch' do
    it 'finds existing branches', vcr: { cassette_name: 'branches/9-4-stable' } do
      expect(described_class.find_branch('9-4-stable').name).to eq '9-4-stable'
    end

    it "returns nil when branch can't be found", vcr: { cassette_name: 'branches/9-4-stable-doesntexist' } do
      expect(described_class.find_branch('9-4-stable-doesntexist')).to be_nil
    end
  end

  describe '.create_branch' do
    it 'creates a branch', vcr: { cassette_name: 'branches/create-test' } do
      branch_name = 'test-branch-from-release-tools'

      response = described_class.create_branch(branch_name, 'master')

      expect(response.name).to eq branch_name
    end
  end

  describe '.cherry_pick' do
    context 'on a successful pick' do
      it 'returns a reasonable response', vcr: { cassette_name: 'cherry_pick/success' } do
        ref = '59af98f133ee229479c6159b15391deb4782a294'

        response = described_class.cherry_pick(ref: ref, target: '11-4-stable')

        expect(response.message).to include("cherry picked from commit #{ref}")
      end

      it 'allows passing of a custom commit message' do
        expect(described_class.client).to receive(:cherry_pick_commit).with(
          ReleaseTools::Project::GitlabCe.path,
          '123abc',
          'master',
          {
            dry_run: false,
            message: 'foo'
          }
        )

        described_class
          .cherry_pick(ref: '123abc', target: 'master', message: 'foo')
      end

      it 'does not pass the message argument to the API when left out' do
        expect(described_class.client).to receive(:cherry_pick_commit).with(
          ReleaseTools::Project::GitlabCe.path,
          '123abc',
          'master',
          { dry_run: false }
        )

        described_class.cherry_pick(ref: '123abc', target: 'master')
      end
    end

    context 'on a failed pick' do
      it 'raises an exception', vcr: { cassette_name: 'cherry_pick/failure' } do
        expect do
          described_class.cherry_pick(
            ref: '396d205e5a503f9f48c223804087a80f7acc6d06',
            target: '11-4-stable'
          )
        end.to raise_error(Gitlab::Error::BadRequest)
      end
    end

    context 'on an invalid ref' do
      it 'raises an `ArgumentError`' do
        expect { described_class.cherry_pick(ref: '', target: 'foo') }
          .to raise_error(ArgumentError)
      end
    end
  end

  describe '.add_to_merge_train' do
    let(:merge_request) { double(iid: 42, project_id: 12) }
    let(:sha) { 'abc123' }

    it 'performs a GraphQL query and return the status' do
      internal_graphql_client = instance_double(ReleaseTools::GraphqlAdapter)
      allow(described_class).to receive(:graphql_client).and_return(internal_graphql_client)

      expect(internal_graphql_client)
        .to receive(:query)
        .with(ReleaseTools::GitlabGraphqlQueries::ADD_TO_MERGE_TRAIN_QUERY, { project: merge_request.project_id, mr: merge_request.iid.to_s, sha: sha })
        .and_return(double(errors: [], merge_request_accept: double(merge_request: double(auto_merge_enabled: true))))

      expect(described_class.add_to_merge_train(merge_request, sha)).to be(true)
    end

    context 'when query return errors' do
      it 'performs a GraphQL query and raises an error', graphql_cassette: 'merge_trains/add-error' do
        expect { described_class.add_to_merge_train(merge_request, sha) }.to raise_error(%|MR couldn't be added to Merge Train: {"mergeRequestAccept":["The resource that you are attempting to access does not exist or you don't have permission to perform this action"]}|)
      end
    end

    context 'when using GitLab API' do
      it 'adds the merge request to the merge train' do
        merge_request = create(:merge_request, iid: 1, project_id: 123, sha: 'abcdef')
        response = double(:response)

        allow(described_class.client)
          .to receive(:post)
          .with(
            '/projects/123/merge_trains/merge_requests/1',
            {
              body: {
                sha: merge_request.sha,
                when_pipeline_succeeds: false
              }
            }
          ).and_return(response)

        expect(described_class.add_to_merge_train(merge_request, merge_request.sha, graphql: false))
          .to eq(response)
      end
    end
  end

  describe '.project_path' do
    it 'returns the correct project path' do
      project = double(path: 'foo/bar')

      expect(described_class.project_path(project)).to eq 'foo/bar'
    end

    it 'returns a String unmodified' do
      project = 'gitlab-org/security/gitlab'

      expect(described_class.project_path(project)).to eq(project)
    end
  end

  describe '.related_merge_requests' do
    it 'returns the related merge requests' do
      page = double(:page)

      allow(described_class.client)
        .to receive(:get)
        .with('/projects/foo%2Fbar/issues/1/related_merge_requests')
        .and_return(page)

      expect(described_class.related_merge_requests('foo/bar', 1)).to eq(page)
    end
  end

  describe '#tree' do
    it 'lists the contents of a directory' do
      project = ReleaseTools::Project::GitlabCe
      tree = double(:tree)

      allow(described_class.client)
        .to receive(:tree)
        .with(project.path, 'foo')
        .and_return(tree)

      expect(described_class.tree(project, 'foo')).to eq(tree)
    end
  end

  describe '#get_file' do
    it 'gets a file from the repository' do
      project = ReleaseTools::Project::GitlabCe
      file = double(:file)

      allow(described_class.client)
        .to receive(:get_file)
        .with(project.path, 'foo', 'bar')
        .and_return(file)

      expect(described_class.get_file(project, 'foo', 'bar')).to eq(file)
    end
  end

  describe '#find_or_create_branch' do
    context 'when the branch exists' do
      it 'returns the branch' do
        branch = double(:branch)

        allow(described_class)
          .to receive(:find_branch)
          .with('foo', ReleaseTools::Project::GitlabCe)
          .and_return(branch)

        expect(described_class.find_or_create_branch('foo', 'master'))
          .to eq(branch)
      end
    end

    context 'when the branch does not exist' do
      it 'creates the branch' do
        branch = double(:branch)
        master = double(:branch, commit: double(:commit, id: '123'))

        allow(described_class)
          .to receive(:find_branch)
          .with('foo', ReleaseTools::Project::GitlabCe)
          .and_return(nil)

        allow(described_class)
          .to receive(:find_branch)
          .with('master', ReleaseTools::Project::GitlabCe)
          .and_return(master)

        allow(described_class)
          .to receive(:create_branch)
          .with('foo', 'master', ReleaseTools::Project::GitlabCe)
          .and_return(branch)

        expect(described_class.find_or_create_branch('foo', 'master'))
          .to eq(branch)

        expect(described_class)
          .to have_received(:create_branch)
      end
    end
  end

  describe '#find_or_create_tag' do
    context 'when the tag exists' do
      it 'returns the tag' do
        tag = double(:tag)
        project = ReleaseTools::Project::GitlabCe

        allow(described_class)
          .to receive(:tag)
          .with(project, { tag: 'foo' })
          .and_return(tag)

        expect(described_class.find_or_create_tag(project, 'foo', 'master'))
          .to eq(tag)
      end
    end

    context 'when the tag does not exist' do
      it 'creates the tag' do
        tag = double(:tag)
        project = ReleaseTools::Project::GitlabCe

        allow(described_class)
          .to receive(:tag)
          .with(project, { tag: 'foo' })
          .and_raise(gitlab_error(:NotFound))

        allow(described_class)
          .to receive(:create_tag)
          .with(project, 'foo', 'master', nil)
          .and_return(tag)

        expect(described_class.find_or_create_tag(project, 'foo', 'master'))
          .to eq(tag)
      end

      it 'supports creating annotated tags' do
        tag = double(:tag)
        project = ReleaseTools::Project::GitlabCe

        allow(described_class)
          .to receive(:tag)
          .with(project, { tag: 'foo' })
          .and_raise(gitlab_error(:NotFound))

        allow(described_class)
          .to receive(:create_tag)
          .with(project, 'foo', 'master', 'foo')
          .and_return(tag)

        output = described_class
          .find_or_create_tag(project, 'foo', 'master', message: 'foo')

        expect(output).to eq(tag)
      end
    end
  end

  describe '#create_tag' do
    it 'creates a tag' do
      project = ReleaseTools::Project::GitlabCe
      tag = double(:tag)

      expect(described_class.client)
        .to receive(:create_tag)
        .with(project.path, 'foo', 'master')
        .and_return(tag)

      expect(described_class.create_tag(project, 'foo', 'master')).to eq(tag)
    end
  end

  describe '#branches' do
    it 'returns the branches for a project' do
      response = double(:response)

      expect(described_class.client)
        .to receive(:branches)
        .with('gitlab-org/gitlab', { search: 'foo' })
        .and_return(response)

      expect(described_class.branches(ReleaseTools::Project::GitlabEe, search: 'foo'))
        .to eq(response)
    end
  end

  describe '.create_merge_request_pipeline' do
    it 'triggers a merge request pipeline' do
      response = double(:response)

      allow(described_class.client)
        .to receive(:post)
        .with('/projects/123/merge_requests/1/pipelines')
        .and_return(response)

      expect(described_class.create_merge_request_pipeline(123, 1)).to eq(response)
    end
  end

  describe '#tags' do
    it 'returns the tags of a project' do
      response = double(:response)

      expect(described_class.client)
        .to receive(:tags)
        .with('gitlab-org/gitlab', { search: 'foo' })
        .and_return(response)

      expect(described_class.tags(ReleaseTools::Project::GitlabEe, search: 'foo'))
        .to eq(response)
    end
  end

  describe '.create_issue' do
    let(:version) { double(:version, milestone_name: '13.6') }
    let(:milestone) { double(:milestone, id: '123') }
    let(:current_user) { double(:user, id: '123') }

    let(:issue) do
      double(:issue,
             title: 'foo',
             description: 'bar',
             confidential?: true,
             version: version,
             labels: 'baz')
    end

    let(:default_options) do
      {
        description: issue.description,
        assignee_ids: [current_user.id],
        milestone_id: milestone.id,
        labels: issue.labels,
        confidential: issue.confidential?
      }
    end

    subject(:create_issue) { described_class.create_issue(issue) }

    before do
      allow(described_class)
        .to receive_messages(milestone: milestone, current_user: current_user)
    end

    it 'creates an issue on the project' do
      expect(described_class.client)
        .to receive(:create_issue).with(
          ReleaseTools::Project::GitlabCe.path,
          issue.title,
          default_options
        )

      create_issue
    end

    context 'with nil version' do
      let(:version) { nil }

      it 'creates an issue on the project' do
        expect(described_class.client)
          .to receive(:create_issue).with(
            ReleaseTools::Project::GitlabCe.path,
            issue.title,
            default_options.except(:milestone_id)
          )

        create_issue
      end
    end

    context 'when the issue responds to assignees' do
      let(:issue) do
        double(:issue,
               title: 'foo',
               description: 'bar',
               confidential?: true,
               version: version,
               labels: 'baz',
               assignees: [456, 789])
      end

      it 'creates an issue with specific assignees' do
        options = default_options.merge(assignee_ids: [456, 789])

        expect(described_class.client)
          .to receive(:create_issue).with(
            ReleaseTools::Project::GitlabCe.path,
            issue.title,
            options
          )

        create_issue
      end
    end

    context 'when the issue responds to due_date' do
      let(:issue) do
        double(:issue,
               title: 'foo',
               description: 'bar',
               confidential?: true,
               version: version,
               labels: 'baz',
               due_date: '2020-01-01')
      end

      it 'creates an issue with the due date' do
        options = default_options.merge(due_date: '2020-01-01')

        expect(described_class.client)
          .to receive(:create_issue).with(
            ReleaseTools::Project::GitlabCe.path,
            issue.title,
            options
          )

        create_issue
      end
    end
  end

  describe '.update_issue' do
    let(:version) { double(:version, milestone_name: '13.6') }
    let(:milestone) { double(:milestone, id: '123') }

    let(:issue) do
      double(:issue,
             iid: 1,
             description: 'bar',
             confidential?: true,
             version: version,
             labels: 'baz')
    end

    let(:default_options) do
      {
        description: issue.description,
        milestone_id: milestone.id,
        labels: issue.labels,
        confidential: issue.confidential?
      }
    end

    subject(:update_issue) { described_class.update_issue(issue) }

    before do
      allow(described_class)
        .to receive(:milestone).and_return(milestone)
    end

    it 'updates the issue' do
      expect(described_class.client)
        .to receive(:edit_issue).with(
          ReleaseTools::Project::GitlabCe.path,
          issue.iid,
          default_options
        )

      update_issue
    end

    context 'with nil version' do
      let(:version) { nil }

      it 'updates the issue' do
        expect(described_class.client)
          .to receive(:edit_issue).with(
            ReleaseTools::Project::GitlabCe.path,
            issue.iid,
            default_options.except(:milestone_id)
          )

        update_issue
      end
    end
  end

  describe '.update_or_create_deployment' do
    it 'updates an existing deployment of the same ref and SHA' do
      project = double('Project')
      environment = 'gprd'

      expect(described_class).to receive(:deployments)
        .with(project, environment, { status: 'running' })
        .and_return([
          double(id: 1, ref: 'master', sha: 'a', status: 'running'),
          double(id: 2, ref: 'master', sha: 'b', status: 'running'),
          double(id: 3, ref: 'master', sha: 'c', status: 'running'),
          double(id: 4, ref: 'master', sha: 'd', status: 'running')
        ])

      expect(described_class).to receive(:update_deployment)
        .with(project, 3, { status: 'success' })

      described_class.update_or_create_deployment(
        project,
        environment,
        ref: 'master',
        sha: 'c',
        status: 'success'
      )
    end

    it 'does not update existing deployment if it already has same status' do
      project = double('Project')
      environment = 'gprd'

      existing_deployment = build(:deployment, id: 3, ref: 'master', sha: 'c', status: 'running')

      expect(described_class).to receive(:deployments)
        .with(project, environment, { status: 'running' })
        .and_return([
          build(:deployment, id: 1, ref: 'master', sha: 'a', status: 'running'),
          build(:deployment, id: 2, ref: 'master', sha: 'b', status: 'running'),
          existing_deployment,
          build(:deployment, id: 4, ref: 'master', sha: 'd', status: 'running')
        ])

      expect(described_class).not_to receive(:update_deployment)
      expect(described_class).not_to receive(:create_deployment)

      deployment = described_class.update_or_create_deployment(
        project,
        environment,
        ref: 'master',
        sha: 'c',
        status: 'running'
      )

      expect(deployment).to eq(existing_deployment)
    end

    it 'creates a new deployment' do
      project = double('Project')
      environment = 'gprd'

      expect(described_class).to receive(:deployments)
        .with(project, environment, { status: 'running' })
        .and_return([double(id: 1, ref: 'master', sha: 'a', status: 'running')])

      expect(described_class).not_to receive(:update_deployment)
      expect(described_class).to receive(:create_deployment)
        .with(project, environment, { ref: 'master', sha: 'b', status: 'running', tag: false })

      described_class.update_or_create_deployment(
        project,
        environment,
        ref: 'master',
        sha: 'b',
        status: 'running'
      )
    end
  end

  describe '.compile_changelog' do
    it 'compiles the changelog' do
      expect(described_class.client).to receive(:post).with(
        "/projects/foo%2Fbar/repository/changelog",
        {
          body: {
            version: '1.1.0',
            to: '123',
            branch: 'main',
            message: "Update changelog for 1.1.0\n\n[ci skip]"
          }
        }
      )

      described_class.compile_changelog('foo/bar', '1.1.0', '123', 'main')
    end

    it 'compiles the changelog without skipping ci' do
      expect(described_class.client).to receive(:post).with(
        "/projects/foo%2Fbar/repository/changelog",
        {
          body: {
            version: '1.1.0',
            to: '123',
            branch: 'main',
            message: "Update changelog for 1.1.0"
          }
        }
      )

      described_class.compile_changelog('foo/bar', '1.1.0', '123', 'main', skip_ci: false)
    end
  end

  describe '.commit_status' do
    it 'returns the commit statuses' do
      response = double(:response)

      expect(described_class.client)
        .to receive(:commit_status)
        .with('gitlab-org/gitlab', '123', { ref: 'master' })
        .and_return(response)

      result = described_class
        .commit_status(ReleaseTools::Project::GitlabEe, '123', ref: 'master')

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

  describe '.merge_request_commits' do
    it 'returns the commits of a merge request' do
      response = double(:response)

      expect(described_class.client)
        .to receive(:merge_request_commits)
        .with(ReleaseTools::Project::GitlabCe.path, 42)
        .and_return(response)

      result = described_class
        .merge_request_commits(ReleaseTools::Project::GitlabCe, 42)

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

  describe '.create_pipeline' do
    let(:project) { ReleaseTools::Project::GitlabEe }
    let(:pipeline_variables) { { key: 'foo', value: 'bar' } }

    it 'creates a pipeline' do
      expect(described_class.client)
        .to receive(:create_pipeline)
        .with('gitlab-org/gitlab', 'master', pipeline_variables)

      described_class.create_pipeline(project, pipeline_variables)
    end

    context 'when provided a branch' do
      it 'creates a pipeline on that branch' do
        expect(described_class.client)
          .to receive(:create_pipeline)
          .with('gitlab-org/gitlab', 'foo', pipeline_variables)

        described_class.create_pipeline(project, pipeline_variables, branch: 'foo')
      end
    end
  end

  describe '.download_raw_job_artifact' do
    it 'returns the raw version of the artifact' do
      project = double(:project, canonical_id: 123)

      expect(described_class.client).to receive(:get).with(
        '/projects/123/jobs/456/artifacts/foo',
        {
          format: nil,
          headers: { Accept: 'application/octet-stream' },
          parser: Gitlab::Request::Parser
        }
      )

      described_class.download_raw_job_artifact(project, 456, 'foo')
    end
  end

  describe '.last_successful_deployment' do
    it 'returns the last successful deployment for a given environment' do
      project = ReleaseTools::Project::GitlabEe
      environment = 'gprd'

      expect(described_class.client).to receive(:deployments).with(
        'gitlab-org/gitlab',
        {
          environment: environment,
          status: 'success',
          order_by: 'id',
          sort: 'desc',
          per_page: 1
        }
      ).and_return([double(id: 1, ref: 'master', sha: 'a', status: 'success')])

      described_class.last_successful_deployment(project.path, environment)
    end
  end

  describe '.cancel_job' do
    it 'cancels the job' do
      project = ReleaseTools::Project::GitlabEe
      job_id = '123'

      expect(described_class.client)
        .to receive(:job_cancel)
        .with('gitlab-org/gitlab', '123')

      described_class.cancel_job(project, job_id)
    end
  end

  describe '.pipeline_variables' do
    context 'with project class' do
      it 'calls pipeline variables API' do
        project = ReleaseTools::Project::GitlabEe
        pipeline_id = '123'
        path = 'gitlab-org%2Fgitlab'

        expect(described_class.client)
          .to receive(:get)
          .with("/projects/#{path}/pipelines/#{pipeline_id}/variables")

        described_class.pipeline_variables(project, pipeline_id)
      end
    end

    context 'with project path' do
      it 'calls pipeline variables API' do
        project = 'gitlab-org/gitlab'
        pipeline_id = '123'
        path = 'gitlab-org%2Fgitlab'

        expect(described_class.client)
          .to receive(:get)
          .with("/projects/#{path}/pipelines/#{pipeline_id}/variables")

        described_class.pipeline_variables(project, pipeline_id)
      end
    end

    context 'with project ID' do
      it 'calls pipeline variables API' do
        project = 20
        pipeline_id = 123

        expect(described_class.client)
          .to receive(:get)
          .with("/projects/#{project}/pipelines/#{pipeline_id}/variables")

        described_class.pipeline_variables(project, pipeline_id)
      end
    end
  end

  describe '.pipeline_schedules' do
    it 'calls the pipeline_schedules API method' do
      project = ReleaseTools::Project::GitlabEe

      expect(described_class.client)
        .to receive(:pipeline_schedules)
        .with('gitlab-org/gitlab')

      described_class.pipeline_schedules(project)
    end
  end

  describe '.pipeline_schedule' do
    it 'calls the pipeline_schedule API method' do
      project = ReleaseTools::Project::GitlabEe
      pipeline_schedule_id = '123'

      expect(described_class.client)
        .to receive(:pipeline_schedule)
        .with('gitlab-org/gitlab', '123')

      described_class.pipeline_schedule(project, pipeline_schedule_id)
    end
  end

  describe '.pipeline_schedule_take_ownership' do
    it 'calls the pipeline_schedule_take_ownership API method' do
      project = ReleaseTools::Project::GitlabEe
      pipeline_schedule_id = '123'

      expect(described_class.client)
        .to receive(:pipeline_schedule_take_ownership)
        .with('gitlab-org/gitlab', '123')

      described_class.pipeline_schedule_take_ownership(project, pipeline_schedule_id)
    end
  end

  describe '.run_pipeline_schedule' do
    let(:pipeline_schedule_id) { 123 }
    let(:project) { ReleaseTools::Project::GitlabEe }

    it 'calls the run_pipeline_schedule API method' do
      expect(described_class.client)
        .to receive(:run_pipeline_schedule)
        .with('gitlab-org/gitlab', pipeline_schedule_id)

      described_class.run_pipeline_schedule(project, pipeline_schedule_id)
    end
  end

  describe '.edit_pipeline_schedule' do
    let(:project) { ReleaseTools::Project::GitlabEe }
    let(:pipeline_schedule_id) { 123 }
    let(:options) { { description: 'Updated schedule' } }

    it 'calls client.edit_pipeline_schedule with correct arguments' do
      expect(described_class.client)
        .to receive(:edit_pipeline_schedule)
        .with('gitlab-org/gitlab', pipeline_schedule_id, options)

      described_class.edit_pipeline_schedule(project, pipeline_schedule_id, options)
    end

    it 'works with an empty options hash' do
      expect(described_class.client)
        .to receive(:edit_pipeline_schedule)
        .with('gitlab-org/gitlab', pipeline_schedule_id, {})

      described_class.edit_pipeline_schedule(project, pipeline_schedule_id)
    end
  end

  describe '.edit_pipeline_schedule_variable' do
    let(:project) { ReleaseTools::Project::GitlabEe }
    let(:pipeline_schedule_id) { 123 }
    let(:variable_key) { 'FOO' }
    let(:options) { { value: "bar" } }

    it 'calls client.edit_pipeline_schedule_variable with correct arguments' do
      expect(described_class.client)
        .to receive(:edit_pipeline_schedule_variable)
        .with('gitlab-org/gitlab', pipeline_schedule_id, variable_key, options)

      described_class.edit_pipeline_schedule_variable(project, pipeline_schedule_id, variable_key, options)
    end
  end

  describe '.next_security_tracking_issue' do
    it 'fetches the next security tracking issue' do
      allow(described_class.client)
        .to receive(:issues)
        .and_return([create(:issue)])

      expect(described_class.client)
        .to receive(:issues)
        .with('gitlab-org/gitlab', labels: 'upcoming security release', state: 'opened')

      described_class.next_security_tracking_issue
    end
  end

  describe '.current_security_task_issue' do
    it 'fetches the current patch release task issue' do
      allow(described_class.client)
        .to receive(:issues)
        .and_return([create(:issue)])

      expect(described_class.client)
        .to receive(:issues)
        .with('gitlab-org/release/tasks', labels: ['security', 'Monthly Release'], state: 'opened')

      described_class.current_security_task_issue
    end
  end

  describe '.security_communication_issue' do
    it 'fetches the security communication issue' do
      allow(described_class.client)
        .to receive(:issues)
        .and_return([create(:issue)])

      expect(described_class.client)
        .to receive(:issues)
        .with(
          'gitlab-com/gl-security/security-communications/communications',
          labels: 'Security Release Blog Alert::development',
          state: 'opened'
        )

      described_class.security_communication_issue
    end
  end

  describe '.security_blog_merge_request' do
    let(:security) { true }

    subject(:merge_request) { described_class.security_blog_merge_request(security: security) }

    it 'fetches the patch release blog post from the security repository' do
      allow(described_class.client)
        .to receive(:merge_requests)
        .and_return([create(:merge_request)])

      expect(described_class.client)
        .to receive(:merge_requests)
        .with(ReleaseTools::Project::WWWGitlabCom.security_path, labels: 'patch release post', state: 'opened')

      merge_request
    end

    context 'in the canonical repository' do
      let(:security) { false }

      it 'fetches the patch release blog post from the canonical repository' do
        allow(described_class.client)
          .to receive(:merge_requests)
          .and_return([create(:merge_request)])

        expect(described_class.client)
          .to receive(:merge_requests)
          .with(ReleaseTools::Project::WWWGitlabCom.path, labels: 'patch release post', state: 'opened')

        merge_request
      end
    end
  end

  describe '.approve_merge_request' do
    it 'calls the approval_merge_request API method' do
      merge_request = create(:merge_request, iid: 123, project: ReleaseTools::Project::GitlabEe)

      expect(described_class.client)
        .to receive(:approve_merge_request)
        .with('gitlab-org/gitlab', 123)

      described_class.approve_merge_request(merge_request.project, merge_request.iid)
    end
  end

  describe '.merge_request_approvals' do
    it 'calls the merge_request_approvals API method' do
      merge_request = create(:merge_request, iid: 123, project: ReleaseTools::Project::GitlabEe)

      expect(described_class.client)
        .to receive(:merge_request_approvals)
        .with('gitlab-org/gitlab', 123)

      described_class.merge_request_approvals(merge_request.project, merge_request.iid)
    end
  end

  describe '.merge_request_pipelines' do
    it 'calls the merge_request_pipelines API method' do
      merge_request = create(:merge_request, iid: 123, project: ReleaseTools::Project::GitlabEe)

      expect(described_class.client)
        .to receive(:merge_request_pipelines)
        .with('gitlab-org/gitlab', 123)

      described_class.merge_request_pipelines(merge_request.project, merge_request.iid)
    end
  end

  describe '.merge_request_diffs' do
    it 'executes a GET call to retrieve the diff' do
      page = double(:page)

      allow(described_class.client)
        .to receive(:get)
        .with('/projects/foo%2Fbar/merge_requests/1/diffs')
        .and_return(page)

      expect(described_class.merge_request_diffs('foo/bar', 1)).to eq(page)
    end
  end

  describe '.update_project_variable' do
    it 'executes a PUT call to update the given variable' do
      key = 'MY_VAR'
      value = 'a value'
      ci_variable = double(:ci_variable)

      allow(described_class.client)
        .to receive(:put)
        .with("/projects/foo%2Fbar/variables/#{key}", body: { value: value })
        .and_return(ci_variable)

      expect(described_class.update_project_variable('foo/bar', key, value)).to eq(ci_variable)
    end
  end

  describe '.project_access_tokens' do
    it 'executes a GET call to fetch all the tokens' do
      tokens = double(:tokens)

      allow(described_class.client)
        .to receive(:get)
        .with('/projects/foo%2Fbar/access_tokens')
        .and_return(tokens)

      expect(described_class.project_access_tokens('foo/bar')).to eq(tokens)
    end
  end

  describe '.rotate_project_access_token' do
    it 'executes a POST call to rotate the token' do
      key = 'MY_VAR'
      expires_at = '2020-01-01'
      result = double(:result)

      allow(described_class.client)
        .to receive(:post)
        .with("/projects/foo%2Fbar/access_tokens/#{key}/rotate", body: { expires_at: expires_at })
        .and_return(result)

      expect(described_class.rotate_project_access_token('foo/bar', key, expires_at: expires_at)).to eq(result)
    end
  end

  describe '.sync_remote_mirror' do
    it 'calls the sync remote endpoint' do
      result = double(:result)

      allow(described_class.client)
        .to receive(:post)
        .with('/projects/foo%2Fbar/remote_mirrors/123/sync')
        .and_return(result)

      expect(described_class.sync_remote_mirror('foo/bar', 123)).to eq(result)
    end
  end
end
