# frozen_string_literal: true

require 'spec_helper'

describe ReleaseTools::Security::IssueTable::Builder do
  subject(:generate) do
    described_class.new(release_manager_comments: '', issues: issues).generate
  end

  let(:issue_and_severity_column_index) { 0 }
  let(:security_target_column_index) { 1 }
  let(:master_mr_merged_column_index) { 2 }
  let(:master_mr_deployed_column_index) { 3 }
  let(:backports_merged_column_index) { 4 }
  let(:bot_comments_column_index) { 5 }
  let(:release_manager_comments_column_index) { 6 }

  let(:mr1) { build(:merge_request, project_id: 1) }
  let(:issue1_mrs) { [mr1] }
  let(:release_issue) { build(:issue, project_id: 1) }
  let(:issue1) { build(:issue, project_id: 1) }
  let(:issues) { [build(:security_implementation_issue, issue: issue1, merge_requests: issue1_mrs)] }
  let(:client) { instance_spy(ReleaseTools::Security::Client) }
  let(:validator) { instance_double(ReleaseTools::Security::MergeRequestsValidator) }

  before do
    allow(ReleaseTools::GitlabClient).to receive_messages(issue_notes: double(auto_paginate: []), merge_requests: [])
  end

  shared_examples 'adding managed versioning comment' do
    context 'when the project is manually released' do
      let(:issue1) { build(:issue, project_id: ReleaseTools::ManagedVersioning::PROJECTS_NEEDING_MANUAL_RELEASES.first.security_id) }

      it 'adds a note about managed versioning' do
        result = generate

        row = find_row(result, issue1.web_url)
        expect(row[bot_comments_column_index]).to include(
          'Managed versioning. May require manual merging.'
        )
      end
    end
  end

  describe '#generate' do
    before do
      allow(ReleaseTools::Security::MergeRequestsValidator)
        .to receive(:new)
        .and_return(validator)

      allow(validator)
        .to receive(:execute)
        .with({ merge_requests: [mr1] })
        .and_return([[mr1], []])
    end

    it 'adds issue URL to first column' do
      result = generate

      row = find_row(result, issue1.web_url)
      expect(row[issue_and_severity_column_index]).to match(/^ +#{issue1.web_url} +$/)
    end

    context 'with severity label' do
      let(:issue1) { build(:issue, project_id: 1, labels: ['severity::2']) }

      it 'adds issue URL and severity label to first column' do
        result = generate

        row = find_row(result, issue1.web_url)
        expect(row[issue_and_severity_column_index]).to match(/^ +#{issue1.web_url} - ~severity::2 +$/)
      end
    end

    context 'when default branch MR has been merged' do
      let(:issue2) do
        build(:issue, project_id: 1)
      end

      let(:mr2) do
        build(
          :merge_request,
          :merged,
          project_id: 1
        )
      end

      let(:issues) do
        [
          build(:security_implementation_issue, issue: issue1, merge_requests: [mr1]),
          build(:security_implementation_issue, issue: issue2, merge_requests: [mr2])
        ]
      end

      it 'adds a checkmark to master MR merged column' do
        result = generate

        row = find_row(result, issue2.web_url)
        expect(row[master_mr_merged_column_index]).to include(':white_check_mark:')
      end

      it 'keeps issues where master MR has been merged at top of table' do
        result = generate

        rows = table_rows(result)
        expect(rows.length).to eq(2)
        expect(rows[0]).to include(issue2.web_url)
        expect(rows[1]).to include(issue1.web_url)
      end
    end

    context 'when backports have been merged' do
      let(:backport_mr) { build(:merge_request, :merged, target_branch: '15-10-stable') }

      let(:issue1_mrs) { [mr1, backport_mr] }

      it 'adds a checkmark to backports merged column' do
        result = generate

        row = find_row(result, issue1.web_url)
        expect(row[backports_merged_column_index]).to include(':white_check_mark:')
      end
    end

    context 'when default MR has been deployed' do
      before do
        allow(ReleaseTools::GitlabClient)
          .to receive(:merge_requests)
          .with(mr1.project_id, { iids: [mr1.iid], environment: 'gprd' })
          .and_return([mr1])
      end

      it 'adds a checkmark to master MR deployed column' do
        result = generate

        row = find_row(result, issue1.web_url)
        expect(row[master_mr_deployed_column_index]).to include(':white_check_mark:')
      end
    end

    context 'when there are pending_reasons' do
      it 'adds a comment to the bot comments column' do
        result = generate

        row = find_row(result, issue1.web_url)
        expect(row[bot_comments_column_index]).to include(
          '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.'
        )
      end

      context 'when issue has already been processed' do
        let(:mr1) { build(:merge_request, :merged, project_id: 1) }

        it 'does not add comment to bot comments column' do
          result = generate

          row = find_row(result, issue1.web_url)
          expect(row[bot_comments_column_index]).to match(/^ +$/)
        end
      end

      it_behaves_like 'adding managed versioning comment'
    end

    context 'with security-target label' do
      before do
        allow(issue1)
          .to receive(:labels)
          .and_return(['security-target'])
      end

      it 'adds a checkmark to the security-target column' do
        result = generate

        row = find_row(result, issue1.web_url)
        expect(row[security_target_column_index]).to include(':white_check_mark:')
      end
    end

    context 'when MWPS has been set on default MR' do
      let(:release_bot_user) { build(:user, id: ReleaseTools::Security::ImplementationIssue::GITLAB_RELEASE_BOT_ID) }

      let(:mr1) do
        build(:merge_request, project_id: 1, merge_when_pipeline_succeeds: true, assignees: [release_bot_user])
      end

      let(:mr2) do
        build(:merge_request, project_id: 1, target_branch: '15-10-stable', assignees: [release_bot_user])
      end

      let(:mr3) do
        build(:merge_request, project_id: 1, target_branch: '15-9-stable', assignees: [release_bot_user])
      end

      let(:mr4) do
        build(:merge_request, project_id: 1, target_branch: '15-8-stable', assignees: [release_bot_user])
      end

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

      before do
        allow(validator)
          .to receive(:execute)
          .with({ merge_requests: issue1_mrs })
          .and_return([issue1_mrs, []])
      end

      it 'adds a comment to bot comment column' do
        result = generate

        row = find_row(result, issue1.web_url)
        expect(row[bot_comments_column_index])
          .to include("MWPS set on default branch MR: https://example.com/foo/bar/-/merge_requests/#{mr1.iid}")
      end

      it_behaves_like 'adding managed versioning comment'
    end

    context 'when release manager comments are passed as parameter' do
      subject(:generate) do
        described_class.new(release_manager_comments: release_manager_comments, issues: issues).generate
      end

      let(:release_manager_comments) do
        {
          issue1.web_url => 'Some comment',
          issue2.web_url => 'Some other comment'
        }
      end

      let(:mr2) do
        build(
          :merge_request,
          :merged,
          project_id: 1
        )
      end

      let(:issue2) { build(:issue, project_id: 1, labels: ['severity::2']) }

      let(:issues) do
        [
          build(:security_implementation_issue, issue: issue1, merge_requests: [mr1]),
          build(:security_implementation_issue, issue: issue2, merge_requests: [mr2])
        ]
      end

      it 'retains release manager comments in table' do
        result = generate

        row = find_row(result, issue1.web_url)
        expect(row[release_manager_comments_column_index]).to include('Some comment')

        row = find_row(result, issue2.web_url)
        expect(row[release_manager_comments_column_index]).to include('Some other comment')
      end

      it 'generates expected table' do
        expected_table =
          <<~EOF
            ## Security issues

            | Issue | ~"security-target" | Master merged? | Deployed? | Backports merged? | Bot Comments | Release manager comments |
            |-------|--------------------|----------------|-----------|-------------------|--------------|--------------------------|
            | #{issue2.web_url} - ~severity::2 |  | :white_check_mark: |  |  |  | Some other comment |
            | #{issue1.web_url} |  |  |  |  | - 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<br/>- The following merge requests need to be assigned to the `@gitlab-release-tools-bot`: #{mr1.web_url} | Some comment |

            ---

            :robot: <sub>This table was generated by [release-tools](https://gitlab.com/gitlab-org/release-tools/).
            Please open an issue in the [Delivery team issue tracker](https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues)
            if you have any suggestions or bug reports.</sub>
          EOF

        expect(generate).to eq(expected_table)
      end
    end
  end

  # @param comment_str [String] is the full markdown comment string
  # @param str_to_find [String] is a string in the row that is to be returned. This can be the issue URL for example.
  # @return [Array<String>] the row that contains str_to_find, split by each column. So each element in the array
  #                         is a column of the row.
  def find_row(comment_str, str_to_find)
    rows = table_rows(comment_str)
    row = rows.detect { |r| r.include?(str_to_find) }
    return unless row

    row
      .delete_prefix('|')
      .delete_suffix('|')
      .split('|')
  end

  # @param comment_str [String] is the full markdown comment string
  # @return [Array<String>] the rows of the table excluding the header rows. Each element in the array
  #                         is a row in the table.
  def table_rows(comment_str)
    rows = comment_str
             .delete_prefix(ReleaseTools::Security::IssueTable::Service::NOTE_HEADER)
             .strip
             .split("\n")
             .select { |r| r.include?('|') }

    # Exclude the header rows
    rows[2..]
  end
end
