/**
 * @license
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import '../../../test/common-test-setup-karma.js';
import './gr-change-actions.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {
  createAccountWithId,
  createApproval,
  createChange,
  createChangeMessages,
  createRevisions,
} from '../../../test/test-data-generators.js';
import {ChangeStatus} from '../../../constants/constants.js';
import {stubRestApi} from '../../../test/test-utils.js';

const basicFixture = fixtureFromElement('gr-change-actions');

const CHERRY_PICK_TYPES = {
  SINGLE_CHANGE: 1,
  TOPIC: 2,
};
// TODO(dhruvsri): remove use of _populateRevertMessage as it's private
suite('gr-change-actions tests', () => {
  let element;

  suite('basic tests', () => {
    setup(() => {
      stubRestApi('getChangeRevisionActions').returns(Promise.resolve({
        cherrypick: {
          method: 'POST',
          label: 'Cherry Pick',
          title: 'Cherry pick change to a different branch',
          enabled: true,
        },
        rebase: {
          method: 'POST',
          label: 'Rebase',
          title: 'Rebase onto tip of branch or parent change',
          enabled: true,
        },
        submit: {
          method: 'POST',
          label: 'Submit',
          title: 'Submit patch set 2 into master',
          enabled: true,
        },
        revert_submission: {
          method: 'POST',
          label: 'Revert submission',
          title: 'Revert this submission',
          enabled: true,
        },
      }));
      stubRestApi('send').callsFake((method, url, payload) => {
        if (method !== 'POST') {
          return Promise.reject(new Error('bad method'));
        }
        if (url === '/changes/test~42/revisions/2/submit') {
          return Promise.resolve({
            ok: true,
            text() { return Promise.resolve(')]}\'\n{}'); },
          });
        } else if (url === '/changes/test~42/revisions/2/rebase') {
          return Promise.resolve({
            ok: true,
            text() { return Promise.resolve(')]}\'\n{}'); },
          });
        }
        return Promise.reject(new Error('bad url'));
      });
      stubRestApi('getProjectConfig').returns(Promise.resolve({}));

      sinon.stub(getPluginLoader(), 'awaitPluginsLoaded')
          .returns(Promise.resolve());

      element = basicFixture.instantiate();
      element.change = {};
      element.changeNum = '42';
      element.latestPatchNum = '2';
      element.actions = {
        '/': {
          method: 'DELETE',
          label: 'Delete Change',
          title: 'Delete change X_X',
          enabled: true,
        },
      };
      element.account = {
        _account_id: 123,
      };
      stubRestApi('getRepoBranches').returns(Promise.resolve([]));

      return element.reload();
    });

    test('show-revision-actions event should fire', done => {
      const spy = sinon.spy(element, '_sendShowRevisionActions');
      element.reload();
      flush(() => {
        assert.isTrue(spy.called);
        done();
      });
    });

    test('primary and secondary actions split properly', () => {
      // Submit should be the only primary action.
      assert.equal(element._topLevelPrimaryActions.length, 1);
      assert.equal(element._topLevelPrimaryActions[0].label, 'Submit');
      assert.equal(element._topLevelSecondaryActions.length,
          element._topLevelActions.length - 1);
    });

    test('revert submission action is skipped', () => {
      assert.equal(element._allActionValues.filter(action =>
        action.__key === 'submit').length, 1);
      assert.equal(element._allActionValues.filter(action =>
        action.__key === 'revert_submission').length, 0);
    });

    test('_shouldHideActions', () => {
      assert.isTrue(element._shouldHideActions(undefined, true));
      assert.isTrue(element._shouldHideActions({base: {}}, false));
      assert.isFalse(element._shouldHideActions({base: ['test']}, false));
    });

    test('plugin revision actions', done => {
      const stub = stubRestApi('getChangeActionURL').returns(
          Promise.resolve('the-url'));
      element.revisionActions = {
        'plugin~action': {},
      };
      assert.isOk(element.revisionActions['plugin~action']);
      flush(() => {
        assert.isTrue(stub.calledWith(
            element.changeNum, element.latestPatchNum, '/plugin~action'));
        assert.equal(element.revisionActions['plugin~action'].__url, 'the-url');
        done();
      });
    });

    test('plugin change actions', async () => {
      const stub = stubRestApi('getChangeActionURL').returns(
          Promise.resolve('the-url'));
      element.actions = {
        'plugin~action': {},
      };
      assert.isOk(element.actions['plugin~action']);
      await flush();
      assert.isTrue(stub.calledWith(
          element.changeNum, undefined, '/plugin~action'));
      assert.equal(element.actions['plugin~action'].__url, 'the-url');
    });

    test('not supported actions are filtered out', () => {
      element.revisionActions = {followup: {}};
      assert.equal(element.querySelectorAll(
          'section gr-button[data-action-type="revision"]').length, 0);
    });

    test('getActionDetails', () => {
      element.revisionActions = {
        'plugin~action': {},
        ...element.revisionActions,
      };
      assert.isUndefined(element.getActionDetails('rubbish'));
      assert.strictEqual(element.revisionActions['plugin~action'],
          element.getActionDetails('plugin~action'));
      assert.strictEqual(element.revisionActions['rebase'],
          element.getActionDetails('rebase'));
    });

    test('hide revision action', done => {
      flush(() => {
        const buttonEl = element.shadowRoot
            .querySelector('[data-action-key="submit"]');
        assert.isOk(buttonEl);
        assert.throws(element.setActionHidden.bind(element, 'invalid type'));
        element.setActionHidden(element.ActionType.REVISION,
            element.RevisionActions.SUBMIT, true);
        assert.lengthOf(element._hiddenActions, 1);
        element.setActionHidden(element.ActionType.REVISION,
            element.RevisionActions.SUBMIT, true);
        assert.lengthOf(element._hiddenActions, 1);
        flush(() => {
          const buttonEl = element.shadowRoot
              .querySelector('[data-action-key="submit"]');
          assert.isNotOk(buttonEl);

          element.setActionHidden(element.ActionType.REVISION,
              element.RevisionActions.SUBMIT, false);
          flush(() => {
            const buttonEl = element.shadowRoot
                .querySelector('[data-action-key="submit"]');
            assert.isOk(buttonEl);
            assert.isFalse(buttonEl.hasAttribute('hidden'));
            done();
          });
        });
      });
    });

    test('buttons exist', done => {
      element._loading = false;
      flush(() => {
        const buttonEls = dom(element.root)
            .querySelectorAll('gr-button');
        const menuItems = element.$.moreActions.items;

        // Total button number is one greater than the number of total actions
        // due to the existence of the overflow menu trigger.
        assert.equal(buttonEls.length + menuItems.length,
            element._allActionValues.length + 1);
        assert.isFalse(element.hidden);
        done();
      });
    });

    test('delete buttons have explicit labels', done => {
      flush(() => {
        const deleteItems = element.$.moreActions.items
            .filter(item => item.id.startsWith('delete'));
        assert.equal(deleteItems.length, 1);
        assert.notEqual(deleteItems[0].name);
        assert.equal(deleteItems[0].name, 'Delete change');
        done();
      });
    });

    test('get revision object from change', () => {
      const revObj = {_number: 2, foo: 'bar'};
      const change = {
        revisions: {
          rev1: {_number: 1},
          rev2: revObj,
        },
      };
      assert.deepEqual(element._getRevision(change, 2), revObj);
    });

    test('_actionComparator sort order', () => {
      const actions = [
        {label: '123', __type: 'change', __key: 'review'},
        {label: 'abc-ro', __type: 'revision'},
        {label: 'abc', __type: 'change'},
        {label: 'def', __type: 'change'},
        {label: 'def-p', __type: 'change', __primary: true},
      ];

      const result = actions.slice();
      result.reverse();
      result.sort(element._actionComparator.bind(element));
      assert.deepEqual(result, actions);
    });

    test('submit change', () => {
      const showSpy = sinon.spy(element, '_showActionDialog');
      stubRestApi('getFromProjectLookup')
          .returns(Promise.resolve('test'));
      sinon.stub(element.$.overlay, 'open').returns(Promise.resolve());
      element.change = {
        revisions: {
          rev1: {_number: 1},
          rev2: {_number: 2},
        },
      };
      element.latestPatchNum = '2';

      const submitButton = element.shadowRoot
          .querySelector('gr-button[data-action-key="submit"]');
      assert.ok(submitButton);
      MockInteractions.tap(submitButton);

      flush();
      assert.isTrue(showSpy.calledWith(element.$.confirmSubmitDialog));
    });

    test('submit change, tap on icon', done => {
      sinon.stub(element.$.confirmSubmitDialog, 'resetFocus').callsFake( done);
      stubRestApi('getFromProjectLookup')
          .returns(Promise.resolve('test'));
      sinon.stub(element.$.overlay, 'open').returns(Promise.resolve());
      element.change = {
        revisions: {
          rev1: {_number: 1},
          rev2: {_number: 2},
        },
      };
      element.latestPatchNum = '2';

      const submitIcon =
          element.shadowRoot
              .querySelector('gr-button[data-action-key="submit"] iron-icon');
      assert.ok(submitIcon);
      MockInteractions.tap(submitIcon);
    });

    test('_handleSubmitConfirm', () => {
      const fireStub = sinon.stub(element, '_fireAction');
      sinon.stub(element, '_canSubmitChange').returns(true);
      element._handleSubmitConfirm();
      assert.isTrue(fireStub.calledOnce);
      assert.deepEqual(fireStub.lastCall.args,
          ['/submit', element.revisionActions.submit, true]);
    });

    test('_handleSubmitConfirm when not able to submit', () => {
      const fireStub = sinon.stub(element, '_fireAction');
      sinon.stub(element, '_canSubmitChange').returns(false);
      element._handleSubmitConfirm();
      assert.isFalse(fireStub.called);
    });

    test('submit change with plugin hook', done => {
      sinon.stub(element, '_canSubmitChange').callsFake(
          () => false);
      const fireActionStub = sinon.stub(element, '_fireAction');
      flush(() => {
        const submitButton = element.shadowRoot
            .querySelector('gr-button[data-action-key="submit"]');
        assert.ok(submitButton);
        MockInteractions.tap(submitButton);
        assert.equal(fireActionStub.callCount, 0);

        done();
      });
    });

    test('chain state', () => {
      assert.equal(element._hasKnownChainState, false);
      element.hasParent = true;
      assert.equal(element._hasKnownChainState, true);
      element.hasParent = false;
    });

    test('_calculateDisabled', () => {
      let hasKnownChainState = false;
      const action = {__key: 'rebase', enabled: true};
      assert.equal(
          element._calculateDisabled(action, hasKnownChainState), true);

      action.__key = 'delete';
      assert.equal(
          element._calculateDisabled(action, hasKnownChainState), false);

      action.__key = 'rebase';
      hasKnownChainState = true;
      assert.equal(
          element._calculateDisabled(action, hasKnownChainState), false);

      action.enabled = false;
      assert.equal(
          element._calculateDisabled(action, hasKnownChainState), false);
    });

    test('rebase change', done => {
      const fireActionStub = sinon.stub(element, '_fireAction');
      const fetchChangesStub = sinon.stub(element.$.confirmRebase,
          'fetchRecentChanges').returns(Promise.resolve([]));
      element._hasKnownChainState = true;
      flush(() => {
        const rebaseButton = element.shadowRoot
            .querySelector('gr-button[data-action-key="rebase"]');
        MockInteractions.tap(rebaseButton);
        const rebaseAction = {
          __key: 'rebase',
          __type: 'revision',
          __primary: false,
          enabled: true,
          label: 'Rebase',
          method: 'POST',
          title: 'Rebase onto tip of branch or parent change',
        };
        assert.isTrue(fetchChangesStub.called);
        element._handleRebaseConfirm({detail: {base: '1234'}});
        assert.deepEqual(fireActionStub.lastCall.args,
            ['/rebase', rebaseAction, true, {base: '1234'}]);
        done();
      });
    });

    test('rebase change fires reload event', done => {
      const eventStub = sinon.stub(element, 'dispatchEvent');
      stubRestApi('getResponseObject').returns(
          Promise.resolve({}));
      element._handleResponse({__key: 'rebase'}, {});
      flush(() => {
        assert.isTrue(eventStub.called);
        assert.equal(eventStub.lastCall.args[0].type, 'reload');
        done();
      });
    });

    test(`rebase dialog gets recent changes each time it's opened`, done => {
      const fetchChangesStub = sinon.stub(element.$.confirmRebase,
          'fetchRecentChanges').returns(Promise.resolve([]));
      element._hasKnownChainState = true;
      const rebaseButton = element.shadowRoot
          .querySelector('gr-button[data-action-key="rebase"]');
      MockInteractions.tap(rebaseButton);
      assert.isTrue(fetchChangesStub.calledOnce);

      flush(() => {
        element.$.confirmRebase.dispatchEvent(
            new CustomEvent('cancel', {
              composed: true, bubbles: true,
            }));
        MockInteractions.tap(rebaseButton);
        assert.isTrue(fetchChangesStub.calledTwice);
        done();
      });
    });

    test('two dialogs are not shown at the same time', async () => {
      element._hasKnownChainState = true;
      await flush();
      const rebaseButton = element.shadowRoot
          .querySelector('gr-button[data-action-key="rebase"]');
      assert.ok(rebaseButton);
      MockInteractions.tap(rebaseButton);
      await flush();
      assert.isFalse(element.$.confirmRebase.hidden);
      stubRestApi('getChanges')
          .returns(Promise.resolve([]));
      element._handleCherrypickTap();
      await flush();
      assert.isTrue(element.$.confirmRebase.hidden);
      assert.isFalse(element.$.confirmCherrypick.hidden);
    });

    test('fullscreen-overlay-opened hides content', () => {
      sinon.spy(element, '_handleHideBackgroundContent');
      element.$.overlay.dispatchEvent(
          new CustomEvent('fullscreen-overlay-opened', {
            composed: true, bubbles: true,
          }));
      assert.isTrue(element._handleHideBackgroundContent.called);
      assert.isTrue(element.$.mainContent.classList.contains('overlayOpen'));
    });

    test('fullscreen-overlay-closed shows content', () => {
      sinon.spy(element, '_handleShowBackgroundContent');
      element.$.overlay.dispatchEvent(
          new CustomEvent('fullscreen-overlay-closed', {
            composed: true, bubbles: true,
          }));
      assert.isTrue(element._handleShowBackgroundContent.called);
      assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
    });

    test('_setReviewOnRevert', () => {
      const review = {labels: {'Foo': 1, 'Bar-Baz': -2}};
      const changeId = 1234;
      sinon.stub(element.jsAPI, 'getReviewPostRevert').returns(review);
      const saveStub = stubRestApi('saveChangeReview')
          .returns(Promise.resolve());
      return element._setReviewOnRevert(changeId).then(() => {
        assert.isTrue(saveStub.calledOnce);
        assert.equal(saveStub.lastCall.args[0], changeId);
        assert.deepEqual(saveStub.lastCall.args[2], review);
      });
    });

    suite('change edits', () => {
      test('disableEdit', () => {
        element.set('editMode', false);
        element.set('editPatchsetLoaded', false);
        element.change = {status: 'NEW'};
        element.set('disableEdit', true);
        flush();

        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="publishEdit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="rebaseEdit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="deleteEdit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="edit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="stopEdit"]'));
      });

      test('shows confirm dialog for delete edit', () => {
        element.set('editMode', true);
        element.set('editPatchsetLoaded', true);

        const fireActionStub = sinon.stub(element, '_fireAction');
        element._handleDeleteEditTap();
        assert.isFalse(element.$.confirmDeleteEditDialog.hidden);
        MockInteractions.tap(
            element.shadowRoot
                .querySelector('#confirmDeleteEditDialog')
                .shadowRoot
                .querySelector('gr-button[primary]'));
        flush();

        assert.equal(fireActionStub.lastCall.args[0], '/edit');
      });

      test('hide publishEdit and rebaseEdit if change is not open', () => {
        element.set('editMode', true);
        element.set('editPatchsetLoaded', true);
        element.change = {status: 'MERGED'};
        flush();

        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="publishEdit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="rebaseEdit"]'));
        assert.isOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="deleteEdit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="edit"]'));
      });

      test('edit patchset is loaded, needs rebase', () => {
        element.set('editMode', true);
        element.set('editPatchsetLoaded', true);
        element.change = {status: 'NEW'};
        element.editBasedOnCurrentPatchSet = false;
        flush();

        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="publishEdit"]'));
        assert.isOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="rebaseEdit"]'));
        assert.isOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="deleteEdit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="edit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="stopEdit"]'));
      });

      test('edit patchset is loaded, does not need rebase', () => {
        element.set('editMode', true);
        element.set('editPatchsetLoaded', true);
        element.change = {status: 'NEW'};
        element.editBasedOnCurrentPatchSet = true;
        flush();

        assert.isOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="publishEdit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="rebaseEdit"]'));
        assert.isOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="deleteEdit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="edit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="stopEdit"]'));
      });

      test('edit mode is loaded, no edit patchset', () => {
        element.set('editMode', true);
        element.set('editPatchsetLoaded', false);
        element.change = {status: 'NEW'};
        flush();

        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="publishEdit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="rebaseEdit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="deleteEdit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="edit"]'));
        assert.isOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="stopEdit"]'));
      });

      test('normal patch set', () => {
        element.set('editMode', false);
        element.set('editPatchsetLoaded', false);
        element.change = {status: 'NEW'};
        flush();

        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="publishEdit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="rebaseEdit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="deleteEdit"]'));
        assert.isOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="edit"]'));
        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="stopEdit"]'));
      });

      test('edit action', done => {
        element.addEventListener('edit-tap', () => { done(); });
        element.set('editMode', true);
        element.change = {status: 'NEW'};
        flush();

        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="edit"]'));
        assert.isOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="stopEdit"]'));
        element.change = {status: 'MERGED'};
        flush();

        assert.isNotOk(element.shadowRoot
            .querySelector('gr-button[data-action-key="edit"]'));
        element.change = {status: 'NEW'};
        element.set('editMode', false);
        flush();

        const editButton = element.shadowRoot
            .querySelector('gr-button[data-action-key="edit"]');
        assert.isOk(editButton);
        MockInteractions.tap(editButton);
      });
    });

    suite('cherry-pick', () => {
      let fireActionStub;

      setup(() => {
        fireActionStub = sinon.stub(element, '_fireAction');
        sinon.stub(window, 'alert');
      });

      test('works', () => {
        element._handleCherrypickTap();
        const action = {
          __key: 'cherrypick',
          __type: 'revision',
          __primary: false,
          enabled: true,
          label: 'Cherry pick',
          method: 'POST',
          title: 'Cherry pick change to a different branch',
        };

        element._handleCherrypickConfirm({
          detail: {
            branch: '',
            type: CHERRY_PICK_TYPES.SINGLE_CHANGE,
          },
        });
        assert.equal(fireActionStub.callCount, 0);

        element.$.confirmCherrypick.branch = 'master';
        element._handleCherrypickConfirm({
          detail: {
            branch: 'master',
            type: CHERRY_PICK_TYPES.SINGLE_CHANGE,
          },
        });
        assert.equal(fireActionStub.callCount, 0); // Still needs a message.

        // Add attributes that are used to determine the message.
        element.$.confirmCherrypick.commitMessage = 'foo message';
        element.$.confirmCherrypick.changeStatus = 'OPEN';
        element.$.confirmCherrypick.commitNum = '123';

        element._handleCherrypickConfirm({
          detail: {
            branch: 'master',
            type: CHERRY_PICK_TYPES.SINGLE_CHANGE,
          },
        });

        assert.equal(element.$.confirmCherrypick.shadowRoot.
            querySelector('#messageInput').value, 'foo message');

        assert.deepEqual(fireActionStub.lastCall.args, [
          '/cherrypick', action, true, {
            destination: 'master',
            base: null,
            message: 'foo message',
            allow_conflicts: false,
          },
        ]);
      });

      test('cherry pick even with conflicts', () => {
        element._handleCherrypickTap();
        const action = {
          __key: 'cherrypick',
          __type: 'revision',
          __primary: false,
          enabled: true,
          label: 'Cherry pick',
          method: 'POST',
          title: 'Cherry pick change to a different branch',
        };

        element.$.confirmCherrypick.branch = 'master';

        // Add attributes that are used to determine the message.
        element.$.confirmCherrypick.commitMessage = 'foo message';
        element.$.confirmCherrypick.changeStatus = 'OPEN';
        element.$.confirmCherrypick.commitNum = '123';

        element._handleCherrypickConflictConfirm();

        assert.deepEqual(fireActionStub.lastCall.args, [
          '/cherrypick', action, true, {
            destination: 'master',
            base: null,
            message: 'foo message',
            allow_conflicts: true,
          },
        ]);
      });

      test('branch name cleared when re-open cherrypick', () => {
        const emptyBranchName = '';
        element.$.confirmCherrypick.branch = 'master';

        element._handleCherrypickTap();
        assert.equal(element.$.confirmCherrypick.branch, emptyBranchName);
      });

      suite('cherry pick topics', () => {
        const changes = [
          {
            change_id: '12345678901234', topic: 'T', subject: 'random',
            project: 'A', status: 'MERGED',
          },
          {
            change_id: '23456', topic: 'T', subject: 'a'.repeat(100),
            project: 'B', status: 'NEW',
          },
        ];
        setup(done => {
          stubRestApi('getChanges')
              .returns(Promise.resolve(changes));
          element._handleCherrypickTap();
          flush(() => {
            const radioButtons = element.$.confirmCherrypick.shadowRoot.
                querySelectorAll(`input[name='cherryPickOptions']`);
            assert.equal(radioButtons.length, 2);
            MockInteractions.tap(radioButtons[1]);
            flush(() => {
              done();
            });
          });
        });

        test('cherry pick topic dialog is rendered', done => {
          const dialog = element.$.confirmCherrypick;
          flush(() => {
            const changesTable = dialog.shadowRoot.querySelector('table');
            const headers = Array.from(changesTable.querySelectorAll('th'));
            const expectedHeadings = ['', 'Change', 'Status', 'Subject',
              'Project', 'Progress', ''];
            const headings = headers.map(header => header.innerText);
            assert.equal(headings.length, expectedHeadings.length);
            for (let i = 0; i < headings.length; i++) {
              assert.equal(headings[i].trim(), expectedHeadings[i]);
            }
            const changeRows = changesTable.querySelectorAll('tbody > tr');
            const change = Array.from(changeRows[0].querySelectorAll('td'))
                .map(e => e.innerText);
            const expectedChange = ['', '1234567890', 'MERGED', 'random', 'A',
              'NOT STARTED', ''];
            for (let i = 0; i < change.length; i++) {
              assert.equal(change[i].trim(), expectedChange[i]);
            }
            done();
          });
        });

        test('changes with duplicate project show an error', done => {
          const dialog = element.$.confirmCherrypick;
          const error = dialog.shadowRoot.querySelector('.error-message');
          assert.equal(error.innerText, '');
          dialog.updateChanges([
            {
              change_id: '12345678901234', topic: 'T', subject: 'random',
              project: 'A',
            },
            {
              change_id: '23456', topic: 'T', subject: 'a'.repeat(100),
              project: 'A',
            },
          ]);
          flush(() => {
            assert.equal(error.innerText, 'Two changes cannot be of the same'
             + ' project');
            done();
          });
        });
      });
    });

    suite('move change', () => {
      let fireActionStub;

      setup(() => {
        fireActionStub = sinon.stub(element, '_fireAction');
        sinon.stub(window, 'alert');
        element.actions = {
          move: {
            method: 'POST',
            label: 'Move',
            title: 'Move the change',
            enabled: true,
          },
        };
      });

      test('works', () => {
        element._handleMoveTap();

        element._handleMoveConfirm();
        assert.equal(fireActionStub.callCount, 0);

        element.$.confirmMove.branch = 'master';
        element._handleMoveConfirm();
        assert.equal(fireActionStub.callCount, 1);
      });

      test('branch name cleared when re-open move', () => {
        const emptyBranchName = '';
        element.$.confirmMove.branch = 'master';

        element._handleMoveTap();
        assert.equal(element.$.confirmMove.branch, emptyBranchName);
      });
    });

    test('custom actions', done => {
      // Add a button with the same key as a server-based one to ensure
      // collisions are taken care of.
      const key = element.addActionButton(element.ActionType.REVISION, 'Bork!');
      element.addEventListener(key + '-tap', e => {
        assert.equal(e.detail.node.getAttribute('data-action-key'), key);
        element.removeActionButton(key);
        flush(() => {
          assert.notOk(element.shadowRoot
              .querySelector('[data-action-key="' + key + '"]'));
          done();
        });
      });
      flush(() => {
        MockInteractions.tap(element.shadowRoot
            .querySelector('[data-action-key="' + key + '"]'));
      });
    });

    test('_setLoadingOnButtonWithKey top-level', () => {
      const key = 'rebase';
      const type = 'revision';
      const cleanup = element._setLoadingOnButtonWithKey(type, key);
      assert.equal(element._actionLoadingMessage, 'Rebasing...');

      const button = element.shadowRoot
          .querySelector('[data-action-key="' + key + '"]');
      assert.isTrue(button.hasAttribute('loading'));
      assert.isTrue(button.disabled);

      assert.isOk(cleanup);
      assert.isFunction(cleanup);
      cleanup();

      assert.isFalse(button.hasAttribute('loading'));
      assert.isFalse(button.disabled);
      assert.isNotOk(element._actionLoadingMessage);
    });

    test('_setLoadingOnButtonWithKey overflow menu', () => {
      const key = 'cherrypick';
      const type = 'revision';
      const cleanup = element._setLoadingOnButtonWithKey(type, key);
      assert.equal(element._actionLoadingMessage, 'Cherry-picking...');
      assert.include(element._disabledMenuActions, 'cherrypick');
      assert.isFunction(cleanup);

      cleanup();

      assert.notOk(element._actionLoadingMessage);
      assert.notInclude(element._disabledMenuActions, 'cherrypick');
    });

    suite('abandon change', () => {
      let alertStub;
      let fireActionStub;

      setup(() => {
        fireActionStub = sinon.stub(element, '_fireAction');
        alertStub = sinon.stub(window, 'alert');
        element.actions = {
          abandon: {
            method: 'POST',
            label: 'Abandon',
            title: 'Abandon the change',
            enabled: true,
          },
        };
        return element.reload();
      });

      test('abandon change with message', done => {
        const newAbandonMsg = 'Test Abandon Message';
        element.$.confirmAbandonDialog.message = newAbandonMsg;
        flush(() => {
          const abandonButton =
              element.shadowRoot
                  .querySelector('gr-button[data-action-key="abandon"]');
          MockInteractions.tap(abandonButton);

          assert.equal(element.$.confirmAbandonDialog.message, newAbandonMsg);
          done();
        });
      });

      test('abandon change with no message', done => {
        flush(() => {
          const abandonButton =
              element.shadowRoot
                  .querySelector('gr-button[data-action-key="abandon"]');
          MockInteractions.tap(abandonButton);

          assert.isUndefined(element.$.confirmAbandonDialog.message);
          done();
        });
      });

      test('works', () => {
        element.$.confirmAbandonDialog.message = 'original message';
        const restoreButton =
            element.shadowRoot
                .querySelector('gr-button[data-action-key="abandon"]');
        MockInteractions.tap(restoreButton);

        element.$.confirmAbandonDialog.message = 'foo message';
        element._handleAbandonDialogConfirm();
        assert.notOk(alertStub.called);

        const action = {
          __key: 'abandon',
          __type: 'change',
          __primary: false,
          enabled: true,
          label: 'Abandon',
          method: 'POST',
          title: 'Abandon the change',
        };
        assert.deepEqual(fireActionStub.lastCall.args, [
          '/abandon', action, false, {
            message: 'foo message',
          }]);
      });
    });

    suite('revert change', () => {
      let fireActionStub;

      setup(() => {
        fireActionStub = sinon.stub(element, '_fireAction');
        element.commitMessage = 'random commit message';
        element.change.current_revision = 'abcdef';
        element.actions = {
          revert: {
            method: 'POST',
            label: 'Revert',
            title: 'Revert the change',
            enabled: true,
          },
        };
        return element.reload();
      });

      test('revert change with plugin hook', done => {
        const newRevertMsg = 'Modified revert msg';
        sinon.stub(element.$.confirmRevertDialog, '_modifyRevertMsg').callsFake(
            () => newRevertMsg);
        element.change = {
          current_revision: 'abc1234',
        };
        stubRestApi('getChanges')
            .returns(Promise.resolve([
              {change_id: '12345678901234', topic: 'T', subject: 'random'},
              {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
            ]));
        sinon.stub(element.$.confirmRevertDialog,
            '_populateRevertSubmissionMessage').callsFake(() => 'original msg');
        flush(() => {
          const revertButton = element.shadowRoot
              .querySelector('gr-button[data-action-key="revert"]');
          MockInteractions.tap(revertButton);
          flush(() => {
            assert.equal(element.$.confirmRevertDialog._message, newRevertMsg);
            done();
          });
        });
      });

      suite('revert change submitted together', () => {
        let getChangesStub;
        setup(() => {
          element.change = {
            submission_id: '199 0',
            current_revision: '2000',
          };
          getChangesStub = stubRestApi('getChanges')
              .returns(Promise.resolve([
                {change_id: '12345678901234', topic: 'T', subject: 'random'},
                {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
              ]));
        });

        test('confirm revert dialog shows both options', done => {
          const revertButton = element.shadowRoot
              .querySelector('gr-button[data-action-key="revert"]');
          MockInteractions.tap(revertButton);
          flush(() => {
            assert.equal(getChangesStub.args[0][1], 'submissionid: "199 0"');
            const confirmRevertDialog = element.$.confirmRevertDialog;
            const revertSingleChangeLabel = confirmRevertDialog
                .shadowRoot.querySelector('.revertSingleChange');
            const revertSubmissionLabel = confirmRevertDialog.
                shadowRoot.querySelector('.revertSubmission');
            assert(revertSingleChangeLabel.innerText.trim() ===
                'Revert single change');
            assert(revertSubmissionLabel.innerText.trim() ===
                'Revert entire submission (2 Changes)');
            let expectedMsg = 'Revert submission 199 0' + '\n\n' +
              'Reason for revert: <INSERT REASONING HERE>' + '\n' +
              'Reverted Changes:' + '\n' +
              '1234567890:random' + '\n' +
              '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
              '\n';
            assert.equal(confirmRevertDialog._message, expectedMsg);
            const radioInputs = confirmRevertDialog.shadowRoot
                .querySelectorAll('input[name="revertOptions"]');
            MockInteractions.tap(radioInputs[0]);
            flush(() => {
              expectedMsg = 'Revert "random commit message"\n\nThis reverts '
               + 'commit 2000.\n\nReason'
               + ' for revert: <INSERT REASONING HERE>\n';
              assert.equal(confirmRevertDialog._message, expectedMsg);
              done();
            });
          });
        });

        test('submit fails if message is not edited', done => {
          const revertButton = element.shadowRoot
              .querySelector('gr-button[data-action-key="revert"]');
          const confirmRevertDialog = element.$.confirmRevertDialog;
          MockInteractions.tap(revertButton);
          const fireStub = sinon.stub(confirmRevertDialog, 'dispatchEvent');
          flush(() => {
            const confirmButton = element.$.confirmRevertDialog.shadowRoot
                .querySelector('gr-dialog')
                .shadowRoot.querySelector('#confirm');
            MockInteractions.tap(confirmButton);
            flush(() => {
              assert.isTrue(confirmRevertDialog._showErrorMessage);
              assert.isFalse(fireStub.called);
              done();
            });
          });
        });

        test('message modification is retained on switching', done => {
          const revertButton = element.shadowRoot
              .querySelector('gr-button[data-action-key="revert"]');
          const confirmRevertDialog = element.$.confirmRevertDialog;
          MockInteractions.tap(revertButton);
          flush(() => {
            const radioInputs = confirmRevertDialog.shadowRoot
                .querySelectorAll('input[name="revertOptions"]');
            const revertSubmissionMsg = 'Revert submission 199 0' + '\n\n' +
            'Reason for revert: <INSERT REASONING HERE>' + '\n' +
            'Reverted Changes:' + '\n' +
            '1234567890:random' + '\n' +
            '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
            '\n';
            const singleChangeMsg =
            'Revert "random commit message"\n\nThis reverts '
              + 'commit 2000.\n\nReason'
              + ' for revert: <INSERT REASONING HERE>\n';
            assert.equal(confirmRevertDialog._message, revertSubmissionMsg);
            const newRevertMsg = revertSubmissionMsg + 'random';
            const newSingleChangeMsg = singleChangeMsg + 'random';
            confirmRevertDialog._message = newRevertMsg;
            MockInteractions.tap(radioInputs[0]);
            flush(() => {
              assert.equal(confirmRevertDialog._message, singleChangeMsg);
              confirmRevertDialog._message = newSingleChangeMsg;
              MockInteractions.tap(radioInputs[1]);
              flush(() => {
                assert.equal(confirmRevertDialog._message, newRevertMsg);
                MockInteractions.tap(radioInputs[0]);
                flush(() => {
                  assert.equal(
                      confirmRevertDialog._message,
                      newSingleChangeMsg
                  );
                  done();
                });
              });
            });
          });
        });
      });

      suite('revert single change', () => {
        setup(() => {
          element.change = {
            submission_id: '199',
            current_revision: '2000',
          };
          stubRestApi('getChanges')
              .returns(Promise.resolve([
                {change_id: '12345678901234', topic: 'T', subject: 'random'},
              ]));
        });

        test('submit fails if message is not edited', done => {
          const revertButton = element.shadowRoot
              .querySelector('gr-button[data-action-key="revert"]');
          const confirmRevertDialog = element.$.confirmRevertDialog;
          MockInteractions.tap(revertButton);
          const fireStub = sinon.stub(confirmRevertDialog, 'dispatchEvent');
          flush(() => {
            const confirmButton = element.$.confirmRevertDialog.shadowRoot
                .querySelector('gr-dialog')
                .shadowRoot.querySelector('#confirm');
            MockInteractions.tap(confirmButton);
            flush(() => {
              assert.isTrue(confirmRevertDialog._showErrorMessage);
              assert.isFalse(fireStub.called);
              done();
            });
          });
        });

        test('confirm revert dialog shows no radio button', done => {
          const revertButton = element.shadowRoot
              .querySelector('gr-button[data-action-key="revert"]');
          MockInteractions.tap(revertButton);
          flush(() => {
            const confirmRevertDialog = element.$.confirmRevertDialog;
            const radioInputs = confirmRevertDialog.shadowRoot
                .querySelectorAll('input[name="revertOptions"]');
            assert.equal(radioInputs.length, 0);
            const msg = 'Revert "random commit message"\n\n'
              + 'This reverts commit 2000.\n\nReason '
              + 'for revert: <INSERT REASONING HERE>\n';
            assert.equal(confirmRevertDialog._message, msg);
            const editedMsg = msg + 'hello';
            confirmRevertDialog._message += 'hello';
            const confirmButton = element.$.confirmRevertDialog.shadowRoot
                .querySelector('gr-dialog')
                .shadowRoot.querySelector('#confirm');
            MockInteractions.tap(confirmButton);
            flush(() => {
              assert.equal(fireActionStub.getCall(0).args[0], '/revert');
              assert.equal(fireActionStub.getCall(0).args[1].__key, 'revert');
              assert.equal(fireActionStub.getCall(0).args[3].message,
                  editedMsg);
              done();
            });
          });
        });
      });
    });

    suite('mark change private', () => {
      setup(() => {
        const privateAction = {
          __key: 'private',
          __type: 'change',
          __primary: false,
          method: 'POST',
          label: 'Mark private',
          title: 'Working...',
          enabled: true,
        };

        element.actions = {
          private: privateAction,
        };

        element.change.is_private = false;

        element.changeNum = '2';
        element.latestPatchNum = '2';

        return element.reload();
      });

      test('make sure the mark private change button is not outside of the ' +
           'overflow menu', done => {
        flush(() => {
          assert.isNotOk(element.shadowRoot
              .querySelector('[data-action-key="private"]'));
          done();
        });
      });

      test('private change', done => {
        flush(() => {
          assert.isOk(
              element.$.moreActions.shadowRoot
                  .querySelector('span[data-id="private-change"]'));
          element.setActionOverflow('change', 'private', false);
          flush();
          assert.isOk(element.shadowRoot
              .querySelector('[data-action-key="private"]'));
          assert.isNotOk(
              element.$.moreActions.shadowRoot
                  .querySelector('span[data-id="private-change"]'));
          done();
        });
      });
    });

    suite('unmark private change', () => {
      setup(() => {
        const unmarkPrivateAction = {
          __key: 'private.delete',
          __type: 'change',
          __primary: false,
          method: 'POST',
          label: 'Unmark private',
          title: 'Working...',
          enabled: true,
        };

        element.actions = {
          'private.delete': unmarkPrivateAction,
        };

        element.change.is_private = true;

        element.changeNum = '2';
        element.latestPatchNum = '2';

        return element.reload();
      });

      test('make sure the unmark private change button is not outside of the ' +
           'overflow menu', done => {
        flush(() => {
          assert.isNotOk(element.shadowRoot
              .querySelector('[data-action-key="private.delete"]'));
          done();
        });
      });

      test('unmark the private change', done => {
        flush(() => {
          assert.isOk(
              element.$.moreActions.shadowRoot
                  .querySelector('span[data-id="private.delete-change"]')
          );
          element.setActionOverflow('change', 'private.delete', false);
          flush();
          assert.isOk(element.shadowRoot
              .querySelector('[data-action-key="private.delete"]'));
          assert.isNotOk(
              element.$.moreActions.shadowRoot
                  .querySelector('span[data-id="private.delete-change"]')
          );
          done();
        });
      });
    });

    suite('delete change', () => {
      let fireActionStub;
      let deleteAction;

      setup(() => {
        fireActionStub = sinon.stub(element, '_fireAction');
        element.change = {
          current_revision: 'abc1234',
        };
        deleteAction = {
          method: 'DELETE',
          label: 'Delete Change',
          title: 'Delete change X_X',
          enabled: true,
        };
        element.actions = {
          '/': deleteAction,
        };
      });

      test('does not delete on action', () => {
        element._handleDeleteTap();
        assert.isFalse(fireActionStub.called);
      });

      test('shows confirm dialog', () => {
        element._handleDeleteTap();
        assert.isFalse(element.shadowRoot
            .querySelector('#confirmDeleteDialog').hidden);
        MockInteractions.tap(
            element.shadowRoot
                .querySelector('#confirmDeleteDialog')
                .shadowRoot
                .querySelector('gr-button[primary]'));
        flush();
        assert.isTrue(fireActionStub.calledWith('/', deleteAction, false));
      });

      test('hides delete confirm on cancel', () => {
        element._handleDeleteTap();
        MockInteractions.tap(
            element.shadowRoot
                .querySelector('#confirmDeleteDialog')
                .shadowRoot
                .querySelector('gr-button:not([primary])'));
        flush();
        assert.isTrue(element.shadowRoot
            .querySelector('#confirmDeleteDialog').hidden);
        assert.isFalse(fireActionStub.called);
      });
    });

    suite('ignore change', () => {
      setup(done => {
        sinon.stub(element, '_fireAction');

        const IgnoreAction = {
          __key: 'ignore',
          __type: 'change',
          __primary: false,
          method: 'PUT',
          label: 'Ignore',
          title: 'Working...',
          enabled: true,
        };

        element.actions = {
          ignore: IgnoreAction,
        };

        element.changeNum = '2';
        element.latestPatchNum = '2';

        element.reload().then(() => { flush(done); });
      });

      test('make sure the ignore button is not outside of the overflow menu',
          () => {
            assert.isNotOk(element.shadowRoot
                .querySelector('[data-action-key="ignore"]'));
          });

      test('ignoring change', () => {
        assert.isOk(element.$.moreActions.shadowRoot
            .querySelector('span[data-id="ignore-change"]'));
        element.setActionOverflow('change', 'ignore', false);
        flush();
        assert.isOk(element.shadowRoot
            .querySelector('[data-action-key="ignore"]'));
        assert.isNotOk(
            element.$.moreActions.shadowRoot
                .querySelector('span[data-id="ignore-change"]'));
      });
    });

    suite('unignore change', () => {
      setup(done => {
        sinon.stub(element, '_fireAction');

        const UnignoreAction = {
          __key: 'unignore',
          __type: 'change',
          __primary: false,
          method: 'PUT',
          label: 'Unignore',
          title: 'Working...',
          enabled: true,
        };

        element.actions = {
          unignore: UnignoreAction,
        };

        element.changeNum = '2';
        element.latestPatchNum = '2';

        element.reload().then(() => { flush(done); });
      });

      test('unignore button is not outside of the overflow menu', () => {
        assert.isNotOk(element.shadowRoot
            .querySelector('[data-action-key="unignore"]'));
      });

      test('unignoring change', () => {
        assert.isOk(
            element.$.moreActions.shadowRoot
                .querySelector('span[data-id="unignore-change"]'));
        element.setActionOverflow('change', 'unignore', false);
        flush();
        assert.isOk(element.shadowRoot
            .querySelector('[data-action-key="unignore"]'));
        assert.isNotOk(
            element.$.moreActions.shadowRoot
                .querySelector('span[data-id="unignore-change"]'));
      });
    });

    suite('reviewed change', () => {
      setup(done => {
        sinon.stub(element, '_fireAction');

        const ReviewedAction = {
          __key: 'reviewed',
          __type: 'change',
          __primary: false,
          method: 'PUT',
          label: 'Mark reviewed',
          title: 'Working...',
          enabled: true,
        };

        element.actions = {
          reviewed: ReviewedAction,
        };

        element.changeNum = '2';
        element.latestPatchNum = '2';

        element.reload().then(() => { flush(done); });
      });

      test('action is enabled', () => {
        assert.equal(element._allActionValues.filter(action =>
          action.__key === 'reviewed').length, 1);
      });

      test('action is skipped when attention set is enabled', () => {
        element._config = {
          change: {enable_attention_set: true},
        };
        assert.equal(element._allActionValues.filter(action =>
          action.__key === 'reviewed').length, 0);
      });

      test('make sure the reviewed button is not outside of the overflow menu',
          () => {
            assert.isNotOk(element.shadowRoot
                .querySelector('[data-action-key="reviewed"]'));
          });

      test('reviewing change', () => {
        assert.isOk(
            element.$.moreActions.shadowRoot
                .querySelector('span[data-id="reviewed-change"]'));
        element.setActionOverflow('change', 'reviewed', false);
        flush();
        assert.isOk(element.shadowRoot
            .querySelector('[data-action-key="reviewed"]'));
        assert.isNotOk(
            element.$.moreActions.shadowRoot
                .querySelector('span[data-id="reviewed-change"]'));
      });
    });

    suite('unreviewed change', () => {
      setup(done => {
        sinon.stub(element, '_fireAction');

        const UnreviewedAction = {
          __key: 'unreviewed',
          __type: 'change',
          __primary: false,
          method: 'PUT',
          label: 'Mark unreviewed',
          title: 'Working...',
          enabled: true,
        };

        element.actions = {
          unreviewed: UnreviewedAction,
        };

        element.changeNum = '2';
        element.latestPatchNum = '2';

        element.reload().then(() => { flush(done); });
      });

      test('unreviewed button not outside of the overflow menu', () => {
        assert.isNotOk(element.shadowRoot
            .querySelector('[data-action-key="unreviewed"]'));
      });

      test('unreviewed change', () => {
        assert.isOk(
            element.$.moreActions.shadowRoot
                .querySelector('span[data-id="unreviewed-change"]'));
        element.setActionOverflow('change', 'unreviewed', false);
        flush();
        assert.isOk(element.shadowRoot
            .querySelector('[data-action-key="unreviewed"]'));
        assert.isNotOk(
            element.$.moreActions.shadowRoot
                .querySelector('span[data-id="unreviewed-change"]'));
      });
    });

    suite('quick approve', () => {
      setup(() => {
        element.change = {
          current_revision: 'abc1234',
          labels: {
            foo: {
              values: {
                '-1': '',
                ' 0': '',
                '+1': '',
              },
            },
          },
          permitted_labels: {
            foo: ['-1', ' 0', '+1'],
          },
        };
        flush();
      });

      test('added when can approve', () => {
        const approveButton =
            element.shadowRoot
                .querySelector('gr-button[data-action-key=\'review\']');
        assert.isNotNull(approveButton);
      });

      test('hide quick approve', () => {
        const approveButton =
            element.shadowRoot
                .querySelector('gr-button[data-action-key=\'review\']');
        assert.isNotNull(approveButton);
        assert.isFalse(element._hideQuickApproveAction);

        // Assert approve button gets removed from list of buttons.
        element.hideQuickApproveAction();
        flush();
        const approveButtonUpdated =
            element.shadowRoot
                .querySelector('gr-button[data-action-key=\'review\']');
        assert.isNull(approveButtonUpdated);
        assert.isTrue(element._hideQuickApproveAction);
      });

      test('is first in list of secondary actions', () => {
        const approveButton = element.$.secondaryActions
            .querySelector('gr-button');
        assert.equal(approveButton.getAttribute('data-label'), 'foo+1');
      });

      test('not added when change is merged', () => {
        element.change.status = ChangeStatus.MERGED;
        flush(() => {
          const approveButton =
          element.shadowRoot
              .querySelector('gr-button[data-action-key=\'review\']');
          assert.isNull(approveButton);
        });
      });

      test('not added when already approved', () => {
        element.change = {
          current_revision: 'abc1234',
          labels: {
            foo: {
              approved: {},
              values: {},
            },
          },
          permitted_labels: {
            foo: [' 0', '+1'],
          },
        };
        flush();
        const approveButton =
            element.shadowRoot
                .querySelector('gr-button[data-action-key=\'review\']');
        assert.isNull(approveButton);
      });

      test('not added when label not permitted', () => {
        element.change = {
          current_revision: 'abc1234',
          labels: {
            foo: {values: {}},
          },
          permitted_labels: {
            bar: [],
          },
        };
        flush();
        const approveButton =
            element.shadowRoot
                .querySelector('gr-button[data-action-key=\'review\']');
        assert.isNull(approveButton);
      });

      test('approves when tapped', () => {
        const fireActionStub = sinon.stub(element, '_fireAction');
        MockInteractions.tap(
            element.shadowRoot
                .querySelector('gr-button[data-action-key=\'review\']'));
        flush();
        assert.isTrue(fireActionStub.called);
        assert.isTrue(fireActionStub.calledWith('/review'));
        const payload = fireActionStub.lastCall.args[3];
        assert.deepEqual(payload.labels, {foo: 1});
      });

      test('not added when multiple labels are required without code review',
          () => {
            element.change = {
              current_revision: 'abc1234',
              labels: {
                foo: {values: {}},
                bar: {values: {}},
              },
              permitted_labels: {
                foo: [' 0', '+1'],
                bar: [' 0', '+1', '+2'],
              },
            };
            flush();
            const approveButton =
            element.shadowRoot
                .querySelector('gr-button[data-action-key=\'review\']');
            assert.isNull(approveButton);
          });

      test('code review shown with multiple missing approval', () => {
        element.change = {
          current_revision: 'abc1234',
          labels: {
            'foo': {values: {}},
            'bar': {values: {}},
            'Code-Review': {
              approved: {},
              values: {
                ' 0': '',
                '+1': '',
                '+2': '',
              },
            },
          },
          permitted_labels: {
            'foo': [' 0', '+1'],
            'bar': [' 0', '+1', '+2'],
            'Code-Review': [' 0', '+1', '+2'],
          },
        };
        flush();
        const approveButton =
            element.shadowRoot
                .querySelector('gr-button[data-action-key=\'review\']');
        assert.isOk(approveButton);
      });

      test('button label for missing approval', () => {
        element.change = {
          current_revision: 'abc1234',
          labels: {
            foo: {
              values: {
                ' 0': '',
                '+1': '',
              },
            },
            bar: {approved: {}, values: {}},
          },
          permitted_labels: {
            foo: [' 0', '+1'],
            bar: [' 0', '+1', '+2'],
          },
        };
        flush();
        const approveButton =
            element.shadowRoot
                .querySelector('gr-button[data-action-key=\'review\']');
        assert.equal(approveButton.getAttribute('data-label'), 'foo+1');
      });

      test('no quick approve if score is not maximal for a label', () => {
        element.change = {
          current_revision: 'abc1234',
          labels: {
            bar: {
              value: 1,
              values: {
                ' 0': '',
                '+1': '',
                '+2': '',
              },
            },
          },
          permitted_labels: {
            bar: [' 0', '+1'],
          },
        };
        flush();
        const approveButton =
            element.shadowRoot
                .querySelector('gr-button[data-action-key=\'review\']');
        assert.isNull(approveButton);
      });

      test('approving label with a non-max score', () => {
        element.change = {
          current_revision: 'abc1234',
          labels: {
            bar: {
              value: 1,
              values: {
                ' 0': '',
                '+1': '',
                '+2': '',
              },
            },
          },
          permitted_labels: {
            bar: [' 0', '+1', '+2'],
          },
        };
        flush();
        const approveButton =
            element.shadowRoot
                .querySelector('gr-button[data-action-key=\'review\']');
        assert.equal(approveButton.getAttribute('data-label'), 'bar+2');
      });

      test('added when can approve an already-approved code review label',
          () => {
            element.change = {
              current_revision: 'abc1234',
              labels: {
                'Code-Review': {
                  approved: {},
                  values: {
                    ' 0': '',
                    '+1': '',
                    '+2': '',
                  },
                },
              },
              permitted_labels: {
                'Code-Review': [' 0', '+1', '+2'],
              },
            };
            flush();
            const approveButton =
                element.shadowRoot
                    .querySelector('gr-button[data-action-key=\'review\']');
            assert.isNotNull(approveButton);
          });

      test('not added when the user has already approved', () => {
        const vote = {
          ...createApproval(),
          _account_id: 123,
          name: 'name',
          value: 2,
        };
        element.change = {
          current_revision: 'abc1234',
          labels: {
            'Code-Review': {
              approved: {},
              values: {
                ' 0': '',
                '+1': '',
                '+2': '',
              },
              all: [vote],
            },
          },
          permitted_labels: {
            'Code-Review': [' 0', '+1', '+2'],
          },
        };
        flush();
        const approveButton =
            element.shadowRoot
                .querySelector('gr-button[data-action-key=\'review\']');
        assert.isNull(approveButton);
      });

      test('not added when user owns the change', () => {
        element.change = {
          current_revision: 'abc1234',
          owner: createAccountWithId(123),
          labels: {
            'Code-Review': {
              approved: {},
              values: {
                ' 0': '',
                '+1': '',
                '+2': '',
              },
            },
          },
          permitted_labels: {
            'Code-Review': [' 0', '+1', '+2'],
          },
        };
        flush();
        const approveButton =
            element.shadowRoot
                .querySelector('gr-button[data-action-key=\'review\']');
        assert.isNull(approveButton);
      });
    });

    test('adds download revision action', () => {
      const handler = sinon.stub();
      element.addEventListener('download-tap', handler);
      assert.ok(element.revisionActions.download);
      element._handleDownloadTap();
      flush();

      assert.isTrue(handler.called);
    });

    test('changing changeNum or patchNum does not reload', () => {
      const reloadStub = sinon.stub(element, 'reload');
      element.changeNum = 123;
      assert.isFalse(reloadStub.called);
      element.latestPatchNum = 456;
      assert.isFalse(reloadStub.called);
    });

    test('_toSentenceCase', () => {
      assert.equal(element._toSentenceCase('blah blah'), 'Blah blah');
      assert.equal(element._toSentenceCase('BLAH BLAH'), 'Blah blah');
      assert.equal(element._toSentenceCase('b'), 'B');
      assert.equal(element._toSentenceCase(''), '');
      assert.equal(element._toSentenceCase('!@#$%^&*()'), '!@#$%^&*()');
    });

    suite('setActionOverflow', () => {
      test('move action from overflow', () => {
        assert.isNotOk(element.shadowRoot
            .querySelector('[data-action-key="cherrypick"]'));
        assert.strictEqual(
            element.$.moreActions.items[0].id, 'cherrypick-revision');
        element.setActionOverflow('revision', 'cherrypick', false);
        flush();
        assert.isOk(element.shadowRoot
            .querySelector('[data-action-key="cherrypick"]'));
        assert.notEqual(
            element.$.moreActions.items[0].id, 'cherrypick-revision');
      });

      test('move action to overflow', () => {
        assert.isOk(element.shadowRoot
            .querySelector('[data-action-key="submit"]'));
        element.setActionOverflow('revision', 'submit', true);
        flush();
        assert.isNotOk(element.shadowRoot
            .querySelector('[data-action-key="submit"]'));
        assert.strictEqual(
            element.$.moreActions.items[3].id, 'submit-revision');
      });

      suite('_waitForChangeReachable', () => {
        let clock;
        setup(() => {
          clock = sinon.useFakeTimers();
        });

        const makeGetChange = numTries => () => {
          if (numTries === 1) {
            return Promise.resolve({_number: 123});
          } else {
            numTries--;
            return Promise.resolve(undefined);
          }
        };

        const tickAndFlush = async repetitions => {
          for (let i = 1; i <= repetitions; i++) {
            clock.tick(1000);
            await flush();
          }
        };

        test('succeed', async () => {
          stubRestApi('getChange').callsFake(makeGetChange(5));
          const promise = element._waitForChangeReachable(123);
          tickAndFlush(5);
          const success = await promise;
          assert.isTrue(success);
        });

        test('fail', async () => {
          stubRestApi('getChange').callsFake(makeGetChange(6));
          const promise = element._waitForChangeReachable(123);
          tickAndFlush(6);
          const success = await promise;
          assert.isFalse(success);
        });
      });
    });

    suite('_send', () => {
      let cleanup;
      let payload;
      let onShowError;
      let onShowAlert;
      let getResponseObjectStub;

      setup(() => {
        cleanup = sinon.stub();
        element.changeNum = 42;
        element.change._number = 42;
        element.latestPatchNum = 12;
        element.change = {
          ...createChange(),
          revisions: createRevisions(element.latestPatchNum),
          messages: createChangeMessages(1),
        };
        payload = {foo: 'bar'};

        onShowError = sinon.stub();
        element.addEventListener('show-error', onShowError);
        onShowAlert = sinon.stub();
        element.addEventListener('show-alert', onShowAlert);
      });

      suite('happy path', () => {
        let sendStub;
        setup(() => {
          stubRestApi('getChangeDetail')
              .returns(Promise.resolve({
                ...createChange(),
                // element has latest info
                revisions: createRevisions(element.latestPatchNum),
                messages: createChangeMessages(1),
              }));
          sendStub = stubRestApi('executeChangeAction')
              .returns(Promise.resolve({}));
          getResponseObjectStub = stubRestApi(
              'getResponseObject');
          sinon.stub(GerritNav,
              'navigateToChange').returns(Promise.resolve(true));
        });

        test('change action', async () => {
          await element._send('DELETE', payload, '/endpoint', false, cleanup);
          assert.isFalse(onShowError.called);
          assert.isTrue(cleanup.calledOnce);
          assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
              undefined, payload));
        });

        suite('show revert submission dialog', () => {
          setup(() => {
            element.change.submission_id = '199';
            element.change.current_revision = '2000';
            stubRestApi('getChanges')
                .returns(Promise.resolve([
                  {change_id: '12345678901234', topic: 'T', subject: 'random'},
                  {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
                ]));
          });

          test('revert submission shows submissionId', done => {
            const expectedMsg = 'Revert submission 199' + '\n\n' +
              'Reason for revert: <INSERT REASONING HERE>' + '\n' +
              'Reverted Changes:' + '\n' +
              '1234567890: random' + '\n' +
              '23456: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
              '\n';
            const modifiedMsg = expectedMsg + 'abcd';
            sinon.stub(element.$.confirmRevertSubmissionDialog,
                '_modifyRevertSubmissionMsg').returns(modifiedMsg);
            element.showRevertSubmissionDialog();
            flush(() => {
              const msg = element.$.confirmRevertSubmissionDialog.message;
              assert.equal(msg, modifiedMsg);
              done();
            });
          });
        });

        suite('single changes revert', () => {
          let navigateToSearchQueryStub;
          setup(() => {
            getResponseObjectStub
                .returns(Promise.resolve({revert_changes: [
                  {change_id: 12345},
                ]}));
            navigateToSearchQueryStub = sinon.stub(GerritNav,
                'navigateToSearchQuery');
          });

          test('revert submission single change', done => {
            element._send('POST', {message: 'Revert submission'},
                '/revert_submission', false, cleanup).then(res => {
              element._handleResponse({__key: 'revert_submission'}, {}).
                  then(() => {
                    assert.isTrue(navigateToSearchQueryStub.called);
                    done();
                  });
            });
          });
        });

        suite('multiple changes revert', () => {
          let showActionDialogStub;
          let navigateToSearchQueryStub;
          setup(() => {
            getResponseObjectStub
                .returns(Promise.resolve({revert_changes: [
                  {change_id: 12345, topic: 'T'},
                  {change_id: 23456, topic: 'T'},
                ]}));
            showActionDialogStub = sinon.stub(element, '_showActionDialog');
            navigateToSearchQueryStub = sinon.stub(GerritNav,
                'navigateToSearchQuery');
          });

          test('revert submission multiple change', done => {
            element._send('POST', {message: 'Revert submission'},
                '/revert_submission', false, cleanup).then(res => {
              element._handleResponse({__key: 'revert_submission'}, {}).then(
                  () => {
                    assert.isFalse(showActionDialogStub.called);
                    assert.isTrue(navigateToSearchQueryStub.calledWith(
                        'topic: T'));
                    done();
                  });
            });
          });
        });

        test('revision action', done => {
          element
              ._send('DELETE', payload, '/endpoint', true, cleanup)
              .then(() => {
                assert.isFalse(onShowError.called);
                assert.isTrue(cleanup.calledOnce);
                assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
                    12, payload));
                done();
              });
        });
      });

      suite('failure modes', () => {
        test('non-latest', () => {
          stubRestApi('getChangeDetail')
              .returns(Promise.resolve({
                ...createChange(),
                // new patchset was uploaded
                revisions: createRevisions(element.latestPatchNum + 1),
                messages: createChangeMessages(1),
              }));
          const sendStub = stubRestApi(
              'executeChangeAction');

          return element._send('DELETE', payload, '/endpoint', true, cleanup)
              .then(() => {
                assert.isTrue(onShowAlert.calledOnce);
                assert.isFalse(onShowError.called);
                assert.isTrue(cleanup.calledOnce);
                assert.isFalse(sendStub.called);
              });
        });

        test('send fails', () => {
          stubRestApi('getChangeDetail')
              .returns(Promise.resolve({
                ...createChange(),
                // element has latest info
                revisions: createRevisions(element.latestPatchNum),
                messages: createChangeMessages(1),
              }));
          const sendStub = stubRestApi(
              'executeChangeAction').callsFake(
              (num, method, patchNum, endpoint, payload, onErr) => {
                onErr();
                return Promise.resolve(null);
              });
          const handleErrorStub = sinon.stub(element, '_handleResponseError');

          return element._send('DELETE', payload, '/endpoint', true, cleanup)
              .then(() => {
                assert.isFalse(onShowError.called);
                assert.isTrue(cleanup.called);
                assert.isTrue(sendStub.calledOnce);
                assert.isTrue(handleErrorStub.called);
              });
        });
      });
    });

    test('_handleAction reports', () => {
      sinon.stub(element, '_fireAction');
      element.actions = {
        key: {
          __key: 'key',
          __type: 'type',
        },
      };

      const reportStub = sinon.stub(element.reporting, 'reportInteraction');
      element._handleAction('type', 'key');
      assert.isTrue(reportStub.called);
      assert.equal(reportStub.lastCall.args[0], 'type-key');
    });
  });

  suite('getChangeRevisionActions returns only some actions', () => {
    let element;

    let changeRevisionActions;

    setup(() => {
      stubRestApi('getChangeRevisionActions').returns(
          Promise.resolve(changeRevisionActions));
      stubRestApi('send').returns(Promise.reject(new Error('error')));
      stubRestApi('getProjectConfig').returns(Promise.resolve({}));

      sinon.stub(getPluginLoader(), 'awaitPluginsLoaded')
          .returns(Promise.resolve());

      element = basicFixture.instantiate();
      // getChangeRevisionActions is not called without
      // set the following properties
      element.change = {};
      element.changeNum = '42';
      element.latestPatchNum = '2';

      stubRestApi('getRepoBranches').returns(Promise.resolve([]));
      return element.reload();
    });

    test('confirmSubmitDialog and confirmRebase properties are changed', () => {
      changeRevisionActions = {};
      element.reload();
      assert.strictEqual(element.$.confirmSubmitDialog.action, null);
      assert.strictEqual(element.$.confirmRebase.rebaseOnCurrent, null);
    });

    test('_computeRebaseOnCurrent', () => {
      const rebaseAction = {
        enabled: true,
        label: 'Rebase',
        method: 'POST',
        title: 'Rebase onto tip of branch or parent change',
      };

      // When rebase is enabled initially, rebaseOnCurrent should be set to
      // true.
      assert.isTrue(element._computeRebaseOnCurrent(rebaseAction));

      delete rebaseAction.enabled;

      // When rebase is not enabled initially, rebaseOnCurrent should be set to
      // false.
      assert.isFalse(element._computeRebaseOnCurrent(rebaseAction));
    });
  });
});

