# frozen_string_literal: true

require 'spec_helper'

describe ReleaseTools::Security::ImplementationIssue do
  let(:project) do
    build(
      :project,
      iid: 1,
      name: 'GitLab'
    )
  end

  let(:references_response) do
    double(
      'Gitlab::ObjectifiedHash',
      short: 430,
      relative: "#430",
      full: 'gitlab-org/release-tools#430'
    )
  end

  let(:issue_response) do
    double(
      'Gitlab::ObjectifiedHash',
      iid: 430,
      project_id: project.iid,
      web_url: 'https://gitlab.com/gitlab-org/security/gitlab/-/issues/430',
      references: references_response,
      labels: []
    )
  end

  let(:release_bot) do
    {
      id: described_class::GITLAB_RELEASE_BOT_ID,
      name: 'GitLab Release Bot'
    }
  end

  let(:mr1) do
    build(
      :merge_request,
      state: described_class::OPENED,
      target_branch: 'master',
      assignees: [release_bot]
    )
  end

  let(:mr2) do
    build(
      :merge_request,
      state: described_class::OPENED,
      target_branch: '12-10-stable-ee',
      assignees: [release_bot]
    )
  end

  let(:mr3) do
    build(
      :merge_request,
      state: described_class::OPENED,
      target_branch: '12-9-stable-ee',
      assignees: [release_bot]
    )
  end

  let(:mr4) do
    build(
      :merge_request,
      state: described_class::OPENED,
      target_branch: '12-8-stable-ee',
      assignees: [release_bot]
    )
  end

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

  subject { described_class.new(issue_response, merge_requests) }

  describe '#project_id' do
    it { expect(subject.project_id).to eq(issue_response.project_id) }
  end

  describe '#iid' do
    it { expect(subject.iid).to eq(issue_response.iid) }
  end

  describe '#merge_requests' do
    it { expect(subject.merge_requests).to match_array(merge_requests) }
  end

  describe '#web_url' do
    it { expect(subject.web_url).to eq(issue_response.web_url) }
  end

  describe '#reference' do
    it { expect(subject.reference).to eq(issue_response.references.full) }
  end

  describe '#ready_to_be_processed?' do
    let(:validator) { double(:validator) }

    before do
      allow(ReleaseTools::Security::MergeRequestsValidator)
        .to receive(:new)
        .and_return(validator)

      allow(validator)
        .to receive(:execute)
        .and_return([merge_requests, []])
    end

    context 'with 4 or more merge requests associated and all of them assigned to GitLab bot' do
      it { is_expected.to be_ready_to_be_processed }

      it 'does not populate pending_reasons' do
        expect(subject.pending_reasons).to be_empty
      end
    end

    context 'with less than 4 associated merge requests' do
      let(:merge_requests) { [mr1, mr2] }

      it { is_expected.not_to be_ready_to_be_processed }

      it 'populates pending_reasons' do
        subject.ready_to_be_processed?

        expect(subject.pending_reasons).to eq([
          "Every security issue is expected to have 4 MRs; 3 backports targeting the versions in the " \
          "[next scheduled patch release](https://gitlab.com/gitlab-org/gitlab/-/issues/?label_name%5B%5D=upcoming%20security%20release) " \
          "and 1 MR targeting master. If this issue legitimately does not need to have all 4 MRs, please add the ~\"reduced backports\" label to this issue"
        ])
      end

      it 'is idempotent' do
        2.times { subject.ready_to_be_processed? }

        expect(subject.pending_reasons).to eq([
          "Every security issue is expected to have 4 MRs; 3 backports targeting the versions in the " \
          "[next scheduled patch release](https://gitlab.com/gitlab-org/gitlab/-/issues/?label_name%5B%5D=upcoming%20security%20release) " \
          "and 1 MR targeting master. If this issue legitimately does not need to have all 4 MRs, please add the ~\"reduced backports\" label to this issue"
        ])
      end

      it 'allows an exception label' do
        allow(issue_response)
          .to receive(:labels)
          .and_return([described_class::BACKPORT_EXCEPTION_LABEL])

        expect(subject).to be_ready_to_be_processed
        expect(subject.pending_reasons).to be_empty
      end

      context 'with no merge requests' do
        let(:merge_requests) { [] }

        it 'rejects when no merge requests are present' do
          subject.ready_to_be_processed?

          expect(subject.pending_reasons).to include(
            "There are no MRs linked to this issue. Follow the steps in the issue description to create the required MRs."
          )
        end
      end
    end

    context 'when a merge request is not assigned to the GitLab Release Bot' do
      let(:assignee1) do
        {
          id: 1234,
          name: 'Joe'
        }
      end

      let(:mr4) do
        build(
          :merge_request,
          state: described_class::OPENED,
          target_branch: '12-10-stable-ee',
          assignees: [assignee1]
        )
      end

      it { is_expected.not_to be_ready_to_be_processed }

      it 'populates pending reason' do
        subject.ready_to_be_processed?

        expect(subject.pending_reasons).to eq(["The following merge requests need to be assigned to the `@gitlab-release-tools-bot`: #{mr4.web_url}"])
      end
    end

    context 'with a merged merge request targeting master' do
      let(:mr1) do
        build(
          :merge_request,
          state: 'merged',
          target_branch: 'master',
          assignees: [release_bot]
        )
      end

      it { is_expected.to be_ready_to_be_processed }
    end

    context 'with merged backports' do
      let(:mr2) do
        build(
          :merge_request,
          state: described_class::MERGED,
          target_branch: '12-10-stable-ee',
          assignees: []
        )
      end

      it { is_expected.not_to be_ready_to_be_processed }

      it 'populates pending reason' do
        subject.ready_to_be_processed?

        expect(subject.pending_reasons).to eq([
          "The following backport MRs need to be in the 'opened' state: #{mr2.web_url}",
          "The following merge requests need to be assigned to the `@gitlab-release-tools-bot`: #{mr2.web_url}"
        ])
      end
    end

    context 'with valid merge requests' do
      it { is_expected.to be_ready_to_be_processed }
    end

    context 'with invalid merge requests' do
      before do
        allow(validator)
        .to receive(:execute)
        .and_return([[mr1, mr2, mr3], [mr4]])
      end

      it { is_expected.not_to be_ready_to_be_processed }

      it 'populates pending reason' do
        subject.ready_to_be_processed?

        expect(subject.pending_reasons).to eq([
          "The following merge requests are not ready. A comment has been posted in each MR with more details: #{mr4.web_url}"
        ])
      end
    end

    context 'with merged merge requests' do
      let(:mr1) do
        double(
          :merge_request,
          state: described_class::MERGED,
          target_branch: 'master',
          assignees: [release_bot]
        )
      end

      it 'does not validate merged merge requests' do
        opened_merge_requests = [mr2, mr3, mr4]

        expect(validator)
          .to receive(:execute)
          .with(merge_requests: opened_merge_requests)

        subject.ready_to_be_processed?
      end
    end
  end

  describe '#merge_requests_targeting_default_branch' do
    let(:merge_requests) { [mr1, mr2, mr3, mr4, mr5] }

    let(:mr5) do
      build(
        :merge_request,
        state: described_class::MERGED,
        target_branch: 'master',
        assignees: [release_bot]
      )
    end

    it 'returns the two merge requests targeting master' do
      expect(subject.merge_requests_targeting_default_branch).to eq([mr1, mr5])
    end
  end

  describe '#merge_request_targeting_default_branch' do
    it 'returns the merge request targeting master' do
      expect(subject.merge_request_targeting_default_branch).to eq(mr1)
    end
  end

  describe '#default_merge_request_handled?' do
    let(:mr1) do
      build(
        :merge_request,
        state: described_class::MERGED,
        target_branch: 'master'
      )
    end

    it 'returns true if merge request targeting master is in merged state' do
      expect(subject.default_merge_request_handled?).to be(true)
    end

    context 'opened MR' do
      let(:mr1) do
        build(
          :merge_request,
          state: described_class::OPENED,
          target_branch: 'master'
        )
      end

      it 'returns false if merge request targeting master is in non-merged state' do
        expect(subject.default_merge_request_handled?).to be(false)
      end
    end

    context 'when there is no default merge request' do
      let(:merge_requests) { [mr2] }
      let(:labels) { [] }

      before do
        allow(issue_response)
          .to receive(:labels)
          .and_return(labels)
      end

      it 'returns false' do
        expect(subject.default_merge_request_handled?).to be(false)
      end

      context 'with reduced backports label' do
        let(:labels) { [described_class::BACKPORT_EXCEPTION_LABEL] }

        it 'returns true' do
          expect(subject.default_merge_request_handled?).to be(true)
        end
      end
    end
  end

  describe '#mwps_set_on_default_merge_request?' do
    let(:mr1) do
      build(
        :merge_request,
        state: described_class::OPENED,
        merge_when_pipeline_succeeds: true,
        target_branch: 'master'
      )
    end

    it 'returns true if merge request targeting master is in open state and MWPS is set' do
      expect(subject.mwps_set_on_default_merge_request?).to be(true)
    end

    context 'when there is no default merge request' do
      let(:merge_requests) { [mr2] }

      it 'returns false' do
        expect(subject.mwps_set_on_default_merge_request?).to be(false)
      end
    end
  end

  describe '#default_merge_request_deployed?' do
    let(:mr1) do
      build(
        :merge_request,
        target_branch: 'master',
        project_id: ReleaseTools::Project::GitlabEe.security_id,
        first_deployed_to_production_at: Time.now.iso8601,
        web_url: 'https://gitlab.com',
        iid: 346
      )
    end

    it 'returns true if API returns MR' do
      allow(ReleaseTools::GitlabClient)
        .to receive(:merge_requests)
        .with(mr1.project_id, { iids: [mr1.iid], environment: 'gprd' })
        .and_return([mr1])

      expect(subject.default_merge_request_deployed?).to be(true)
    end

    it 'returns false if API returns empty response' do
      allow(ReleaseTools::GitlabClient)
        .to receive(:merge_requests)
        .with(mr1.project_id, { iids: [mr1.iid], environment: 'gprd' })
        .and_return([])

      expect(subject.default_merge_request_deployed?).to be(false)
    end

    context 'when there is no default merge request' do
      let(:merge_requests) { [mr2] }

      it 'returns false' do
        expect(subject.default_merge_request_deployed?).to be(false)
      end
    end
  end

  describe '#backports_merged?' do
    let(:mr1) do
      build(
        :merge_request,
        state: described_class::OPENED,
        target_branch: '12-7-stable-ee',
        assignees: [release_bot]
      )
    end

    let(:mr2) do
      build(
        :merge_request,
        state: described_class::MERGED,
        target_branch: '12-8-stable-ee',
        assignees: [release_bot]
      )
    end

    let(:mr3) do
      double(
        :merge_request,
        state: described_class::MERGED,
        target_branch: '12-9-stable-ee',
        assignees: [release_bot]
      )
    end

    context 'when all backports have not been merged' do
      let(:merge_requests) { [mr1, mr2] }

      it 'returns false' do
        expect(subject.backports_merged?).to be(false)
      end
    end

    context 'when all backports have been merged' do
      let(:merge_requests) { [mr2, mr3] }

      it 'returns true' do
        expect(subject.backports_merged?).to be(true)
      end
    end

    context 'when there are no backports' do
      let(:merge_requests) { [] }

      it 'returns false' do
        expect(subject.backports_merged?).to be(false)
      end
    end
  end

  describe '#backports' do
    let(:mr5) do
      double(
        :merge_request,
        target_branch: '12-10-stable',
        assignees: [release_bot]
      )
    end

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

    it 'returns merge requests targeting stable branches' do
      merge_requests_targeting_stable_branches = [
        mr2,
        mr3,
        mr5
      ]

      expect(subject.backports)
        .to match_array(merge_requests_targeting_stable_branches)
    end
  end

  describe '#processed?' do
    let(:mr1) { build(:merge_request, state: described_class::MERGED) }
    let(:mr2) { build(:merge_request, state: described_class::MERGED) }

    let(:merge_requests) do
      [mr1, mr2]
    end

    context 'with all merge requests merged' do
      it 'returns true' do
        issue1 = described_class.new(double.as_null_object, merge_requests)

        expect(issue1).to be_processed
      end
    end

    context 'with a merge request opened' do
      let(:mr2) do
        build(
          :merge_request,
          state: described_class::OPENED
        )
      end

      it 'returns false' do
        issue1 = described_class.new(double.as_null_object, merge_requests)

        expect(issue1).not_to be_processed
      end
    end
  end

  describe '#allowed_to_early_merge?' do
    let(:mr) do
      build(
        :merge_request,
        target_branch: 'master',
        state: described_class::OPENED,
        project_id: ReleaseTools::Project::GitlabEe.security_id
      )
    end

    subject do
      described_class.new(issue_response, [mr])
    end

    context 'with an MR targeting master' do
      context 'with an opened MR that belongs to GitLab Security' do
        it { is_expected.to be_allowed_to_early_merge }
      end

      context 'when an MR belongs to other project' do
        let(:mr) do
          build(
            :merge_request,
            state: described_class::OPENED,
            target_branch: 'master',
            project_id: 1
          )
        end

        it { is_expected.not_to be_allowed_to_early_merge }
      end

      context 'with a merged MR' do
        let(:mr) do
          build(
            :merge_request,
            target_branch: 'master',
            state: described_class::MERGED,
            project_id: ReleaseTools::Project::GitlabEe.security_id
          )
        end

        it { is_expected.not_to be_allowed_to_early_merge }
      end
    end

    context 'with no MR targeting master' do
      let(:mr) do
        build(
          :merge_request,
          target_branch: '13-3-stable-ee',
          project_id: 123
        )
      end

      it { is_expected.not_to be_allowed_to_early_merge }
    end
  end

  describe '#pending_merge_requests' do
    context 'with a GitLab or Omnibus security issue' do
      it 'returns backports' do
        described_class::PROJECTS_ALLOWED_TO_EARLY_MERGE.each do |project_id|
          issue_response = double(
            project_id: project_id, iid: 1, web_url: '', references: references_response
          )

          issue = described_class.new(issue_response, merge_requests)

          expect(issue.pending_merge_requests).to match_array(issue.backports)
        end
      end
    end

    context 'with another security issue' do
      it 'returns opened merge requests' do
        issue = described_class.new(issue_response, merge_requests)

        expect(issue.pending_merge_requests).to match_array(merge_requests)
      end
    end
  end

  describe '#cves_issue' do
    let(:finder) { instance_double(ReleaseTools::Security::RelatedIssuesFinder) }
    let(:cves_issue) { create(:issue) }

    before do
      allow(ReleaseTools::Security::RelatedIssuesFinder)
        .to receive(:new)
        .and_return(finder)

      allow(finder)
        .to receive(:cves_issue)
        .and_return(cves_issue)
    end

    context 'when the cve issue can be found' do
      it 'returns a CVES issue instance' do
        expect(subject.cves_issue)
          .to be_a_instance_of(ReleaseTools::Security::CvesIssue)
      end
    end

    context 'when the cve issue cannot be found' do
      let(:cves_issue) { nil }

      it 'returns nothing' do
        expect(subject.cves_issue).to be_nil
      end
    end
  end

  describe '#canonical_issue' do
    let(:finder) { instance_double(ReleaseTools::Security::RelatedIssuesFinder) }
    let(:canonical_issue) { create(:issue) }

    before do
      allow(ReleaseTools::Security::RelatedIssuesFinder)
        .to receive(:new)
        .and_return(finder)

      allow(finder)
        .to receive(:canonical_issue)
        .and_return(canonical_issue)
    end

    it 'returns the canonical issue' do
      expect(subject.canonical_issue).to eq(canonical_issue)
    end

    context 'when the cve issue cannot be found' do
      let(:canonical_issue) { nil }

      it 'returns nothing' do
        expect(subject.canonical_issue).to be_nil
      end
    end
  end
end
