require 'spec_helper'

describe Issuable::BulkUpdateService do
  let(:user)    { create(:user) }
  let(:project) { create(:project, :repository, namespace: user.namespace) }

  def bulk_update(issuables, extra_params = {})
    bulk_update_params = extra_params
      .reverse_merge(issuable_ids: Array(issuables).map(&:id).join(','))

    type = Array(issuables).first.model_name.param_key
    Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute(type)
  end

  describe 'close issues' do
    let(:issues) { create_list(:issue, 2, project: project) }

    it 'succeeds and returns the correct number of issues updated' do
      result = bulk_update(issues, state_event: 'close')

      expect(result[:success]).to be_truthy
      expect(result[:count]).to eq(issues.count)
    end

    it 'closes all the issues passed' do
      bulk_update(issues, state_event: 'close')

      expect(project.issues.opened).to be_empty
      expect(project.issues.closed).not_to be_empty
    end

    context 'when issue for a different project is created' do
      let(:private_project) { create(:project, :private) }
      let(:issue) { create(:issue, project: private_project, author: user) }

      context 'when user has access to the project' do
        it 'closes all issues passed' do
          private_project.add_maintainer(user)

          bulk_update(issues + [issue], state_event: 'close')

          expect(project.issues.opened).to be_empty
          expect(project.issues.closed).not_to be_empty
          expect(private_project.issues.closed).not_to be_empty
        end
      end

      context 'when user does not have access to project' do
        it 'only closes all issues that the user has access to' do
          bulk_update(issues + [issue], state_event: 'close')

          expect(project.issues.opened).to be_empty
          expect(project.issues.closed).not_to be_empty
          expect(private_project.issues.closed).to be_empty
        end
      end
    end
  end

  describe 'reopen issues' do
    let(:issues) { create_list(:closed_issue, 2, project: project) }

    it 'succeeds and returns the correct number of issues updated' do
      result = bulk_update(issues, state_event: 'reopen')

      expect(result[:success]).to be_truthy
      expect(result[:count]).to eq(issues.count)
    end

    it 'reopens all the issues passed' do
      bulk_update(issues, state_event: 'reopen')

      expect(project.issues.closed).to be_empty
      expect(project.issues.opened).not_to be_empty
    end
  end

  describe 'updating merge request assignee' do
    let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignee: user) }

    context 'when the new assignee ID is a valid user' do
      it 'succeeds' do
        new_assignee = create(:user)
        project.add_developer(new_assignee)

        result = bulk_update(merge_request, assignee_id: new_assignee.id)

        expect(result[:success]).to be_truthy
        expect(result[:count]).to eq(1)
      end

      it 'updates the assignee to the user ID passed' do
        assignee = create(:user)
        project.add_developer(assignee)

        expect { bulk_update(merge_request, assignee_id: assignee.id) }
          .to change { merge_request.reload.assignee }.from(user).to(assignee)
      end
    end

    context "when the new assignee ID is #{IssuableFinder::NONE}" do
      it 'unassigns the issues' do
        expect { bulk_update(merge_request, assignee_id: IssuableFinder::NONE) }
          .to change { merge_request.reload.assignee }.to(nil)
      end
    end

    context 'when the new assignee ID is not present' do
      it 'does not unassign' do
        expect { bulk_update(merge_request, assignee_id: nil) }
          .not_to change { merge_request.reload.assignee }
      end
    end
  end

  describe 'updating issue assignee' do
    let(:issue) { create(:issue, project: project, assignees: [user]) }

    context 'when the new assignee ID is a valid user' do
      it 'succeeds' do
        new_assignee = create(:user)
        project.add_developer(new_assignee)

        result = bulk_update(issue, assignee_ids: [new_assignee.id])

        expect(result[:success]).to be_truthy
        expect(result[:count]).to eq(1)
      end

      it 'updates the assignee to the user ID passed' do
        assignee = create(:user)
        project.add_developer(assignee)
        expect { bulk_update(issue, assignee_ids: [assignee.id]) }
          .to change { issue.reload.assignees.first }.from(user).to(assignee)
      end
    end

    context "when the new assignee ID is #{IssuableFinder::NONE}" do
      it "unassigns the issues" do
        expect { bulk_update(issue, assignee_ids: [IssuableFinder::NONE.to_s]) }
          .to change { issue.reload.assignees.count }.from(1).to(0)
      end
    end

    context 'when the new assignee ID is not present' do
      it 'does not unassign' do
        expect { bulk_update(issue, assignee_ids: []) }
          .not_to change { issue.reload.assignees }
      end
    end
  end

  describe 'updating milestones' do
    let(:issue)     { create(:issue, project: project) }
    let(:milestone) { create(:milestone, project: project) }

    it 'succeeds' do
      result = bulk_update(issue, milestone_id: milestone.id)

      expect(result[:success]).to be_truthy
      expect(result[:count]).to eq(1)
    end

    it 'updates the issue milestone' do
      expect { bulk_update(issue, milestone_id: milestone.id) }
        .to change { issue.reload.milestone }.from(nil).to(milestone)
    end
  end

  describe 'updating labels' do
    def create_issue_with_labels(labels)
      create(:labeled_issue, project: project, labels: labels)
    end

    let(:bug) { create(:label, project: project) }
    let(:regression) { create(:label, project: project) }
    let(:merge_requests) { create(:label, project: project) }

    let(:issue_all_labels) { create_issue_with_labels([bug, regression, merge_requests]) }
    let(:issue_bug_and_regression) { create_issue_with_labels([bug, regression]) }
    let(:issue_bug_and_merge_requests) { create_issue_with_labels([bug, merge_requests]) }
    let(:issue_no_labels) { create(:issue, project: project) }
    let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests, issue_no_labels] }

    let(:labels) { [] }
    let(:add_labels) { [] }
    let(:remove_labels) { [] }

    let(:bulk_update_params) do
      {
        label_ids:        labels.map(&:id),
        add_label_ids:    add_labels.map(&:id),
        remove_label_ids: remove_labels.map(&:id)
      }
    end

    before do
      bulk_update(issues, bulk_update_params)
    end

    context 'when label_ids are passed' do
      let(:issues) { [issue_all_labels, issue_no_labels] }
      let(:labels) { [bug, regression] }

      it 'updates the labels of all issues passed to the labels passed' do
        expect(issues.map(&:reload).map(&:label_ids)).to all(match_array(labels.map(&:id)))
      end

      it 'does not update issues not passed in' do
        expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
      end

      context 'when those label IDs are empty' do
        let(:labels) { [] }

        it 'updates the issues passed to have no labels' do
          expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty)
        end
      end
    end

    context 'when add_label_ids are passed' do
      let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] }
      let(:add_labels) { [bug, regression, merge_requests] }

      it 'adds those label IDs to all issues passed' do
        expect(issues.map(&:reload).map(&:label_ids)).to all(include(*add_labels.map(&:id)))
      end

      it 'does not update issues not passed in' do
        expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
      end
    end

    context 'when remove_label_ids are passed' do
      let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] }
      let(:remove_labels) { [bug, regression, merge_requests] }

      it 'removes those label IDs from all issues passed' do
        expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty)
      end

      it 'does not update issues not passed in' do
        expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
      end
    end

    context 'when add_label_ids and remove_label_ids are passed' do
      let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] }
      let(:add_labels) { [bug] }
      let(:remove_labels) { [merge_requests] }

      it 'adds the label IDs to all issues passed' do
        expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id))
      end

      it 'removes the label IDs from all issues passed' do
        expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id)
      end

      it 'does not update issues not passed in' do
        expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
      end
    end

    context 'when add_label_ids and label_ids are passed' do
      let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests] }
      let(:labels) { [merge_requests] }
      let(:add_labels) { [regression] }

      it 'adds the label IDs to all issues passed' do
        expect(issues.map(&:reload).map(&:label_ids)).to all(include(regression.id))
      end

      it 'ignores the label IDs parameter' do
        expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id))
      end

      it 'does not update issues not passed in' do
        expect(issue_no_labels.label_ids).to be_empty
      end
    end

    context 'when remove_label_ids and label_ids are passed' do
      let(:issues) { [issue_no_labels, issue_bug_and_regression] }
      let(:labels) { [merge_requests] }
      let(:remove_labels) { [regression] }

      it 'removes the label IDs from all issues passed' do
        expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id)
      end

      it 'ignores the label IDs parameter' do
        expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id)
      end

      it 'does not update issues not passed in' do
        expect(issue_all_labels.label_ids).to contain_exactly(bug.id, regression.id, merge_requests.id)
      end
    end

    context 'when add_label_ids, remove_label_ids, and label_ids are passed' do
      let(:issues) { [issue_bug_and_merge_requests, issue_no_labels] }
      let(:labels) { [regression] }
      let(:add_labels) { [bug] }
      let(:remove_labels) { [merge_requests] }

      it 'adds the label IDs to all issues passed' do
        expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id))
      end

      it 'removes the label IDs from all issues passed' do
        expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id)
      end

      it 'ignores the label IDs parameter' do
        expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id)
      end

      it 'does not update issues not passed in' do
        expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
      end
    end
  end

  describe 'subscribe to issues' do
    let(:issues) { create_list(:issue, 2, project: project) }

    it 'subscribes the given user' do
      bulk_update(issues, subscription_event: 'subscribe')

      expect(issues).to all(be_subscribed(user, project))
    end
  end

  describe 'unsubscribe from issues' do
    let(:issues) do
      create_list(:closed_issue, 2, project: project) do |issue|
        issue.subscriptions.create(user: user, project: project, subscribed: true)
      end
    end

    it 'unsubscribes the given user' do
      bulk_update(issues, subscription_event: 'unsubscribe')

      issues.each do |issue|
        expect(issue).not_to be_subscribed(user, project)
      end
    end
  end
end
