# frozen_string_literal: true

require 'spec_helper'

describe ReleaseTools::Security::MergeRequestsMerger do
  let(:client) { spy(:client) }
  let(:batch_merger) { described_class.new }
  let(:issue_crawler) { instance_double(ReleaseTools::Security::IssueCrawler) }
  let(:mwps_service) { double(:mwps_service) }
  let(:author) { create(:user, id: 123, username: 'joe') }
  let(:notifier) { instance_double(ReleaseTools::Slack::ReleaseJobEndNotifier, send_notification: true) }
  let(:default_branch) { 'master' }
  let(:mr1) do
    create(
      :merge_request,
      state: 'opened',
      iid: 1,
      project_id: 1,
      source_branch: 'security-fixes-test-vulnerability',
      target_branch: default_branch,
      web_url: 'https://test.com/security/-/merge_requests/1',
      author: author
    )
  end

  let(:mr2) do
    create(
      :merge_request,
      state: 'opened',
      iid: 2,
      project_id: 1,
      target_branch: '12-10-stable-ee',
      web_url: 'https://test.com/security/-/merge_requests/2',
      author: author
    )
  end

  let(:mr3) do
    create(
      :merge_request,
      state: 'opened',
      iid: 3,
      project_id: 1,
      target_branch: '12-9-stable-ee',
      web_url: 'https://test.com/security/-/merge_requests/3',
      author: author
    )
  end

  let(:mr4) do
    create(
      :merge_request,
      state: 'opened',
      iid: 4,
      project_id: 1,
      target_branch: '12-8-stable-ee',
      web_url: 'https://test.com/security/-/merge_requests/4',
      author: author
    )
  end

  let(:merge_requests) { [mr1, mr2, mr3, mr4] }

  let(:issue1) do
    create(
      :issue,
      iid: 1,
      project_id: 1,
      web_url: 'https://test.com/security/-/issues/1',
      merge_requests: merge_requests,
      merge_request_targeting_default_branch: mr1,
      merge_requests_targeting_default_branch: [mr1],
      backports: [mr2, mr3, mr4],
      allowed_to_early_merge?: true
    )
  end

  let(:issues) { [issue1] }

  before do
    stub_const('ReleaseTools::Security::Client', client)

    allow(ReleaseTools::Security::IssueCrawler)
      .to receive(:new)
      .and_return(issue_crawler)

    allow(ReleaseTools::Security::IssueTable::Service)
      .to receive(:new)
      .and_return(double(execute: nil))

    allow(ReleaseTools::Slack::ReleaseJobEndNotifier)
      .to receive(:new)
      .and_return(notifier)
  end

  describe '#execute' do
    before do
      allow(issue_crawler)
        .to receive(:upcoming_security_issues_and_merge_requests)
        .and_return(issues)
    end

    context 'when processing MRs targeting default branch' do
      let(:batch_merger) do
        described_class.new(merge_default: true)
      end

      before do
        allow(ReleaseTools::Security::MergeWhenPipelineSucceedsService)
          .to receive(:new)
          .with(client, mr1)
          .and_return(mwps_service)
      end

      it 'only process MR targeting default branch' do
        expect(mwps_service).to receive(:execute)

        expect(ReleaseTools::Slack::ReleaseJobEndNotifier).to receive(:new)
          .with(job_type: 'Default merge', status: :success, release_type: :patch)
          .and_return(notifier)

        without_dry_run do
          batch_merger.execute
        end
      end

      context 'without an MR targeting default branch' do
        let(:issue1) do
          create(
            :issue,
            iid: 1,
            web_url: 'https://test.com/security/-/issues/1',
            merge_requests: merge_requests,
            merge_request_targeting_default_branch: nil,
            backports: [mr2, mr3, mr4],
            allowed_to_early_merge?: false
          )
        end

        it 'does nothing' do
          expect(mwps_service).not_to receive(:execute)

          without_dry_run do
            batch_merger.execute
          end
        end
      end

      context 'when a security issue is not allowed to early merge' do
        let(:issue1) do
          create(
            :issue,
            iid: 1,
            web_url: 'https://test.com/security/-/issues/1',
            merge_requests: merge_requests,
            merge_request_targeting_default_branch: mr1,
            backports: [mr2, mr3, mr4],
            allowed_to_early_merge?: false
          )
        end

        it 'does not process MR targeting default branch' do
          expect(mwps_service).not_to receive(:execute)

          without_dry_run do
            batch_merger.execute
          end
        end
      end

      context 'when there is an error' do
        it 'sends a slack failure notification and raises' do
          expect(mwps_service).to receive(:execute).and_raise(StandardError)

          expect(ReleaseTools::Slack::ReleaseJobEndNotifier).to receive(:new)
            .with(job_type: 'Default merge', status: :failed, release_type: :patch)
            .and_return(notifier)

          expect { without_dry_run { batch_merger.execute } }.to raise_error(StandardError)
        end
      end
    end

    context 'when pending merge requests' do
      let(:issue1) do
        create(
          :issue,
          iid: 1,
          web_url: 'https://test.com/security/-/issues/1',
          merge_requests: merge_requests,
          merge_request_targeting_default_branch: mr1,
          backports: [mr2, mr3, mr4],
          allowed_to_early_merge?: true,
          pending_merge_requests: [mr2, mr3, mr4],
          default_merge_request_handled?: true
        )
      end

      let(:merge_request) do
        create(
          :merge_request,
          target_branch: default_branch,
          merge_commit_sha: 'asdf',
          project_id: 1,
          web_url: 'https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/1'
        )
      end

      before do
        issue1.backports.each do |mr|
          allow(client)
            .to receive(:accept_merge_request)
            .with(mr.project_id, mr.iid, squash: true, should_remove_source_branch: true)
            .and_return(merge_request)
        end
      end

      it 'processes pending merge requests' do
        issue1.backports.each do |backport|
          expect(client)
            .to receive(:accept_merge_request)
            .with(backport.project_id, backport.iid, squash: true, should_remove_source_branch: true)
        end

        expect(ReleaseTools::Slack::ReleaseJobEndNotifier).to receive(:new)
          .with(job_type: 'Merge', status: :success, release_type: :patch)
          .and_return(notifier)

        without_dry_run do
          batch_merger.execute
        end
      end

      it 'updates the patch release status metric with closed status' do
        expect(ReleaseTools::Metrics::PatchReleaseStatus).to receive(:new).with(status: :closed)
          .and_return(instance_double(ReleaseTools::Metrics::PatchReleaseStatus, execute: true))

        without_dry_run do
          batch_merger.execute
        end
      end

      context 'when the default MR is not merged' do
        before do
          allow(issue1).to receive(:default_merge_request_handled?).and_return(false)
        end

        it 'skips the issue when the default MR is not merged' do
          expect(client).not_to receive(:accept_merge_request)

          expect(batch_merger.logger).to receive(:fatal).with(
            'Issue skipped. Default merge request has not been merged',
            { issue: issue1.web_url }
          )

          expect(ReleaseTools::Slack::ReleaseJobEndNotifier).to receive(:new)
            .with(job_type: 'Merge', status: :failed, release_type: :patch)
            .and_return(notifier)

          without_dry_run do
            batch_merger.execute
          end
        end
      end

      context 'when there is an error' do
        it 'sends a slack failure notification and raises' do
          expect(client).to receive(:accept_merge_request).and_raise(StandardError)

          expect(ReleaseTools::Slack::ReleaseJobEndNotifier).to receive(:new)
            .with(job_type: 'Merge', status: :failed, release_type: :patch)
            .and_return(notifier)

          expect { without_dry_run { batch_merger.execute } }.to raise_error(StandardError)
        end
      end
    end

    context 'when security implementation issues are not ready' do
      it 'does not process the issues' do
        allow(issue_crawler)
          .to receive(:upcoming_security_issues_and_merge_requests)
          .and_return([])

        expect(batch_merger)
          .not_to receive(:process_merge_requests)

        batch_merger.execute
      end
    end
  end
end
