# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Notes::QuickActionsService, feature_category: :text_editors do
  let_it_be_with_reload(:project) { create(:project, :repository) }

  shared_context 'note on noteable' do
    let_it_be(:maintainer) { create(:user, maintainer_of: project) }
    let_it_be(:assignee) { create(:user) }

    before_all do
      project.add_maintainer(assignee)
    end
  end

  shared_examples 'note on noteable that supports quick actions' do
    before do
      note.note = note_text
    end

    let!(:milestone) { create(:milestone, project: project) }
    let!(:labels) { create_pair(:label, project: project) }

    describe 'note with only command' do
      describe '/close, /label, /assign & /milestone' do
        let(:note_text) do
          %(/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
        end

        it 'closes noteable, sets labels, assigns, and sets milestone to noteable, and leave no note' do
          content = execute(note)

          expect(content).to be_empty
          expect(note.noteable).to be_closed
          expect(note.noteable.labels).to match_array(labels)
          expect(note.noteable.assignees).to eq([assignee])
          expect(note.noteable.milestone).to eq(milestone)
        end
      end

      context '/relate' do
        let_it_be(:issue) { create(:issue, project: project) }
        let_it_be(:other_issue) { create(:issue, project: project) }

        let(:note_text) { "/relate #{other_issue.to_reference}" }
        let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }

        context 'user cannot relate issues', :sidekiq_inline do
          before do
            project.team.find_member(maintainer.id).destroy!
            project.update!(visibility: Gitlab::VisibilityLevel::PUBLIC)
          end

          it 'does not create issue relation' do
            expect { execute(note) }.not_to change { IssueLink.count }
          end
        end

        context 'user is allowed to relate issues' do
          it 'creates issue relation' do
            expect { execute(note) }.to change { IssueLink.count }.by(1)
          end
        end
      end

      describe '/reopen' do
        before do
          note.noteable.close!
          expect(note.noteable).to be_closed
        end

        let(:note_text) { '/reopen' }

        it 'opens the noteable, and leave no note' do
          content = execute(note)

          expect(content).to be_empty
          expect(note.noteable).to be_open
        end
      end

      describe '/spend' do
        context 'when note is not persisted' do
          let(:note_text) { '/spend 1h' }

          it 'adds time to noteable, adds timelog with nil note_id and has no content' do
            content = execute(note)

            expect(content).to be_empty
            expect(note.noteable.time_spent).to eq(3600)
            expect(Timelog.last.note_id).to be_nil
          end
        end

        context 'when note is persisted' do
          let(:note_text) { "a note \n/spend 1h" }

          it 'updates the spent time and populates timelog with note_id' do
            new_content, update_params = service.execute(note)
            note.update!(note: new_content)
            service.apply_updates(update_params, note)

            expect(Timelog.last.note_id).to eq(note.id)
          end
        end

        context 'with a timecategory' do
          let!(:timelog_category) { create(:timelog_category, name: 'bob', namespace: project.root_namespace) }
          let(:note_text) { "a note \n/spend 1h [timecategory:bob]" }

          it 'sets the category of the new timelog' do
            new_content, update_params = service.execute(note)
            note.update!(note: new_content)
            service.apply_updates(update_params, note)

            expect(Timelog.last.timelog_category_id).to eq(timelog_category.id)
          end
        end

        context 'adds a system note' do
          context 'when not specifying a date' do
            let(:note_text) { "/spend 1h" }

            it 'includes the date, time and timezone' do
              allow_any_instance_of(User).to receive(:timezone).and_return('Hawaii') # rubocop:disable RSpec/AnyInstanceOf -- It's not the next instance

              travel_to(Time.utc(2025, 5, 1)) do
                _, update_params = service.execute(note)
                service.apply_updates(update_params, note)

                expect(Note.last.note).to eq('added 1h of time spent at 2025-04-30 14:00:00 -1000')
              end
            end
          end

          context 'when specifying a date' do
            let(:note_text) { "/spend 1h 2020-01-01" }

            it 'includes the date, time and timezone' do
              _, update_params = service.execute(note)
              service.apply_updates(update_params, note)

              expect(Note.last.note).to eq('added 1h of time spent at 2020-01-01 12:00:00 UTC')
            end
          end
        end
      end
    end

    describe '/estimate' do
      before do
        # reset to 10 minutes before each test
        note.noteable.update!(time_estimate: 600)
      end

      shared_examples 'does not update time_estimate and displays the correct error message' do
        it 'shows validation error message' do
          content, update_params = service.execute(note)
          service_response = service.apply_updates(update_params, note)

          expect(content).to be_empty
          expect(service_response.message).to include('Time estimate must have a valid format and be greater than or equal to zero.')
          expect(note.noteable.reload.time_estimate).to eq(600)
        end
      end

      context 'when the time estimate is valid' do
        let(:note_text) { '/estimate 1h' }

        it 'adds time estimate to noteable' do
          content = execute(note)

          expect(content).to be_empty
          expect(note.noteable.reload.time_estimate).to eq(3600)
        end
      end

      context 'when the time estimate is 0' do
        let(:note_text) { '/estimate 0' }

        it 'adds time estimate to noteable' do
          content = execute(note)

          expect(content).to be_empty
          expect(note.noteable.reload.time_estimate).to eq(0)
        end
      end

      context 'when the time estimate is invalid' do
        let(:note_text) { '/estimate a' }

        include_examples "does not update time_estimate and displays the correct error message"
      end

      context 'when the time estimate is partially invalid' do
        let(:note_text) { '/estimate 1d 3id' }

        include_examples "does not update time_estimate and displays the correct error message"
      end

      context 'when the time estimate is negative' do
        let(:note_text) { '/estimate -1h' }

        include_examples "does not update time_estimate and displays the correct error message"
      end
    end

    describe '/confidential' do
      let_it_be_with_reload(:noteable) { create(:work_item, :issue, project: project) }
      let_it_be(:note_text) { '/confidential' }
      let(:note) { create(:note, noteable: noteable, project: project, note: note_text) }

      context 'when work item does not have children' do
        it 'leaves the note empty' do
          expect(execute(note)).to be_empty
        end

        it 'marks work item as confidential' do
          expect { execute(note) }.to change { noteable.reload.confidential }.from(false).to(true)
        end
      end

      context 'when work item has children' do
        before do
          create(:parent_link, work_item: task, work_item_parent: noteable)
        end

        context 'when children are not confidential' do
          let(:task) { create(:work_item, :task, project: project) }

          it 'does not mark parent work item as confidential' do
            expect { execute(note) }.to not_change { noteable.reload.confidential }.from(false)
            expect(noteable.errors[:base]).to include('All child items must be confidential in order to turn on confidentiality.')
          end
        end

        context 'when children are confidential' do
          let(:task) { create(:work_item, :confidential, :task, project: project) }

          it 'marks parent work item as confidential' do
            expect { execute(note) }.to change { noteable.reload.confidential }.from(false).to(true)
          end
        end
      end
    end

    describe 'note with command & text' do
      describe '/close, /label, /assign & /milestone' do
        let(:note_text) do
          %(HELLO\n/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\n/milestone %"#{milestone.name}"\nWORLD)
        end

        it 'closes noteable, sets labels, assigns, and sets milestone to noteable' do
          content = execute(note)

          expect(content).to eq "HELLO\nWORLD"
          expect(note.noteable).to be_closed
          expect(note.noteable.labels).to match_array(labels)
          expect(note.noteable.assignees).to eq([assignee])
          expect(note.noteable.milestone).to eq(milestone)
        end
      end

      describe '/reopen' do
        before do
          note.noteable.close
          expect(note.noteable).to be_closed
        end

        let(:note_text) { "HELLO\n/reopen\nWORLD" }

        it 'opens the noteable' do
          content = execute(note)

          expect(content).to eq "HELLO\nWORLD"
          expect(note.noteable).to be_open
        end
      end
    end

    describe '/milestone' do
      let(:issue) { create(:issue, project: project) }
      let(:note_text) { %(/milestone %"#{milestone.name}") }
      let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }

      context 'on an incident' do
        before do
          issue.update!(work_item_type: WorkItems::Type.default_by_type(:incident))
        end

        it 'leaves the note empty' do
          expect(execute(note)).to be_empty
        end

        it 'assigns the milestone' do
          expect { execute(note) }.to change { issue.reload.milestone }.from(nil).to(milestone)
        end
      end

      context 'on a merge request' do
        let(:note_mr) { create(:note_on_merge_request, project: project, note: note_text) }

        it 'leaves the note empty' do
          expect(execute(note_mr)).to be_empty
        end

        it 'assigns the milestone' do
          expect { execute(note) }.to change { issue.reload.milestone }.from(nil).to(milestone)
        end
      end
    end

    describe '/remove_milestone' do
      let(:issue) { create(:issue, project: project, milestone: milestone) }
      let(:note_text) { '/remove_milestone' }
      let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }

      context 'on an issue' do
        it 'leaves the note empty' do
          expect(execute(note)).to be_empty
        end

        it 'removes the milestone' do
          expect { execute(note) }.to change { issue.reload.milestone }.from(milestone).to(nil)
        end
      end

      context 'on an incident' do
        before do
          issue.update!(work_item_type: WorkItems::Type.default_by_type(:incident))
        end

        it 'leaves the note empty' do
          expect(execute(note)).to be_empty
        end

        it 'removes the milestone' do
          expect { execute(note) }.to change { issue.reload.milestone }.from(milestone).to(nil)
        end
      end

      context 'on a merge request' do
        let(:note_mr) { create(:note_on_merge_request, project: project, note: note_text) }

        it 'leaves the note empty' do
          expect(execute(note_mr)).to be_empty
        end

        it 'removes the milestone' do
          expect { execute(note) }.to change { issue.reload.milestone }.from(milestone).to(nil)
        end
      end
    end

    describe '/add_child' do
      let_it_be_with_reload(:noteable) { create(:work_item, :objective, project: project) }
      let_it_be(:child) { create(:work_item, :objective, project: project) }
      let_it_be(:second_child) { create(:work_item, :objective, project: project) }
      let_it_be(:note_text) { "/add_child #{child.to_reference}, #{second_child.to_reference}" }
      let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }
      let(:children) { [child, second_child] }

      it_behaves_like 'adds child work items'

      context 'when using work item full reference' do
        let_it_be(:note_text) { "/add_child #{child.to_reference(full: true)}, #{second_child.to_reference(full: true)}" }

        it_behaves_like 'adds child work items'
      end

      context 'when using work item URL' do
        let_it_be(:project_path) { "#{Gitlab.config.gitlab.url}/#{project.full_path}" }
        let_it_be(:url) { "#{project_path}/work_items/#{child.iid}, #{project_path}/issues/#{second_child.iid}" }
        let_it_be(:note_text) { "/add_child #{url}" }

        it_behaves_like 'adds child work items'
      end
    end

    describe '/remove_child' do
      let_it_be_with_reload(:noteable) { create(:work_item, :objective, project: project) }
      let_it_be_with_reload(:child) { create(:work_item, :objective, project: project) }
      let_it_be(:note_text) { "/remove_child #{child.to_reference}" }
      let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }

      before do
        create(:parent_link, work_item_parent: noteable, work_item: child)
      end

      shared_examples 'removes child work item' do
        it 'leaves the note empty' do
          expect(execute(note)).to be_empty
        end

        it 'removes child work item' do
          expect { execute(note) }.to change { WorkItems::ParentLink.count }.by(-1)

          expect(noteable.valid?).to be_truthy
          expect(noteable.work_item_children).to be_empty
        end
      end

      context 'when using work item reference' do
        let_it_be(:note_text) { "/remove_child #{child.to_reference(full: true)}" }

        it_behaves_like 'removes child work item'
      end

      context 'when using work item iid' do
        it_behaves_like 'removes child work item'
      end

      context 'when using work item URL' do
        let_it_be(:project_path) { "#{Gitlab.config.gitlab.url}/#{project.full_path}" }
        let_it_be(:url) { "#{project_path}/work_items/#{child.iid}" }
        let_it_be(:note_text) { "/remove_child #{url}" }

        it_behaves_like 'removes child work item'
      end
    end

    describe '/set_parent' do
      let_it_be_with_reload(:noteable) { create(:work_item, :objective, project: project) }
      let_it_be_with_reload(:parent) { create(:work_item, :objective, project: project) }
      let_it_be(:note_text) { "/set_parent #{parent.to_reference}" }
      let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }

      context 'when using work item reference' do
        let_it_be(:note_text) { "/set_parent #{project.full_path}#{parent.to_reference}" }

        it_behaves_like 'sets work item parent'
      end

      context 'when using work item iid' do
        let_it_be(:note_text) { "/set_parent #{parent.to_reference}" }

        it_behaves_like 'sets work item parent'
      end

      context 'when using work item URL' do
        let_it_be(:url) { "#{Gitlab.config.gitlab.url}/#{project.full_path}/work_items/#{parent.iid}" }
        let_it_be(:note_text) { "/set_parent #{url}" }

        it_behaves_like 'sets work item parent'
      end

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

        let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }

        it 'does not assign the parent' do
          expect { execute(note) }.not_to change { noteable.reload.work_item_parent }
        end
      end
    end

    describe '/remove_parent' do
      let_it_be_with_reload(:parent) { create(:work_item, :objective, project: project) }
      let_it_be_with_reload(:noteable) { create(:work_item, :objective, project: project) }
      let_it_be(:note_text) { "/remove_parent" }
      let(:note) { create(:note, noteable: noteable, project: project, note: note_text) }

      before do
        create(:parent_link, work_item_parent: parent, work_item: noteable)
      end

      it 'leaves the note empty' do
        expect(execute(note)).to be_empty
      end

      it 'removes work item parent' do
        execute(note)

        expect(noteable.valid?).to be_truthy
        expect(noteable.work_item_parent).to eq(nil)
      end

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

        let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }

        it 'does not assign the parent' do
          expect { execute(note) }.not_to change { noteable.reload.work_item_parent }
        end
      end
    end

    describe '/promote_to' do
      shared_examples 'promotes work item' do |from:, to:|
        it 'leaves the note empty' do
          expect(execute(note)).to be_empty
        end

        it 'promotes to provided type' do
          expect { execute(note) }.to change { noteable.work_item_type.base_type }.from(from).to(to)
        end
      end

      context 'when user is not allowed to promote work item' do
        let_it_be_with_reload(:noteable) { create(:work_item, :task, project: project) }
        let_it_be(:note_text) { '/promote_to issue' }
        let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }

        before do
          project.team.find_member(maintainer.id).destroy!
          project.update!(visibility: Gitlab::VisibilityLevel::PUBLIC)
        end

        it 'does not promote work item' do
          expect { execute(note) }.not_to change { noteable.work_item_type.base_type }
        end
      end

      context 'on a task' do
        let_it_be_with_reload(:noteable) { create(:work_item, :task, project: project) }
        let_it_be(:note_text) { '/promote_to Issue' }
        let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }

        it_behaves_like 'promotes work item', from: 'task', to: 'issue'

        context 'when type name is lower case' do
          let_it_be(:note_text) { '/promote_to issue' }

          it_behaves_like 'promotes work item', from: 'task', to: 'issue'
        end
      end

      context 'on an issue' do
        let_it_be_with_reload(:noteable) { create(:work_item, :issue, project: project) }
        let_it_be(:note_text) { '/promote_to Incident' }
        let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }

        it_behaves_like 'promotes work item', from: 'issue', to: 'incident'

        context 'when type name is lower case' do
          let_it_be(:note_text) { '/promote_to incident' }

          it_behaves_like 'promotes work item', from: 'issue', to: 'incident'
        end
      end
    end

    context 'when existing note contains quick actions' do
      let(:note_text) { "foo\n/close\nbar" }

      before do
        note.save!
        note.note = edit_note_text
      end

      context 'when a quick action exists in original note' do
        let(:edit_note_text) { "foo\n/close\nbar\nbaz" }

        it 'sanitizes/removes any quick actions and does not execute them' do
          content = execute(note)

          expect(content).to eq "foo\nbar\nbaz"
          expect(note.noteable.open?).to be_truthy
        end
      end

      context 'when a new quick action is used in new note' do
        let(:edit_note_text) { "bar\n/react :smile:\nfoo" }

        it 'executes any quick actions not in unedited note' do
          content = execute(note)

          expect(content).to eq "bar\nfoo"
          expect(note.noteable.award_emoji.first.name).to eq 'smile'
          expect(note.noteable.open?).to be_truthy
        end
      end
    end
  end

  describe '.supported?' do
    include_context 'note on noteable'

    let(:note) { create(:note_on_issue, project: project) }

    context 'with a note on an issue' do
      it 'returns true' do
        expect(described_class.supported?(note)).to be_truthy
      end
    end

    context 'with a note on a commit' do
      let(:note) { create(:note_on_commit, project: project) }

      it 'returns false' do
        expect(described_class.supported?(note)).to be_truthy
      end
    end
  end

  describe '#supported?' do
    include_context 'note on noteable'

    it 'delegates to the class method' do
      service = described_class.new(project, maintainer)
      note = create(:note_on_issue, project: project)

      expect(described_class).to receive(:supported?).with(note)

      service.supported?(note)
    end
  end

  describe '#execute' do
    include_context 'note on noteable'

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

    it_behaves_like 'note on noteable that supports quick actions' do
      let_it_be(:issue, reload: true) { create(:issue, project: project) }
      let(:note) { build(:note_on_issue, project: project, noteable: issue) }
    end

    it_behaves_like 'note on noteable that supports quick actions' do
      let_it_be(:incident, reload: true) { create(:incident, project: project) }
      let(:note) { build(:note_on_issue, project: project, noteable: incident) }
    end

    it_behaves_like 'note on noteable that supports quick actions' do
      let(:merge_request) { create(:merge_request, source_project: project) }
      let(:note) { build(:note_on_merge_request, project: project, noteable: merge_request) }
    end

    describe '/create_merge_request' do
      let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }

      context 'when noteable is a work item' do
        let_it_be(:noteable) { create(:work_item, project: project) }

        context 'when no branch name is provided' do
          let(:note_text) { '/create_merge_request' }

          it 'creates a merge request with default branch name', :aggregate_failures do
            expect { execute(note) }.to change { MergeRequest.count }.by(1)

            expect(MergeRequest.last.source_branch).to eq(noteable.to_branch_name)
          end

          context 'when work item type does not have the development widget' do
            let_it_be(:work_item_type) { create(:work_item_type, :non_default) }
            let_it_be(:noteable) { create(:work_item, project: project, work_item_type: work_item_type) }

            it 'does not create a merge request' do
              expect { execute(note) }.to not_change { MergeRequest.count }
            end
          end
        end

        context 'when a branch name is provided' do
          let(:note_text) { '/create_merge_request test-branch-1' }

          it 'creates a merge request with default branch name', :aggregate_failures do
            expect { execute(note) }.to change { MergeRequest.count }.by(1)

            expect(MergeRequest.last.source_branch).to eq('test-branch-1')
          end
        end
      end
    end

    context 'note on work item that supports quick actions' do
      let_it_be(:work_item, reload: true) { create(:work_item, project: project) }

      let(:note) { build(:note_on_work_item, project: project, noteable: work_item) }

      let!(:labels) { create_pair(:label, project: project) }

      before do
        note.note = note_text
      end

      describe 'note with only command' do
        describe '/close, /label & /assign' do
          let(:note_text) do
            %(/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\n)
          end

          it 'closes noteable, sets labels, assigns and leave no note' do
            content = execute(note)

            expect(content).to be_empty
            expect(note.noteable).to be_closed
            expect(note.noteable.labels).to match_array(labels)
            expect(note.noteable.assignees).to eq([assignee])
          end
        end

        describe '/reopen' do
          before do
            note.noteable.close!
            expect(note.noteable).to be_closed
          end

          let(:note_text) { '/reopen' }

          it 'opens the noteable, and leave no note' do
            content = execute(note)

            expect(content).to be_empty
            expect(note.noteable).to be_open
          end
        end
      end

      describe 'note with command & text' do
        describe '/close, /label, /assign' do
          let(:note_text) do
            %(HELLO\n/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\nWORLD)
          end

          it 'closes noteable, sets labels, assigns, and sets milestone to noteable' do
            content = execute(note)

            expect(content).to eq "HELLO\nWORLD"
            expect(note.noteable).to be_closed
            expect(note.noteable.labels).to match_array(labels)
            expect(note.noteable.assignees).to eq([assignee])
          end
        end

        describe '/reopen' do
          before do
            note.noteable.close
            expect(note.noteable).to be_closed
          end

          let(:note_text) { "HELLO\n/reopen\nWORLD" }

          it 'opens the noteable' do
            content = execute(note)

            expect(content).to eq "HELLO\nWORLD"
            expect(note.noteable).to be_open
          end
        end
      end

      describe '/subscribe or /unsubscribe' do
        shared_examples 'when applying to work_item' do
          it 'leaves the note empty' do
            expect(execute(note)).to be_empty
          end

          it 'triggers work item updated subscription' do
            expect(GraphqlTriggers).to receive(:work_item_updated).with(work_item)

            execute(note)
          end
        end

        describe '/subscribe' do
          let_it_be(:note_text) { '/subscribe' }

          it_behaves_like 'when applying to work_item'
        end

        describe '/unsubscribe' do
          let_it_be(:note_text) { '/unsubscribe' }

          before do
            work_item.subscribe(maintainer, project)
          end

          it_behaves_like 'when applying to work_item'
        end
      end
    end
  end

  describe '#apply_updates' do
    include_context 'note on noteable'

    let_it_be_with_reload(:issue) { create(:issue, project: project) }
    let_it_be_with_reload(:work_item) { create(:work_item, :issue, project: project) }
    let_it_be_with_reload(:merge_request) { create(:merge_request, source_project: project) }
    let_it_be_with_reload(:issue_note) { create(:note_on_issue, project: project, noteable: issue) }
    let_it_be_with_reload(:work_item_note) { create(:note, project: project, noteable: work_item) }
    let_it_be_with_reload(:mr_note) { create(:note_on_merge_request, project: project, noteable: merge_request) }
    let_it_be_with_reload(:commit_note) { create(:note_on_commit, project: project) }
    let(:update_params) { {} }

    subject(:apply_updates) { described_class.new(project, maintainer).apply_updates(update_params, note) }

    context 'with a note on an issue' do
      let(:note) { issue_note }

      it 'returns successful service response if update returned no errors' do
        update_params[:confidential] = true
        expect(apply_updates.success?).to be true
      end

      it 'returns service response with errors if update failed' do
        update_params[:title] = ""
        expect(apply_updates.success?).to be false
        expect(apply_updates.message).to include("Title can't be blank")
      end
    end

    context 'with a note on a merge request' do
      let(:note) { mr_note }

      it 'returns successful service response if update returned no errors' do
        update_params[:title] = 'New title'
        expect(apply_updates.success?).to be true
      end

      it 'returns service response with errors if update failed' do
        update_params[:title] = ""
        expect(apply_updates.success?).to be false
        expect(apply_updates.message).to include("Title can't be blank")
      end
    end

    context 'with a note on a work item' do
      let(:note) { work_item_note }

      before do
        update_params[:confidential] = true
      end

      it 'returns successful service response if update returned no errors' do
        expect(apply_updates.success?).to be true
      end

      it 'returns service response with errors if update failed' do
        task = create(:work_item, :task, project: project)
        create(:parent_link, work_item: task, work_item_parent: work_item)

        expect(apply_updates.success?).to be false
        expect(apply_updates.message).to include(
          "A confidential issue must have only confidential children. Make any child items confidential and try again."
        )
      end
    end

    context 'with a note on a commit' do
      let(:note) { commit_note }

      it 'returns successful service response if update returned no errors' do
        update_params[:tag_name] = 'test'
        expect(apply_updates.success?).to be true
      end

      it 'returns service response with errors if update failed' do
        update_params[:tag_name] = '-test'
        expect(apply_updates.success?).to be false
        expect(apply_updates.message).to include('Tag name invalid')
      end
    end
  end

  context 'CE restriction for issue assignees' do
    describe '/assign' do
      let_it_be(:assignee) { create(:user) }
      let_it_be(:maintainer) { create(:user) }
      let(:service) { described_class.new(project, maintainer) }
      let(:note) { create(:note_on_issue, note: note_text, project: project) }

      let(:note_text) do
        %(/assign @#{assignee.username} @#{maintainer.username}\n")
      end

      before_all do
        project.add_maintainer(maintainer)
        project.add_maintainer(assignee)
      end

      before do
        stub_licensed_features(multiple_issue_assignees: false)
      end

      it 'adds only one assignee from the list' do
        execute(note)

        expect(note.noteable.assignees.count).to eq(1)
      end
    end
  end

  def execute(note)
    content, update_params = service.execute(note)
    service.apply_updates(update_params, note)

    content
  end
end
