Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module QuickActions
  class InterpretService < BaseService
    include Gitlab::Utils::StrongMemoize
    include Gitlab::QuickActions::Dsl
    include Gitlab::QuickActions::IssueActions
    include Gitlab::QuickActions::IssuableActions
    include Gitlab::QuickActions::IssueAndMergeRequestActions
    include Gitlab::QuickActions::MergeRequestActions
    include Gitlab::QuickActions::CommitActions
    include Gitlab::QuickActions::CommonActions
    include Gitlab::QuickActions::RelateActions
    include Gitlab::QuickActions::WorkItemActions

    attr_reader :quick_action_target

    # Counts how many commands have been executed.
    # Used to display relevant feedback on UI when a note
    # with only commands has been processed.
    attr_accessor :commands_executed_count

    # Takes an quick_action_target and returns an array of all the available commands
    # represented with .to_h
    def available_commands(quick_action_target)
      @quick_action_target = quick_action_target

      self.class.command_definitions.map do |definition|
        next unless definition.available?(self)

        definition.to_h(self)
      end.compact
    end

    # Takes a text and interprets the commands that are extracted from it.
    # Returns the content without commands, a hash of changes to be applied to a record
    # and a string containing the execution_message to show to the user.
    def execute(content, quick_action_target, only: nil)
      return [content, {}, ''] unless current_user.can?(:use_quick_actions)

      @quick_action_target = quick_action_target
      @updates = {}
      @execution_message = {}

      content, commands = extractor.extract_commands(content, only: only)
      extract_updates(commands)

      [content, @updates, execution_messages_for(commands), command_names(commands)]
    end

    # Takes a text and interprets the commands that are extracted from it.
    # Returns the content without commands, and array of changes explained.
    # `keep_actions: true` will keep the quick actions in the content.
    def explain(content, quick_action_target, keep_actions: false)
      return [content, []] unless current_user.can?(:use_quick_actions)

      @quick_action_target = quick_action_target

      content, commands = extractor(keep_actions).extract_commands(content)
      commands = explain_commands(commands)
      [content, commands]
    end

    private

    def failed_parse(message)
      raise Gitlab::QuickActions::CommandDefinition::ParseError, message
    end

    def extractor(keep_actions = false)
      Gitlab::QuickActions::Extractor.new(self.class.command_definitions, keep_actions: keep_actions)
    end

    # Find users for commands like /assign
    #
    # eg. /assign me and @jane and jack
    def extract_users(params)
      Gitlab::QuickActions::UsersExtractor
        .new(current_user, project: project, group: group, target: quick_action_target, text: params)
        .execute

    rescue Gitlab::QuickActions::UsersExtractor::Error => err
      extract_users_failed(err)
    end

    def extract_users_failed(err)
      case err
      when Gitlab::QuickActions::UsersExtractor::MissingError
        failed_parse(format(_("Failed to find users for %{missing}"), missing: err.message))
      when Gitlab::QuickActions::UsersExtractor::TooManyRefsError
        failed_parse(format(_('Too many references. Quick actions are limited to at most %{max_count} user references'),
                 max_count: err.limit))
      when Gitlab::QuickActions::UsersExtractor::TooManyFoundError
        failed_parse(format(_("Too many users found. Quick actions are limited to at most %{max_count} users"),
                 max_count: err.limit))
      else
        Gitlab::ErrorTracking.track_and_raise_for_dev_exception(err)
        failed_parse(_('Something went wrong'))
      end
    end

    def find_milestones(project, params = {})
      group_ids = project.group.self_and_ancestors.select(:id) if project.group

      MilestonesFinder.new(params.merge(project_ids: [project.id], group_ids: group_ids)).execute
    end

    def parent
      project || group
    end

    def group
      strong_memoize(:group) do
        quick_action_target.group if quick_action_target.respond_to?(:group)
      end
    end

    def find_labels(labels_params = nil)
      extract_references(labels_params, :label) | find_labels_by_name_no_tilde(labels_params)
    end

    def find_labels_by_name_no_tilde(labels_params)
      return Label.none if label_with_tilde?(labels_params)

      finder_params = { include_ancestor_groups: true }
      finder_params[:project_id] = project.id if project
      finder_params[:group_id] = group.id if group
      finder_params[:name] = extract_label_names(labels_params) if labels_params

      LabelsFinder.new(current_user, finder_params).execute
    end

    def label_with_tilde?(labels_params)
      labels_params&.include?('~')
    end

    def extract_label_names(labels_params)
      # '"A" "A B C" A B' => ["A", "A B C", "A", "B"]
      labels_params.scan(/"([^"]+)"|([^ ]+)/).flatten.compact
    end

    def find_label_references(labels_param, format = :id)
      labels_to_reference(find_labels(labels_param), format)
    end

    def labels_to_reference(labels, format = :id)
      labels.map { |l| l.to_reference(format: format) }
    end

    def find_label_ids(labels_param)
      find_labels(labels_param).map(&:id)
    end

    def explain_commands(commands)
      map_commands(commands, :explain)
    end

    def execution_messages_for(commands)
      map_commands(commands, :execute_message).join(' ')
    end

    def map_commands(commands, method)
      commands.map do |name_or_alias, arg|
        definition = self.class.definition_by_name(name_or_alias)
        next unless definition

        case method
        when :explain
          definition.explain(self, arg)
        when :execute_message
          @execution_message[definition.name.to_sym] || definition.execute_message(self, arg)
        end
      end.compact
    end

    def command_names(commands)
      commands.flatten.map do |name|
        definition = self.class.definition_by_name(name)
        next unless definition

        name
      end.compact
    end

    def extract_updates(commands)
      commands.each do |name, arg|
        definition = self.class.definition_by_name(name)
        next unless definition

        definition.execute(self, arg)
        usage_ping_tracking(definition.name, arg)
      end
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def extract_references(arg, type)
      return [] unless arg

      ext = Gitlab::ReferenceExtractor.new(project, current_user)

      ext.analyze(arg, author: current_user, group: group)

      ext.references(type)
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def usage_ping_tracking(quick_action_name, arg)
      Gitlab::UsageDataCounters::QuickActionActivityUniqueCounter.track_unique_action(
        quick_action_name.to_s,
        args: arg&.strip,
        user: current_user
      )
    end

    def can?(ability, object)
      Ability.allowed?(current_user, ability, object)
    end
  end
end

QuickActions::InterpretService.prepend_mod_with('QuickActions::InterpretService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe QuickActions::InterpretService, feature_category: :team_planning do
  include AfterNextHelpers

  let_it_be(:group) { create(:group, :crm_enabled) }
  let_it_be(:public_project) { create(:project, :public, group: group) }
  let_it_be(:repository_project) { create(:project, :repository) }
  let_it_be(:project) { public_project }
  let_it_be(:developer) { create(:user) }
  let_it_be(:developer2) { create(:user) }
  let_it_be(:developer3) { create(:user) }
  let_it_be_with_reload(:issue) { create(:issue, project: project) }
  let_it_be(:inprogress) { create(:label, project: project, title: 'In Progress') }
  let_it_be(:helmchart) { create(:label, project: project, title: 'Helm Chart Registry') }
  let_it_be(:bug) { create(:label, project: project, title: 'Bug') }

  let(:milestone) { create(:milestone, project: project, title: '9.10') }
  let(:commit) { create(:commit, project: project) }
  let(:current_user) { developer }

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

  before_all do
    public_project.add_developer(developer)
    repository_project.add_developer(developer)
  end

  before do
    stub_licensed_features(
      multiple_issue_assignees: false,
      multiple_merge_request_reviewers: false,
      multiple_merge_request_assignees: false
    )
  end

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

    shared_examples 'reopen command' do
      it 'returns state_event: "reopen" if content contains /reopen' do
        issuable.close!
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(state_event: 'reopen')
      end

      it 'returns the reopen message' do
        issuable.close!
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Reopened this #{issuable.to_ability_name.humanize(capitalize: false)}.")
      end
    end

    shared_examples 'close command' do
      it 'returns state_event: "close" if content contains /close' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(state_event: 'close')
      end

      it 'returns the close message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Closed this #{issuable.to_ability_name.humanize(capitalize: false)}.")
      end
    end

    shared_examples 'title command' do
      it 'populates title: "A brand new title" if content contains /title A brand new title' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(title: 'A brand new title')
      end

      it 'returns the title message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq(%(Changed the title to "A brand new title".))
      end
    end

    shared_examples 'milestone command' do
      it 'fetches milestone and populates milestone_id if content contains /milestone' do
        milestone # populate the milestone
        _, updates, _ = service.execute(content, issuable)

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

      it 'returns the milestone message' do
        milestone # populate the milestone
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Set the milestone to #{milestone.to_reference}.")
      end

      it 'returns empty milestone message when milestone is wrong' do
        _, _, message = service.execute('/milestone %wrong-milestone', issuable)

        expect(message).to be_empty
      end
    end

    shared_examples 'remove_milestone command' do
      it 'populates milestone_id: nil if content contains /remove_milestone' do
        issuable.update!(milestone_id: milestone.id)
        _, updates, _ = service.execute(content, issuable)

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

      it 'returns removed milestone message' do
        issuable.update!(milestone_id: milestone.id)
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Removed #{milestone.to_reference} milestone.")
      end
    end

    shared_examples 'label command' do
      it 'fetches label ids and populates add_label_ids if content contains /label' do
        bug # populate the label
        inprogress # populate the label
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to match(add_label_ids: contain_exactly(bug.id, inprogress.id))
      end

      it 'returns the label message' do
        bug # populate the label
        inprogress # populate the label
        _, _, message = service.execute(content, issuable)

        # Compare message without making assumptions about ordering.
        expect(message).to match %r{Added ~".*" ~".*" labels.}
        expect(message).to include(bug.to_reference(format: :name))
        expect(message).to include(inprogress.to_reference(format: :name))
      end
    end

    shared_examples 'multiple label command' do
      it 'fetches label ids and populates add_label_ids if content contains multiple /label' do
        bug # populate the label
        inprogress # populate the label
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(add_label_ids: [inprogress.id, bug.id])
      end
    end

    shared_examples 'multiple label with same argument' do
      it 'prevents duplicate label ids and populates add_label_ids if content contains multiple /label' do
        inprogress # populate the label
        _, updates, _ = service.execute(content, issuable)

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

    shared_examples 'multiword label name starting without ~' do
      it 'fetches label ids and populates add_label_ids if content contains /label' do
        _, updates = service.execute(content, issuable)

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

    shared_examples 'label name is included in the middle of another label name' do
      it 'ignores the sublabel when the content contains the includer label name' do
        create(:label, project: project, title: 'Chart')

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

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

    shared_examples 'unlabel command' do
      it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do
        issuable.update!(label_ids: [inprogress.id]) # populate the label
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(remove_label_ids: [inprogress.id])
      end

      it 'returns the unlabel message' do
        issuable.update!(label_ids: [inprogress.id]) # populate the label
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Removed #{inprogress.to_reference(format: :name)} label.")
      end
    end

    shared_examples 'multiple unlabel command' do
      it 'fetches label ids and populates remove_label_ids if content contains  mutiple /unlabel' do
        issuable.update!(label_ids: [inprogress.id, bug.id]) # populate the label
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(remove_label_ids: [inprogress.id, bug.id])
      end
    end

    shared_examples 'unlabel command with no argument' do
      it 'populates label_ids: [] if content contains /unlabel with no arguments' do
        issuable.update!(label_ids: [inprogress.id]) # populate the label
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(label_ids: [])
      end
    end

    shared_examples 'relabel command' do
      it 'populates label_ids: [] if content contains /relabel' do
        issuable.update!(label_ids: [bug.id]) # populate the label
        inprogress # populate the label
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(label_ids: [inprogress.id])
      end

      it 'returns the relabel message' do
        issuable.update!(label_ids: [bug.id]) # populate the label
        inprogress # populate the label
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Replaced all labels with #{inprogress.to_reference(format: :name)} label.")
      end
    end

    shared_examples 'todo command' do
      it 'populates todo_event: "add" if content contains /todo' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(todo_event: 'add')
      end

      it 'returns the todo message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq('Added a to do.')
      end
    end

    shared_examples 'done command' do
      it 'populates todo_event: "done" if content contains /done' do
        TodoService.new.mark_todo(issuable, developer)
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(todo_event: 'done')
      end

      it 'returns the done message' do
        TodoService.new.mark_todo(issuable, developer)
        _, _, message = service.execute(content, issuable)

        expect(message).to eq('Marked to do as done.')
      end
    end

    shared_examples 'subscribe command' do
      it 'populates subscription_event: "subscribe" if content contains /subscribe' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(subscription_event: 'subscribe')
      end

      it 'returns the subscribe message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Subscribed to this #{issuable.to_ability_name.humanize(capitalize: false)}.")
      end
    end

    shared_examples 'unsubscribe command' do
      it 'populates subscription_event: "unsubscribe" if content contains /unsubscribe' do
        issuable.subscribe(developer, project)
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(subscription_event: 'unsubscribe')
      end

      it 'returns the unsubscribe message' do
        issuable.subscribe(developer, project)
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Unsubscribed from this #{issuable.to_ability_name.humanize(capitalize: false)}.")
      end
    end

    shared_examples 'due command' do
      let(:expected_date) { Date.new(2016, 8, 28) }

      it 'populates due_date: Date.new(2016, 8, 28) if content contains /due 2016-08-28' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(due_date: expected_date)
      end

      it 'returns due_date message: Date.new(2016, 8, 28) if content contains /due 2016-08-28' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Set the due date to #{expected_date.to_fs(:medium)}.")
      end
    end

    shared_examples 'remove_due_date command' do
      before do
        issuable.update!(due_date: Date.today)
      end

      it 'populates due_date: nil if content contains /remove_due_date' do
        _, updates, _ = service.execute(content, issuable)

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

      it 'returns Removed the due date' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq('Removed the due date.')
      end
    end

    shared_examples 'draft command' do
      it 'returns wip_event: "draft"' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(wip_event: 'draft')
      end

      it 'returns the draft message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Marked this #{issuable.to_ability_name.humanize(capitalize: false)} as a draft.")
      end
    end

    shared_examples 'draft/ready command no action' do
      it 'returns the no action message if there is no change to the status' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("No change to this #{issuable.to_ability_name.humanize(capitalize: false)}'s draft status.")
      end
    end

    shared_examples 'ready command' do
      it 'returns wip_event: "ready"' do
        issuable.update!(title: issuable.draft_title)
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(wip_event: 'ready')
      end

      it 'returns the ready message' do
        issuable.update!(title: issuable.draft_title)
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Marked this #{issuable.to_ability_name.humanize(capitalize: false)} as ready.")
      end
    end

    shared_examples 'estimate command' do
      it 'populates time_estimate: 3600 if content contains /estimate 1h' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(time_estimate: 3600)
      end

      it 'returns the time_estimate formatted message' do
        _, _, message = service.execute('/estimate 79d', issuable)

        expect(message).to eq('Set time estimate to 3mo 3w 4d.')
      end
    end

    shared_examples 'spend command' do
      it 'populates spend_time: 3600 if content contains /spend 1h' do
        freeze_time do
          _, updates, _ = service.execute(content, issuable)

          expect(updates).to eq(spend_time: {
                                  duration: 3600,
                                  user_id: developer.id,
                                  spent_at: DateTime.current
                                })
        end
      end
    end

    shared_examples 'spend command with negative time' do
      it 'populates spend_time: -7200 if content contains -120m' do
        freeze_time do
          _, updates, _ = service.execute(content, issuable)

          expect(updates).to eq(spend_time: {
                                  duration: -7200,
                                  user_id: developer.id,
                                  spent_at: DateTime.current
                                })
        end
      end

      it 'returns the spend_time message including the formatted duration and verb' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq('Subtracted 2h spent time.')
      end
    end

    shared_examples 'spend command with valid date' do
      it 'populates spend time: 1800 with date in date type format' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(spend_time: {
                                duration: 1800,
                                user_id: developer.id,
                                spent_at: Date.parse(date)
                              })
      end
    end

    shared_examples 'spend command with invalid date' do
      it 'will not create any note and timelog' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq({})
      end
    end

    shared_examples 'spend command with future date' do
      it 'will not create any note and timelog' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq({})
      end
    end

    shared_examples 'remove_estimate command' do
      it 'populates time_estimate: 0 if content contains /remove_estimate' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(time_estimate: 0)
      end

      it 'returns the remove_estimate message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq('Removed time estimate.')
      end
    end

    shared_examples 'remove_time_spent command' do
      it 'populates spend_time: :reset if content contains /remove_time_spent' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(spend_time: { duration: :reset, user_id: developer.id })
      end

      it 'returns the remove_time_spent message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq('Removed spent time.')
      end
    end

    shared_examples 'lock command' do
      let(:issue) { create(:issue, project: project, discussion_locked: false) }
      let(:merge_request) { create(:merge_request, source_project: project, discussion_locked: false) }

      it 'returns discussion_locked: true if content contains /lock' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(discussion_locked: true)
      end

      it 'returns the lock discussion message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq('Locked the discussion.')
      end
    end

    shared_examples 'unlock command' do
      let(:issue) { create(:issue, project: project, discussion_locked: true) }
      let(:merge_request) { create(:merge_request, source_project: project, discussion_locked: true) }

      it 'returns discussion_locked: true if content contains /unlock' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(discussion_locked: false)
      end

      it 'returns the unlock discussion message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq('Unlocked the discussion.')
      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 'merge immediately command' do
      let(:project) { repository_project }

      it 'runs merge command if content contains /merge' do
        _, updates, _ = service.execute(content, issuable)

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

      it 'returns them merge message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq('Merged this merge request.')
      end
    end

    shared_examples 'merge automatically command' do
      let(:project) { repository_project }

      before do
        stub_licensed_features(merge_request_approvers: true) if Gitlab.ee?
      end

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

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

        if Gitlab.ee?
          expect(message).to eq('Scheduled to merge this merge request (Merge when checks pass).')
        else
          expect(message).to eq('Scheduled to merge this merge request (Merge when pipeline succeeds).')
        end
      end
    end

    shared_examples 'award command' do
      it 'toggle award 100 emoji if content contains /award :100:' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(emoji_award: "100")
      end

      it 'returns the award message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq('Toggled :100: emoji award.')
      end
    end

    shared_examples 'duplicate command' do
      it 'fetches issue and populates canonical_issue_id if content contains /duplicate issue_reference' do
        issue_duplicate # populate the issue
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(canonical_issue_id: issue_duplicate.id)
      end

      it 'returns the duplicate message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Closed this issue. Marked as related to, and a duplicate of, #{issue_duplicate.to_reference(project)}.")
      end

      it 'includes duplicate reference' do
        _, explanations = service.explain(content, issuable)

        expect(explanations).to eq(["Closes this issue. Marks as related to, and a duplicate of, #{issue_duplicate.to_reference(project)}."])
      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)

        expect(message).to eq("Copied labels and milestone from #{source_issuable.to_reference}.")
      end
    end

    describe 'move issue command' do
      it 'returns the move issue message' do
        _, _, message = service.execute("/move #{project.full_path}", issue)

        expect(message).to eq("Moved this issue to #{project.full_path}.")
      end

      it 'returns move issue failure message when the referenced issue is not found' do
        _, _, message = service.execute('/move invalid', issue)

        expect(message).to eq(_("Failed to move this issue because target project doesn't exist."))
      end
    end

    shared_examples 'confidential command' do
      it 'marks issue as confidential if content contains /confidential' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(confidential: true)
      end

      it 'returns the confidential message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq('Made this issue confidential.')
      end

      context 'when issuable is already confidential' do
        before do
          issuable.update!(confidential: true)
        end

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

          expect(message).to eq('Could not apply confidential command.')
        end

        it 'is not part of the available commands' do
          expect(service.available_commands(issuable)).not_to include(a_hash_including(name: :confidential))
        end
      end
    end

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

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

    shared_examples 'shrug command' do
      it 'appends ¯\_(ツ)_/¯ to the comment' do
        new_content, _, _ = service.execute(content, issuable)

        expect(new_content).to end_with(described_class::SHRUG)
      end
    end

    shared_examples 'tableflip command' do
      it 'appends (╯°□°)╯︵ ┻━┻ to the comment' do
        new_content, _, _ = service.execute(content, issuable)

        expect(new_content).to end_with(described_class::TABLEFLIP)
      end
    end

    shared_examples 'tag command' do
      it 'tags a commit' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(tag_name: tag_name, tag_message: tag_message)
      end

      it 'returns the tag message' do
        _, _, message = service.execute(content, issuable)

        if tag_message.present?
          expect(message).to eq(%(Tagged this commit to #{tag_name} with "#{tag_message}".))
        else
          expect(message).to eq("Tagged this commit to #{tag_name}.")
        end
      end
    end

    shared_examples 'assign command' do
      it 'assigns to me' do
        cmd = '/assign me'

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

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

      it 'does not assign to group members' do
        grp = create(:group)
        grp.add_developer(developer)
        grp.add_developer(developer2)

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

        _, updates, message = service.execute(cmd, issuable)

        expect(updates).to be_blank
        expect(message).to include('Failed to find users')
      end

      context 'when there are too many references' do
        before do
          stub_const('Gitlab::QuickActions::UsersExtractor::MAX_QUICK_ACTION_USERS', 2)
        end

        it 'says what went wrong' do
          cmd = '/assign her and you, me and them'

          _, updates, message = service.execute(cmd, issuable)

          expect(updates).to be_blank
          expect(message).to include('Too many references. Quick actions are limited to at most 2 user references')
        end
      end

      context 'when the user extractor raises an uninticipated error' do
        before do
          allow_next(Gitlab::QuickActions::UsersExtractor)
            .to receive(:execute).and_raise(Gitlab::QuickActions::UsersExtractor::Error)
        end

        it 'tracks the exception in dev, and reports a generic message in production' do
          expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).twice

          _, updates, message = service.execute('/assign some text', issuable)

          expect(updates).to be_blank
          expect(message).to include('Something went wrong')
        end
      end

      it 'assigns to users with escaped underscores' do
        user = create(:user)
        base = user.username
        user.update!(username: "#{base}_new")
        issuable.project.add_developer(user)

        cmd = "/assign @#{base}\\_new"

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

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

      it 'assigns to a single user' do
        _, updates, _ = service.execute(content, issuable)

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

      it 'returns the assign message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Assigned #{developer.to_reference}.")
      end

      context 'when the reference does not match the exact case' do
        let(:user) { create(:user) }
        let(:content) { "/assign #{user.to_reference.upcase}" }

        it 'assigns to the user' do
          issuable.project.add_developer(user)

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

          expect(content).not_to include(user.to_reference)
          expect(updates).to eq(assignee_ids: [user.id])
          expect(message).to eq("Assigned #{user.to_reference}.")
        end
      end

      context 'when the user has a private profile' do
        let(:user) { create(:user, :private_profile) }
        let(:content) { "/assign #{user.to_reference}" }

        it 'assigns to the user' do
          issuable.project.add_developer(user)

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

          expect(updates).to eq(assignee_ids: [user.id])
          expect(message).to eq("Assigned #{user.to_reference}.")
        end
      end
    end

    shared_examples 'assign_reviewer command' do
      it 'assigns a reviewer to a single user' do
        _, updates, message = service.execute(content, issuable)

        expect(updates).to eq(reviewer_ids: [developer.id])
        expect(message).to eq("Assigned #{developer.to_reference} as reviewer.")
      end
    end

    shared_examples 'unassign_reviewer command' do
      it 'removes a single reviewer' do
        _, updates, message = service.execute(content, issuable)

        expect(updates).to eq(reviewer_ids: [])
        expect(message).to eq("Removed reviewer #{developer.to_reference}.")
      end
    end

    it_behaves_like 'reopen command' do
      let(:content) { '/reopen' }
      let(:issuable) { issue }
    end

    it_behaves_like 'reopen command' do
      let(:content) { '/reopen' }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'close command' do
      let(:content) { '/close' }
      let(:issuable) { issue }
    end

    it_behaves_like 'close command' do
      let(:content) { '/close' }
      let(:issuable) { merge_request }
    end

    context 'merge command' do
      let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: merge_request.diff_head_sha }) }
      let(:merge_request) { create(:merge_request, source_project: repository_project) }

      it_behaves_like 'merge immediately command' do
        let(:content) { '/merge' }
        let(:issuable) { merge_request }
      end

      context 'when the head pipeline of merge request is running' do
        before do
          create(:ci_pipeline, :detached_merge_request_pipeline, merge_request: merge_request)
          merge_request.update_head_pipeline
        end

        it_behaves_like 'merge automatically command' do
          let(:content) { '/merge' }
          let(:issuable) { merge_request }
        end
      end

      context 'can not be merged when logged user does not have permissions' do
        let(:service) { described_class.new(project, create(:user)) }

        it_behaves_like 'failed command', 'Could not apply merge command.' do
          let(:content) { "/merge" }
          let(:issuable) { merge_request }
        end
      end

      context 'can not be merged when sha does not match' do
        let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: 'othersha' }) }

        it_behaves_like 'failed command', 'Branch has been updated since the merge was requested.' do
          let(:content) { "/merge" }
          let(:issuable) { merge_request }
        end
      end

      context 'when sha is missing' do
        let(:project) { repository_project }
        let(:service) { described_class.new(project, developer, {}) }

        it_behaves_like 'failed command', 'The `/merge` quick action requires the SHA of the head of the branch.' do
          let(:content) { "/merge" }
          let(:issuable) { merge_request }
        end
      end

      context 'issue can not be merged' do
        it_behaves_like 'failed command', 'Could not apply merge command.' do
          let(:content) { "/merge" }
          let(:issuable) { issue }
        end
      end

      context 'non persisted merge request  cant be merged' do
        it_behaves_like 'failed command', 'Could not apply merge command.' do
          let(:content) { "/merge" }
          let(:issuable) { build(:merge_request) }
        end
      end

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

    it_behaves_like 'title command' do
      let(:content) { '/title A brand new title' }
      let(:issuable) { issue }
    end

    it_behaves_like 'title command' do
      let(:content) { '/title A brand new title' }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'failed command' do
      let(:content) { '/title' }
      let(:issuable) { issue }
    end

    context 'assign command with one user' do
      it_behaves_like 'assign command' do
        let(:content) { "/assign @#{developer.username}" }
        let(:issuable) { issue }
      end

      it_behaves_like 'assign command' do
        let(:content) { "/assign @#{developer.username}" }
        let(:issuable) { create(:incident, project: project) }
      end

      it_behaves_like 'assign command' do
        let(:content) { "/assign @#{developer.username}" }
        let(:issuable) { merge_request }
      end
    end

    # CE does not have multiple assignees
    context 'assign command with multiple assignees' do
      before do
        project.add_developer(developer2)
      end

      # There's no guarantee that the reference extractor will preserve
      # the order of the mentioned users since this is dependent on the
      # order in which rows are returned. We just ensure that at least
      # one of the mentioned users is assigned.
      shared_examples 'assigns to one of the two users' do
        let(:content) { "/assign @#{developer.username} @#{developer2.username}" }

        it 'assigns to a single user' do
          _, updates, message = service.execute(content, issuable)

          expect(updates[:assignee_ids].count).to eq(1)
          assignee = updates[:assignee_ids].first
          expect([developer.id, developer2.id]).to include(assignee)

          user = assignee == developer.id ? developer : developer2

          expect(message).to match("Assigned #{user.to_reference}.")
        end
      end

      it_behaves_like 'assigns to one of the two users' do
        let(:content) { "/assign @#{developer.username} @#{developer2.username}" }
        let(:issuable) { issue }
      end

      it_behaves_like 'assigns to one of the two users' do
        let(:content) { "/assign @#{developer.username} @#{developer2.username}" }
        let(:issuable) { merge_request }
      end
    end

    context 'assign command with me alias' do
      it_behaves_like 'assign command' do
        let(:content) { '/assign me' }
        let(:issuable) { issue }
      end

      it_behaves_like 'assign command' do
        let(:content) { '/assign me' }
        let(:issuable) { merge_request }
      end
    end

    context 'assign command with me alias and whitespace' do
      it_behaves_like 'assign command' do
        let(:content) { '/assign  me ' }
        let(:issuable) { issue }
      end

      it_behaves_like 'assign command' do
        let(:content) { '/assign  me ' }
        let(:issuable) { merge_request }
      end
    end

    it_behaves_like 'failed command', 'a parse error' do
      let(:content) { '/assign @abcd1234' }
      let(:issuable) { issue }
      let(:match_msg) { eq "Could not apply assign command. Failed to find users for '@abcd1234'." }
    end

    it_behaves_like 'failed command', "Failed to assign a user because no user was found." do
      let(:content) { '/assign' }
      let(:issuable) { issue }
    end

    describe 'assign_reviewer command' do
      let(:content) { "/assign_reviewer @#{developer.username}" }
      let(:issuable) { merge_request }

      context 'with one user' do
        it_behaves_like 'assign_reviewer command'
      end

      context 'with an issue instead of a merge request' do
        let(:issuable) { issue }

        it_behaves_like 'failed command', 'Could not apply assign_reviewer command.'
      end

      # CE does not have multiple reviewers
      context 'assign command with multiple assignees' do
        before do
          project.add_developer(developer2)
        end

        # There's no guarantee that the reference extractor will preserve
        # the order of the mentioned users since this is dependent on the
        # order in which rows are returned. We just ensure that at least
        # one of the mentioned users is assigned.
        context 'assigns to one of the two users' do
          let(:content) { "/assign_reviewer @#{developer.username} @#{developer2.username}" }

          it 'assigns to a single reviewer' do
            _, updates, message = service.execute(content, issuable)

            expect(updates[:reviewer_ids].count).to eq(1)
            reviewer = updates[:reviewer_ids].first
            expect([developer.id, developer2.id]).to include(reviewer)

            user = reviewer == developer.id ? developer : developer2

            expect(message).to match("Assigned #{user.to_reference} as reviewer.")
          end
        end
      end

      context 'with "me" alias' do
        let(:content) { '/assign_reviewer me' }

        it_behaves_like 'assign_reviewer command'
      end

      context 'with an alias and whitespace' do
        let(:content) { '/assign_reviewer  me ' }

        it_behaves_like 'assign_reviewer command'
      end

      context 'with @all' do
        let(:content) { "/assign_reviewer @all" }

        it_behaves_like 'failed command', 'a parse error' do
          let(:match_msg) { eq "Could not apply assign_reviewer command. Failed to find users for '@all'." }
        end
      end

      context 'with an incorrect user' do
        let(:content) { '/assign_reviewer @abcd1234' }

        it_behaves_like 'failed command', 'a parse error' do
          let(:match_msg) { eq "Could not apply assign_reviewer command. Failed to find users for '@abcd1234'." }
        end
      end

      context 'with the "reviewer" alias' do
        let(:content) { "/reviewer @#{developer.username}" }

        it_behaves_like 'assign_reviewer command'
      end

      context 'with the "request_review" alias' do
        let(:content) { "/request_review @#{developer.username}" }

        it_behaves_like 'assign_reviewer command'
      end

      context 'with no user' do
        let(:content) { '/assign_reviewer' }

        it_behaves_like 'failed command', "Failed to assign a reviewer because no user was specified."
      end

      context 'with extra text' do
        let(:content) { "/assign_reviewer #{developer.to_reference} do it!" }

        it_behaves_like 'failed command', 'a parse error' do
          let(:match_msg) { eq "Could not apply assign_reviewer command. Failed to find users for 'do' and 'it!'." }
        end
      end
    end

    describe 'unassign_reviewer command' do
      # CE does not have multiple reviewers, so basically anything
      # after /unassign_reviewer (including whitespace) will remove
      # all the current reviewers.
      let(:issuable) { create(:merge_request, reviewers: [developer]) }
      let(:content) { "/unassign_reviewer @#{developer.username}" }

      context 'with one user' do
        it_behaves_like 'unassign_reviewer command'
      end

      context 'with an issue instead of a merge request' do
        let(:issuable) { issue }

        it_behaves_like 'failed command', 'Could not apply unassign_reviewer command.'
      end

      context 'with anything after the command' do
        let(:content) { '/unassign_reviewer supercalifragilisticexpialidocious' }

        it_behaves_like 'unassign_reviewer command'
      end

      context 'with the "remove_reviewer" alias' do
        let(:content) { "/remove_reviewer @#{developer.username}" }

        it_behaves_like 'unassign_reviewer command'
      end

      context 'with no user' do
        let(:content) { '/unassign_reviewer' }

        it_behaves_like 'unassign_reviewer command'
      end
    end

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

      context 'Issue' do
        it 'populates assignee_ids: [] if content contains /unassign' do
          issue.update!(assignee_ids: [developer.id])
          _, updates, _ = service.execute(content, issue)

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

        it 'returns the unassign message for all the assignee if content contains /unassign' do
          issue.update!(assignee_ids: [developer.id, developer2.id])
          _, _, message = service.execute(content, issue)

          expect(message).to eq("Removed assignees #{developer.to_reference} and #{developer2.to_reference}.")
        end
      end

      context 'Merge Request' do
        it 'populates assignee_ids: [] if content contains /unassign' do
          merge_request.update!(assignee_ids: [developer.id])
          _, updates, _ = service.execute(content, merge_request)

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

        it 'returns the unassign message for all the assignee if content contains /unassign' do
          merge_request.update!(assignee_ids: [developer.id, developer2.id])
          _, _, message = service.execute(content, merge_request)

          expect(message).to eq("Removed assignees #{developer.to_reference} and #{developer2.to_reference}.")
        end
      end
    end

    context 'project milestones' do
      before do
        milestone
      end

      it_behaves_like 'milestone command' do
        let(:content) { "/milestone %#{milestone.title}" }
        let(:issuable) { issue }
      end

      it_behaves_like 'milestone command' do
        let(:content) { "/milestone %#{milestone.title}" }
        let(:issuable) { merge_request }
      end
    end

    context 'only group milestones available' do
      let_it_be(:ancestor_group) { create(:group) }
      let_it_be(:group) { create(:group, parent: ancestor_group) }
      let_it_be(:project) { create(:project, :public, namespace: group) }
      let_it_be(:milestone) { create(:milestone, group: ancestor_group, title: '10.0') }

      before_all do
        project.add_developer(developer)
      end

      it_behaves_like 'milestone command' do
        let(:content) { "/milestone %#{milestone.title}" }
        let(:issuable) { issue }
      end

      it_behaves_like 'milestone command' do
        let(:content) { "/milestone %#{milestone.title}" }
        let(:issuable) { merge_request }
      end
    end

    it_behaves_like 'remove_milestone command' do
      let(:content) { '/remove_milestone' }
      let(:issuable) { issue }
    end

    it_behaves_like 'remove_milestone command' do
      let(:content) { '/remove_milestone' }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'label command' do
      let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) }
      let(:issuable) { issue }
    end

    it_behaves_like 'label command' do
      let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) }
      let(:issuable) { merge_request }
    end

    context 'with a colon label' do
      let(:bug) { create(:label, project: project, title: 'Category:Bug') }
      let(:inprogress) { create(:label, project: project, title: 'status:in:progress') }

      context 'when quoted' do
        let(:content) { %(/label ~"#{inprogress.title}" ~"#{bug.title}" ~unknown) }

        it_behaves_like 'label command' do
          let(:issuable) { merge_request }
        end

        it_behaves_like 'label command' do
          let(:issuable) { issue }
        end
      end

      context 'when unquoted' do
        let(:content) { %(/label ~#{inprogress.title} ~#{bug.title} ~unknown) }

        it_behaves_like 'label command' do
          let(:issuable) { merge_request }
        end

        it_behaves_like 'label command' do
          let(:issuable) { issue }
        end
      end
    end

    context 'with a scoped label' do
      let(:bug) { create(:label, :scoped, project: project) }
      let(:inprogress) { create(:label, project: project, title: 'three::part::label') }

      context 'when quoted' do
        let(:content) { %(/label ~"#{inprogress.title}" ~"#{bug.title}" ~unknown) }

        it_behaves_like 'label command' do
          let(:issuable) { merge_request }
        end

        it_behaves_like 'label command' do
          let(:issuable) { issue }
        end
      end

      context 'when unquoted' do
        let(:content) { %(/label ~#{inprogress.title} ~#{bug.title} ~unknown) }

        it_behaves_like 'label command' do
          let(:issuable) { merge_request }
        end

        it_behaves_like 'label command' do
          let(:issuable) { issue }
        end
      end
    end

    it_behaves_like 'multiple label command' do
      let(:content) { %(/label ~"#{inprogress.title}" \n/label ~#{bug.title}) }
      let(:issuable) { issue }
    end

    it_behaves_like 'multiple label with same argument' do
      let(:content) { %(/label ~"#{inprogress.title}" \n/label ~#{inprogress.title}) }
      let(:issuable) { issue }
    end

    it_behaves_like 'multiword label name starting without ~' do
      let(:content) { %(/label "#{helmchart.title}") }
      let(:issuable) { issue }
    end

    it_behaves_like 'multiword label name starting without ~' do
      let(:content) { %(/label "#{helmchart.title}") }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'label name is included in the middle of another label name' do
      let(:content) { %(/label ~"#{helmchart.title}") }
      let(:issuable) { issue }
    end

    it_behaves_like 'label name is included in the middle of another label name' do
      let(:content) { %(/label ~"#{helmchart.title}") }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'unlabel command' do
      let(:content) { %(/unlabel ~"#{inprogress.title}") }
      let(:issuable) { issue }
    end

    it_behaves_like 'unlabel command' do
      let(:content) { %(/unlabel ~"#{inprogress.title}") }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'multiple unlabel command' do
      let(:content) { %(/unlabel ~"#{inprogress.title}" \n/unlabel ~#{bug.title}) }
      let(:issuable) { issue }
    end

    it_behaves_like 'unlabel command with no argument' do
      let(:content) { %(/unlabel) }
      let(:issuable) { issue }
    end

    it_behaves_like 'unlabel command with no argument' do
      let(:content) { %(/unlabel) }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'relabel command' do
      let(:content) { %(/relabel ~"#{inprogress.title}") }
      let(:issuable) { issue }
    end

    it_behaves_like 'relabel command' do
      let(:content) { %(/relabel ~"#{inprogress.title}") }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'done command' do
      let(:content) { '/done' }
      let(:issuable) { issue }
    end

    it_behaves_like 'done command' do
      let(:content) { '/done' }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'done command' do
      let(:content) { '/done' }
      let(:issuable) { work_item }
    end

    it_behaves_like 'subscribe command' do
      let(:content) { '/subscribe' }
      let(:issuable) { issue }
    end

    it_behaves_like 'subscribe command' do
      let(:content) { '/subscribe' }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'subscribe command' do
      let(:content) { '/subscribe' }
      let(:issuable) { work_item }
    end

    it_behaves_like 'unsubscribe command' do
      let(:content) { '/unsubscribe' }
      let(:issuable) { issue }
    end

    it_behaves_like 'unsubscribe command' do
      let(:content) { '/unsubscribe' }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'unsubscribe command' do
      let(:content) { '/unsubscribe' }
      let(:issuable) { work_item }
    end

    it_behaves_like 'failed command', 'Could not apply due command.' do
      let(:content) { '/due 2016-08-28' }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'remove_due_date command' do
      let(:content) { '/remove_due_date' }
      let(:issuable) { issue }
    end

    it_behaves_like 'draft command' do
      let(:content) { '/draft' }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'draft/ready command no action' do
      let(:content) { '/draft' }
      let(:issuable) { merge_request }

      before do
        issuable.update!(title: issuable.draft_title)
      end
    end

    it_behaves_like 'draft/ready command no action' do
      let(:content) { '/ready' }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'ready command' do
      let(:content) { '/ready' }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'failed command', 'Could not apply remove_due_date command.' do
      let(:content) { '/remove_due_date' }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'estimate command' do
      let(:content) { '/estimate 1h' }
      let(:issuable) { issue }
    end

    it_behaves_like 'estimate command' do
      let(:content) { '/estimate_time 1h' }
      let(:issuable) { issue }
    end

    it_behaves_like 'failed command' do
      let(:content) { '/estimate' }
      let(:issuable) { issue }
    end

    context 'when provided an invalid estimate' do
      let(:content) { '/estimate abc' }
      let(:issuable) { issue }

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

        expect(updates[:time_estimate]).to be_nil
      end

      it "returns empty message" do
        _, _, message = service.execute(content, issuable)

        expect(message).to be_empty
      end
    end

    it_behaves_like 'spend command' do
      let(:content) { '/spend 1h' }
      let(:issuable) { issue }
    end

    it_behaves_like 'spend command' do
      let(:content) { '/spent 1h' }
      let(:issuable) { issue }
    end

    it_behaves_like 'spend command' do
      let(:content) { '/spend_time 1h' }
      let(:issuable) { issue }
    end

    it_behaves_like 'spend command with negative time' do
      let(:content) { '/spend -120m' }
      let(:issuable) { issue }
    end

    it_behaves_like 'spend command with negative time' do
      let(:content) { '/spent -120m' }
      let(:issuable) { issue }
    end

    it_behaves_like 'spend command with valid date' do
      let(:date) { '2016-02-02' }
      let(:content) { "/spend 30m #{date}" }
      let(:issuable) { issue }
    end

    it_behaves_like 'spend command with valid date' do
      let(:date) { '2016-02-02' }
      let(:content) { "/spent 30m #{date}" }
      let(:issuable) { issue }
    end

    it_behaves_like 'spend command with invalid date' do
      let(:content) { '/spend 30m 17-99-99' }
      let(:issuable) { issue }
    end

    it_behaves_like 'spend command with invalid date' do
      let(:content) { '/spent 30m 17-99-99' }
      let(:issuable) { issue }
    end

    it_behaves_like 'spend command with future date' do
      let(:content) { '/spend 30m 6017-10-10' }
      let(:issuable) { issue }
    end

    it_behaves_like 'spend command with future date' do
      let(:content) { '/spent 30m 6017-10-10' }
      let(:issuable) { issue }
    end

    it_behaves_like 'failed command' do
      let(:content) { '/spend' }
      let(:issuable) { issue }
    end

    it_behaves_like 'failed command' do
      let(:content) { '/spent' }
      let(:issuable) { issue }
    end

    it_behaves_like 'failed command' do
      let(:content) { '/spend abc' }
      let(:issuable) { issue }
    end

    it_behaves_like 'failed command' do
      let(:content) { '/spent abc' }
      let(:issuable) { issue }
    end

    it_behaves_like 'remove_estimate command' do
      let(:content) { '/remove_estimate' }
      let(:issuable) { issue }
    end

    it_behaves_like 'remove_estimate command' do
      let(:content) { '/remove_time_estimate' }
      let(:issuable) { issue }
    end

    it_behaves_like 'remove_time_spent command' do
      let(:content) { '/remove_time_spent' }
      let(:issuable) { issue }
    end

    context '/confidential' do
      it_behaves_like 'confidential command' do
        let(:content) { '/confidential' }
        let(:issuable) { issue }
      end

      it_behaves_like 'confidential command' do
        let(:content) { '/confidential' }
        let(:issuable) { create(:incident, project: project) }
      end

      context 'when non-member is creating a new issue' do
        let(:service) { described_class.new(project, create(:user)) }

        it_behaves_like 'confidential command' do
          let(:content) { '/confidential' }
          let(:issuable) { build(:issue, project: project) }
        end
      end
    end

    it_behaves_like 'lock command' do
      let(:content) { '/lock' }
      let(:issuable) { issue }
    end

    it_behaves_like 'lock command' do
      let(:content) { '/lock' }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'unlock command' do
      let(:content) { '/unlock' }
      let(:issuable) { issue }
    end

    it_behaves_like 'unlock command' do
      let(:content) { '/unlock' }
      let(:issuable) { merge_request }
    end

    context '/todo' do
      let(:content) { '/todo' }

      context 'if issuable is an Issue' do
        it_behaves_like 'todo command' do
          let(:issuable) { issue }
        end
      end

      context 'if issuable is a work item' do
        it_behaves_like 'todo command' do
          let(:issuable) { work_item }
        end
      end

      context 'if issuable is a MergeRequest' do
        it_behaves_like 'todo command' do
          let(:issuable) { merge_request }
        end
      end

      context 'if issuable is a Commit' do
        it_behaves_like 'failed command', 'Could not apply todo command.' do
          let(:issuable) { commit }
        end
      end
    end

    context '/due command' do
      it 'returns invalid date format message when the due date is invalid' do
        issue = build(:issue, project: project)

        _, _, message = service.execute('/due invalid date', issue)

        expect(message).to eq(_('Failed to set due date because the date format is invalid.'))
      end

      it_behaves_like 'due command' do
        let(:content) { '/due 2016-08-28' }
        let(:issuable) { issue }
      end

      it_behaves_like 'due command' do
        let(:content) { '/due tomorrow' }
        let(:issuable) { issue }
        let(:expected_date) { Date.tomorrow }
      end

      it_behaves_like 'due command' do
        let(:content) { '/due 5 days from now' }
        let(:issuable) { issue }
        let(:expected_date) { 5.days.from_now.to_date }
      end

      it_behaves_like 'due command' do
        let(:content) { '/due in 2 days' }
        let(:issuable) { issue }
        let(:expected_date) { 2.days.from_now.to_date }
      end
    end

    context '/copy_metadata command' do
      let(:todo_label) { create(:label, project: project, title: 'To Do') }
      let(:inreview_label) { create(:label, project: project, title: 'In Review') }

      it 'is available when the user is a developer' do
        expect(service.available_commands(issue)).to include(a_hash_including(name: :copy_metadata))
      end

      context 'when the user does not have permission' do
        let(:guest) { create(:user) }
        let(:service) { described_class.new(project, guest) }

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

      it_behaves_like 'failed command' do
        let(:content) { '/copy_metadata' }
        let(:issuable) { issue }
      end

      it_behaves_like 'copy_metadata command' do
        let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) }

        let(:content) { "/copy_metadata #{source_issuable.to_reference}" }
        let(:issuable) { build(:issue, project: project) }
      end

      it_behaves_like 'copy_metadata command' do
        let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) }

        let(:content) { "/copy_metadata #{source_issuable.to_reference}" }
        let(:issuable) { issue }
      end

      context 'when the parent issuable has a milestone' do
        it_behaves_like 'copy_metadata command' do
          let(:source_issuable) { create(:labeled_issue, project: project, labels: [todo_label, inreview_label], milestone: milestone) }

          let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" }
          let(:issuable) { issue }
        end
      end

      context 'when more than one issuable is passed' do
        it_behaves_like 'copy_metadata command' do
          let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) }
          let(:other_label) { create(:label, project: project, title: 'Other') }
          let(:other_source_issuable) { create(:labeled_issue, project: project, labels: [other_label]) }

          let(:content) { "/copy_metadata #{source_issuable.to_reference} #{other_source_issuable.to_reference}" }
          let(:issuable) { issue }
        end
      end

      context 'cross project references' do
        it_behaves_like 'failed command' do
          let(:other_project) { create(:project, :public) }
          let(:source_issuable) { create(:labeled_issue, project: other_project, labels: [todo_label, inreview_label]) }
          let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" }
          let(:issuable) { issue }
        end

        it_behaves_like 'failed command' do
          let(:content) { "/copy_metadata imaginary##{non_existing_record_iid}" }
          let(:issuable) { issue }
        end

        it_behaves_like 'failed command' do
          let(:other_project) { create(:project, :private) }
          let(:source_issuable) { create(:issue, project: other_project) }

          let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" }
          let(:issuable) { issue }
        end
      end
    end

    context '/duplicate command' do
      it_behaves_like 'duplicate command' do
        let(:issue_duplicate) { create(:issue, project: project) }
        let(:content) { "/duplicate #{issue_duplicate.to_reference}" }
        let(:issuable) { issue }
      end

      it_behaves_like 'failed command' do
        let(:content) { '/duplicate' }
        let(:issuable) { issue }
      end

      context 'cross project references' do
        it_behaves_like 'duplicate command' do
          let(:other_project) { create(:project, :public) }
          let(:issue_duplicate) { create(:issue, project: other_project) }
          let(:content) { "/duplicate #{issue_duplicate.to_reference(project)}" }
          let(:issuable) { issue }
        end

        it_behaves_like 'failed command', _('Failed to mark this issue as a duplicate because referenced issue was not found.') do
          let(:content) { "/duplicate imaginary##{non_existing_record_iid}" }
          let(:issuable) { issue }
        end

        it_behaves_like 'failed command', _('Failed to mark this issue as a duplicate because referenced issue was not found.') do
          let(:other_project) { create(:project, :private) }
          let(:issue_duplicate) { create(:issue, project: other_project) }

          let(:content) { "/duplicate #{issue_duplicate.to_reference(project)}" }
          let(:issuable) { issue }
        end
      end
    end

    context 'when current_user cannot :admin_issue' do
      let(:visitor) { create(:user) }
      let(:issue) { create(:issue, project: project, author: visitor) }
      let(:service) { described_class.new(project, visitor) }

      it_behaves_like 'failed command', 'Could not apply assign command.' do
        let(:content) { "/assign @#{developer.username}" }
        let(:issuable) { issue }
      end

      it_behaves_like 'failed command', 'Could not apply unassign command.' do
        let(:content) { '/unassign' }
        let(:issuable) { issue }
      end

      it_behaves_like 'failed command', 'Could not apply milestone command.' do
        let(:content) { "/milestone %#{milestone.title}" }
        let(:issuable) { issue }
      end

      it_behaves_like 'failed command', 'Could not apply remove_milestone command.' do
        let(:content) { '/remove_milestone' }
        let(:issuable) { issue }
      end

      it_behaves_like 'failed command', 'Could not apply label command.' do
        let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) }
        let(:issuable) { issue }
      end

      it_behaves_like 'failed command', 'Could not apply unlabel command.' do
        let(:content) { %(/unlabel ~"#{inprogress.title}") }
        let(:issuable) { issue }
      end

      it_behaves_like 'failed command', 'Could not apply relabel command.' do
        let(:content) { %(/relabel ~"#{inprogress.title}") }
        let(:issuable) { issue }
      end

      it_behaves_like 'failed command', 'Could not apply due command.' do
        let(:content) { '/due tomorrow' }
        let(:issuable) { issue }
      end

      it_behaves_like 'failed command', 'Could not apply remove_due_date command.' do
        let(:content) { '/remove_due_date' }
        let(:issuable) { issue }
      end

      it_behaves_like 'failed command', 'Could not apply confidential command.' do
        let(:content) { '/confidential' }
        let(:issuable) { issue }
      end

      it_behaves_like 'failed command', 'Could not apply lock command.' do
        let(:content) { '/lock' }
        let(:issuable) { issue }
      end

      it_behaves_like 'failed command', 'Could not apply unlock command.' do
        let(:content) { '/unlock' }
        let(:issuable) { issue }
      end
    end

    context '/award command' do
      it_behaves_like 'award command' do
        let(:content) { '/award :100:' }
        let(:issuable) { issue }
      end

      it_behaves_like 'award command' do
        let(:content) { '/award :100:' }
        let(:issuable) { merge_request }
      end

      it_behaves_like 'award command' do
        let(:content) { '/award :100:' }
        let(:issuable) { work_item }
      end

      context 'ignores command with no argument' do
        it_behaves_like 'failed command' do
          let(:content) { '/award' }
          let(:issuable) { issue }
        end

        it_behaves_like 'failed command' do
          let(:content) { '/award' }
          let(:issuable) { work_item }
        end
      end

      context 'ignores non-existing / invalid  emojis' do
        it_behaves_like 'failed command' do
          let(:content) { '/award noop' }
          let(:issuable) { issue }
        end

        it_behaves_like 'failed command' do
          let(:content) { '/award :lorem_ipsum:' }
          let(:issuable) { issue }
        end

        it_behaves_like 'failed command' do
          let(:content) { '/award :lorem_ipsum:' }
          let(:issuable) { work_item }
        end
      end

      context 'if issuable is a Commit' do
        let(:content) { '/award :100:' }
        let(:issuable) { commit }

        it_behaves_like 'failed command', 'Could not apply award command.'
      end
    end

    context '/shrug command' do
      it_behaves_like 'shrug command' do
        let(:content) { '/shrug people are people' }
        let(:issuable) { issue }
      end

      it_behaves_like 'shrug command' do
        let(:content) { '/shrug' }
        let(:issuable) { issue }
      end
    end

    context '/tableflip command' do
      it_behaves_like 'tableflip command' do
        let(:content) { '/tableflip curse your sudden but enviable betrayal' }
        let(:issuable) { issue }
      end

      it_behaves_like 'tableflip command' do
        let(:content) { '/tableflip' }
        let(:issuable) { issue }
      end
    end

    context '/target_branch command' do
      let(:non_empty_project) { create(:project, :repository) }
      let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) }
      let(:service) { described_class.new(non_empty_project, developer) }

      it 'updates target_branch if /target_branch command is executed' do
        _, updates, _ = service.execute('/target_branch merge-test', merge_request)

        expect(updates).to eq(target_branch: 'merge-test')
      end

      it 'handles blanks around param' do
        _, updates, _ = service.execute('/target_branch  merge-test     ', merge_request)

        expect(updates).to eq(target_branch: 'merge-test')
      end

      context 'ignores command with no argument' do
        it_behaves_like 'failed command', 'Could not apply target_branch command.' do
          let(:content) { '/target_branch' }
          let(:issuable) { another_merge_request }
        end
      end

      context 'ignores non-existing target branch' do
        it_behaves_like 'failed command', 'Could not apply target_branch command.' do
          let(:content) { '/target_branch totally_non_existing_branch' }
          let(:issuable) { another_merge_request }
        end
      end

      it 'returns the target_branch message' do
        _, _, message = service.execute('/target_branch merge-test', merge_request)

        expect(message).to eq('Set target branch to merge-test.')
      end
    end

    context '/board_move command' do
      let_it_be(:todo) { create(:label, project: project, title: 'To Do') }
      let_it_be(:inreview) { create(:label, project: project, title: 'In Review') }
      let(:content) { %(/board_move ~"#{inreview.title}") }

      let_it_be(:board) { create(:board, project: project) }
      let_it_be(:todo_list) { create(:list, board: board, label: todo) }
      let_it_be(:inreview_list) { create(:list, board: board, label: inreview) }
      let_it_be(:inprogress_list) { create(:list, board: board, label: inprogress) }

      it 'populates remove_label_ids for all current board columns' do
        issue.update!(label_ids: [todo.id, inprogress.id])

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

        expect(updates[:remove_label_ids]).to match_array([todo.id, inprogress.id])
      end

      it 'populates add_label_ids with the id of the given label' do
        _, updates, _ = service.execute(content, issue)

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

      it 'does not include the given label id in remove_label_ids' do
        issue.update!(label_ids: [todo.id, inreview.id])

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

        expect(updates[:remove_label_ids]).to match_array([todo.id])
      end

      it 'does not remove label ids that are not lists on the board' do
        issue.update!(label_ids: [todo.id, bug.id])

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

        expect(updates[:remove_label_ids]).to match_array([todo.id])
      end

      it 'returns board_move message' do
        issue.update!(label_ids: [todo.id, inprogress.id])

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

        expect(message).to eq("Moved issue to ~#{inreview.id} column in the board.")
      end

      context 'if the project has multiple boards' do
        let(:issuable) { issue }

        before do
          create(:board, project: project)
        end

        it_behaves_like 'failed command', 'Could not apply board_move command.'
      end

      context 'if the given label does not exist' do
        let(:issuable) { issue }
        let(:content) { '/board_move ~"Fake Label"' }

        it_behaves_like 'failed command', 'Failed to move this issue because label was not found.'
      end

      context 'if multiple labels are given' do
        let(:issuable) { issue }
        let(:content) { %(/board_move ~"#{inreview.title}" ~"#{todo.title}") }

        it_behaves_like 'failed command', 'Failed to move this issue because only a single label can be provided.'
      end

      context 'if the given label is not a list on the board' do
        let(:issuable) { issue }
        let(:content) { %(/board_move ~"#{bug.title}") }

        it_behaves_like 'failed command', 'Failed to move this issue because label was not found.'
      end

      context 'if issuable is not an Issue' do
        let(:issuable) { merge_request }

        it_behaves_like 'failed command', 'Could not apply board_move command.'
      end
    end

    context '/tag command' do
      let(:issuable) { commit }

      context 'ignores command with no argument' do
        it_behaves_like 'failed command' do
          let(:content) { '/tag' }
        end
      end

      context 'tags a commit with a tag name' do
        it_behaves_like 'tag command' do
          let(:tag_name) { 'v1.2.3' }
          let(:tag_message) { nil }
          let(:content) { "/tag #{tag_name}" }
        end
      end

      context 'tags a commit with a tag name and message' do
        it_behaves_like 'tag command' do
          let(:tag_name) { 'v1.2.3' }
          let(:tag_message) { 'Stable release' }
          let(:content) { "/tag #{tag_name} #{tag_message}" }
        end
      end
    end

    it 'limits to commands passed' do
      content = "/shrug test\n/close"

      text, commands = service.execute(content, issue, only: [:shrug])

      expect(commands).to be_empty
      expect(text).to eq("test #{described_class::SHRUG}\n/close")
    end

    it 'preserves leading whitespace' do
      content = " - list\n\n/close\n\ntest\n\n"

      text, _ = service.execute(content, issue)

      expect(text).to eq(" - list\n\ntest")
    end

    it 'tracks MAU for commands' do
      content = "/shrug test\n/assign me\n/milestone %4"

      expect(Gitlab::UsageDataCounters::QuickActionActivityUniqueCounter)
        .to receive(:track_unique_action)
        .with('shrug', args: 'test', user: developer)

      expect(Gitlab::UsageDataCounters::QuickActionActivityUniqueCounter)
        .to receive(:track_unique_action)
        .with('assign', args: 'me', user: developer)

      expect(Gitlab::UsageDataCounters::QuickActionActivityUniqueCounter)
        .to receive(:track_unique_action)
        .with('milestone', args: '%4', user: developer)

      service.execute(content, issue)
    end

    context '/create_merge_request command' do
      let(:branch_name) { '1-feature' }
      let(:content) { "/create_merge_request #{branch_name}" }
      let(:issuable) { issue }

      context 'if issuable is not an Issue' do
        let(:issuable) { merge_request }

        it_behaves_like 'failed command', 'Could not apply create_merge_request command.'
      end

      context "when logged user cannot create_merge_requests in the project" do
        let(:project) { create(:project, :archived) }

        before do
          project.add_developer(developer)
        end

        it_behaves_like 'failed command', 'Could not apply create_merge_request command.'
      end

      context 'when logged user cannot push code to the project' do
        let(:project) { create(:project, :private) }
        let(:service) { described_class.new(project, create(:user)) }

        it_behaves_like 'failed command', 'Could not apply create_merge_request command.'
      end

      it 'populates create_merge_request with branch_name and issue iid' do
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(create_merge_request: { branch_name: branch_name, issue_iid: issuable.iid })
      end

      it 'returns the create_merge_request message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Created branch '#{branch_name}' and a merge request to resolve this issue.")
      end
    end

    context 'submit_review command' do
      using RSpec::Parameterized::TableSyntax

      where(:note) do
        [
          'I like it',
          '/submit_review'
        ]
      end

      with_them do
        let(:content) { '/submit_review' }
        let!(:draft_note) { create(:draft_note, note: note, merge_request: merge_request, author: developer) }

        it 'submits the users current review' do
          _, _, message = service.execute(content, merge_request)

          expect { draft_note.reload }.to raise_error(ActiveRecord::RecordNotFound)
          expect(message).to eq('Submitted the current review.')
        end
      end

      context 'when parameters are passed' do
        context 'with approve parameter' do
          it 'calls MergeRequests::ApprovalService service' do
            expect_next_instance_of(
              MergeRequests::ApprovalService, project: merge_request.project, current_user: current_user
            ) do |service|
              expect(service).to receive(:execute).with(merge_request)
            end

            _, _, message = service.execute('/submit_review approve', merge_request)

            expect(message).to eq('Submitted the current review.')
          end
        end

        context 'with review state parameter' do
          it 'calls MergeRequests::UpdateReviewerStateService service' do
            expect_next_instance_of(
              MergeRequests::UpdateReviewerStateService, project: merge_request.project, current_user: current_user
            ) do |service|
              expect(service).to receive(:execute).with(merge_request, 'requested_changes')
            end

            _, _, message = service.execute('/submit_review requested_changes', merge_request)

            expect(message).to eq('Submitted the current review.')
          end
        end
      end
    end

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

      context "when `mr_request_changes` feature flag is disabled" do
        before do
          stub_feature_flags(mr_request_changes: false)
        end

        it 'does not call MergeRequests::UpdateReviewerStateService' do
          expect(MergeRequests::UpdateReviewerStateService).not_to receive(:new)

          service.execute(content, merge_request)
        end
      end

      context "when the user is a reviewer" do
        before do
          create(:merge_request_reviewer, merge_request: merge_request, reviewer: current_user)
        end

        it 'calls MergeRequests::UpdateReviewerStateService with requested_changes' do
          expect_next_instance_of(
            MergeRequests::UpdateReviewerStateService,
            project: project, current_user: current_user
          ) do |service|
            expect(service).to receive(:execute).with(merge_request, "requested_changes").and_return({ status: :success })
          end

          _, _, message = service.execute(content, merge_request)

          expect(message).to eq('Changes requested to the current merge request.')
        end

        it 'returns error message from MergeRequests::UpdateReviewerStateService' do
          expect_next_instance_of(
            MergeRequests::UpdateReviewerStateService,
            project: project, current_user: current_user
          ) do |service|
            expect(service).to receive(:execute).with(merge_request, "requested_changes").and_return({ status: :error, message: 'Error' })
          end

          _, _, message = service.execute(content, merge_request)

          expect(message).to eq('Error')
        end
      end

      context "when the user is not a reviewer" do
        it 'does not call MergeRequests::UpdateReviewerStateService' do
          expect(MergeRequests::UpdateReviewerStateService).not_to receive(:new)

          service.execute(content, merge_request)
        end
      end

      it_behaves_like 'approve command unavailable' do
        let(:issuable) { issue }
      end
    end

    it_behaves_like 'issues link quick action', :relate do
      let(:user) { developer }
    end

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

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

      shared_examples 'command with failure' do
        it 'does not destroy issues relation' do
          expect { unlink_issues }.not_to change { IssueLink.count }
        end

        it 'return correct execution message' do
          expect(unlink_issues[2]).to eq('No linked issue matches the provided parameter.')
        end
      end

      context 'when command includes linked issue' do
        let_it_be(:link1) { create(:issue_link, source: issue, target: other_issue) }
        let_it_be(:link2) { create(:issue_link, source: issue, target: private_issue) }

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

        context 'when user has no access' do
          let(:content) { "/unlink #{private_issue.to_reference(issue)}" }

          it_behaves_like 'command with failure'
        end
      end

      context 'when provided issue is not linked' do
        it_behaves_like 'command with failure'
      end
    end

    context 'invite_email command' do
      let_it_be(:issuable) { issue }

      it_behaves_like 'failed command', "No email participants were added. Either none were provided, or they already exist." do
        let(:content) { '/invite_email' }
      end

      context 'with existing email participant' do
        let(:content) { '/invite_email a@gitlab.com' }

        before do
          issuable.issue_email_participants.create!(email: "a@gitlab.com")
        end

        it_behaves_like 'failed command', "No email participants were added. Either none were provided, or they already exist."
      end

      context 'with new email participants' do
        let(:content) { '/invite_email a@gitlab.com b@gitlab.com' }

        subject(:add_emails) { service.execute(content, issuable) }

        it 'returns message' do
          _, _, message = add_emails

          expect(message).to eq('Added a@gitlab.com and b@gitlab.com.')
        end

        it 'adds 2 participants' do
          expect { add_emails }.to change { issue.issue_email_participants.count }.by(2)
        end

        context 'with mixed case email' do
          let(:content) { '/invite_email FirstLast@GitLab.com' }

          it 'returns correctly cased message' do
            _, _, message = add_emails

            expect(message).to eq('Added FirstLast@GitLab.com.')
          end
        end

        context 'with invalid email' do
          let(:content) { '/invite_email a@gitlab.com bad_email' }

          it 'only adds valid emails' do
            expect { add_emails }.to change { issue.issue_email_participants.count }.by(1)
          end
        end

        context 'with existing email' do
          let(:content) { '/invite_email a@gitlab.com existing@gitlab.com' }

          it 'only adds new emails' do
            issue.issue_email_participants.create!(email: 'existing@gitlab.com')

            expect { add_emails }.to change { issue.issue_email_participants.count }.by(1)
          end

          it 'only adds new (case insensitive) emails' do
            issue.issue_email_participants.create!(email: 'EXISTING@gitlab.com')

            expect { add_emails }.to change { issue.issue_email_participants.count }.by(1)
          end
        end

        context 'with duplicate email' do
          let(:content) { '/invite_email a@gitlab.com a@gitlab.com' }

          it 'only adds unique new emails' do
            expect { add_emails }.to change { issue.issue_email_participants.count }.by(1)
          end
        end

        context 'with more than 6 emails' do
          let(:content) { '/invite_email a@gitlab.com b@gitlab.com c@gitlab.com d@gitlab.com e@gitlab.com f@gitlab.com g@gitlab.com' }

          it 'only adds 6 new emails' do
            expect { add_emails }.to change { issue.issue_email_participants.count }.by(6)
          end
        end

        context 'with feature flag disabled' do
          before do
            stub_feature_flags(issue_email_participants: false)
          end

          it 'does not add any participants' do
            expect { add_emails }.not_to change { issue.issue_email_participants.count }
          end
        end
      end

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

      context 'with non-persisted issue' do
        let(:issuable) { build(:issue) }

        it 'is not part of the available commands' do
          expect(service.available_commands(issuable)).not_to include(a_hash_including(name: :invite_email))
        end
      end
    end

    context 'severity command' do
      let_it_be_with_reload(:issuable) { create(:incident, project: project) }

      subject(:set_severity) { service.execute(content, issuable) }

      it_behaves_like 'failed command', 'No severity matches the provided parameter' do
        let(:content) { '/severity something' }
      end

      shared_examples 'updates the severity' do |new_severity|
        it do
          expect { set_severity }.to change { issuable.severity }.from('unknown').to(new_severity)
        end
      end

      context 'when quick action is used on creation' do
        let(:content) { '/severity s3' }
        let(:issuable) { build(:incident, project: project) }

        it_behaves_like 'updates the severity', 'medium'

        context 'issuable does not support severity' do
          let(:issuable) { build(:issue, project: project) }

          it_behaves_like 'failed command', ''
        end
      end

      context 'severity given with S format' do
        let(:content) { '/severity s3' }

        it_behaves_like 'updates the severity', 'medium'
      end

      context 'severity given with number format' do
        let(:content) { '/severity 3' }

        it_behaves_like 'updates the severity', 'medium'
      end

      context 'severity given with text format' do
        let(:content) { '/severity medium' }

        it_behaves_like 'updates the severity', 'medium'
      end

      context 'an issuable that does not support severity' do
        let_it_be_with_reload(:issuable) { create(:issue, project: project) }

        it_behaves_like 'failed command', 'Could not apply severity command.' do
          let(:content) { '/severity s3' }
        end
      end
    end

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

      it 'approves the current merge request' do
        service.execute(content, merge_request)

        expect(merge_request.approved_by_users).to eq([developer])
      end

      context "when the user can't approve" do
        before do
          project.team.truncate
          project.add_guest(developer)
        end

        it 'does not approve the MR' do
          service.execute(content, merge_request)

          expect(merge_request.approved_by_users).to be_empty
        end
      end

      it_behaves_like 'approve command unavailable' do
        let(:issuable) { issue }
      end
    end

    context 'unapprove command' do
      let!(:merge_request) { create(:merge_request, source_project: project) }
      let(:content) { '/unapprove' }

      before do
        service.execute('/approve', merge_request)
      end

      it 'unapproves the current merge request' do
        service.execute(content, merge_request)

        expect(merge_request.approved_by_users).to be_empty
      end

      it 'calls MergeRequests::UpdateReviewerStateService' do
        expect_next_instance_of(
          MergeRequests::UpdateReviewerStateService,
          project: project, current_user: current_user
        ) do |service|
          expect(service).to receive(:execute).with(merge_request, "unreviewed")
        end

        service.execute(content, merge_request)
      end

      context "when the user can't unapprove" do
        before do
          project.team.truncate
          project.add_guest(developer)
        end

        it 'does not unapprove the MR' do
          service.execute(content, merge_request)

          expect(merge_request.approved_by_users).to eq([developer])
        end

        it_behaves_like 'unapprove command unavailable' do
          let(:issuable) { issue }
        end
      end
    end

    context 'crm_contact commands' do
      let_it_be(:new_contact) { create(:contact, group: group) }
      let_it_be(:existing_contact) { create(:contact, group: group) }

      let(:add_command) { service.execute("/add_contacts #{new_contact.email}", issue) }
      let(:remove_command) { service.execute("/remove_contacts #{existing_contact.email}", issue) }

      before do
        issue.project.group.add_developer(developer)
        create(:issue_customer_relations_contact, issue: issue, contact: existing_contact)
      end

      it 'add_contacts command adds the contact' do
        _, updates, message = add_command

        expect(updates).to eq(add_contacts: [new_contact.email])
        expect(message).to eq('One or more contacts were successfully added.')
      end

      it 'remove_contacts command removes the contact' do
        _, updates, message = remove_command

        expect(updates).to eq(remove_contacts: [existing_contact.email])
        expect(message).to eq('One or more contacts were successfully removed.')
      end
    end

    context 'when using an alias' do
      it 'returns the correct execution message' do
        content = "/labels ~#{bug.title}"

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

        expect(message).to eq("Added ~\"Bug\" label.")
      end
    end

    it_behaves_like 'quick actions that change work item type'

    context '/set_parent command' do
      let_it_be(:parent) { create(:work_item, :issue, project: project) }
      let_it_be(:work_item) { create(:work_item, :task, project: project) }
      let_it_be(:parent_ref) { parent.to_reference(project) }

      let(:content) { "/set_parent #{parent_ref}" }

      it 'returns success message' do
        _, _, message = service.execute(content, work_item)

        expect(message).to eq('Work item parent set successfully')
      end

      it 'sets correct update params' do
        _, updates, _ = service.execute(content, work_item)

        expect(updates).to eq(set_parent: parent)
      end
    end
  end

  describe '#explain' do
    let(:service) { described_class.new(project, developer) }
    let(:merge_request) { create(:merge_request, source_project: project) }

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

      it 'includes issuable name' do
        content_result, explanations = service.explain(content, issue)

        expect(content_result).to eq('')
        expect(explanations).to eq(['Closes this issue.'])
      end
    end

    describe 'reopen command' do
      let(:content) { '/reopen' }
      let(:merge_request) { create(:merge_request, :closed, source_project: project) }

      it 'includes issuable name' do
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to eq(['Reopens this merge request.'])
      end
    end

    describe 'title command' do
      let(:content) { '/title This is new title' }

      it 'includes new title' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(['Changes the title to "This is new title".'])
      end
    end

    describe 'assign command' do
      shared_examples 'assigns developer' do
        it 'tells us we will assign the developer' do
          _, explanations = service.explain(content, merge_request)

          expect(explanations).to eq(["Assigns @#{developer.username}."])
        end
      end

      context 'when using a reference' do
        let(:content) { "/assign @#{developer.username}" }

        include_examples 'assigns developer'
      end

      context 'when using a bare username' do
        let(:content) { "/assign #{developer.username}" }

        include_examples 'assigns developer'
      end

      context 'when using me' do
        let(:content) { "/assign me" }

        include_examples 'assigns developer'
      end

      context 'when there are unparseable arguments' do
        let(:arg) { "#{developer.username} to this issue" }
        let(:content) { "/assign #{arg}" }

        it 'tells us why we cannot do that' do
          _, explanations = service.explain(content, merge_request)

          expect(explanations)
            .to contain_exactly "Problem with assign command: Failed to find users for 'to', 'this', and 'issue'."
        end
      end
    end

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

      it 'includes current assignee reference' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(["Removes assignee @#{developer.username}."])
      end
    end

    describe 'unassign_reviewer command' do
      let(:content) { '/unassign_reviewer' }
      let(:merge_request) { create(:merge_request, source_project: project, reviewers: [developer]) }

      it 'includes current assignee reference' do
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to eq(["Removes reviewer @#{developer.username}."])
      end
    end

    describe 'assign_reviewer command' do
      let(:content) { "/assign_reviewer #{developer.to_reference}" }
      let(:merge_request) { create(:merge_request, source_project: project, assignees: [developer]) }

      it 'includes only the user reference' do
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to eq(["Assigns #{developer.to_reference} as reviewer."])
      end
    end

    describe 'milestone command' do
      let(:content) { '/milestone %wrong-milestone' }
      let!(:milestone) { create(:milestone, project: project, title: '9.10') }

      it 'is empty when milestone reference is wrong' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq([])
      end
    end

    describe 'remove milestone command' do
      let(:content) { '/remove_milestone' }
      let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }

      it 'includes current milestone name' do
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to eq(['Removes %"9.10" milestone.'])
      end
    end

    describe 'label command' do
      let(:content) { '/label ~missing' }
      let!(:label) { create(:label, project: project) }

      it 'is empty when there are no correct labels' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq([])
      end
    end

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

      it 'says all labels if no parameter provided' do
        merge_request.update!(label_ids: [bug.id])
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to eq([_('Removes all labels.')])
      end
    end

    describe 'relabel command' do
      let(:content) { "/relabel #{bug.title}" }
      let(:feature) { create(:label, project: project, title: 'Feature') }

      it 'includes label name' do
        issue.update!(label_ids: [feature.id])
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(["Replaces all labels with ~#{bug.id} label."])
      end
    end

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

      it 'includes issuable name' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(['Subscribes to this issue.'])
      end
    end

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

      it 'includes issuable name' do
        merge_request.subscribe(developer, project)
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to eq(['Unsubscribes from this merge request.'])
      end
    end

    describe 'due command' do
      let(:content) { '/due April 1st 2016' }

      it 'includes the date' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(['Sets the due date to Apr 1, 2016.'])
      end
    end

    describe 'draft command set' do
      let(:content) { '/draft' }

      it 'includes the new status' do
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to match_array(['Marks this merge request as a draft.'])
      end

      it 'includes the no change message when status unchanged' do
        merge_request.update!(title: merge_request.draft_title)
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to match_array(["No change to this merge request's draft status."])
      end
    end

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

      it 'includes the new status' do
        merge_request.update!(title: merge_request.draft_title)
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to match_array(['Marks this merge request as ready.'])
      end

      it 'includes the no change message when status unchanged' do
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to match_array(["No change to this merge request's draft status."])
      end
    end

    describe 'award command' do
      let(:content) { '/award :confetti_ball: ' }

      it 'includes the emoji' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(['Toggles :confetti_ball: emoji award.'])
      end
    end

    describe 'estimate command' do
      context 'positive estimation' do
        let(:content) { '/estimate 79d' }

        it 'includes the formatted duration' do
          _, explanations = service.explain(content, merge_request)

          expect(explanations).to eq(['Sets time estimate to 3mo 3w 4d.'])
        end
      end

      context 'zero estimation' do
        let(:content) { '/estimate 0' }

        it 'includes the formatted duration' do
          _, explanations = service.explain(content, merge_request)

          expect(explanations).to eq(['Removes time estimate.'])
        end
      end

      context 'negative estimation' do
        let(:content) { '/estimate -79d' }

        it 'does not explain' do
          _, explanations = service.explain(content, merge_request)

          expect(explanations).to be_empty
        end
      end

      context 'invalid estimation' do
        let(:content) { '/estimate a' }

        it 'does not explain' do
          _, explanations = service.explain(content, merge_request)

          expect(explanations).to be_empty
        end
      end
    end

    describe 'spend command' do
      it 'includes the formatted duration and proper verb when using /spend' do
        _, explanations = service.explain('/spend -120m', issue)

        expect(explanations).to eq(['Subtracts 2h spent time.'])
      end

      it 'includes the formatted duration and proper verb when using /spent' do
        _, explanations = service.explain('/spent -120m', issue)

        expect(explanations).to eq(['Subtracts 2h spent time.'])
      end
    end

    describe 'target branch command' do
      let(:content) { '/target_branch my-feature ' }

      it 'includes the branch name' do
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to eq(['Sets target branch to my-feature.'])
      end
    end

    describe 'board move command' do
      let(:content) { "/board_move ~#{bug.title}" }
      let!(:board) { create(:board, project: project) }

      it 'includes the label name' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(["Moves issue to ~#{bug.id} column in the board."])
      end
    end

    describe 'move issue to another project command' do
      let(:content) { '/move test/project' }

      it 'includes the project name' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(["Moves this issue to test/project."])
      end
    end

    describe 'tag a commit' do
      describe 'with a tag name' do
        context 'without a message' do
          let(:content) { '/tag v1.2.3' }

          it 'includes the tag name only' do
            _, explanations = service.explain(content, commit)

            expect(explanations).to eq(["Tags this commit to v1.2.3."])
          end
        end

        context 'with an empty message' do
          let(:content) { '/tag v1.2.3 ' }

          it 'includes the tag name only' do
            _, explanations = service.explain(content, commit)

            expect(explanations).to eq(["Tags this commit to v1.2.3."])
          end
        end
      end

      describe 'with a tag name and message' do
        let(:content) { '/tag v1.2.3 Stable release' }

        it 'includes the tag name and message' do
          _, explanations = service.explain(content, commit)

          expect(explanations).to eq(["Tags this commit to v1.2.3 with \"Stable release\"."])
        end
      end
    end

    describe 'create a merge request' do
      context 'with no branch name' do
        let(:content) { '/create_merge_request' }

        it 'uses the default branch name' do
          _, explanations = service.explain(content, issue)

          expect(explanations).to eq([_('Creates a branch and a merge request to resolve this issue.')])
        end

        it 'returns the execution message using the default branch name' do
          _, _, message = service.execute(content, issue)

          expect(message).to eq(_('Created a branch and a merge request to resolve this issue.'))
        end
      end

      context 'with a branch name' do
        let(:content) { '/create_merge_request foo' }

        it 'uses the given branch name' do
          _, explanations = service.explain(content, issue)

          expect(explanations).to eq(["Creates branch 'foo' and a merge request to resolve this issue."])
        end

        it 'returns the execution message using the given branch name' do
          _, _, message = service.execute(content, issue)

          expect(message).to eq("Created branch 'foo' and a merge request to resolve this issue.")
        end
      end
    end

    describe "#commands_executed_count" do
      it 'counts commands executed' do
        content = "/close and \n/assign me and \n/title new title"

        service.execute(content, issue)

        expect(service.commands_executed_count).to eq(3)
      end
    end

    describe 'crm commands' do
      let(:add_contacts) { '/add_contacts' }
      let(:remove_contacts) { '/remove_contacts' }

      before_all do
        group.add_developer(developer)
      end

      context 'when group has no contacts' do
        it '/add_contacts is not available' do
          _, explanations = service.explain(add_contacts, issue)

          expect(explanations).to be_empty
        end
      end

      context 'when group has contacts' do
        let!(:contact) { create(:contact, group: group) }

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

          expect(explanations).to contain_exactly("Add customer relation contact(s).")
        end

        context 'when issue has no contacts' do
          it '/remove_contacts is not available' do
            _, explanations = service.explain(remove_contacts, issue)

            expect(explanations).to be_empty
          end
        end

        context 'when issue has contacts' do
          let!(:issue_contact) { create(:issue_customer_relations_contact, issue: issue, contact: contact) }

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

            expect(explanations).to contain_exactly("Remove customer relation contact(s).")
          end
        end
      end
    end

    context 'with keep_actions' do
      let(:content) { '/close' }

      it 'keeps quick actions' do
        content_result, explanations = service.explain(content, issue, keep_actions: true)

        expect(content_result).to eq("\n/close")
        expect(explanations).to eq(['Closes this issue.'])
      end

      it 'removes the quick action' do
        content_result, explanations = service.explain(content, issue, keep_actions: false)

        expect(content_result).to eq('')
        expect(explanations).to eq(['Closes this issue.'])
      end
    end

    describe 'type command' do
      let_it_be(:project) { create(:project, :private) }
      let_it_be(:work_item) { create(:work_item, :task, project: project) }

      let(:command) { '/type issue' }

      it 'has command available' do
        _, explanations = service.explain(command, work_item)

        expect(explanations)
          .to contain_exactly("Converts work item to issue. Widgets not supported in new type are removed.")
      end
    end

    describe 'relate and unlink commands' do
      let_it_be(:other_issue) { create(:issue, project: project).to_reference(issue) }
      let(:relate_content) { "/relate #{other_issue}" }
      let(:unlink_content) { "/unlink #{other_issue}" }

      context 'when user has permissions' do
        it '/relate command is available' do
          _, explanations = service.explain(relate_content, issue)

          expect(explanations).to eq(["Marks this issue as related to #{other_issue}."])
        end

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

          expect(explanations).to eq(["Removes link with #{other_issue}."])
        end
      end

      context 'when user has insufficient permissions' do
        before do
          allow(Ability).to receive(:allowed?).and_call_original
          allow(Ability).to receive(:allowed?).with(current_user, :admin_issue_link, issue).and_return(false)
        end

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

          expect(explanations).to be_empty
        end

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

          expect(explanations).to be_empty
        end
      end
    end

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

      context 'when work item supports promotion' do
        let_it_be(:task) { build(:work_item, :task, project: project) }

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

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

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

    describe '/set_parent command' do
      let_it_be(:parent) { create(:work_item, :issue, project: project) }
      let_it_be(:work_item) { create(:work_item, :task, project: project) }
      let_it_be(:parent_ref) { parent.to_reference(project) }

      let(:command) { "/set_parent #{parent_ref}" }

      shared_examples 'command is available' do
        it 'explanation contains correct message' do
          _, explanations = service.explain(command, work_item)

          expect(explanations)
            .to contain_exactly("Change work item's parent to #{parent_ref}.")
        end

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

      shared_examples 'command is not available' do
        it 'explanation is empty' do
          _, explanations = service.explain(command, work_item)

          expect(explanations).to eq([])
        end

        it 'does not contain command' do
          expect(service.available_commands(work_item)).not_to include(a_hash_including(name: :set_parent))
        end
      end

      context 'when user can admin link' do
        it_behaves_like 'command is available'

        context 'when work item type does not support a parent' do
          let_it_be(:work_item) { build(:work_item, :incident, project: project) }

          it_behaves_like 'command is not available'
        end
      end

      context 'when user cannot admin link' do
        subject(:service) { described_class.new(project, create(:user)) }

        it_behaves_like 'command is not available'
      end
    end

    describe '/add_child command' do
      let_it_be(:child) { create(:work_item, :issue, project: project) }
      let_it_be(:work_item) { create(:work_item, :objective, project: project) }
      let_it_be(:child_ref) { child.to_reference(project) }

      let(:command) { "/add_child #{child_ref}" }

      shared_examples 'command is available' do
        it 'explanation contains correct message' do
          _, explanations = service.explain(command, work_item)

          expect(explanations)
            .to contain_exactly("Add #{child_ref} to this work item as child(ren).")
        end

        it 'contains command' do
          expect(service.available_commands(work_item)).to include(a_hash_including(name: :add_child))
        end
      end

      shared_examples 'command is not available' do
        it 'explanation is empty' do
          _, explanations = service.explain(command, work_item)

          expect(explanations).to eq([])
        end

        it 'does not contain command' do
          expect(service.available_commands(work_item)).not_to include(a_hash_including(name: :add_child))
        end
      end

      context 'when user can admin link' do
        it_behaves_like 'command is available'

        context 'when work item type does not support children' do
          let_it_be(:work_item) { build(:work_item, :key_result, project: project) }

          it_behaves_like 'command is not available'
        end
      end

      context 'when user cannot admin link' do
        subject(:service) { described_class.new(project, create(:user)) }

        it_behaves_like 'command is not available'
      end
    end
  end

  describe '#available_commands' do
    context 'when Guest is creating a new issue' do
      let_it_be(:guest) { create(:user) }

      let(:issue) { build(:issue, project: public_project) }
      let(:service) { described_class.new(project, guest) }

      before_all do
        public_project.add_guest(guest)
      end

      it 'includes commands to set metadata' do
        # milestone action is only available when project has a milestone
        milestone

        available_commands = service.available_commands(issue)

        expect(available_commands).to include(
          a_hash_including(name: :label),
          a_hash_including(name: :milestone),
          a_hash_including(name: :copy_metadata),
          a_hash_including(name: :assign),
          a_hash_including(name: :due)
        )
      end
    end
  end
end