# frozen_string_literal: true

require 'spec_helper'

RSpec.describe QuickActions::InterpretService, feature_category: :team_planning do
  let(:current_user) { create(:user) }
  let_it_be(:developer) { create(:user) }
  let(:developer2) { create(:user) }
  let(:user) { create(:user) }
  let_it_be(:user2) { create(:user) }
  let_it_be(:user3) { create(:user) }
  let_it_be_with_refind(:group) { create(:group) }
  let_it_be_with_refind(:project) { create(:project, :repository, :public, group: group) }
  let_it_be_with_reload(:issue) { create(:issue, project: project) }

  let(:service) { described_class.new(container: project, current_user: current_user) }

  before do
    stub_licensed_features(multiple_issue_assignees: true,
      multiple_merge_request_reviewers: true,
      multiple_merge_request_assignees: true)

    project.add_developer(current_user)
    project.add_developer(developer)
  end

  shared_examples 'quick action is unavailable' do |action|
    it 'does not recognize action' do
      expect(service.available_commands(target).map { |command| command[:name] }).not_to include(action)
    end
  end

  shared_examples 'quick action is available' do |action|
    it 'does recognize action' do
      expect(service.available_commands(target).map { |command| command[:name] }).to include(action)
    end
  end

  shared_examples 'failed command' do |error_msg|
    let(:match_msg) { error_msg ? eq(error_msg) : be_empty }

    it 'populates {} if content contains an unsupported command' do
      _, updates, _ = service.execute(content, issuable)

      expect(updates).to be_empty
    end

    it "returns #{error_msg || 'an empty'} message" do
      _, _, message = service.execute(content, issuable)

      expect(message).to match_msg
    end
  end

  shared_examples 'copy_metadata command' do
    it 'fetches issue or merge request and copies labels and milestone if content contains /copy_metadata reference' do
      source_issuable # populate the issue
      todo_label # populate this label
      inreview_label # populate this label
      _, updates, _ = service.execute(content, issuable)

      expect(updates[:add_label_ids]).to match_array([inreview_label.id, todo_label.id])

      if source_issuable.milestone
        expect(updates[:milestone_id]).to eq(source_issuable.milestone.id)
      else
        expect(updates).not_to have_key(:milestone_id)
      end
    end

    it 'returns the copy metadata message' do
      _, _, message = service.execute("/copy_metadata #{source_issuable.to_reference}", issuable)
      translated_string = _("Copied labels and milestone from %{source_issuable_to_reference}.")
      formatted_message = format(translated_string, source_issuable_to_reference: source_issuable.to_reference.to_s)

      expect(message).to eq(formatted_message)
    end
  end

  describe '#execute' do
    let(:merge_request) { create(:merge_request, source_project: project) }

    context 'assign command' do
      context 'there is a group' do
        let(:group) { create(:group) }

        before do
          group.add_developer(user)
          group.add_developer(user2)
          group.add_developer(user3)
        end

        it 'assigns to group members' do
          cmd = "/assign #{group.to_reference}"

          _, updates, _ = service.execute(cmd, issue)

          expect(updates).to include(assignee_ids: match_array([user.id, user2.id, user3.id]))
        end

        it 'does not assign to more than QuickActions::UsersFinder::MAX_QUICK_ACTION_USERS' do
          stub_const('Gitlab::QuickActions::UsersExtractor::MAX_QUICK_ACTION_USERS', 2)

          cmd = "/assign #{group.to_reference}"

          _, updates, messages = service.execute(cmd, issue)

          expect(updates).to be_blank
          expect(messages).to include('Too many users')
        end
      end

      context 'Issue' do
        it 'fetches assignees and populates them if content contains /assign' do
          issue.update!(assignee_ids: [user.id, user2.id])

          _, updates = service.execute("/unassign @#{user2.username}\n/assign @#{user3.username}", issue)

          expect(updates[:assignee_ids]).to match_array([user.id, user3.id])
        end

        context 'with test_case issue type' do
          it 'does not mark to update assignee' do
            test_case = create(:quality_test_case, project: project)

            _, updates = service.execute("/assign @#{user3.username}", test_case)

            expect(updates[:assignee_ids]).to eq(nil)
          end
        end

        context 'assign command with multiple assignees' do
          it 'fetches assignee and populates assignee_ids if content contains /assign' do
            issue.update!(assignee_ids: [user.id])

            _, updates = service.execute("/unassign @#{user.username}\n/assign @#{user2.username} @#{user3.username}", issue)

            expect(updates[:assignee_ids]).to match_array([user2.id, user3.id])
          end
        end
      end

      context 'Merge Request' do
        let(:merge_request) { create(:merge_request, source_project: project) }

        it 'fetches assignees and populates them if content contains /assign' do
          merge_request.update!(assignee_ids: [user.id])

          _, updates = service.execute("/assign @#{user2.username}", merge_request)

          expect(updates[:assignee_ids]).to match_array([user.id, user2.id])
        end

        context 'assign command with a group of users' do
          let(:group) { create(:group) }
          let(:project) { create(:project, group: group) }
          let(:group_members) { create_list(:user, 3) }
          let(:command) { "/assign #{group.to_reference}" }

          before do
            group_members.each { group.add_developer(_1) }
          end

          it 'adds group members' do
            merge_request.update!(assignee_ids: [user.id])

            _, updates = service.execute(command, merge_request)

            expect(updates[:assignee_ids]).to match_array [user.id, *group_members.map(&:id)]
          end
        end

        context 'assign command with multiple assignees' do
          it 'fetches assignee and populates assignee_ids if content contains /assign' do
            merge_request.update!(assignee_ids: [user.id])

            _, updates = service.execute("/assign @#{user.username}\n/assign @#{user2.username} @#{user3.username}", issue)

            expect(updates[:assignee_ids]).to match_array([user.id, user2.id, user3.id])
          end

          context 'unlicensed' do
            before do
              stub_licensed_features(multiple_merge_request_assignees: false)
            end

            it 'does not recognize /assign with multiple user references' do
              merge_request.update!(assignee_ids: [user.id])

              _, updates = service.execute("/assign @#{user2.username} @#{user3.username}", merge_request)

              expect(updates[:assignee_ids]).to match_array([user2.id])
            end
          end
        end
      end
    end

    context 'assign_reviewer command' do
      context 'with a merge request' do
        let(:merge_request) { create(:merge_request, source_project: project) }

        it 'fetches reviewers and populates them if content contains /assign_reviewer' do
          merge_request.update!(reviewer_ids: [user.id])

          _, updates = service.execute("/assign_reviewer @#{user2.username}\n/assign_reviewer @#{user3.username}", merge_request)

          expect(updates[:reviewer_ids]).to match_array([user.id, user2.id, user3.id])
        end

        context 'assign command with multiple reviewers' do
          it 'assigns multiple reviewers while respecting previous assignments' do
            merge_request.update!(reviewer_ids: [user.id])

            _, updates = service.execute("/assign_reviewer @#{user.username}\n/assign_reviewer @#{user2.username} @#{user3.username}", merge_request)

            expect(updates[:reviewer_ids]).to match_array([user.id, user2.id, user3.id])
          end
        end
      end
    end

    context 'unassign_reviewer command' do
      let(:content) { '/unassign_reviewer' }
      let(:merge_request) { create(:merge_request, source_project: project) }
      let(:merge_request_not_persisted) { build(:merge_request, source_project: project) }

      context 'unassign_reviewer command with multiple assignees' do
        it 'unassigns both reviewers if content contains /unassign_reviewer @user @user1' do
          merge_request.update!(reviewer_ids: [user.id, user2.id, user3.id])

          _, updates = service.execute("/unassign_reviewer @#{user.username} @#{user2.username}", merge_request)

          expect(updates[:reviewer_ids]).to match_array([user3.id])
        end

        it 'does not unassign reviewers if the content cannot be parsed' do
          merge_request.update!(reviewer_ids: [user.id, user2.id, user3.id])

          _, updates, msg = service.execute("/unassign_reviewer nobody", merge_request)

          expect(updates[:reviewer_ids]).to be_nil
          expect(msg).to eq "Could not apply unassign_reviewer command. Failed to find users for 'nobody'."
        end

        context 'with "me" alias' do
          context 'when the current user is referenced both by username and "me"' do
            let(:content) do
              <<-QUICKACTION
/assign me
/assign_reviewer #{developer2.to_reference} #{current_user.to_reference}
/unassign_reviewer me
              QUICKACTION
            end

            it 'will correctly remove the reviewer' do
              _, updates, _ = service.execute(content, merge_request)

              expect(updates[:reviewer_ids]).to match_array([developer2.id])
            end

            it 'will correctly remove the reviewer even for non-persisted merge requests' do
              _, updates, _ = service.execute(content, merge_request_not_persisted)

              expect(updates[:reviewer_ids]).to match_array([developer2.id])
            end
          end
        end
      end
    end

    context 'unassign command' do
      let(:content) { '/unassign' }

      context 'Issue' do
        it 'unassigns user if content contains /unassign @user' do
          issue.update!(assignee_ids: [user.id, user2.id])

          _, updates = service.execute("/assign @#{user3.username}\n/unassign @#{user2.username}", issue)

          expect(updates[:assignee_ids]).to match_array([user.id, user3.id])
        end

        it 'unassigns both users if content contains /unassign @user @user1' do
          issue.update!(assignee_ids: [user.id, user2.id])

          _, updates = service.execute("/assign @#{user3.username}\n/unassign @#{user2.username} @#{user3.username}", issue)

          expect(updates[:assignee_ids]).to match_array([user.id])
        end

        it 'unassigns all the users if content contains /unassign' do
          issue.update!(assignee_ids: [user.id, user2.id])

          _, updates = service.execute("/assign @#{user3.username}\n/unassign", issue)

          expect(updates[:assignee_ids]).to be_empty
        end

        it 'does not apply command if the argument cannot be parsed' do
          issue.update!(assignee_ids: [user.id, user2.id])

          _, updates, msg = service.execute("/assign nobody", issue)

          expect(updates[:assignee_ids]).to be_nil
          expect(msg).to eq "Could not apply assign command. Failed to find users for 'nobody'."
        end
      end

      context 'with a Merge Request' do
        let(:merge_request) { create(:merge_request, source_project: project) }

        it 'unassigns user if content contains /unassign @user' do
          merge_request.update!(assignee_ids: [user.id, user2.id])

          _, updates = service.execute("/unassign @#{user2.username}", merge_request)

          expect(updates[:assignee_ids]).to match_array([user.id])
        end

        describe 'applying unassign command with multiple assignees' do
          it 'unassigns both users if content contains /unassign @user @user1' do
            merge_request.update!(assignee_ids: [user.id, user2.id, user3.id])

            _, updates = service.execute("/unassign @#{user.username} @#{user2.username}", merge_request)

            expect(updates[:assignee_ids]).to match_array([user3.id])
          end

          context 'when unlicensed' do
            before do
              stub_licensed_features(multiple_merge_request_assignees: false)
            end

            it 'does not recognize /unassign @user' do
              merge_request.update!(assignee_ids: [user.id, user2.id, user3.id])

              _, updates = service.execute("/unassign @#{user.username}", merge_request)

              expect(updates[:assignee_ids]).to be_empty
            end
          end
        end
      end
    end

    context 'reassign command' do
      let(:content) { "/reassign @#{current_user.username}" }

      context 'Merge Request' do
        let(:merge_request) { create(:merge_request, source_project: project) }

        context 'unlicensed' do
          before do
            stub_licensed_features(multiple_merge_request_assignees: false)
          end

          it 'does not recognize /reassign @user' do
            _, updates = service.execute(content, merge_request)

            expect(updates).to be_empty
          end
        end

        it 'reassigns user if content contains /reassign @user' do
          _, updates = service.execute("/reassign @#{current_user.username}", merge_request)

          expect(updates[:assignee_ids]).to match_array([current_user.id])
        end

        context 'it reassigns multiple users' do
          let(:additional_user) { create(:user) }

          it 'reassigns user if content contains /reassign @user' do
            _, updates = service.execute("/reassign @#{current_user.username} @#{additional_user.username}", merge_request)

            expect(updates[:assignee_ids]).to match_array([current_user.id, additional_user.id])
          end
        end
      end

      context 'Issue' do
        let(:content) { "/reassign @#{current_user.username}" }

        before do
          issue.update!(assignee_ids: [user.id])
        end

        context 'unlicensed' do
          before do
            stub_licensed_features(multiple_issue_assignees: false)
          end

          it 'does not recognize /reassign @user' do
            _, updates = service.execute(content, issue)

            expect(updates).to be_empty
          end
        end

        it 'reassigns user if content contains /reassign @user' do
          _, updates = service.execute("/reassign @#{current_user.username}", issue)

          expect(updates[:assignee_ids]).to match_array([current_user.id])
        end

        context 'with test_case issue type' do
          it 'does not mark to update assignee' do
            test_case = create(:quality_test_case, project: project)

            _, updates = service.execute("/reassign @#{current_user.username}", test_case)

            expect(updates[:assignee_ids]).to eq(nil)
          end
        end

        context 'it reassigns multiple users' do
          let(:additional_user) { create(:user) }

          it 'reassigns user if content contains /reassign @user' do
            _, updates = service.execute("/reassign @#{current_user.username} @#{additional_user.username}", issue)

            expect(updates[:assignee_ids]).to match_array([current_user.id, additional_user.id])
          end
        end
      end
    end

    context 'reassign_reviewer command' do
      let(:content) { "/reassign_reviewer @#{current_user.username}" }

      context 'unlicensed' do
        before do
          stub_licensed_features(multiple_merge_request_reviewers: false)
        end

        it 'does not recognize /reassign_reviewer @user' do
          content = "/reassign_reviewer @#{current_user.username}"
          _, updates = service.execute(content, merge_request)

          expect(updates).to be_empty
        end
      end

      it 'reassigns reviewer if content contains /reassign_reviewer @user' do
        _, updates = service.execute("/reassign_reviewer @#{current_user.username}", merge_request)

        expect(updates[:reviewer_ids]).to match_array([current_user.id])
      end
    end

    context 'iteration command' do
      let_it_be(:root_group) { create(:group, :private) }
      let_it_be(:group) { create(:group, :private, parent: root_group) }
      let_it_be(:project) { create(:project, :private, :repository, group: group) }
      let_it_be_with_reload(:work_item_issue) { create(:work_item, project: project) }

      context 'when iterations are enabled' do
        before do
          stub_licensed_features(iterations: true)
        end

        context 'when iteration exists' do
          let_it_be(:iteration) { create(:iteration, iterations_cadence: create(:iterations_cadence, group: group)) }

          let(:content) { "/iteration #{iteration.to_reference(project)}" }

          context 'with permissions' do
            before do
              group.add_developer(current_user)
            end

            it 'does not assign iteration when reference does not match any iteration' do
              _, updates, message = service.execute("/iteration *iteration:#{non_existing_record_id}", issue)

              expect(updates).to be_empty
              expect(message).to eq(_("Could not apply iteration command. Failed to find the referenced iteration."))
            end

            shared_examples 'assigns iteration' do |factory|
              let(:issuable) do
                factory == :issue ? issue : build(factory, project: project)
              end

              it 'assigns an iteration' do
                _, updates, message = service.execute(content, issuable)

                expect(updates).to eq(iteration: iteration)
                expect(message).to eq("Set the iteration to #{iteration.to_reference}.")
              end
            end

            it_behaves_like 'assigns iteration', :issue

            context 'when issuable is a work item' do
              it_behaves_like 'assigns iteration', :work_item
            end

            context 'when issuable is an incident' do
              it_behaves_like 'assigns iteration', :incident
            end

            context 'when iteration is started' do
              before do
                iteration.start!
              end

              it_behaves_like 'assigns iteration', :issue
            end
          end

          context 'when the user does not have enough permissions' do
            before do
              allow(current_user).to receive(:can?).with(:use_quick_actions).and_return(true)
              allow(current_user).to receive(:can?).with(:admin_issue, project).and_return(false)
              allow(current_user).to receive(:can?).with(:admin_work_item, project).and_return(false)
            end

            it 'returns an error message' do
              [issue, work_item_issue].each do |issuable|
                _, updates, message = service.execute(content, issuable)

                expect(updates).to be_empty
                expect(message).to eq('Could not apply iteration command.')
              end
            end
          end
        end

        context 'with --current and --next options' do
          before do
            group.add_developer(current_user)
          end

          context "with iterations cadence reference" do
            let_it_be(:cadence) { create(:iterations_cadence, title: "one cadence", group: root_group) }
            let_it_be(:past_iteration) { create(:iteration, :with_due_date, iterations_cadence: cadence, start_date: 10.days.ago) }
            let_it_be(:current_iteration) { create(:iteration, :with_due_date, iterations_cadence: cadence, start_date: 2.days.ago) }
            let_it_be(:next_iteration) { create(:iteration, :with_due_date, iterations_cadence: cadence, start_date: 10.days.from_now) }

            let_it_be(:another_cadence) { create(:iterations_cadence, title: "another cadence", group: root_group) }
            let_it_be(:another_current_iteration) { create(:iteration, :with_due_date, iterations_cadence: another_cadence, start_date: 2.days.ago) }

            let_it_be(:empty_cadence) { create(:iterations_cadence, title: "empty cadence", group: root_group) }

            let_it_be(:inaccessible_cadence) { create(:iterations_cadence, title: "another cadence") }

            it 'does not assign any iteration when the referenced cadence is empty' do
              _, updates, message = service.execute("/iteration #{empty_cadence.to_reference} --current", issue)
              expect(updates).to be_empty
              expect(message).to eq(_('Could not apply iteration command. No current iteration found for the cadence.'))

              _, updates, message = service.execute("/iteration #{empty_cadence.to_reference} --next", issue)
              expect(updates).to be_empty
              expect(message).to eq(_('Could not apply iteration command. No upcoming iteration found for the cadence.'))
            end

            it 'does not assign any iteration when referencing non-existent iterations cadence' do
              _, updates, message = service.execute("/iteration [cadence:\"foobar cadence\"] --current", issue)
              expect(updates).to be_empty
              expect(message).to eq(_('Could not apply iteration command. Failed to find the referenced iteration cadence.'))

              _, updates, message = service.execute("/iteration [cadence:#{non_existing_record_id}] --current", issue)
              expect(updates).to be_empty
              expect(message).to eq(_('Could not apply iteration command. Failed to find the referenced iteration cadence.'))
            end

            it 'does not assign any iteration when referencing unauthorized iterations cadence' do
              _, updates, message = service.execute("/iteration #{inaccessible_cadence.to_reference} --current", issue)
              expect(updates).to be_empty
              expect(message).to eq(_('Could not apply iteration command. Failed to find the referenced iteration cadence.'))
            end

            it 'does not assign any iteration when option are missing' do
              _, updates, message = service.execute("/iteration #{cadence.to_reference}", issue)
              expect(updates).to be_empty
              expect(message).to eq(_("Could not apply iteration command. Missing option --current or --next."))
            end

            context 'with --current option' do
              where(:content) do
                [
                  [lazy { "/iteration #{cadence.to_reference} --current" }],
                  [lazy { "/iteration [cadence:\"#{cadence.title}\"] --current" }]
                ]
              end

              with_them do
                it 'assigns the current iteration of the referenced iterations cadence' do
                  _, updates, message = service.execute(content, issue)

                  expect(updates).to eq(iteration: current_iteration)
                  expect(message).to eq("Set the iteration to #{current_iteration.to_reference}.")
                end
              end
            end

            context 'with --next option' do
              where(:content) do
                [
                  [lazy { "/iteration #{cadence.to_reference} --next" }],
                  [lazy { "/iteration [cadence:\"#{cadence.title}\"] --next" }]
                ]
              end

              with_them do
                it 'assigns the next upcoming iteration of the referenced iterations cadence' do
                  _, updates, message = service.execute(content, issue)

                  expect(updates).to eq(iteration: next_iteration)
                  expect(message).to eq("Set the iteration to #{next_iteration.to_reference}.")
                end
              end
            end
          end

          context "without iterations cadence reference" do
            let_it_be(:cadence) { create(:iterations_cadence, title: "cadence", group: group) }
            let_it_be(:current_iteration) { create(:iteration, :with_due_date, iterations_cadence: cadence, start_date: 2.days.ago) }
            let_it_be(:next_iteration) { create(:iteration, :with_due_date, iterations_cadence: cadence, start_date: 10.days.from_now) }

            context 'when a group hierarchy has a single iterations cadence' do
              it 'assigns the current iteration from the iterations cadence' do
                _, updates, message = service.execute("/iteration --current", issue)

                expect(updates).to eq(iteration: current_iteration)
                expect(message).to eq("Set the iteration to #{current_iteration.to_reference}.")
              end

              it 'assigns the next iteration from the iterations cadence' do
                _, updates, message = service.execute("/iteration --next", issue)

                expect(updates).to eq(iteration: next_iteration)
                expect(message).to eq("Set the iteration to #{next_iteration.to_reference}.")
              end
            end

            context 'when a group hierarchy has multiple iterations cadences' do
              before_all do
                create(:iterations_cadence, title: "another cadence", group: group)
              end

              it 'does not assign any iteration' do
                _, updates, message = service.execute("/iteration --current", issue)
                expect(updates).to be_empty
                expect(message).to eq(_('Could not apply iteration command. There are multiple cadences but no cadence is specified.'))

                _, updates, message = service.execute("/iteration --next", issue)
                expect(updates).to be_empty
                expect(message).to eq(_('Could not apply iteration command. There are multiple cadences but no cadence is specified.'))
              end
            end
          end
        end
      end

      context 'when iterations are disabled' do
        let_it_be(:iteration) { create(:iteration, iterations_cadence: create(:iterations_cadence, group: group)) }

        let(:content) { "/iteration #{iteration.to_reference(project)}" }

        before do
          stub_licensed_features(iterations: false)
        end

        it 'does not recognize /iteration' do
          _, updates = service.execute(content, issue)

          expect(updates).to be_empty
        end
      end
    end

    context 'remove_iteration command' do
      let_it_be(:iteration) { create(:iteration, iterations_cadence: create(:iterations_cadence, group: group)) }

      let(:content) { '/remove_iteration' }

      context 'when iterations are enabled' do
        before do
          stub_licensed_features(iterations: true)
          issue.update!(iteration: iteration)
        end

        shared_examples 'removes iteration' do |factory|
          let(:issuable) { create(factory, project: project, iteration: iteration) }

          it 'removes an assigned iteration' do
            _, updates, message = service.execute(content, issuable)

            expect(updates).to eq(iteration: nil)
            expect(message).to eq("Removed #{iteration.to_reference} iteration.")
          end
        end

        it_behaves_like 'removes iteration', :issue

        context 'when issuable is a work item' do
          it_behaves_like 'removes iteration', :work_item
        end

        context 'when issuable is an incident' do
          it_behaves_like 'removes iteration', :incident
        end

        context 'when the user does not have enough permissions' do
          before do
            allow(current_user).to receive(:can?).with(:use_quick_actions).and_return(true)
            allow(current_user).to receive(:can?).with(:admin_issue, project).and_return(false)
            allow(current_user).to receive(:can?).with(:admin_work_item, project).and_return(false)
          end

          let_it_be(:work_item_issue) { create(:work_item, :issue, project: project, iteration: iteration) }

          it 'returns an error message' do
            [issue, work_item_issue].each do |issuable|
              _, updates, message = service.execute(content, issuable)

              expect(updates).to be_empty
              expect(message).to eq('Could not apply remove_iteration command.')
            end
          end
        end
      end

      context 'when iterations are disabled' do
        before do
          stub_licensed_features(iterations: false)
        end

        it 'does not recognize /remove_iteration' do
          _, updates = service.execute(content, issue)

          expect(updates).to be_empty
        end
      end
    end

    context 'set_parent command' do
      context 'on an issue' do
        let_it_be(:issue) { create(:issue, project: project) }

        context 'when epics are enabled' do
          before do
            stub_licensed_features(epics: true)
          end

          it 'allows the /set_parent command' do
            expect(service.available_commands(issue)).to include(a_hash_including(name: :set_parent))
          end
        end

        context 'when epics are disabled' do
          before do
            stub_licensed_features(epics: false)
          end

          it 'does not allow the /set_parent command' do
            expect(service.available_commands(issue)).not_to include(a_hash_including(name: :set_parent))
          end
        end
      end

      context 'on a work_item' do
        let_it_be(:work_item) { create(:work_item, :task, project: project) }

        it 'allows the /set_parent command' do
          expect(service.available_commands(work_item)).to include(a_hash_including(name: :set_parent))
        end
      end
    end

    context 'epic command' do
      context 'on an issue' do
        let_it_be_with_reload(:epic) { create(:epic, group: group) }
        let_it_be_with_reload(:private_epic) { create(:epic, group: create(:group, :private)) }
        let(:content) { "/epic #{epic.to_reference(project)}" }

        context 'when epics are enabled' do
          before do
            stub_licensed_features(epics: true)
          end

          context 'when epic exists' do
            it 'assigns an issue to an epic' do
              _, updates, message = service.execute(content, issue)

              expect(updates).to eq(epic: epic)
              expect(message).to eq('Added an issue to an epic.')
            end

            context 'when it is confidential' do
              before do
                epic.update!(confidential: true)
                epic.sync_object.update!(confidential: true)
                group.add_developer(current_user)
              end

              it 'shows an error' do
                _, updates, message = service.execute(content, issue)

                expect(updates).to be_empty
                expect(message).to eq('Cannot assign a confidential parent item to a non-confidential child item. Make the child item confidential and try again.')
              end
            end

            context 'when an issue belongs to a project without group' do
              let_it_be(:user_project) { create(:project) }
              let(:issue)              { create(:issue, project: user_project) }

              before do
                user_project.add_guest(user)
              end

              it 'does not assign an issue to an epic' do
                _, updates = service.execute(content, issue)

                expect(updates).to be_empty
              end
            end

            context 'when issue is already added to epic' do
              it 'returns error message' do
                issue = create(:issue, project: project, epic: epic)
                WorkItem.find(issue.id).update!(work_item_parent: epic.sync_object)

                _, updates, message = service.execute(content, issue)

                expect(updates).to be_empty
                expect(message).to eq("#{issue.to_reference} has already been added to parent #{epic.sync_object.to_reference}.")
              end
            end

            context 'when issuable does not support epics' do
              it 'does not assign an incident to an epic' do
                incident = create(:incident, project: project)

                _, updates = service.execute(content, incident)

                expect(updates).to be_empty
              end
            end
          end

          context 'when epic does not exist' do
            let(:content) { "/epic none" }

            it 'does not assign an issue to an epic' do
              _, updates, message = service.execute(content, issue)

              expect(updates).to be_empty
              expect(message).to eq("This parent item does not exist or you don't have sufficient permission.")
            end
          end

          context 'when user has no permissions to read epic' do
            let(:content) { "/epic #{private_epic.to_reference(project)}" }

            it 'does not assign an issue to an epic' do
              _, updates, message = service.execute(content, issue)

              expect(updates).to be_empty
              expect(message).to eq("This parent item does not exist or you don't have sufficient permission.")
            end
          end

          context 'when user has no access to the issue' do
            before do
              allow(current_user).to receive(:can?).and_call_original
              allow(current_user).to receive(:can?).with(:admin_issue_relation, issue).and_return(false)
            end

            it 'returns error' do
              _, updates, message = service.execute(content, issue)

              expect(updates).to be_empty
              expect(message).to eq('Could not apply set_parent command.')
            end
          end
        end

        context 'when epics are disabled' do
          it 'does not recognize /epic' do
            _, updates = service.execute(content, issue)

            expect(updates).to be_empty
          end
        end
      end

      context 'on a work item' do
        let_it_be(:work_item_issue) { create(:work_item, :issue, project: project) }
        let_it_be(:epic) { create(:epic, :with_synced_work_item, group: group) }
        let(:content) { "/epic #{epic.to_reference}" }

        context 'when epics are enabled' do
          before do
            stub_licensed_features(epics: true)
          end

          context 'when epic exists' do
            it 'assigns an issue to an epic' do
              _, updates, message = service.execute(content, work_item_issue)

              expect(updates).to eq(set_parent: epic.sync_object)
              expect(message).to eq('Parent item set successfully.')
            end
          end
        end

        context 'when epics are disabled' do
          it 'does not recognize /epic' do
            _, updates = service.execute(content, work_item_issue)

            expect(updates).to be_empty
          end
        end
      end
    end

    context 'label command for epics' do
      let(:epic) { create(:epic, group: group) }
      let(:label) { create(:group_label, title: 'bug', group: group) }
      let(:project_label) { create(:label, title: 'project_label') }
      let(:content) { "/label ~#{label.title} ~#{project_label.title}" }

      let(:service) { described_class.new(container: group, current_user: current_user) }

      context 'when epics are enabled' do
        before do
          stub_licensed_features(epics: true)
        end

        context 'when a user has permissions to label an epic' do
          before do
            group.add_developer(current_user)
          end

          it 'populates valid label ids' do
            _, updates = service.execute(content, epic)

            expect(updates).to eq(add_label_ids: [label.id])
          end
        end

        context 'when a user does not have permissions to label an epic' do
          it 'does not populate any labels' do
            _, updates = service.execute(content, epic)

            expect(updates).to be_empty
          end
        end
      end

      context 'when epics are disabled' do
        it 'does not populate any labels' do
          group.add_developer(current_user)

          _, updates = service.execute(content, epic)

          expect(updates).to be_empty
        end
      end
    end

    context '/label command' do
      context 'when target is a group level work item' do
        let(:current_user) { developer }
        let_it_be(:new_group) { create(:group, developers: developer) }
        let_it_be(:group_level_work_item) { create(:work_item, :group_level, namespace: new_group) }

        let(:service) { described_class.new(container: group, current_user: current_user) }

        context 'with group level work items license' do
          before do
            stub_licensed_features(epics: true)
          end

          # This spec was introduced just to validate that the label finder scopes que query to a single group.
          # The command checks that labels are available as part of the condition.
          # Query was timing out in .com https://gitlab.com/gitlab-org/gitlab/-/issues/441123
          it 'is not available when there are no labels associated with the group' do
            expect(service.available_commands(group_level_work_item)).not_to include(a_hash_including(name: :label))
          end

          context 'when a label exists at the group level' do
            before do
              create(:group_label, group: group)
            end

            it 'is available' do
              expect(service.available_commands(group_level_work_item)).to include(a_hash_including(name: :label))
            end

            context 'without group level work items license' do
              before do
                stub_licensed_features(epics: false)
              end

              it 'is not available' do
                expect(service.available_commands(group_level_work_item)).not_to include(a_hash_including(name: :label))
              end
            end
          end
        end
      end
    end

    context 'remove_epic command' do
      let(:epic) { create(:epic, group: group) }
      let(:content) { "/remove_epic" }

      before do
        stub_licensed_features(epics: true)
        issue.update!(epic: epic)
      end

      context 'when epics are disabled' do
        before do
          stub_licensed_features(epics: false)
        end

        it 'does not recognize /remove_epic' do
          _, updates = service.execute(content, issue)

          expect(updates).to be_empty
        end
      end

      context 'when subepics are enabled' do
        before do
          stub_licensed_features(epics: true, subepics: true)
        end

        it 'unassigns an issue from an epic' do
          _, updates = service.execute(content, issue)

          expect(updates).to eq(epic: nil)
        end
      end

      context 'when issuable does not support epics' do
        it 'does not recognize /remove_epic' do
          incident = create(:incident, project: project, epic: epic)

          _, updates = service.execute(content, incident)

          expect(updates).to be_empty
        end
      end

      context 'when user has no access to the issue' do
        before do
          allow(current_user).to receive(:can?).and_call_original
          allow(current_user).to receive(:can?).with(:admin_issue_relation, issue).and_return(false)
        end

        it 'returns error' do
          _, updates, message = service.execute(content, issue)

          expect(updates).to be_empty
          expect(message).to eq('Could not apply remove_epic command.')
        end
      end
    end

    context 'epic hierarchy commands' do
      it_behaves_like 'execute epic hierarchy commands'
    end

    context '/copy_metadata command' do
      let(:another_group) { build(:group) }

      before do
        stub_licensed_features(epics: true)
        another_group.add_planner(current_user)
        group.add_planner(current_user)
      end

      context "when a work item type epic is passed" do
        let(:todo_label) { create(:group_label, group: group, title: 'To Do') }
        let(:inreview_label) { create(:group_label, group: group, title: 'In Review') }
        let(:milestone) { create(:milestone, :on_group, group: group, title: '9.10') }
        let(:service) { described_class.new(container: group, current_user: current_user) }
        let(:source_issuable) do
          create(:work_item, :epic, namespace: group, milestone: milestone).tap do |wi|
            wi.labels << [todo_label, inreview_label]
          end
        end

        let(:content) { "/copy_metadata #{source_issuable.to_reference(group)}" }

        it_behaves_like 'copy_metadata command' do
          let(:issuable) { create(:work_item, namespace: group) }
        end

        it_behaves_like 'failed command' do
          let(:issuable) { create(:work_item, namespace: another_group) }
        end
      end
    end

    shared_examples 'weight command' do
      it 'populates weight specified by the /weight command' do
        _, updates = service.execute(content, issuable)

        expect(updates).to eq(weight: weight)
      end
    end

    shared_examples 'clear weight command' do
      it 'populates weight: nil if content contains /clear_weight' do
        issuable.update!(weight: 5)

        _, updates = service.execute(content, issuable)

        expect(updates).to eq(weight: nil)
      end

      it 'unsets weight if weight is 0' do
        issuable.update!(weight: 0)

        _, updates = service.execute(content, issuable)

        expect(updates).to eq(weight: nil)
      end
    end

    context 'issuable weights licensed' do
      let(:issuable) { issue }

      before do
        stub_licensed_features(issue_weights: true)
      end

      context 'weight' do
        let(:content) { "/weight #{weight}" }

        it_behaves_like 'weight command' do
          let(:weight) { 5 }
        end

        it_behaves_like 'weight command' do
          let(:weight) { 0 }
        end

        context 'when weight is negative' do
          it 'does not populate weight' do
            content = "/weight -10"
            _, updates = service.execute(content, issuable)

            expect(updates).to be_empty
          end
        end
      end

      context 'clear_weight' do
        it_behaves_like 'clear weight command' do
          let(:content) { '/clear_weight' }
        end
      end
    end

    context 'issuable weights unlicensed' do
      before do
        stub_licensed_features(issue_weights: false)
      end

      it 'does not recognise /weight X' do
        _, updates = service.execute('/weight 5', issue)

        expect(updates).to be_empty
      end

      it 'does not recognise /clear_weight' do
        _, updates = service.execute('/clear_weight', issue)

        expect(updates).to be_empty
      end
    end

    context 'issuable weights not supported by type' do
      let_it_be(:incident) { create(:incident, project: project) }

      before do
        stub_licensed_features(issue_weights: true)
      end

      it 'does not recognise /weight X' do
        _, updates = service.execute('/weight 5', incident)

        expect(updates).to be_empty
      end

      it 'does not recognise /clear_weight' do
        _, updates = service.execute('/clear_weight', incident)

        expect(updates).to be_empty
      end
    end

    shared_examples 'health_status command' do
      it 'populates health_status specified by the /health_status command' do
        _, updates = service.execute(content, issuable)

        expect(updates).to eq(health_status: health_status)
      end
    end

    shared_examples 'clear_health_status command' do
      it 'populates health_status: nil if content contains /clear_health_status' do
        issuable.update!(health_status: 'on_track')

        _, updates = service.execute(content, issuable)

        expect(updates).to eq(health_status: nil)
      end
    end

    context 'issuable health statuses licensed' do
      let(:issuable) { issue }

      before do
        stub_licensed_features(issuable_health_status: true)
      end

      context 'health_status' do
        let(:content) { "/health_status #{health_status}" }

        it_behaves_like 'health_status command' do
          let(:health_status) { 'on_track' }
        end

        it_behaves_like 'health_status command' do
          let(:health_status) { 'at_risk' }
        end

        context 'when health_status is invalid' do
          it 'does not populate health_status' do
            content = "/health_status unknown"
            _, updates = service.execute(content, issuable)

            expect(updates).to be_empty
          end
        end

        context 'when the user does not have enough permissions' do
          before do
            allow(current_user).to receive(:can?).with(:use_quick_actions).and_return(true)
            allow(current_user).to receive(:can?).with(:admin_issue, issuable).and_return(false)
          end

          it 'returns an error message' do
            content = "/health_status on_track"
            _, updates, message = service.execute(content, issuable)

            expect(updates).to be_empty
            expect(message).to eq('Could not apply health_status command.')
          end
        end
      end

      context 'clear_health_status' do
        it_behaves_like 'clear_health_status command' do
          let(:content) { '/clear_health_status' }
        end

        context 'when the user does not have enough permissions' do
          before do
            allow(current_user).to receive(:can?).with(:use_quick_actions).and_return(true)
            allow(current_user).to receive(:can?).with(:admin_issue, issuable).and_return(false)
          end

          it 'returns an error message' do
            content = "/clear_health_status"
            _, updates, message = service.execute(content, issuable)

            expect(updates).to be_empty
            expect(message).to eq('Could not apply clear_health_status command.')
          end
        end
      end
    end

    context 'issuable health_status unlicensed' do
      before do
        stub_licensed_features(issuable_health_status: false)
      end

      it 'does not recognise /health_status X' do
        _, updates = service.execute('/health_status needs_attention', issue)

        expect(updates).to be_empty
      end

      it 'does not recognise /clear_health_status' do
        _, updates = service.execute('/clear_health_status', issue)

        expect(updates).to be_empty
      end
    end

    context 'issuable health_status not supported by type' do
      let_it_be(:incident) { create(:incident, project: project) }

      before do
        stub_licensed_features(issuable_health_status: true)
      end

      it 'does not recognise /health_status X' do
        _, updates = service.execute('/health_status on_track', incident)

        expect(updates).to be_empty
      end

      it 'does not recognise /clear_health_status' do
        _, updates = service.execute('/clear_health_status', incident)

        expect(updates).to be_empty
      end
    end

    shared_examples 'empty command' do
      it 'populates {} if content contains an unsupported command' do
        _, updates = service.execute(content, issuable)

        expect(updates).to be_empty
      end
    end

    context 'not persisted merge request can not be merged' do
      it_behaves_like 'empty command' do
        let(:content) { "/merge" }
        let(:issuable) { build(:merge_request, source_project: project) }
      end
    end

    context 'not approved merge request can not be merged' do
      before do
        merge_request.target_project.update!(approvals_before_merge: 1)
      end

      it_behaves_like 'empty command' do
        let(:content) { "/merge" }
        let(:issuable) { build(:merge_request, source_project: project) }
      end
    end

    context 'when the merge request is not approved' do
      let!(:rule) { create(:any_approver_rule, merge_request: merge_request, approvals_required: 1) }
      let(:content) { '/merge' }

      context 'when "merge_when_checks_pass" is enabled' do
        let(:service) do
          described_class.new(
            container: project,
            current_user: current_user,
            params: { merge_request_diff_head_sha: merge_request.diff_head_sha }
          )
        end

        it 'runs merge command and returns merge message' do
          _, updates, message = service.execute(content, merge_request)

          expect(updates).to eq(merge: merge_request.diff_head_sha)

          expect(message).to eq('Scheduled to merge this merge request (Merge when checks pass).')
        end
      end
    end

    context 'when the merge request is blocked' do
      let(:content) { '/merge' }
      let(:service) do
        described_class.new(
          container: project,
          current_user: current_user,
          params: { merge_request_diff_head_sha: issuable.diff_head_sha }
        )
      end

      let(:issuable) { create(:merge_request, :blocked, source_project: project) }

      before do
        stub_licensed_features(blocking_merge_requests: true)
      end

      it 'runs merge command and returns merge message' do
        _, updates, message = service.execute(content, issuable)

        expect(updates).to eq(merge: issuable.diff_head_sha)

        expect(message).to eq('Scheduled to merge this merge request (Merge when checks pass).')
      end
    end

    context 'approved merge request can be merged' do
      before do
        merge_request.update!(approvals_before_merge: 1)
        merge_request.approvals.create!(user: current_user)
      end

      it_behaves_like 'empty command' do
        let(:content) { "/merge" }
        let(:issuable) { build(:merge_request, source_project: project) }
      end
    end

    context 'confidential command' do
      context 'for test cases' do
        it 'does mark to update confidential attribute' do
          issuable = create(:quality_test_case, project: project)

          _, updates, message = service.execute('/confidential', issuable)

          expect(message).to eq('Made this issue confidential.')
          expect(updates[:confidential]).to eq(true)
        end
      end

      context 'for requirements' do
        it 'fails supports confidentiality condition' do
          issuable = create(:issue, :requirement, project: project)

          _, updates, message = service.execute('/confidential', issuable)

          expect(message).to eq('Could not apply confidential command.')
          expect(updates[:confidential]).to be_nil
        end
      end

      context 'for epics' do
        let_it_be(:target_epic) { create(:epic, group: group) }
        let(:content) { '/confidential' }

        before do
          stub_licensed_features(epics: true)
          group.add_developer(current_user)
        end

        shared_examples 'command not applied' do
          it 'returns unsuccessful execution message' do
            _, updates, message = service.execute(content, target_epic)

            expect(message).to eq(execution_message)
            expect(updates[:confidential]).to eq(true)
          end
        end

        it 'returns correct explain message' do
          _, explanations = service.explain(content, target_epic)

          expect(explanations).to match_array(['Makes this epic confidential.'])
        end

        it 'returns successful execution message' do
          _, updates, message = service.execute(content, target_epic)

          expect(message).to eq('Made this epic confidential.')
          expect(updates[:confidential]).to eq(true)
        end

        context 'when epic has non-confidential issues' do
          before do
            target_epic.update!(confidential: false)
            issue.update!(confidential: false)
            create(:epic_issue, epic: target_epic, issue: issue)
          end

          it_behaves_like 'command not applied' do
            let_it_be(:execution_message) do
              'Cannot make the epic confidential if it contains non-confidential issues'
            end
          end
        end

        context 'when epic has non-confidential epics' do
          before do
            target_epic.update!(confidential: false)
            create(:epic, group: group, parent: target_epic, confidential: false)
          end

          it_behaves_like 'command not applied' do
            let_it_be(:execution_message) do
              'Cannot make the epic confidential if it contains non-confidential child epics'
            end
          end
        end

        context 'when a user has no permissions to set confidentiality' do
          before do
            group.add_guest(current_user)
          end

          it 'does not update epic confidentiality' do
            _, updates, message = service.execute(content, target_epic)

            expect(message).to eq('Could not apply confidential command.')
            expect(updates[:confidential]).to be_nil
          end
        end
      end
    end

    context 'blocking issues commands' do
      let(:user) { current_user }

      it_behaves_like 'issues link quick action', :blocks
      it_behaves_like 'issues link quick action', :blocked_by
    end

    context 'unlink command' do
      let_it_be(:unlink_target) { create(:issue, project: project) }
      let(:content) { "/unlink #{unlink_target.to_reference(issue)}" }

      subject(:unlink_issues) { service.execute(content, issue) }

      shared_examples 'command applied successfully' do
        it 'executes command successfully' do
          expect { unlink_issues }.to change { IssueLink.count }.by(-1)
          expect(unlink_issues[2]).to eq("Removed linked item #{unlink_target.to_reference(issue)}.")
          expect(issue.notes.last.note).to eq("removed the relation with #{unlink_target.to_reference}")
          expect(unlink_target.notes.last.note).to eq("removed the relation with #{issue.to_reference}")
        end
      end

      context 'when command includes blocking issue' do
        before do
          create(:issue_link, source: unlink_target, target: issue, link_type: 'blocks')
        end

        it_behaves_like 'command applied successfully'
      end

      context 'when command includes blocked issue' do
        before do
          create(:issue_link, source: issue, target: unlink_target, link_type: 'blocks')
        end

        it_behaves_like 'command applied successfully'
      end

      context 'when target is not an issue' do
        let_it_be(:unlink_target) { create(:work_item, :epic, namespace: group) }
        let_it_be(:unlink_source) { create(:work_item, :epic, namespace: group) }
        let_it_be(:issue) { unlink_source }

        before do
          group.add_owner(current_user)
          stub_licensed_features(epics: true)

          create(:issue_link, source: unlink_source, target: unlink_target, link_type: 'relates_to')
        end

        it_behaves_like 'command applied successfully'
      end

      context 'when provided issue is not linked' do
        it 'fails to execute command' do
          expect { unlink_issues }.not_to change { IssueLink.count }
          expect(unlink_issues[2]).to eq('No linked issue matches the provided parameter.')
        end
      end
    end

    describe 'status command' do
      shared_examples 'a failed command execution' do
        it 'fails with message' do
          _, updates, message = execute_command

          expect(message).to eq(expected_message)
          expect(updates).not_to have_key(:status)
        end
      end

      shared_examples 'a successful command execution' do
        it 'adds status reference to updates with message' do
          _, updates, message = execute_command

          expect(message).to eq(expected_message)
          expect(updates[:status]).to have_attributes(
            id: 2,
            name: 'In progress'
          )
        end
      end

      shared_examples 'command is not available' do
        it 'is not part of the available commands' do
          expect(service.available_commands(work_item)).not_to include(a_hash_including(name: :status))
        end
      end

      let_it_be_with_reload(:work_item) { create(:work_item, :task, project: project) }

      let(:content) { '/status in progress' }
      let(:expected_message) { format(s_("WorkItemStatus|Status set to %{status_name}."), status_name: 'In progress') }
      let(:generic_error_message) { 'Could not apply status command.' }

      subject(:execute_command) { service.execute(content, work_item) }

      before do
        stub_licensed_features(work_item_status: true)
      end

      it 'is part of the available commands' do
        expect(service.available_commands(work_item)).to include(a_hash_including(name: :status))
      end

      it 'returns correct explain message' do
        _, explanations = service.explain(content, work_item)

        expect(explanations).to match_array([
          # Lower case version because we use status_name and not status object
          format(s_("WorkItemStatus|Set status to %{status_name}."), status_name: 'in progress')
        ])
      end

      it_behaves_like 'a successful command execution'

      context 'when status name does not reference a valid status' do
        let(:content) { '/status invalid' }
        let(:expected_message) do
          format(
            s_("WorkItemStatus|%{status_name} is not a valid status for this item."), { status_name: 'invalid' }
          )
        end

        it_behaves_like 'a failed command execution'
      end

      context 'when status widget is not available for work item type' do
        let_it_be_with_reload(:work_item) { create(:work_item, :ticket, project: project) }

        let(:expected_message) { generic_error_message }

        it_behaves_like 'command is not available'
        it_behaves_like 'a failed command execution'
      end

      context 'when work_item_status licensed feature is disabled' do
        let(:expected_message) { generic_error_message }

        before do
          stub_licensed_features(work_item_status: false)
        end

        it_behaves_like 'command is not available'
        it_behaves_like 'a failed command execution'
      end

      context 'when work_item_status_feature_flag feature flag is disabled' do
        let(:expected_message) { generic_error_message }

        before do
          stub_feature_flags(work_item_status_feature_flag: false)
        end

        it_behaves_like 'command is not available'
        it_behaves_like 'a failed command execution'
      end
    end

    it_behaves_like 'quick actions that change work item type ee'
  end

  describe '#explain' do
    describe 'health_status command' do
      let(:content) { '/health_status on_track' }

      context 'issuable health statuses licensed' do
        before do
          stub_licensed_features(issuable_health_status: true)
        end

        it 'includes the value' do
          _, explanations = service.explain(content, issue)
          expect(explanations).to eq(['Sets health status to on_track.'])
        end
      end
    end

    describe 'epic command' do
      before do
        stub_licensed_features(epics: true)
      end

      context 'for an issue' do
        let(:issue) { create(:issue, project: project) }
        let(:epic) { create(:epic, group: project.group) }

        let(:content) { "/epic #{epic.to_reference}" }

        it 'applies the correct explanation' do
          _, explanations = service.explain(content, issue)
          expect(explanations).to eq(["Set #{epic.to_reference} as this item's parent item."])
        end
      end

      context 'for a work_item' do
        let(:issue_work_item) { create(:work_item, :issue, project: project) }
        let(:epic_work_item) { create(:work_item, :epic, namespace: project.group) }

        let(:content) { "/epic #{epic_work_item.to_reference}" }

        it 'applies the correct explanation' do
          _, explanations = service.explain(content, issue_work_item)
          expect(explanations).to eq(["Set #{epic_work_item.to_reference} as this item's parent item."])
        end
      end
    end

    describe 'milestone command' do
      context 'on group-level work items' do
        let_it_be(:group_milestone) { create(:milestone, group: group, title: 'Group Milestone') }
        let_it_be(:group_work_item) { create(:work_item, :epic, :group_level, namespace: group) }
        let_it_be(:group_service) { described_class.new(container: group, current_user: developer) }

        before do
          group.add_developer(developer)
          stub_licensed_features(epics: true)
        end

        it 'includes the milestone command in available commands for group level work items' do
          expect(group_service.available_commands(group_work_item)).to include(a_hash_including(name: :milestone))
        end

        it 'updates the milestone on a group level work item' do
          _, updates, _ = group_service.execute("/milestone %\"#{group_milestone.title}\"", group_work_item)

          expect(updates).to eq(milestone_id: group_milestone.id)
        end
      end
    end

    describe 'unassign command' do
      let(:content) { '/unassign' }
      let(:issue) { create(:issue, project: project, assignees: [user, user2]) }

      it "includes all assignees' references" do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(["Removes assignees @#{user.username} and @#{user2.username}."])
      end
    end

    describe 'unassign command with assignee references' do
      let(:content) { "/unassign @#{user.username} @#{user3.username}" }
      let(:issue) { create(:issue, project: project, assignees: [user, user2, user3]) }

      it 'includes only selected assignee references' do
        _, explanations = service.explain(content, issue)

        expect(explanations.first).to match(/Removes assignees/)
        expect(explanations.first).to match("@#{user3.username}")
        expect(explanations.first).to match("@#{user.username}")
      end
    end

    describe 'weight command' do
      let(:content) { '/weight 4' }

      it 'includes the number' do
        _, explanations = service.explain(content, issue)
        expect(explanations).to eq(['Sets weight to 4.'])
      end

      context 'for a work item type that does not support weight' do
        let_it_be(:target) { create(:work_item, :epic, :group_level, namespace: group) }

        it_behaves_like 'quick action is unavailable', :weight

        it '/weight explain message is not available' do
          _, explanations = service.explain(content, target)

          expect(explanations).to be_empty
        end
      end
    end

    describe 'linked items commands' do
      let_it_be(:guest) { create(:user) }
      let_it_be(:restricted_project) { create(:project) }
      let_it_be(:ref1) { create(:issue, project: project).to_reference }
      let_it_be(:ref2) { create(:issue, project: project).to_reference }
      let_it_be(:ref3) { create(:work_item, project: project).to_reference }
      let_it_be(:ref4) { create(:work_item, :epic, namespace: group).to_reference }
      let_it_be(:ref5) { create(:work_item, :epic_with_legacy_epic, namespace: group).to_reference(full: true) }

      let(:target) { issue }

      before do
        issue.project.add_guest(guest)
      end

      context 'with /blocks' do
        let(:blocks_command) { "/blocks #{ref1} #{ref2} #{ref3} #{ref4}" }

        context 'with sufficient permissions' do
          before do
            issue.project.add_developer(current_user)
          end

          it '/blocks is available' do
            _, explanations = service.explain(blocks_command, issue)

            expect(explanations).to contain_exactly("Set this issue as blocking #{[ref1, ref2, ref3, ref4].to_sentence}.")
          end

          context 'with task as target' do
            let_it_be(:task) { create(:work_item, :task, project: project) }
            let(:target) { task }

            it 'replaces issue in explanation with task' do
              _, explanations = service.explain(blocks_command, task)

              expect(explanations).to contain_exactly("Set this task as blocking #{[ref1, ref2, ref3, ref4].to_sentence}.")
            end
          end

          context 'when licensed feature is not available' do
            before do
              stub_licensed_features(blocked_issues: false)
            end

            it_behaves_like 'quick action is unavailable', :blocks
          end

          context 'when target is not an issue' do
            let(:target) { create(:epic, group: group) }

            it_behaves_like 'quick action is unavailable', :blocks
          end
        end

        context 'with insufficient permissions' do
          let_it_be(:target) { create(:issue, project: restricted_project) }
          let(:current_user) { guest }

          it_behaves_like 'quick action is unavailable', :blocks
        end
      end

      context 'with /relate' do
        let(:relate_command) { "/relate #{ref1} #{ref2} #{ref3} #{ref4}" }

        context 'with sufficient permissions' do
          before do
            issue.project.add_developer(current_user)
          end

          it '/relate is available' do
            _, explanations = service.explain(relate_command, issue)

            expect(explanations).to contain_exactly(
              "Added #{[ref1, ref2, ref3, ref4].to_sentence} as a linked item related to this issue."
            )
          end

          it '/relate execution method' do
            _, _, message = service.execute(relate_command, issue)

            expect(message).to eq(
              "Added #{[ref1, ref2, ref3, ref4].to_sentence} as a linked item related to this issue."
            )
          end

          context 'when target is not an issue' do
            let(:target) { create(:epic, group: group) }

            it_behaves_like 'quick action is unavailable', :relate
          end

          context 'when target is a work item epic with a legacy epic' do
            let(:target) { create(:work_item, :epic_with_legacy_epic, namespace: group) }
            let(:relate_command) { "/relate #{ref5}" }

            before do
              group.add_developer(current_user)
              stub_licensed_features(epics: true, related_epics: true)
            end

            it '/relate execution method' do
              expect { service.execute(relate_command, target) }
                .to change { IssueLink.count }.by(1)
                .and change { Epic::RelatedEpicLink.count }.by(1)
            end
          end
        end

        context 'with insufficient permissions' do
          let_it_be(:target) { create(:issue, project: restricted_project) }
          let(:current_user) { guest }

          it_behaves_like 'quick action is unavailable', :relate
        end
      end

      context 'with /unlink' do
        let(:unlink_command) { "/unlink #{ref1}" }

        context 'with sufficient permissions' do
          before do
            issue.project.add_guest(current_user)
          end

          it '/unlink is available' do
            _, explanations = service.explain(unlink_command, issue)

            expect(explanations)
              .to contain_exactly("Removes linked item #{project.issues.second.to_reference(issue)}.")
          end

          context 'when target is not an issue' do
            let(:target) { create(:epic, group: group) }

            it_behaves_like 'quick action is unavailable', :unlink
          end
        end

        context 'with insufficient permissions' do
          let_it_be(:target) { create(:issue, project: restricted_project) }
          let(:current_user) { guest }

          it_behaves_like 'quick action is unavailable', :blocks
        end
      end

      context 'with /blocked_by' do
        let(:blocked_by_command) { "/blocked_by #{ref1} #{ref2} #{ref3} #{ref4}" }

        context 'with sufficient permissions' do
          before do
            issue.project.add_guest(current_user)
          end

          it '/blocked_by is available' do
            _, explanations = service.explain(blocked_by_command, issue)

            expect(explanations)
              .to contain_exactly("Set this issue as blocked by #{[ref1, ref2, ref3, ref4].to_sentence}.")
          end

          context 'when licensed feature is not available' do
            before do
              stub_licensed_features(blocked_issues: false)
            end

            it_behaves_like 'quick action is unavailable', :blocked_by
          end

          context 'when target is not an issue' do
            let(:target) { create(:epic, group: group) }

            it_behaves_like 'quick action is unavailable', :blocked_by
          end
        end

        context 'with insufficient permissions' do
          let_it_be(:target) { create(:issue, project: restricted_project) }
          let(:current_user) { guest }

          it_behaves_like 'quick action is unavailable', :blocked_by
        end
      end
    end

    describe 'promote_to command' do
      let(:content) { '/promote_to objective' }

      context 'when work item supports promotion' do
        context 'with key result' do
          let_it_be(:key_result) { build(:work_item, :key_result, project: project) }

          it 'includes the value' do
            _, explanations = service.explain(content, key_result)
            expect(explanations).to eq(['Promotes item to objective.'])
          end
        end

        context 'with issue' do
          let_it_be(:issue) { build(:work_item, :issue, project: project) }

          where(type: %w[incident epic])

          with_them do
            it 'includes the type in the explanation' do
              _, explanations = service.explain("/promote_to #{type}", issue)
              expect(explanations).to eq(["Promotes item to #{type}."])
            end
          end
        end
      end

      context 'when work item does not support promotion' do
        let_it_be(:requirement) { build(:work_item, :requirement, project: project) }

        it 'does not include the value' do
          _, explanations = service.explain(content, requirement)
          expect(explanations).to be_empty
        end
      end
    end

    describe 'checkin_reminder command' do
      let(:checkin_reminder_command) { "/checkin_reminder weekly" }

      context 'for a work item type that supports reminders' do
        let(:objective) { create(:work_item, :objective, project: project, author: current_user) }

        it '/checkin_reminder is available' do
          _, explanations = service.explain(checkin_reminder_command, objective)

          expect(explanations).to contain_exactly("Sets checkin reminder frequency to weekly.")
        end
      end

      context 'for a work item type that does not support reminders' do
        let(:key_result) { create(:work_item, :key_result, project: project) }

        it '/checkin_reminder is available' do
          _, explanations = service.explain(checkin_reminder_command, key_result)

          expect(explanations).not_to contain_exactly("Sets checkin reminder frequency to weekly.")
        end
      end
    end

    describe 'epic hierarchy commands' do
      it_behaves_like 'explain epic hierarchy commands'
    end
  end

  context '/duplicate command' do
    before do
      stub_licensed_features(epics: true)
      group.add_developer(current_user)
    end

    context 'when canonical item is an epic' do
      let(:duplicate_item) { create(:work_item, :epic_with_legacy_epic, namespace: group) }

      context 'when epic work item' do
        context 'with reference' do
          it_behaves_like 'duplicate command' do
            let(:content) { "/duplicate #{duplicate_item.to_reference(project, full: true)}" }
            let(:issuable) { issue }
          end
        end

        context 'with url' do
          it_behaves_like 'duplicate command' do
            let(:content) { "/duplicate #{Gitlab::UrlBuilder.build(duplicate_item)}" }
            let(:issuable) { issue }
          end
        end
      end

      context 'when legacy epic' do
        context 'with reference' do
          it_behaves_like 'duplicate command' do
            let(:content) { "/duplicate #{duplicate_item.sync_object.to_reference(project, full: true)}" }
            let(:issuable) { issue }
          end
        end

        context 'with url' do
          it_behaves_like 'duplicate command' do
            let(:content) { "/duplicate #{Gitlab::UrlBuilder.build(duplicate_item.sync_object)}" }
            let(:issuable) { issue }
          end
        end
      end
    end

    context 'when duplicate item is an epic work item' do
      let(:canonical_item) { create(:work_item, :epic_with_legacy_epic, namespace: group) }
      let(:duplicate_item) { issue }

      let(:service) { described_class.new(container: group, current_user: current_user) }

      context 'with reference' do
        it_behaves_like 'duplicate command' do
          let(:content) { "/duplicate #{duplicate_item.to_reference(group)}" }
          let(:issuable) { canonical_item }
        end
      end

      context 'with url' do
        it_behaves_like 'duplicate command' do
          let(:content) { "/duplicate #{Gitlab::UrlBuilder.build(duplicate_item)}" }
          let(:issuable) { canonical_item }
        end
      end
    end
  end

  describe 'clone issue command' do
    let(:content) { "/clone #{group.full_path}" }
    let(:group_service) { described_class.new(container: group, current_user: current_user) }
    let_it_be(:work_item) { create(:work_item, :epic_with_legacy_epic, :group_level, namespace: group) }

    before do
      group.add_maintainer(current_user)
      stub_licensed_features(epics: true)
    end

    context "when work item type is an epic" do
      it "/clone is available" do
        _, explanations = group_service.explain(content, work_item)

        expected_string = "Clones this item, without comments, to #{group.full_path}."
        expect(explanations).to match_array([_(expected_string)])
      end

      it "recognizes the clone action when move and clone commands are supported" do
        expect(service.available_commands(work_item).pluck(:name)).to include(:clone)
      end

      it "does not recognize the clone action when move and clone commands are not supported" do
        allow(work_item).to receive(:supports_move_and_clone?).and_return(false)
        expect(service.available_commands(work_item).pluck(:name)).not_to include(:clone)
      end

      it 'returns the clone item message' do
        _, _, message = service.execute("/clone #{group.full_path}", work_item)
        translated_string = _("Cloned this item to %{group_full_path}.")
        formatted_message = format(translated_string, group_full_path: group.full_path.to_s)

        expect(message).to  eq(formatted_message)
      end

      it 'returns clone item failure message when the referenced group is not found' do
        _, _, message = service.execute('/clone invalid', work_item)

        expect(message).to eq(_("Unable to clone. Target project or group doesn't exist or doesn't support this item type."))
      end

      it 'returns clone item failure message when the path provided is to a project' do
        _, _, message = service.execute("/clone #{project.full_path}", work_item)

        expect(message).to eq(_("Unable to clone. Target project or group doesn't exist or doesn't support this item type."))
      end

      it 'returns clone item failure message when the referenced group not authorized' do
        _, _, message = service.execute("/clone #{create(:group).full_path}", work_item)

        expect(message).to eq(_("Unable to clone. Insufficient permissions."))
      end
    end
  end

  describe '/move issue command' do
    let(:target_group) { create(:group) }
    let(:content) { "/move #{target_group.full_path}" }
    let(:group_service) { described_class.new(container: group, current_user: current_user) }
    let_it_be(:work_item) { create(:work_item, :epic_with_legacy_epic, :group_level, namespace: group) }

    before do
      group.add_maintainer(current_user)
      target_group.add_maintainer(current_user)
      stub_licensed_features(epics: true)
    end

    context "when work item type is epic" do
      it "/move is available" do
        _, explanations = group_service.explain(content, work_item)

        expected_string = "Moves this item to #{target_group.full_path}."
        expect(explanations).to match_array([_(expected_string)])
      end

      it 'recognizes the move action when move and clone is supported' do
        expect(service.available_commands(work_item).pluck(:name)).to include(:move)
      end

      it "does not recognize the move action when move and clone commands are not supported" do
        allow(work_item).to receive(:supports_move_and_clone?).and_return(false)
        expect(service.available_commands(work_item).pluck(:name)).not_to include(:move)
      end

      it "returns the move item message" do
        _, _, message = service.execute(content, work_item)
        translated_string = _("Moved this item to %{group_full_path}.")
        formatted_message = format(translated_string, group_full_path: target_group.full_path.to_s)

        expect(message).to  eq(formatted_message)
      end

      it "returns move item failure message when target group is not found" do
        _, _, message = service.execute('/move invalid', work_item)

        expect(message).to eq(_("Unable to move. Target project or group doesn't exist or doesn't support this item type."))
      end

      it "returns move item failure message when the path provided is to a project" do
        _, _, message = service.execute("/move #{project.full_path}", work_item)

        expect(message).to eq(_("Unable to move. Target project or group doesn't exist or doesn't support this item type."))
      end

      it 'returns move item failure message when the referenced group not authorized' do
        _, _, message = service.execute("/move #{create(:group).full_path}", work_item)

        expect(message).to eq(_("Unable to move. Insufficient permissions."))
      end
    end
  end
end
