/**
 * @license
 * Copyright (C) 2015 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-comment.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import {__testOnly_UNSAVED_MESSAGE} from './gr-comment.js';
import {SpecialFilePath, Side} from '../../../constants/constants.js';
import {stubRestApi, stubStorage} from '../../../test/test-utils.js';

const basicFixture = fixtureFromElement('gr-comment');

const draftFixture = fixtureFromTemplate(html`
<gr-comment draft="true"></gr-comment>
`);

function isVisible(el) {
  assert.ok(el);
  return getComputedStyle(el).getPropertyValue('display') !== 'none';
}

suite('gr-comment tests', () => {
  suite('basic tests', () => {
    let element;

    let openOverlaySpy;

    setup(() => {
      stubRestApi('getAccount').returns(Promise.resolve({
        email: 'dhruvsri@google.com',
        name: 'Dhruv Srivastava',
        _account_id: 1083225,
        avatars: [{url: 'abc', height: 32}],
      }));
      element = basicFixture.instantiate();
      element.comment = {
        author: {
          name: 'Mr. Peanutbutter',
          email: 'tenn1sballchaser@aol.com',
        },
        id: 'baf0414d_60047215',
        line: 5,
        message: 'is this a crossover episode!?',
        updated: '2015-12-08 19:48:33.843000000',
      };

      openOverlaySpy = sinon.spy(element, '_openOverlay');
    });

    teardown(() => {
      openOverlaySpy.getCalls().forEach(call => {
        call.args[0].remove();
      });
    });

    test('collapsible comments', () => {
      // When a comment (not draft) is loaded, it should be collapsed
      assert.isTrue(element.collapsed);
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('gr-formatted-text')),
      'gr-formatted-text is not visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.actions')),
      'actions are not visible');
      assert.isNotOk(element.textarea, 'textarea is not visible');

      // The header middle content is only visible when comments are collapsed.
      // It shows the message in a condensed way, and limits to a single line.
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.collapsedContent')),
      'header middle content is visible');

      // When the header row is clicked, the comment should expand
      MockInteractions.tap(element.$.header);
      assert.isFalse(element.collapsed);
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('gr-formatted-text')),
      'gr-formatted-text is visible');
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.actions')),
      'actions are visible');
      assert.isNotOk(element.textarea, 'textarea is not visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.collapsedContent')),
      'header middle content is not visible');
    });

    test('clicking on date link fires event', () => {
      element.side = 'PARENT';
      const stub = sinon.stub();
      element.addEventListener('comment-anchor-tap', stub);
      flush();
      const dateEl = element.shadowRoot
          .querySelector('.date');
      assert.ok(dateEl);
      MockInteractions.tap(dateEl);

      assert.isTrue(stub.called);
      assert.deepEqual(stub.lastCall.args[0].detail,
          {side: element.side, number: element.comment.line});
    });

    test('message is not retrieved from storage when other edits', done => {
      const storageStub = stubStorage('getDraftComment');
      const loadSpy = sinon.spy(element, '_loadLocalDraft');

      element.changeNum = 1;
      element.patchNum = 1;
      element.comment = {
        author: {
          name: 'Mr. Peanutbutter',
          email: 'tenn1sballchaser@aol.com',
        },
        line: 5,
      };
      flush(() => {
        assert.isTrue(loadSpy.called);
        assert.isFalse(storageStub.called);
        done();
      });
    });

    test('message is retrieved from storage when no other edits', done => {
      const storageStub = stubStorage('getDraftComment');
      const loadSpy = sinon.spy(element, '_loadLocalDraft');

      element.changeNum = 1;
      element.patchNum = 1;
      element.comment = {
        author: {
          name: 'Mr. Peanutbutter',
          email: 'tenn1sballchaser@aol.com',
        },
        line: 5,
        path: 'test',
      };
      flush(() => {
        assert.isTrue(loadSpy.called);
        assert.isTrue(storageStub.called);
        done();
      });
    });

    test('_getPatchNum', () => {
      element.side = 'PARENT';
      element.patchNum = 1;
      assert.equal(element._getPatchNum(), 'PARENT');
      element.side = 'REVISION';
      assert.equal(element._getPatchNum(), 1);
    });

    test('comment expand and collapse', () => {
      element.collapsed = true;
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('gr-formatted-text')),
      'gr-formatted-text is not visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.actions')),
      'actions are not visible');
      assert.isNotOk(element.textarea, 'textarea is not visible');
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.collapsedContent')),
      'header middle content is visible');

      element.collapsed = false;
      assert.isFalse(element.collapsed);
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('gr-formatted-text')),
      'gr-formatted-text is visible');
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.actions')),
      'actions are visible');
      assert.isNotOk(element.textarea, 'textarea is not visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.collapsedContent')),
      'header middle content is is not visible');
    });

    suite('while editing', () => {
      setup(() => {
        element.editing = true;
        element._messageText = 'test';
        sinon.stub(element, '_handleCancel');
        sinon.stub(element, '_handleSave');
        flush();
      });

      suite('when text is empty', () => {
        setup(() => {
          element._messageText = '';
          element.comment = {};
        });

        test('esc closes comment when text is empty', () => {
          MockInteractions.pressAndReleaseKeyOn(
              element.textarea, 27); // esc
          assert.isTrue(element._handleCancel.called);
        });

        test('ctrl+enter does not save', () => {
          MockInteractions.pressAndReleaseKeyOn(
              element.textarea, 13, 'ctrl'); // ctrl + enter
          assert.isFalse(element._handleSave.called);
        });

        test('meta+enter does not save', () => {
          MockInteractions.pressAndReleaseKeyOn(
              element.textarea, 13, 'meta'); // meta + enter
          assert.isFalse(element._handleSave.called);
        });

        test('ctrl+s does not save', () => {
          MockInteractions.pressAndReleaseKeyOn(
              element.textarea, 83, 'ctrl'); // ctrl + s
          assert.isFalse(element._handleSave.called);
        });
      });

      test('esc does not close comment that has content', () => {
        MockInteractions.pressAndReleaseKeyOn(
            element.textarea, 27); // esc
        assert.isFalse(element._handleCancel.called);
      });

      test('ctrl+enter saves', () => {
        MockInteractions.pressAndReleaseKeyOn(
            element.textarea, 13, 'ctrl'); // ctrl + enter
        assert.isTrue(element._handleSave.called);
      });

      test('meta+enter saves', () => {
        MockInteractions.pressAndReleaseKeyOn(
            element.textarea, 13, 'meta'); // meta + enter
        assert.isTrue(element._handleSave.called);
      });

      test('ctrl+s saves', () => {
        MockInteractions.pressAndReleaseKeyOn(
            element.textarea, 83, 'ctrl'); // ctrl + s
        assert.isTrue(element._handleSave.called);
      });
    });

    test('delete comment button for non-admins is hidden', () => {
      element._isAdmin = false;
      assert.isFalse(element.shadowRoot
          .querySelector('.action.delete')
          .classList.contains('showDeleteButtons'));
    });

    test('delete comment button for admins with draft is hidden', () => {
      element._isAdmin = false;
      element.draft = true;
      assert.isFalse(element.shadowRoot
          .querySelector('.action.delete')
          .classList.contains('showDeleteButtons'));
    });

    test('delete comment', done => {
      const stub = stubRestApi('deleteComment').returns(Promise.resolve({}));
      sinon.spy(element.confirmDeleteOverlay, 'open');
      element.changeNum = 42;
      element.patchNum = 0xDEADBEEF;
      element._isAdmin = true;
      assert.isTrue(element.shadowRoot
          .querySelector('.action.delete')
          .classList.contains('showDeleteButtons'));
      MockInteractions.tap(element.shadowRoot
          .querySelector('.action.delete'));
      flush(() => {
        element.confirmDeleteOverlay.open.lastCall.returnValue.then(() => {
          const dialog =
              window.confirmDeleteOverlay
                  .querySelector('#confirmDeleteComment');
          dialog.message = 'removal reason';
          element._handleConfirmDeleteComment();
          assert.isTrue(stub.calledWith(
              42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason'));
          done();
        });
      });
    });

    suite('draft update reporting', () => {
      let endStub;
      let getTimerStub;
      let mockEvent;

      setup(() => {
        mockEvent = {preventDefault() {}};
        sinon.stub(element, 'save')
            .returns(Promise.resolve({}));
        sinon.stub(element, '_discardDraft')
            .returns(Promise.resolve({}));
        endStub = sinon.stub();
        getTimerStub = sinon.stub(element.reporting, 'getTimer')
            .returns({end: endStub});
      });

      test('create', () => {
        element.patchNum = 1;
        element.comment = {};
        return element._handleSave(mockEvent).then(() => {
          assert.equal(element.shadowRoot.querySelector('gr-account-label').
              shadowRoot.querySelector('span.name').innerText.trim(),
          'Dhruv Srivastava');
          assert.isTrue(endStub.calledOnce);
          assert.isTrue(getTimerStub.calledOnce);
          assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment');
        });
      });

      test('update', () => {
        element.comment = {id: 'abc_123'};
        return element._handleSave(mockEvent).then(() => {
          assert.isTrue(endStub.calledOnce);
          assert.isTrue(getTimerStub.calledOnce);
          assert.equal(getTimerStub.lastCall.args[0], 'UpdateDraftComment');
        });
      });

      test('discard', () => {
        element.comment = {id: 'abc_123'};
        sinon.stub(element, '_closeConfirmDiscardOverlay');
        return element._handleConfirmDiscard(mockEvent).then(() => {
          assert.isTrue(endStub.calledOnce);
          assert.isTrue(getTimerStub.calledOnce);
          assert.equal(getTimerStub.lastCall.args[0], 'DiscardDraftComment');
        });
      });
    });

    test('edit reports interaction', () => {
      const reportStub = sinon.stub(element.reporting,
          'recordDraftInteraction');
      element.draft = true;
      flush();
      MockInteractions.tap(element.shadowRoot
          .querySelector('.edit'));
      assert.isTrue(reportStub.calledOnce);
    });

    test('discard reports interaction', () => {
      const reportStub = sinon.stub(element.reporting,
          'recordDraftInteraction');
      element.draft = true;
      flush();
      MockInteractions.tap(element.shadowRoot
          .querySelector('.discard'));
      assert.isTrue(reportStub.calledOnce);
    });

    test('failed save draft request', done => {
      element.draft = true;
      element.changeNum = 1;
      element.patchNum = 1;
      const updateRequestStub = sinon.stub(element, '_updateRequestToast');
      const diffDraftStub =
        stubRestApi('saveDiffDraft').returns(
            Promise.resolve({ok: false}));
      element._saveDraft({id: 'abc_123'});
      flush(() => {
        let args = updateRequestStub.lastCall.args;
        assert.deepEqual(args, [0, true]);
        assert.equal(element._getSavingMessage(...args),
            __testOnly_UNSAVED_MESSAGE);
        assert.equal(element.shadowRoot.querySelector('.draftLabel').innerText,
            'DRAFT(Failed to save)');
        assert.isTrue(isVisible(element.shadowRoot
            .querySelector('.save')), 'save is visible');
        diffDraftStub.returns(
            Promise.resolve({ok: true}));
        element._saveDraft({id: 'abc_123'});
        flush(() => {
          args = updateRequestStub.lastCall.args;
          assert.deepEqual(args, [0]);
          assert.equal(element._getSavingMessage(...args),
              'All changes saved');
          assert.equal(element.shadowRoot.querySelector('.draftLabel')
              .innerText, 'DRAFT');
          assert.isFalse(isVisible(element.shadowRoot
              .querySelector('.save')), 'save is not visible');
          assert.isFalse(element._unableToSave);
          done();
        });
      });
    });

    test('failed save draft request with promise failure', done => {
      element.draft = true;
      element.changeNum = 1;
      element.patchNum = 1;
      const updateRequestStub = sinon.stub(element, '_updateRequestToast');
      const diffDraftStub =
        stubRestApi('saveDiffDraft').returns(
            Promise.reject(new Error()));
      element._saveDraft({id: 'abc_123'});
      flush(() => {
        let args = updateRequestStub.lastCall.args;
        assert.deepEqual(args, [0, true]);
        assert.equal(element._getSavingMessage(...args),
            __testOnly_UNSAVED_MESSAGE);
        assert.equal(element.shadowRoot.querySelector('.draftLabel').innerText,
            'DRAFT(Failed to save)');
        assert.isTrue(isVisible(element.shadowRoot
            .querySelector('.save')), 'save is visible');
        diffDraftStub.returns(
            Promise.resolve({ok: true}));
        element._saveDraft({id: 'abc_123'});
        flush(() => {
          args = updateRequestStub.lastCall.args;
          assert.deepEqual(args, [0]);
          assert.equal(element._getSavingMessage(...args),
              'All changes saved');
          assert.equal(element.shadowRoot.querySelector('.draftLabel')
              .innerText, 'DRAFT');
          assert.isFalse(isVisible(element.shadowRoot
              .querySelector('.save')), 'save is not visible');
          assert.isFalse(element._unableToSave);
          done();
        });
      });
    });
  });

  suite('gr-comment draft tests', () => {
    let element;

    setup(() => {
      stubRestApi('getAccount').returns(Promise.resolve(null));
      stubRestApi('saveDiffDraft').returns(Promise.resolve({
        ok: true,
        text() {
          return Promise.resolve(
              ')]}\'\n{' +
              '"id": "baf0414d_40572e03",' +
              '"path": "/path/to/file",' +
              '"line": 5,' +
              '"updated": "2015-12-08 21:52:36.177000000",' +
              '"message": "saved!"' +
              '}'
          );
        },
      }));
      stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
      element = draftFixture.instantiate();
      sinon.stub(element.storage, 'getDraftComment').returns(null);
      element.changeNum = 42;
      element.patchNum = 1;
      element.editing = false;
      element.comment = {
        diffSide: Side.RIGHT,
        __draft: true,
        __draftID: 'temp_draft_id',
        path: '/path/to/file',
        line: 5,
      };
      element.diffSide = Side.RIGHT;
    });

    test('button visibility states', () => {
      element.showActions = false;
      assert.isTrue(element.shadowRoot
          .querySelector('.humanActions').hasAttribute('hidden'));
      assert.isTrue(element.shadowRoot
          .querySelector('.robotActions').hasAttribute('hidden'));

      element.showActions = true;
      assert.isFalse(element.shadowRoot
          .querySelector('.humanActions').hasAttribute('hidden'));
      assert.isTrue(element.shadowRoot
          .querySelector('.robotActions').hasAttribute('hidden'));

      element.draft = true;
      flush();
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.edit')), 'edit is visible');
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.discard')), 'discard is visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.save')), 'save is not visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.cancel')), 'cancel is not visible');
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.resolve')), 'resolve is visible');
      assert.isFalse(element.shadowRoot
          .querySelector('.humanActions').hasAttribute('hidden'));
      assert.isTrue(element.shadowRoot
          .querySelector('.robotActions').hasAttribute('hidden'));

      element.editing = true;
      flush();
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.edit')), 'edit is not visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.discard')), 'discard not visible');
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.save')), 'save is visible');
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.cancel')), 'cancel is visible');
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.resolve')), 'resolve is visible');
      assert.isFalse(element.shadowRoot
          .querySelector('.humanActions').hasAttribute('hidden'));
      assert.isTrue(element.shadowRoot
          .querySelector('.robotActions').hasAttribute('hidden'));

      element.draft = false;
      element.editing = false;
      flush();
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.edit')), 'edit is not visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.discard')),
      'discard is not visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.save')), 'save is not visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.cancel')), 'cancel is not visible');
      assert.isFalse(element.shadowRoot
          .querySelector('.humanActions').hasAttribute('hidden'));
      assert.isTrue(element.shadowRoot
          .querySelector('.robotActions').hasAttribute('hidden'));

      element.comment.id = 'foo';
      element.draft = true;
      element.editing = true;
      flush();
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.cancel')), 'cancel is visible');
      assert.isFalse(element.shadowRoot
          .querySelector('.humanActions').hasAttribute('hidden'));
      assert.isTrue(element.shadowRoot
          .querySelector('.robotActions').hasAttribute('hidden'));

      // Delete button is not hidden by default
      assert.isFalse(element.shadowRoot.querySelector('#deleteBtn').hidden);

      element.isRobotComment = true;
      element.draft = true;
      assert.isTrue(element.shadowRoot
          .querySelector('.humanActions').hasAttribute('hidden'));
      assert.isFalse(element.shadowRoot
          .querySelector('.robotActions').hasAttribute('hidden'));

      // It is not expected to see Robot comment drafts, but if they appear,
      // they will behave the same as non-drafts.
      element.draft = false;
      assert.isTrue(element.shadowRoot
          .querySelector('.humanActions').hasAttribute('hidden'));
      assert.isFalse(element.shadowRoot
          .querySelector('.robotActions').hasAttribute('hidden'));

      // A robot comment with run ID should display plain text.
      element.set(['comment', 'robot_run_id'], 'text');
      element.editing = false;
      element.collapsed = false;
      flush();
      assert.isTrue(element.shadowRoot
          .querySelector('.robotRun.link').textContent === 'Run Details');

      // A robot comment with run ID and url should display a link.
      element.set(['comment', 'url'], '/path/to/run');
      flush();
      assert.notEqual(getComputedStyle(element.shadowRoot
          .querySelector('.robotRun.link')).display,
      'none');

      // Delete button is hidden for robot comments
      assert.isTrue(element.shadowRoot.querySelector('#deleteBtn').hidden);
    });

    test('collapsible drafts', () => {
      assert.isTrue(element.collapsed);
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('gr-formatted-text')),
      'gr-formatted-text is not visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.actions')),
      'actions are not visible');
      assert.isNotOk(element.textarea, 'textarea is not visible');
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.collapsedContent')),
      'header middle content is visible');

      MockInteractions.tap(element.$.header);
      assert.isFalse(element.collapsed);
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('gr-formatted-text')),
      'gr-formatted-text is visible');
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.actions')),
      'actions are visible');
      assert.isNotOk(element.textarea, 'textarea is not visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.collapsedContent')),
      'header middle content is is not visible');

      // When the edit button is pressed, should still see the actions
      // and also textarea
      element.draft = true;
      flush();
      MockInteractions.tap(element.shadowRoot
          .querySelector('.edit'));
      flush();
      assert.isFalse(element.collapsed);
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('gr-formatted-text')),
      'gr-formatted-text is not visible');
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.actions')),
      'actions are visible');
      assert.isTrue(isVisible(element.textarea), 'textarea is visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.collapsedContent')),
      'header middle content is not visible');

      // When toggle again, everything should be hidden except for textarea
      // and header middle content should be visible
      MockInteractions.tap(element.$.header);
      assert.isTrue(element.collapsed);
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('gr-formatted-text')),
      'gr-formatted-text is not visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.actions')),
      'actions are not visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('gr-textarea')),
      'textarea is not visible');
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.collapsedContent')),
      'header middle content is visible');

      // When toggle again, textarea should remain open in the state it was
      // before
      MockInteractions.tap(element.$.header);
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('gr-formatted-text')),
      'gr-formatted-text is not visible');
      assert.isTrue(isVisible(element.shadowRoot
          .querySelector('.actions')),
      'actions are visible');
      assert.isTrue(isVisible(element.textarea), 'textarea is visible');
      assert.isFalse(isVisible(element.shadowRoot
          .querySelector('.collapsedContent')),
      'header middle content is not visible');
    });

    test('robot comment layout', done => {
      const comment = {robot_id: 'happy_robot_id',
        url: '/robot/comment',
        author: {
          name: 'Happy Robot',
          display_name: 'Display name Robot',
        }, ...element.comment};
      element.comment = comment;
      element.collapsed = false;
      flush(() => {
        let runIdMessage;
        runIdMessage = element.shadowRoot
            .querySelector('.runIdMessage');
        assert.isFalse(runIdMessage.hidden);

        const runDetailsLink = element.shadowRoot
            .querySelector('.robotRunLink');
        assert.isTrue(runDetailsLink.href.indexOf(element.comment.url) !== -1);

        const robotServiceName = element.shadowRoot
            .querySelector('.robotName');
        assert.equal(robotServiceName.textContent.trim(), 'happy_robot_id');

        const authorName = element.shadowRoot
            .querySelector('.robotId');
        assert.isTrue(authorName.innerText === 'Happy Robot');

        element.collapsed = true;
        flush();
        runIdMessage = element.shadowRoot
            .querySelector('.runIdMessage');
        assert.isTrue(runIdMessage.hidden);
        done();
      });
    });

    test('author name fallback to email', done => {
      const comment = {url: '/robot/comment',
        author: {
          email: 'test@test.com',
        }, ...element.comment};
      element.comment = comment;
      element.collapsed = false;
      flush(() => {
        const authorName = element.shadowRoot
            .querySelector('gr-account-label')
            .shadowRoot.querySelector('span.name');
        assert.equal(authorName.innerText.trim(), 'test@test.com');
        done();
      });
    });

    test('patchset level comment', done => {
      const comment = {...element.comment,
        path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS, line: undefined,
        range: undefined};
      element.comment = comment;
      flush();
      MockInteractions.tap(element.shadowRoot
          .querySelector('.edit'));
      assert.isTrue(element.editing);

      element._messageText = 'hello world';
      const eraseMessageDraftSpy = sinon.spy(element.storage,
          'eraseDraftComment');
      const mockEvent = {preventDefault: sinon.stub()};
      element._handleSave(mockEvent);
      flush(() => {
        assert.isTrue(eraseMessageDraftSpy.called);
        done();
      });
    });

    test('draft creation/cancellation', done => {
      assert.isFalse(element.editing);
      element.draft = true;
      flush();
      MockInteractions.tap(element.shadowRoot
          .querySelector('.edit'));
      assert.isTrue(element.editing);

      element._messageText = '';
      const eraseMessageDraftSpy = sinon.spy(element, '_eraseDraftComment');

      // Save should be disabled on an empty message.
      let disabled = element.shadowRoot
          .querySelector('.save').hasAttribute('disabled');
      assert.isTrue(disabled, 'save button should be disabled.');
      element._messageText = '     ';
      disabled = element.shadowRoot
          .querySelector('.save').hasAttribute('disabled');
      assert.isTrue(disabled, 'save button should be disabled.');

      const updateStub = sinon.stub();
      element.addEventListener('comment-update', updateStub);

      let numDiscardEvents = 0;
      element.addEventListener('comment-discard', e => {
        numDiscardEvents++;
        assert.isFalse(eraseMessageDraftSpy.called);
        if (numDiscardEvents === 2) {
          assert.isFalse(updateStub.called);
          done();
        }
      });
      MockInteractions.tap(element.shadowRoot
          .querySelector('.cancel'));
      element.fireUpdateTask.flush();
      element._messageText = '';
      flush();
      MockInteractions.pressAndReleaseKeyOn(element.textarea, 27); // esc
    });

    test('draft discard removes message from storage', done => {
      element._messageText = '';
      const eraseMessageDraftSpy = sinon.spy(element, '_eraseDraftComment');
      sinon.stub(element, '_closeConfirmDiscardOverlay');

      element.addEventListener('comment-discard', e => {
        assert.isTrue(eraseMessageDraftSpy.called);
        done();
      });
      element._handleConfirmDiscard({preventDefault: sinon.stub()});
    });

    test('storage is cleared only after save success', () => {
      element._messageText = 'test';
      const eraseStub = sinon.stub(element, '_eraseDraftComment');
      stubRestApi('getResponseObject')
          .returns(Promise.resolve({}));

      sinon.stub(element, '_saveDraft').returns(Promise.resolve({ok: false}));

      const savePromise = element.save();
      assert.isFalse(eraseStub.called);
      return savePromise.then(() => {
        assert.isFalse(eraseStub.called);

        element._saveDraft.restore();
        sinon.stub(element, '_saveDraft')
            .returns(Promise.resolve({ok: true}));
        return element.save().then(() => {
          assert.isTrue(eraseStub.called);
        });
      });
    });

    test('_computeSaveDisabled', () => {
      const comment = {unresolved: true};
      const msgComment = {message: 'test', unresolved: true};
      assert.equal(element._computeSaveDisabled('', comment, false), true);
      assert.equal(element._computeSaveDisabled('test', comment, false), false);
      assert.equal(element._computeSaveDisabled('', msgComment, false), true);
      assert.equal(
          element._computeSaveDisabled('test', msgComment, false), false);
      assert.equal(
          element._computeSaveDisabled('test2', msgComment, false), false);
      assert.equal(element._computeSaveDisabled('test', comment, true), false);
      assert.equal(element._computeSaveDisabled('', comment, true), true);
      assert.equal(element._computeSaveDisabled('', comment, false), true);
    });

    suite('confirm discard', () => {
      let discardStub;
      let overlayStub;
      let mockEvent;

      setup(() => {
        discardStub = sinon.stub(element, '_discardDraft');
        overlayStub = sinon.stub(element, '_openOverlay')
            .returns(Promise.resolve());
        mockEvent = {preventDefault: sinon.stub()};
      });

      test('confirms discard of comments with message text', () => {
        element._messageText = 'test';
        element._handleDiscard(mockEvent);
        assert.isTrue(overlayStub.calledWith(element.confirmDiscardOverlay));
        assert.isFalse(discardStub.called);
      });

      test('no confirmation for comments without message text', () => {
        element._messageText = '';
        element._handleDiscard(mockEvent);
        assert.isFalse(overlayStub.called);
        assert.isTrue(discardStub.calledOnce);
      });
    });

    test('ctrl+s saves comment', done => {
      const stub = sinon.stub(element, 'save').callsFake(() => {
        assert.isTrue(stub.called);
        stub.restore();
        done();
        return Promise.resolve();
      });
      element._messageText = 'is that the horse from horsing around??';
      element.editing = true;
      flush();
      MockInteractions.pressAndReleaseKeyOn(
          element.textarea.$.textarea.textarea,
          83, 'ctrl'); // 'ctrl + s'
    });

    test('draft saving/editing', done => {
      const dispatchEventStub = sinon.stub(element, 'dispatchEvent');

      element.draft = true;
      flush();
      MockInteractions.tap(element.shadowRoot
          .querySelector('.edit'));
      element._messageText = 'good news, everyone!';
      element.fireUpdateTask.flush();
      element.storeTask.flush();
      assert.equal(dispatchEventStub.lastCall.args[0].type, 'comment-update');
      assert.isTrue(dispatchEventStub.calledTwice);

      element._messageText = 'good news, everyone!';
      element.fireUpdateTask.flush();
      element.storeTask.flush();
      assert.isTrue(dispatchEventStub.calledTwice);

      MockInteractions.tap(element.shadowRoot
          .querySelector('.save'));

      assert.isTrue(element.disabled,
          'Element should be disabled when creating draft.');

      element._xhrPromise.then(draft => {
        assert.equal(dispatchEventStub.lastCall.args[0].type, 'comment-save');
        assert.isFalse(element.storeTask.isActive());

        assert.deepEqual(dispatchEventStub.lastCall.args[0].detail, {
          comment: {
            __draft: true,
            __draftID: 'temp_draft_id',
            id: 'baf0414d_40572e03',
            line: 5,
            message: 'saved!',
            path: '/path/to/file',
            updated: '2015-12-08 21:52:36.177000000',
          },
          patchNum: 1,
        });
        assert.isFalse(element.disabled,
            'Element should be enabled when done creating draft.');
        assert.equal(draft.message, 'saved!');
        assert.isFalse(element.editing);
      }).then(() => {
        MockInteractions.tap(element.shadowRoot
            .querySelector('.edit'));
        element._messageText = 'You’ll be delivering a package to Chapek 9, ' +
            'a world where humans are killed on sight.';
        MockInteractions.tap(element.shadowRoot
            .querySelector('.save'));
        assert.isTrue(element.disabled,
            'Element should be disabled when updating draft.');

        element._xhrPromise.then(draft => {
          assert.isFalse(element.disabled,
              'Element should be enabled when done updating draft.');
          assert.equal(draft.message, 'saved!');
          assert.isFalse(element.editing);
          dispatchEventStub.restore();
          done();
        });
      });
    });

    test('draft prevent save when disabled', () => {
      const saveStub = sinon.stub(element, 'save').returns(Promise.resolve());
      element.showActions = true;
      element.draft = true;
      flush();
      MockInteractions.tap(element.$.header);
      MockInteractions.tap(element.shadowRoot
          .querySelector('.edit'));
      element._messageText = 'good news, everyone!';
      element.fireUpdateTask.flush();
      element.storeTask.flush();

      element.disabled = true;
      MockInteractions.tap(element.shadowRoot
          .querySelector('.save'));
      assert.isFalse(saveStub.called);

      element.disabled = false;
      MockInteractions.tap(element.shadowRoot
          .querySelector('.save'));
      assert.isTrue(saveStub.calledOnce);
    });

    test('proper event fires on resolve, comment is not saved', done => {
      const save = sinon.stub(element, 'save');
      element.addEventListener('comment-update', e => {
        assert.isTrue(e.detail.comment.unresolved);
        assert.isFalse(save.called);
        done();
      });
      MockInteractions.tap(element.shadowRoot
          .querySelector('.resolve input'));
    });

    test('resolved comment state indicated by checkbox', () => {
      sinon.stub(element, 'save');
      element.comment = {unresolved: false};
      assert.isTrue(element.shadowRoot
          .querySelector('.resolve input').checked);
      element.comment = {unresolved: true};
      assert.isFalse(element.shadowRoot
          .querySelector('.resolve input').checked);
    });

    test('resolved checkbox saves with tap when !editing', () => {
      element.editing = false;
      const save = sinon.stub(element, 'save');

      element.comment = {unresolved: false};
      assert.isTrue(element.shadowRoot
          .querySelector('.resolve input').checked);
      element.comment = {unresolved: true};
      assert.isFalse(element.shadowRoot
          .querySelector('.resolve input').checked);
      assert.isFalse(save.called);
      MockInteractions.tap(element.$.resolvedCheckbox);
      assert.isTrue(element.shadowRoot
          .querySelector('.resolve input').checked);
      assert.isTrue(save.called);
    });

    suite('draft saving messages', () => {
      test('_getSavingMessage', () => {
        assert.equal(element._getSavingMessage(0), 'All changes saved');
        assert.equal(element._getSavingMessage(1), 'Saving 1 draft...');
        assert.equal(element._getSavingMessage(2), 'Saving 2 drafts...');
        assert.equal(element._getSavingMessage(3), 'Saving 3 drafts...');
      });

      test('_show{Start,End}Request', () => {
        const updateStub = sinon.stub(element, '_updateRequestToast');
        element._numPendingDraftRequests.number = 1;

        element._showStartRequest();
        assert.isTrue(updateStub.calledOnce);
        assert.equal(updateStub.lastCall.args[0], 2);
        assert.equal(element._numPendingDraftRequests.number, 2);

        element._showEndRequest();
        assert.isTrue(updateStub.calledTwice);
        assert.equal(updateStub.lastCall.args[0], 1);
        assert.equal(element._numPendingDraftRequests.number, 1);

        element._showEndRequest();
        assert.isTrue(updateStub.calledThrice);
        assert.equal(updateStub.lastCall.args[0], 0);
        assert.equal(element._numPendingDraftRequests.number, 0);
      });
    });

    test('cancelling an unsaved draft discards, persists in storage', () => {
      const discardSpy = sinon.spy(element, '_fireDiscard');
      const storeStub = stubStorage('setDraftComment');
      const eraseStub = stubStorage('eraseDraftComment');
      element._messageText = 'test text';
      flush();
      element.storeTask.flush();

      assert.isTrue(storeStub.called);
      assert.equal(storeStub.lastCall.args[1], 'test text');
      element._handleCancel({preventDefault: () => {}});
      assert.isTrue(discardSpy.called);
      assert.isFalse(eraseStub.called);
    });

    test('cancelling edit on a saved draft does not store', () => {
      element.comment.id = 'foo';
      const discardSpy = sinon.spy(element, '_fireDiscard');
      const storeStub = stubStorage('setDraftComment');
      element._messageText = 'test text';
      flush();
      if (element.storeTask) element.storeTask.flush();

      assert.isFalse(storeStub.called);
      element._handleCancel({preventDefault: () => {}});
      assert.isTrue(discardSpy.called);
    });

    test('deleting text from saved draft and saving deletes the draft', () => {
      element.comment = {id: 'foo', message: 'test'};
      element._messageText = '';
      const discardStub = sinon.stub(element, '_discardDraft');

      element.save();
      assert.isTrue(discardStub.called);
    });

    test('_handleFix fires create-fix event', done => {
      element.addEventListener('create-fix-comment', e => {
        assert.deepEqual(e.detail, element._getEventPayload());
        done();
      });
      element.isRobotComment = true;
      element.comments = [element.comment];
      flush();

      MockInteractions.tap(element.shadowRoot
          .querySelector('.fix'));
    });

    test('do not show Please Fix button if human reply exists', () => {
      element.comments = [
        {
          robot_id: 'happy_robot_id',
          robot_run_id: '5838406743490560',
          fix_suggestions: [
            {
              fix_id: '478ff847_3bf47aaf',
              description: 'Make the smiley happier by giving it a nose.',
              replacements: [
                {
                  path: 'Documentation/config-gerrit.txt',
                  range: {
                    start_line: 10,
                    start_character: 7,
                    end_line: 10,
                    end_character: 9,
                  },
                  replacement: ':-)',
                },
              ],
            },
          ],
          author: {
            _account_id: 1030912,
            name: 'Alice Kober-Sotzek',
            email: 'aliceks@google.com',
            avatars: [
              {
                url: '/s32-p/photo.jpg',
                height: 32,
              },
              {
                url: '/AaAdOFzPlFI/s56-p/photo.jpg',
                height: 56,
              },
              {
                url: '/AaAdOFzPlFI/s100-p/photo.jpg',
                height: 100,
              },
              {
                url: '/AaAdOFzPlFI/s120-p/photo.jpg',
                height: 120,
              },
            ],
          },
          patch_set: 1,
          id: 'eb0d03fd_5e95904f',
          line: 10,
          updated: '2017-04-04 15:36:17.000000000',
          message: 'This is a robot comment with a fix.',
          unresolved: false,
          diffSide: Side.RIGHT,
          collapsed: false,
        },
        {
          __draft: true,
          __draftID: '0.wbrfbwj89sa',
          __date: '2019-12-04T13:41:03.689Z',
          path: 'Documentation/config-gerrit.txt',
          patchNum: 1,
          side: 'REVISION',
          diffSide: Side.RIGHT,
          line: 10,
          in_reply_to: 'eb0d03fd_5e95904f',
          message: '> This is a robot comment with a fix.\n\nPlease fix.',
          unresolved: true,
        },
      ];
      element.comment = element.comments[0];
      flush();
      assert.isNull(element.shadowRoot
          .querySelector('robotActions gr-button'));
    });

    test('show Please Fix if no human reply', () => {
      element.comments = [
        {
          robot_id: 'happy_robot_id',
          robot_run_id: '5838406743490560',
          fix_suggestions: [
            {
              fix_id: '478ff847_3bf47aaf',
              description: 'Make the smiley happier by giving it a nose.',
              replacements: [
                {
                  path: 'Documentation/config-gerrit.txt',
                  range: {
                    start_line: 10,
                    start_character: 7,
                    end_line: 10,
                    end_character: 9,
                  },
                  replacement: ':-)',
                },
              ],
            },
          ],
          author: {
            _account_id: 1030912,
            name: 'Alice Kober-Sotzek',
            email: 'aliceks@google.com',
            avatars: [
              {
                url: '/s32-p/photo.jpg',
                height: 32,
              },
              {
                url: '/AaAdOFzPlFI/s56-p/photo.jpg',
                height: 56,
              },
              {
                url: '/AaAdOFzPlFI/s100-p/photo.jpg',
                height: 100,
              },
              {
                url: '/AaAdOFzPlFI/s120-p/photo.jpg',
                height: 120,
              },
            ],
          },
          patch_set: 1,
          id: 'eb0d03fd_5e95904f',
          line: 10,
          updated: '2017-04-04 15:36:17.000000000',
          message: 'This is a robot comment with a fix.',
          unresolved: false,
          diffSide: Side.RIGHT,
          collapsed: false,
        },
      ];
      element.comment = element.comments[0];
      flush();
      assert.isNotNull(element.shadowRoot
          .querySelector('.robotActions gr-button'));
    });

    test('_handleShowFix fires open-fix-preview event', done => {
      element.addEventListener('open-fix-preview', e => {
        assert.deepEqual(e.detail, element._getEventPayload());
        done();
      });
      element.comment = {fix_suggestions: [{}]};
      element.isRobotComment = true;
      flush();

      MockInteractions.tap(element.shadowRoot
          .querySelector('.show-fix'));
    });
  });

  suite('respectful tips', () => {
    let element;

    let clock;
    setup(() => {
      stubRestApi('getAccount').returns(Promise.resolve(null));
      clock = sinon.useFakeTimers();
    });

    teardown(() => {
      clock.restore();
      sinon.restore();
    });

    test('show tip when no cached record', done => {
      element = draftFixture.instantiate();
      const respectfulGetStub =
          sinon.stub(element.storage, 'getRespectfulTipVisibility');
      const respectfulSetStub =
          sinon.stub(element.storage, 'setRespectfulTipVisibility');
      respectfulGetStub.returns(null);
      // fake random
      element.getRandomNum = () => 0;
      element.comment = {__editing: true, __draft: true};
      flush(() => {
        assert.isTrue(respectfulGetStub.called);
        assert.isTrue(respectfulSetStub.called);
        assert.isTrue(
            !!element.shadowRoot.querySelector('.respectfulReviewTip')
        );
        done();
      });
    });

    test('add 14-day delays once dismissed', done => {
      element = draftFixture.instantiate();
      const respectfulGetStub =
          sinon.stub(element.storage, 'getRespectfulTipVisibility');
      const respectfulSetStub =
          sinon.stub(element.storage, 'setRespectfulTipVisibility');
      respectfulGetStub.returns(null);
      // fake random
      element.getRandomNum = () => 0;
      element.comment = {__editing: true, __draft: true};
      flush(() => {
        assert.isTrue(respectfulGetStub.called);
        assert.isTrue(respectfulSetStub.called);
        assert.isTrue(respectfulSetStub.lastCall.args[0] === undefined);
        assert.isTrue(
            !!element.shadowRoot.querySelector('.respectfulReviewTip')
        );

        MockInteractions.tap(element.shadowRoot
            .querySelector('.respectfulReviewTip .close'));
        flush();
        assert.isTrue(respectfulSetStub.lastCall.args[0] === 14);
        done();
      });
    });

    test('do not show tip when fall out of probability', done => {
      element = draftFixture.instantiate();
      const respectfulGetStub =
          sinon.stub(element.storage, 'getRespectfulTipVisibility');
      const respectfulSetStub =
          sinon.stub(element.storage, 'setRespectfulTipVisibility');
      respectfulGetStub.returns(null);
      // fake random
      element.getRandomNum = () => 3;
      element.comment = {__editing: true, __draft: true};
      flush(() => {
        assert.isTrue(respectfulGetStub.called);
        assert.isFalse(respectfulSetStub.called);
        assert.isFalse(
            !!element.shadowRoot.querySelector('.respectfulReviewTip')
        );
        done();
      });
    });

    test('show tip when editing changed to true', done => {
      element = draftFixture.instantiate();
      const respectfulGetStub =
          sinon.stub(element.storage, 'getRespectfulTipVisibility');
      const respectfulSetStub =
          sinon.stub(element.storage, 'setRespectfulTipVisibility');
      respectfulGetStub.returns(null);
      // fake random
      element.getRandomNum = () => 0;
      element.comment = {__editing: false};
      flush(() => {
        assert.isFalse(respectfulGetStub.called);
        assert.isFalse(respectfulSetStub.called);
        assert.isFalse(
            !!element.shadowRoot.querySelector('.respectfulReviewTip')
        );

        element.editing = true;
        flush(() => {
          assert.isTrue(respectfulGetStub.called);
          assert.isTrue(respectfulSetStub.called);
          assert.isTrue(
              !!element.shadowRoot.querySelector('.respectfulReviewTip')
          );
          done();
        });
      });
    });

    test('no tip when cached record', done => {
      element = draftFixture.instantiate();
      const respectfulGetStub =
          sinon.stub(element.storage, 'getRespectfulTipVisibility');
      const respectfulSetStub =
          sinon.stub(element.storage, 'setRespectfulTipVisibility');
      respectfulGetStub.returns({});
      // fake random
      element.getRandomNum = () => 0;
      element.comment = {__editing: true, __draft: true};
      flush(() => {
        assert.isTrue(respectfulGetStub.called);
        assert.isFalse(respectfulSetStub.called);
        assert.isFalse(
            !!element.shadowRoot.querySelector('.respectfulReviewTip')
        );
        done();
      });
    });
  });
});

