# frozen_string_literal: true

require 'spec_helper'

RSpec.describe API::MergeRequestApprovalRules do
  let_it_be(:user) { create(:user) }
  let_it_be(:other_user) { create(:user) }
  let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
  let(:merge_request) { create(:merge_request, author: user, source_project: project, target_project: project) }

  shared_examples_for 'a protected API endpoint for merge request approval rule action' do
    context 'disable_overriding_approvers_per_merge_request is set to true' do
      before do
        project.update!(disable_overriding_approvers_per_merge_request: true)

        action
      end

      it 'responds with 403' do
        expect(response).to have_gitlab_http_status(:forbidden)
      end
    end

    context 'disable_overriding_approvers_per_merge_request is set to false' do
      before do
        project.update!(disable_overriding_approvers_per_merge_request: false)

        action
      end

      context 'user cannot update merge request' do
        let(:current_user) { other_user }

        it 'responds with 403' do
          expect(response).to have_gitlab_http_status(:forbidden)
        end
      end
    end
  end

  shared_examples_for 'a protected API endpoint that only allows action on regular merge request approval rule' do
    context 'approval rule is not a regular type' do
      let(:approval_rule) { create(:code_owner_rule, merge_request: merge_request) }

      it 'responds with 403' do
        expect(response).to have_gitlab_http_status(:forbidden)
      end
    end
  end

  describe 'GET /projects/:id/merge_requests/:merge_request_iid/approval_rules' do
    let(:current_user) { other_user }
    let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/approval_rules" }

    context 'user cannot read merge request' do
      before do
        project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)

        get api(url, other_user)
      end

      it 'responds with 403' do
        expect(response).to have_gitlab_http_status(:forbidden)
      end
    end

    context 'use can read merge request' do
      let(:approver) { create(:user) }
      let(:group) { create(:group) }
      let(:source_rule) { nil }
      let(:users) { [approver] }
      let(:groups) { [group] }

      let!(:mr_approval_rule) do
        create(
          :approval_merge_request_rule,
          merge_request: merge_request,
          approval_project_rule: source_rule,
          users: users,
          groups: groups
        )
      end

      before do
        group.add_developer(approver)
        merge_request.approvals.create(user: approver)

        get api(url, current_user)
      end

      it 'matches the response schema' do
        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to match_response_schema('public_api/v4/merge_request_approval_rules', dir: 'ee')

        rules = json_response

        expect(rules.size).to eq(1)
        expect(rules.first['name']).to eq(mr_approval_rule.name)
        expect(rules.first['approvals_required']).to eq(mr_approval_rule.approvals_required)
        expect(rules.first['rule_type']).to eq(mr_approval_rule.rule_type)
        expect(rules.first['section']).to be_nil
        expect(rules.first['contains_hidden_groups']).to eq(false)
        expect(rules.first['source_rule']).to be_nil
        expect(rules.first['eligible_approvers']).to match([hash_including('id' => approver.id)])
        expect(rules.first['users']).to match([hash_including('id' => approver.id)])
        expect(rules.first['groups']).to match([hash_including('id' => group.id)])
      end

      context 'groups contain private groups' do
        let(:group) { create(:group, :private) }

        context 'current_user cannot see private group' do
          it 'hides private group' do
            rules = json_response

            expect(rules.first['contains_hidden_groups']).to eq(true)
            expect(rules.first['groups']).to be_empty
          end
        end

        context 'current_user can see private group' do
          let(:current_user) { approver }

          it 'shows private group' do
            rules = json_response

            expect(rules.first['contains_hidden_groups']).to eq(false)
            expect(rules.first['groups']).to match([hash_including('id' => group.id)])
          end
        end
      end

      context 'has existing merge request rule that overrides a project-level rule' do
        let(:source_rule) { create(:approval_project_rule, project: project) }

        it 'includes source_rule' do
          expect(json_response.first['source_rule']['approvals_required']).to eq(source_rule.approvals_required)
        end
      end
    end
  end

  describe 'POST /projects/:id/merge_requests/:merge_request_iid/approval_rules' do
    let(:current_user) { user }
    let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/approval_rules" }
    let(:approver) { create(:user) }
    let(:other_approver) { create(:user) }
    let(:group) { create(:group) }
    let(:other_group) { create(:group) }
    let(:approval_project_rule_id) { nil }
    let(:approver_params) do
      {
        user_ids: user_ids,
        group_ids: group_ids
      }
    end

    let(:user_ids) { [] }
    let(:group_ids) { [] }

    let(:params) do
      {
        name: 'Test',
        approvals_required: 1,
        approval_project_rule_id: approval_project_rule_id
      }.merge(approver_params)
    end

    let(:action) { post api(url, current_user), params: params }

    it_behaves_like 'a protected API endpoint for merge request approval rule action'

    context 'when user can update merge request and approval rules can be overridden' do
      before do
        project.update!(disable_overriding_approvers_per_merge_request: false)
        project.add_developer(approver)
        project.add_developer(other_approver)
        group.add_developer(approver)
        other_group.add_developer(other_approver)

        action
      end

      it 'matches the response schema' do
        expect(response).to have_gitlab_http_status(:created)
        expect(response).to match_response_schema('public_api/v4/merge_request_approval_rule', dir: 'ee')

        rule = json_response

        expect(rule['name']).to eq(params[:name])
        expect(rule['approvals_required']).to eq(params[:approvals_required])
        expect(rule['rule_type']).to eq('any_approver')
        expect(rule['contains_hidden_groups']).to eq(false)
        expect(rule['source_rule']).to be_nil
        expect(rule['eligible_approvers']).to be_empty
        expect(rule['users']).to be_empty
        expect(rule['groups']).to be_empty
      end

      context 'users are passed' do
        let(:user_ids) { "#{approver.id},#{other_approver.id}" }

        it 'includes users' do
          rule = json_response

          expect(rule['eligible_approvers'].map { |approver| approver['id'] }).to contain_exactly(approver.id, other_approver.id)
          expect(rule['users'].map { |user| user['id'] }).to contain_exactly(approver.id, other_approver.id)
        end
      end

      context 'groups are passed' do
        let(:group_ids) { "#{group.id},#{other_group.id}" }

        it 'includes groups' do
          rule = json_response

          expect(rule['eligible_approvers'].map { |approver| approver['id'] }).to contain_exactly(approver.id, other_approver.id)
          expect(rule['groups'].map { |group| group['id'] }).to contain_exactly(group.id, other_group.id)
        end
      end

      context 'approval_project_rule_id is passed' do
        let(:approval_project_rule) do
          create(
            :approval_project_rule,
            project: project,
            users: [approver],
            groups: [group]
          )
        end

        let(:approval_project_rule_id) { approval_project_rule.id }

        context 'with blank approver params' do
          it 'copies the attributes from the project rule except approvers' do
            rule = json_response

            expect(rule['name']).to eq(approval_project_rule.name)
            expect(rule['approvals_required']).to eq(params[:approvals_required])
            expect(rule['source_rule']['approvals_required']).to eq(approval_project_rule.approvals_required)
            expect(rule['eligible_approvers']).to eq([])
            expect(rule['users']).to eq([])
            expect(rule['groups']).to eq([])
          end
        end

        context 'with omitted approver params' do
          let(:approver_params) { {} }

          it 'copies the attributes from the project rule except approvals_required' do
            rule = json_response

            expect(rule['name']).to eq(approval_project_rule.name)
            expect(rule['approvals_required']).to eq(params[:approvals_required])
            expect(rule['source_rule']['approvals_required']).to eq(approval_project_rule.approvals_required)
            expect(rule['eligible_approvers']).to match([hash_including('id' => approver.id)])
            expect(rule['users']).to match([hash_including('id' => approver.id)])
            expect(rule['groups']).to match([hash_including('id' => group.id)])
          end
        end
      end
    end
  end

  describe 'PUT /projects/:id/merge_requests/:merge_request_iid/approval_rules/:approval_rule_id' do
    let(:current_user) { user }
    let(:existing_approver) { create(:user) }
    let(:existing_group) { create(:group) }

    let(:approval_rule) do
      create(
        :approval_merge_request_rule,
        merge_request: merge_request,
        name: 'Old Name',
        approvals_required: 2,
        users: [existing_approver],
        groups: [existing_group]
      )
    end

    let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/approval_rules/#{approval_rule.id}" }
    let(:new_approver) { create(:user) }
    let(:new_group) { create(:group) }
    let(:user_ids) { [] }
    let(:group_ids) { [] }
    let(:remove_hidden_groups) { nil }
    let(:other_approver) { create(:user) }
    let(:other_group) { create(:group) }

    let(:params) do
      {
        name: 'Test',
        approvals_required: 1,
        user_ids: user_ids,
        group_ids: group_ids,
        remove_hidden_groups: remove_hidden_groups
      }
    end

    let(:action) { put api(url, current_user), params: params }

    it_behaves_like 'a protected API endpoint for merge request approval rule action'

    context 'when user can update merge request and approval rules can be overridden' do
      before do
        project.update!(disable_overriding_approvers_per_merge_request: false)
        project.add_developer(existing_approver)
        project.add_developer(new_approver)
        project.add_developer(other_approver)
        existing_group.add_developer(existing_approver)
        new_group.add_developer(new_approver)
        other_group.add_developer(other_approver)

        action
      end

      it_behaves_like 'a protected API endpoint that only allows action on regular merge request approval rule'

      it 'matches the response schema' do
        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to match_response_schema('public_api/v4/merge_request_approval_rule', dir: 'ee')

        rule = json_response

        expect(rule['name']).to eq(params[:name])
        expect(rule['approvals_required']).to eq(params[:approvals_required])
        expect(rule['rule_type']).to eq(approval_rule.rule_type)
        expect(rule['contains_hidden_groups']).to eq(false)
        expect(rule['source_rule']).to be_nil
        expect(rule['eligible_approvers']).to be_empty
        expect(rule['users']).to be_empty
        expect(rule['groups']).to be_empty
      end

      context 'users are passed' do
        let(:user_ids) { "#{new_approver.id},#{existing_approver.id}" }

        it 'changes users' do
          rule = json_response

          expect(rule['eligible_approvers'].map { |approver| approver['id'] }).to contain_exactly(new_approver.id, existing_approver.id)
          expect(rule['users'].map { |user| user['id'] }).to contain_exactly(new_approver.id, existing_approver.id)
        end
      end

      context 'groups are passed' do
        let(:group_ids) { "#{new_group.id},#{other_group.id}" }

        it 'changes groups' do
          rule = json_response

          expect(rule['eligible_approvers'].map { |approver| approver['id'] }).to contain_exactly(new_approver.id, other_approver.id)
          expect(rule['groups'].map { |group| group['id'] }).to contain_exactly(new_group.id, other_group.id)
        end
      end

      context 'remove_hidden_groups is passed' do
        let(:existing_group) { create(:group, :private) }

        context 'when set to true' do
          let(:remove_hidden_groups) { true }

          it 'removes the existing private group' do
            expect(approval_rule.reload.groups).not_to include(existing_group)
          end
        end

        context 'when set to false' do
          let(:remove_hidden_groups) { false }

          it 'does not remove the existing private group' do
            expect(approval_rule.reload.groups).to include(existing_group)
          end
        end
      end
    end
  end

  describe 'DELETE /projects/:id/merge_requests/:merge_request_iid/approval_rules/:approval_rule_id' do
    let(:current_user) { user }
    let(:approval_rule) { create(:approval_merge_request_rule, merge_request: merge_request) }
    let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/approval_rules/#{approval_rule.id}" }
    let(:action) { delete api(url, current_user) }

    it_behaves_like 'a protected API endpoint for merge request approval rule action'

    context 'when user can update merge request and approval rules can be overridden' do
      before do
        project.update!(disable_overriding_approvers_per_merge_request: false)

        action
      end

      it_behaves_like 'a protected API endpoint that only allows action on regular merge request approval rule'

      it 'responds with 204' do
        expect(response).to have_gitlab_http_status(:no_content)
      end
    end
  end
end