/**
 * @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-diff-view.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {ChangeStatus} from '../../../constants/constants.js';
import {TestKeyboardShortcutBinder, stubRestApi} from '../../../test/test-utils.js';
import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
import {ChangeComments, _testOnly_findCommentById, _testOnly_getCommentsForPath} from '../gr-comment-api/gr-comment-api.js';
import {GerritView} from '../../../services/router/router-model.js';
import {
  createChange,
  createRevisions,
  createComment,
} from '../../../test/test-data-generators.js';
import {EditPatchSetNum} from '../../../types/common.js';

const basicFixture = fixtureFromElement('gr-diff-view');

const blankFixture = fixtureFromElement('div');

suite('gr-diff-view tests', () => {
  suite('basic tests', () => {
    let element;
    let clock;

    suiteSetup(() => {
      const kb = TestKeyboardShortcutBinder.push();
      kb.bindShortcut(Shortcut.LEFT_PANE, 'shift+left');
      kb.bindShortcut(Shortcut.RIGHT_PANE, 'shift+right');
      kb.bindShortcut(Shortcut.NEXT_LINE, 'j', 'down');
      kb.bindShortcut(Shortcut.PREV_LINE, 'k', 'up');
      kb.bindShortcut(Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
      kb.bindShortcut(Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
      kb.bindShortcut(Shortcut.NEW_COMMENT, 'c');
      kb.bindShortcut(Shortcut.SAVE_COMMENT, 'ctrl+s');
      kb.bindShortcut(Shortcut.NEXT_FILE, ']');
      kb.bindShortcut(Shortcut.PREV_FILE, '[');
      kb.bindShortcut(Shortcut.NEXT_CHUNK, 'n');
      kb.bindShortcut(Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
      kb.bindShortcut(Shortcut.PREV_CHUNK, 'p');
      kb.bindShortcut(Shortcut.PREV_COMMENT_THREAD, 'shift+p');
      kb.bindShortcut(Shortcut.OPEN_REPLY_DIALOG, 'a');
      kb.bindShortcut(Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
      kb.bindShortcut(Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
      kb.bindShortcut(Shortcut.UP_TO_CHANGE, 'u');
      kb.bindShortcut(Shortcut.OPEN_DIFF_PREFS, ',');
      kb.bindShortcut(Shortcut.TOGGLE_DIFF_MODE, 'm');
      kb.bindShortcut(Shortcut.TOGGLE_FILE_REVIEWED, 'r');
      kb.bindShortcut(Shortcut.TOGGLE_ALL_DIFF_CONTEXT, 'shift+x');
      kb.bindShortcut(Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
      kb.bindShortcut(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
      kb.bindShortcut(Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
      kb.bindShortcut(Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
      kb.bindShortcut(Shortcut.TOGGLE_BLAME, 'b');
      kb.bindShortcut(Shortcut.OPEN_FILE_LIST, 'f');
    });

    suiteTeardown(() => {
      TestKeyboardShortcutBinder.pop();
    });

    const PARENT = 'PARENT';

    function getFilesFromFileList(fileList) {
      const changeFilesByPath = fileList.reduce((files, path) => {
        files[path] = {};
        return files;
      }, {});
      return {
        sortedFileList: fileList,
        changeFilesByPath,
      };
    }

    let getDiffChangeDetailStub;
    let getReviewedFilesStub;
    setup(async () => {
      clock = sinon.useFakeTimers();
      stubRestApi('getConfig').returns(Promise.resolve({change: {}}));
      stubRestApi('getLoggedIn').returns(Promise.resolve(false));
      stubRestApi('getProjectConfig').returns(Promise.resolve({}));
      getDiffChangeDetailStub = stubRestApi('getDiffChangeDetail').returns(
          Promise.resolve({}));
      stubRestApi('getChangeFiles').returns(Promise.resolve({}));
      stubRestApi('saveFileReviewed').returns(Promise.resolve());
      stubRestApi('getDiffComments').returns(Promise.resolve({}));
      stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
      stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
      stubRestApi('getPortedComments').returns(Promise.resolve({}));
      getReviewedFilesStub = stubRestApi('getReviewedFiles').returns(
          Promise.resolve([]));

      element = basicFixture.instantiate();
      element._changeNum = '42';
      element._path = 'some/path.txt';
      element._change = {};
      element._diff = {content: []};
      element._patchRange = {
        patchNum: 77,
        basePatchNum: 'PARENT',
      };
      sinon.stub(element.$.commentAPI, 'loadAll').returns(Promise.resolve({
        _comments: {'/COMMIT_MSG': [
          {
            ...createComment(),
            id: 'c1',
            line: 10,
            patch_set: 2,
            path: '/COMMIT_MSG',
          }, {
            ...createComment(),
            id: 'c3',
            line: 10,
            patch_set: 'PARENT',
            path: '/COMMIT_MSG',
          },
        ]},
        computeCommentThreadCount: () => {},
        computeCommentsString: () => '',
        computeUnresolvedNum: () => {},
        getPaths: () => {},
        getThreadsBySideForFile: () => [],
        getCommentsForPath: _testOnly_getCommentsForPath,
        findCommentById: _testOnly_findCommentById,

      }));
      await element._loadComments();
      await flush();
    });

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

    test('params change triggers diffViewDisplayed()', () => {
      sinon.stub(element.reporting, 'diffViewDisplayed');
      sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
      sinon.stub(element, '_initPatchRange');
      sinon.stub(element, '_getFiles');
      sinon.spy(element, '_paramsChanged');
      element.params = {
        view: GerritNav.View.DIFF,
        changeNum: '42',
        patchNum: 2,
        basePatchNum: 1,
        path: '/COMMIT_MSG',
      };
      element._path = '/COMMIT_MSG';
      element._patchRange = {};
      return element._paramsChanged.returnValues[0].then(() => {
        assert.isTrue(element.reporting.diffViewDisplayed.calledOnce);
      });
    });

    suite('comment route', () => {
      let initLineOfInterestAndCursorStub; let getUrlStub; let replaceStateStub;
      setup(() => {
        initLineOfInterestAndCursorStub =
        sinon.stub(element, '_initLineOfInterestAndCursor');
        getUrlStub = sinon.stub(GerritNav, 'getUrlForDiffById');
        replaceStateStub = sinon.stub(history, 'replaceState');
        sinon.stub(element, '_getFiles');
        sinon.stub(element.reporting, 'diffViewDisplayed');
        sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
        sinon.spy(element, '_paramsChanged');
        sinon.stub(element, '_getChangeDetail').returns(Promise.resolve({
          ...createChange(),
          revisions: createRevisions(11),
        }));
      });

      test('comment url resolves to comment.patch_set vs latest', () => {
        element.params = {
          view: GerritNav.View.DIFF,
          changeNum: '42',
          commentLink: true,
          commentId: 'c1',
        };
        element._change = {
          ...createChange(),
          revisions: createRevisions(11),
        };
        return element._paramsChanged.returnValues[0].then(() => {
          assert.isTrue(initLineOfInterestAndCursorStub.
              calledWithExactly(true));
          assert.equal(element._focusLineNum, 10);
          assert.equal(element._patchRange.patchNum, 11);
          assert.equal(element._patchRange.basePatchNum, 2);
          assert.isTrue(replaceStateStub.called);
          assert.isTrue(getUrlStub.calledWithExactly('42', 'test-project',
              '/COMMIT_MSG', 11, 2, 10, true));
        });
      });
    });

    test('params change causes blame to load if it was set to true', () => {
      // Blame loads for subsequent files if it was loaded for one file
      element._isBlameLoaded = true;
      sinon.stub(element.reporting, 'diffViewDisplayed');
      sinon.stub(element, '_loadBlame');
      sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
      sinon.spy(element, '_paramsChanged');
      sinon.stub(element, '_initPatchRange');
      sinon.stub(element, '_getFiles');
      element.params = {
        view: GerritNav.View.DIFF,
        changeNum: '42',
        patchNum: 2,
        basePatchNum: 1,
        path: '/COMMIT_MSG',
      };
      element._path = '/COMMIT_MSG';
      element._patchRange = {};
      return element._paramsChanged.returnValues[0].then(() => {
        assert.isTrue(element._isBlameLoaded);
        assert.isTrue(element._loadBlame.calledOnce);
      });
    });

    test('unchanged diff X vs latest from comment links navigates to base vs X'
        , () => {
          const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
          sinon.stub(element.reporting, 'diffViewDisplayed');
          sinon.stub(element, '_loadBlame');
          sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
          sinon.stub(element, '_isFileUnchanged').returns(true);
          sinon.spy(element, '_paramsChanged');
          sinon.stub(element, '_getChangeDetail').returns(Promise.resolve({
            ...createChange(),
            revisions: createRevisions(11),
          }));
          element.params = {
            view: GerritNav.View.DIFF,
            changeNum: '42',
            path: '/COMMIT_MSG',
            commentLink: true,
            commentId: 'c1',
          };
          element._change = {
            ...createChange(),
            revisions: createRevisions(11),
          };
          return element._paramsChanged.returnValues[0].then(() => {
            assert.isTrue(diffNavStub.lastCall.calledWithExactly(
                element._change, '/COMMIT_MSG', 2, 'PARENT', 10));
          });
        });

    test('unchanged diff Base vs latest from comment does not navigate'
        , () => {
          const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
          sinon.stub(element.reporting, 'diffViewDisplayed');
          sinon.stub(element, '_loadBlame');
          sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
          sinon.stub(element, '_isFileUnchanged').returns(true);
          sinon.spy(element, '_paramsChanged');
          sinon.stub(element, '_getChangeDetail').returns(Promise.resolve({
            ...createChange(),
            revisions: createRevisions(11),
          }));
          element.params = {
            view: GerritNav.View.DIFF,
            changeNum: '42',
            path: '/COMMIT_MSG',
            commentLink: true,
            commentId: 'c3',
          };
          element._change = {
            ...createChange(),
            revisions: createRevisions(11),
          };
          return element._paramsChanged.returnValues[0].then(() => {
            assert.isFalse(diffNavStub.called);
          });
        });

    test('_isFileUnchanged', () => {
      let diff = {
        content: [
          {a: 'abcd', ab: 'ef'},
          {b: 'ancd', a: 'xx'},
        ],
      };
      assert.equal(element._isFileUnchanged(diff), false);
      diff = {
        content: [
          {ab: 'abcd'},
          {ab: 'ancd'},
        ],
      };
      assert.equal(element._isFileUnchanged(diff), true);
      diff = {
        content: [
          {a: 'abcd', ab: 'ef', common: true},
          {b: 'ancd', ab: 'xx'},
        ],
      };
      assert.equal(element._isFileUnchanged(diff), false);
      diff = {
        content: [
          {a: 'abcd', ab: 'ef', common: true},
          {b: 'ancd', ab: 'xx', common: true},
        ],
      };
      assert.equal(element._isFileUnchanged(diff), true);
    });

    test('diff toast to go to latest is shown and not base', async () => {
      sinon.stub(element.reporting, 'diffViewDisplayed');
      sinon.stub(element, '_loadBlame');
      sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
      sinon.spy(element, '_paramsChanged');
      getDiffChangeDetailStub.returns(
          Promise.resolve({
            ...createChange(),
            revisions: createRevisions(11),
          }));
      element._patchRange = {
        patchNum: 2,
        basePatchNum: 1,
      };
      sinon.stub(element, '_isFileUnchanged').returns(false);
      const toastStub =
          sinon.stub(element, '_displayDiffBaseAgainstLeftToast');
      element.params = {
        view: GerritNav.View.DIFF,
        changeNum: '42',
        project: 'p',
        commentId: 'c1',
        commentLink: true,
      };
      await element._paramsChanged.returnValues[0];
      assert.isTrue(toastStub.called);
    });

    test('toggle left diff with a hotkey', () => {
      const toggleLeftDiffStub = sinon.stub(
          element.$.diffHost, 'toggleLeftDiff');
      MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
      assert.isTrue(toggleLeftDiffStub.calledOnce);
    });

    test('keyboard shortcuts', () => {
      element._changeNum = '42';
      element._patchRange = {
        basePatchNum: PARENT,
        patchNum: 10,
      };
      element._change = {
        _number: 42,
        revisions: {
          a: {_number: 10, commit: {parents: []}},
        },
      };
      element._files = getFilesFromFileList(
          ['chell.go', 'glados.txt', 'wheatley.md']);
      element._path = 'glados.txt';
      element.changeViewState.selectedFileIndex = 1;
      element._loggedIn = true;

      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');

      MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
      assert(changeNavStub.lastCall.calledWith(element._change),
          'Should navigate to /c/42/');

      MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
      assert(diffNavStub.lastCall.calledWith(element._change, 'wheatley.md',
          10, PARENT), 'Should navigate to /c/42/10/wheatley.md');
      element._path = 'wheatley.md';
      assert.equal(element.changeViewState.selectedFileIndex, 2);
      assert.isTrue(element._loading);

      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
      assert(diffNavStub.lastCall.calledWith(element._change, 'glados.txt',
          10, PARENT), 'Should navigate to /c/42/10/glados.txt');
      element._path = 'glados.txt';
      assert.equal(element.changeViewState.selectedFileIndex, 1);
      assert.isTrue(element._loading);

      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
      assert(diffNavStub.lastCall.calledWith(element._change, 'chell.go', 10,
          PARENT), 'Should navigate to /c/42/10/chell.go');
      element._path = 'chell.go';
      assert.equal(element.changeViewState.selectedFileIndex, 0);
      assert.isTrue(element._loading);

      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
      assert(changeNavStub.lastCall.calledWith(element._change),
          'Should navigate to /c/42/');
      assert.equal(element.changeViewState.selectedFileIndex, 0);
      assert.isTrue(element._loading);

      const showPrefsStub =
          sinon.stub(element.$.diffPreferencesDialog, 'open').callsFake(
              () => Promise.resolve());

      MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
      assert(showPrefsStub.calledOnce);

      element.disableDiffPrefs = true;
      MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
      assert(showPrefsStub.calledOnce);

      let scrollStub = sinon.stub(element.$.cursor, 'moveToNextChunk');
      MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
      assert(scrollStub.calledOnce);

      scrollStub = sinon.stub(element.$.cursor, 'moveToPreviousChunk');
      MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
      assert(scrollStub.calledOnce);

      scrollStub = sinon.stub(element.$.cursor, 'moveToNextCommentThread');
      MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
      assert(scrollStub.calledOnce);

      scrollStub = sinon.stub(element.$.cursor,
          'moveToPreviousCommentThread');
      MockInteractions.pressAndReleaseKeyOn(element, 80, 'shift', 'p');
      assert(scrollStub.calledOnce);

      const computeContainerClassStub = sinon.stub(element.$.diffHost.$.diff,
          '_computeContainerClass');
      MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
      assert(computeContainerClassStub.lastCall.calledWithExactly(
          false, 'SIDE_BY_SIDE', true));

      MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
      assert(computeContainerClassStub.lastCall.calledWithExactly(
          false, 'SIDE_BY_SIDE', false));

      sinon.stub(element, '_setReviewed');
      sinon.spy(element, '_handleToggleFileReviewed');
      element.$.reviewed.checked = false;
      MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
      assert.isFalse(element._setReviewed.called);
      assert.isTrue(element._handleToggleFileReviewed.calledOnce);

      MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
      assert.isTrue(element._handleToggleFileReviewed.calledOnce);

      clock.tick(1000);

      MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
      assert.isTrue(element._handleToggleFileReviewed.calledTwice);
      assert.isTrue(element._setReviewed.called);
      assert.equal(element._setReviewed.lastCall.args[0], true);
    });

    test('moveToNextCommentThread navigates to next file', () => {
      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
      const diffChangeStub = sinon.stub(element, '_navigateToChange');
      sinon.stub(element.$.cursor, 'isAtEnd').returns(true);
      element._changeNum = '42';
      const comment = {
        'wheatley.md': [{
          ...createComment(),
          patch_set: 10,
          line: 21,
        }],
      };
      element._changeComments = new ChangeComments(comment);
      element._patchRange = {
        basePatchNum: PARENT,
        patchNum: 10,
      };
      element._change = {
        _number: 42,
        revisions: {
          a: {_number: 10, commit: {parents: []}},
        },
      };
      element._files = getFilesFromFileList(
          ['chell.go', 'glados.txt', 'wheatley.md']);
      element._path = 'glados.txt';
      element.changeViewState.selectedFileIndex = 1;
      element._loggedIn = true;

      MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
      flush();
      assert.isTrue(diffNavStub.calledWithExactly(
          element._change, 'wheatley.md', 10, PARENT, 21));

      element._path = 'wheatley.md'; // navigated to next file

      MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
      flush();

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

    test('shift+x shortcut toggles all diff context', () => {
      const toggleStub = sinon.stub(element.$.diffHost, 'toggleAllContext');
      MockInteractions.pressAndReleaseKeyOn(element, 88, 'shift', 'x');
      flush();
      assert.isTrue(toggleStub.called);
    });

    test('diff against base', () => {
      element._patchRange = {
        basePatchNum: 5,
        patchNum: 10,
      };
      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
      element._handleDiffAgainstBase(new CustomEvent(''));
      const args = diffNavStub.getCall(0).args;
      assert.equal(args[2], 10);
      assert.isNotOk(args[3]);
    });

    test('diff against latest', () => {
      element._change = {
        ...createChange(),
        revisions: createRevisions(12),
      };
      element._patchRange = {
        basePatchNum: 5,
        patchNum: 10,
      };
      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
      element._handleDiffAgainstLatest(new CustomEvent(''));
      const args = diffNavStub.getCall(0).args;
      assert.equal(args[2], 12);
      assert.equal(args[3], 5);
    });

    test('_handleDiffBaseAgainstLeft', () => {
      element._change = {
        ...createChange(),
        revisions: createRevisions(10),
      };
      element._patchRange = {
        patchNum: 3,
        basePatchNum: 1,
      };
      element.params = {};
      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
      element._handleDiffBaseAgainstLeft(new CustomEvent(''));
      assert(diffNavStub.called);
      const args = diffNavStub.getCall(0).args;
      assert.equal(args[2], 1);
      assert.equal(args[3], 'PARENT');
      assert.isNotOk(args[4]);
    });

    test('_handleDiffBaseAgainstLeft when initially navigating to a comment',
        () => {
          element._change = {
            ...createChange(),
            revisions: createRevisions(10),
          };
          element._patchRange = {
            patchNum: 3,
            basePatchNum: 1,
          };
          sinon.stub(element, '_paramsChanged');
          element.params = {commentLink: true, view: GerritView.DIFF};
          element._focusLineNum = 10;
          sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
          const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
          element._handleDiffBaseAgainstLeft(new CustomEvent(''));
          assert(diffNavStub.called);
          const args = diffNavStub.getCall(0).args;
          assert.equal(args[2], 1);
          assert.equal(args[3], 'PARENT');
          assert.equal(args[4], 10);
        });

    test('_handleDiffRightAgainstLatest', () => {
      element._change = {
        ...createChange(),
        revisions: createRevisions(10),
      };
      element._patchRange = {
        basePatchNum: 1,
        patchNum: 3,
      };
      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
      element._handleDiffRightAgainstLatest(new CustomEvent(''));
      assert(diffNavStub.called);
      const args = diffNavStub.getCall(0).args;
      assert.equal(args[2], 10);
      assert.equal(args[3], 3);
    });

    test('_handleDiffBaseAgainstLatest', () => {
      element._change = {
        ...createChange(),
        revisions: createRevisions(10),
      };
      element._patchRange = {
        basePatchNum: 1,
        patchNum: 3,
      };
      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
      element._handleDiffBaseAgainstLatest(new CustomEvent(''));
      assert(diffNavStub.called);
      const args = diffNavStub.getCall(0).args;
      assert.equal(args[2], 10);
      assert.isNotOk(args[3]);
    });

    test('A fires an error event when not logged in', done => {
      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
      sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
      const loggedInErrorSpy = sinon.spy();
      element.addEventListener('show-auth-required', loggedInErrorSpy);
      MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
      flush(() => {
        assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
          'should only work when the user is logged in.');
        assert.isNull(window.sessionStorage.getItem(
            'changeView.showReplyDialog'));
        assert.isTrue(loggedInErrorSpy.called);
        done();
      });
    });

    test('A navigates to change with logged in', done => {
      element._changeNum = '42';
      element._patchRange = {
        basePatchNum: 5,
        patchNum: 10,
      };
      element._change = {
        _number: 42,
        revisions: {
          a: {_number: 10, commit: {parents: []}},
          b: {_number: 5, commit: {parents: []}},
        },
      };
      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
      sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
      const loggedInErrorSpy = sinon.spy();
      element.addEventListener('show-auth-required', loggedInErrorSpy);
      MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
      flush(() => {
        assert.isTrue(element.changeViewState.showReplyDialog);
        assert(changeNavStub.lastCall.calledWithExactly(element._change, 10,
            5), 'Should navigate to /c/42/5..10');
        assert.isFalse(loggedInErrorSpy.called);
        done();
      });
    });

    test('A navigates to change with old patch number with logged in', done => {
      element._changeNum = '42';
      element._patchRange = {
        basePatchNum: PARENT,
        patchNum: 1,
      };
      element._change = {
        _number: 42,
        revisions: {
          a: {_number: 1, commit: {parents: []}},
          b: {_number: 2, commit: {parents: []}},
        },
      };
      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
      sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
      const loggedInErrorSpy = sinon.spy();
      element.addEventListener('show-auth-required', loggedInErrorSpy);
      MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
      flush(() => {
        assert.isTrue(element.changeViewState.showReplyDialog);
        assert(changeNavStub.lastCall.calledWithExactly(element._change, 1,
            PARENT), 'Should navigate to /c/42/1');
        assert.isFalse(loggedInErrorSpy.called);
        done();
      });
    });

    test('keyboard shortcuts with patch range', () => {
      element._changeNum = '42';
      element._patchRange = {
        basePatchNum: 5,
        patchNum: 10,
      };
      element._change = {
        _number: 42,
        revisions: {
          a: {_number: 10, commit: {parents: []}},
          b: {_number: 5, commit: {parents: []}},
        },
      };
      element._files = getFilesFromFileList(
          ['chell.go', 'glados.txt', 'wheatley.md']);
      element._path = 'glados.txt';

      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');

      MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
      assert(changeNavStub.lastCall.calledWithExactly(element._change, 10,
          5), 'Should navigate to /c/42/5..10');

      MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
      assert.isTrue(element._loading);
      assert(diffNavStub.lastCall.calledWithExactly(element._change,
          'wheatley.md', 10, 5, undefined),
      'Should navigate to /c/42/5..10/wheatley.md');
      element._path = 'wheatley.md';

      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
      assert.isTrue(element._loading);
      assert(diffNavStub.lastCall.calledWithExactly(element._change,
          'glados.txt', 10, 5, undefined),
      'Should navigate to /c/42/5..10/glados.txt');
      element._path = 'glados.txt';

      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
      assert.isTrue(element._loading);
      assert(diffNavStub.lastCall.calledWithExactly(
          element._change,
          'chell.go',
          10,
          5,
          undefined),
      'Should navigate to /c/42/5..10/chell.go');
      element._path = 'chell.go';

      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
      assert.isTrue(element._loading);
      assert(changeNavStub.lastCall.calledWithExactly(element._change, 10,
          5),
      'Should navigate to /c/42/5..10');

      assert.isUndefined(element.changeViewState.showDownloadDialog);
      MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd');
      assert.isTrue(element.changeViewState.showDownloadDialog);
    });

    test('keyboard shortcuts with old patch number', () => {
      element._changeNum = '42';
      element._patchRange = {
        basePatchNum: PARENT,
        patchNum: 1,
      };
      element._change = {
        _number: 42,
        revisions: {
          a: {_number: 1, commit: {parents: []}},
          b: {_number: 2, commit: {parents: []}},
        },
      };
      element._files = getFilesFromFileList(
          ['chell.go', 'glados.txt', 'wheatley.md']);
      element._path = 'glados.txt';

      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');

      MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
      assert(changeNavStub.lastCall.calledWithExactly(element._change, 1,
          PARENT), 'Should navigate to /c/42/1');

      MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
      assert(diffNavStub.lastCall.calledWithExactly(element._change,
          'wheatley.md', 1, PARENT, undefined),
      'Should navigate to /c/42/1/wheatley.md');
      element._path = 'wheatley.md';

      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
      assert(diffNavStub.lastCall.calledWithExactly(element._change,
          'glados.txt', 1, PARENT, undefined),
      'Should navigate to /c/42/1/glados.txt');
      element._path = 'glados.txt';

      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
      assert(diffNavStub.lastCall.calledWithExactly(
          element._change,
          'chell.go',
          1,
          PARENT,
          undefined), 'Should navigate to /c/42/1/chell.go');
      element._path = 'chell.go';

      changeNavStub.reset();
      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
      assert(changeNavStub.lastCall.calledWithExactly(element._change, 1,
          PARENT), 'Should navigate to /c/42/1');
      assert.isTrue(changeNavStub.calledOnce);
    });

    test('edit should redirect to edit page', done => {
      element._loggedIn = true;
      element._path = 't.txt';
      element._patchRange = {
        basePatchNum: PARENT,
        patchNum: 1,
      };
      element._change = {
        _number: 42,
        project: 'gerrit',
        status: ChangeStatus.NEW,
        revisions: {
          a: {_number: 1, commit: {parents: []}},
          b: {_number: 2, commit: {parents: []}},
        },
      };
      const redirectStub = sinon.stub(GerritNav, 'navigateToRelativeUrl');
      flush(() => {
        const editBtn = element.shadowRoot
            .querySelector('.editButton gr-button');
        assert.isTrue(!!editBtn);
        MockInteractions.tap(editBtn);
        assert.isTrue(redirectStub.called);
        assert.isTrue(redirectStub.lastCall.calledWithExactly(
            GerritNav.getEditUrlForDiff(
                element._change,
                element._path,
                element._patchRange.patchNum
            )));
        done();
      });
    });

    test('edit should redirect to edit page with line number', done => {
      const lineNumber = 42;
      element._loggedIn = true;
      element._path = 't.txt';
      element._patchRange = {
        basePatchNum: PARENT,
        patchNum: 1,
      };
      element._change = {
        _number: 42,
        project: 'gerrit',
        status: ChangeStatus.NEW,
        revisions: {
          a: {_number: 1, commit: {parents: []}},
          b: {_number: 2, commit: {parents: []}},
        },
      };
      sinon.stub(element.$.cursor, 'getAddress')
          .returns({number: lineNumber, isLeftSide: false});
      const redirectStub = sinon.stub(GerritNav, 'navigateToRelativeUrl');
      flush(() => {
        const editBtn = element.shadowRoot
            .querySelector('.editButton gr-button');
        assert.isTrue(!!editBtn);
        MockInteractions.tap(editBtn);
        assert.isTrue(redirectStub.called);
        assert.isTrue(redirectStub.lastCall.calledWithExactly(
            GerritNav.getEditUrlForDiff(
                element._change,
                element._path,
                element._patchRange.patchNum,
                lineNumber
            )));
        done();
      });
    });

    function isEditVisibile({loggedIn, changeStatus}) {
      return new Promise(resolve => {
        element._loggedIn = loggedIn;
        element._path = 't.txt';
        element._patchRange = {
          basePatchNum: PARENT,
          patchNum: 1,
        };
        element._change = {
          _number: 42,
          status: changeStatus,
          revisions: {
            a: {_number: 1, commit: {parents: []}},
            b: {_number: 2, commit: {parents: []}},
          },
        };
        flush(() => {
          const editBtn = element.shadowRoot
              .querySelector('.editButton gr-button');
          resolve(!!editBtn);
        });
      });
    }

    test('edit visible only when logged and status NEW', async () => {
      for (const changeStatus of Object.keys(ChangeStatus)) {
        assert.isFalse(await isEditVisibile({loggedIn: false, changeStatus}),
            `loggedIn: false, changeStatus: ${changeStatus}`);

        if (changeStatus !== ChangeStatus.NEW) {
          assert.isFalse(await isEditVisibile({loggedIn: true, changeStatus}),
              `loggedIn: true, changeStatus: ${changeStatus}`);
        } else {
          assert.isTrue(await isEditVisibile({loggedIn: true, changeStatus}),
              `loggedIn: true, changeStatus: ${changeStatus}`);
        }
      }
    });

    test('edit visible when logged and status NEW', async () => {
      assert.isTrue(await isEditVisibile(
          {loggedIn: true, changeStatus: ChangeStatus.NEW}));
    });

    test('edit hidden when logged and status ABANDONED', async () => {
      assert.isFalse(await isEditVisibile(
          {loggedIn: true, changeStatus: ChangeStatus.ABANDONED}));
    });

    test('edit hidden when logged and status MERGED', async () => {
      assert.isFalse(await isEditVisibile(
          {loggedIn: true, changeStatus: ChangeStatus.MERGED}));
    });

    suite('diff prefs hidden', () => {
      test('when no prefs or logged out', () => {
        element.disableDiffPrefs = false;
        element._loggedIn = false;
        flush();
        assert.isTrue(element.$.diffPrefsContainer.hidden);

        element._loggedIn = true;
        flush();
        assert.isTrue(element.$.diffPrefsContainer.hidden);

        element._loggedIn = false;
        element._prefs = {font_size: '12'};
        flush();
        assert.isTrue(element.$.diffPrefsContainer.hidden);

        element._loggedIn = true;
        flush();
        assert.isFalse(element.$.diffPrefsContainer.hidden);
      });

      test('when disableDiffPrefs is set', () => {
        element._loggedIn = true;
        element._prefs = {font_size: '12'};
        element.disableDiffPrefs = false;
        flush();

        assert.isFalse(element.$.diffPrefsContainer.hidden);
        element.disableDiffPrefs = true;
        flush();

        assert.isTrue(element.$.diffPrefsContainer.hidden);
      });
    });

    test('prefsButton opens gr-diff-preferences', () => {
      const handlePrefsTapSpy = sinon.spy(element, '_handlePrefsTap');
      const overlayOpenStub = sinon.stub(element.$.diffPreferencesDialog,
          'open');
      const prefsButton =
          element.root.querySelector('.prefsButton');

      MockInteractions.tap(prefsButton);

      assert.isTrue(handlePrefsTapSpy.called);
      assert.isTrue(overlayOpenStub.called);
    });

    suite('url params', () => {
      setup(() => {
        sinon.stub(element, '_getFiles');
        sinon.stub(
            GerritNav,
            'getUrlForDiff')
            .callsFake((c, p, pn, bpn) => `${c._number}-${p}-${pn}-${bpn}`);
        sinon.stub(
            GerritNav
            , 'getUrlForChange')
            .callsFake((c, pn, bpn) => `${c._number}-${pn}-${bpn}`);
      });

      test('_formattedFiles', () => {
        element._changeNum = '42';
        element._patchRange = {
          basePatchNum: PARENT,
          patchNum: 10,
        };
        element._change = {_number: 42};
        element._files = getFilesFromFileList(
            ['chell.go', 'glados.txt', 'wheatley.md',
              '/COMMIT_MSG', '/MERGE_LIST']);
        element._path = 'glados.txt';
        const expectedFormattedFiles = [
          {
            text: 'chell.go',
            mobileText: 'chell.go',
            value: 'chell.go',
            bottomText: '',
            file: {
              __path: 'chell.go',
            },
          }, {
            text: 'glados.txt',
            mobileText: 'glados.txt',
            value: 'glados.txt',
            bottomText: '',
            file: {
              __path: 'glados.txt',
            },
          }, {
            text: 'wheatley.md',
            mobileText: 'wheatley.md',
            value: 'wheatley.md',
            bottomText: '',
            file: {
              __path: 'wheatley.md',
            },
          },
          {
            text: 'Commit message',
            mobileText: 'Commit message',
            value: '/COMMIT_MSG',
            bottomText: '',
            file: {
              __path: '/COMMIT_MSG',
            },
          },
          {
            text: 'Merge list',
            mobileText: 'Merge list',
            value: '/MERGE_LIST',
            bottomText: '',
            file: {
              __path: '/MERGE_LIST',
            },
          },
        ];

        assert.deepEqual(element._formattedFiles, expectedFormattedFiles);
        assert.equal(element._formattedFiles[1].value, element._path);
      });

      test('prev/up/next links', () => {
        element._changeNum = '42';
        element._patchRange = {
          basePatchNum: PARENT,
          patchNum: 10,
        };
        element._change = {
          _number: 42,
          revisions: {
            a: {_number: 10, commit: {parents: []}},
          },
        };
        element._files = getFilesFromFileList(
            ['chell.go', 'glados.txt', 'wheatley.md']);
        element._path = 'glados.txt';
        flush();
        const linkEls = element.root.querySelectorAll('.navLink');
        assert.equal(linkEls.length, 3);
        assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-PARENT');
        assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
        assert.equal(linkEls[2].getAttribute('href'),
            '42-wheatley.md-10-PARENT');
        element._path = 'wheatley.md';
        flush();
        assert.equal(linkEls[0].getAttribute('href'),
            '42-glados.txt-10-PARENT');
        assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
        assert.equal(linkEls[2].getAttribute('href'), '42-undefined-undefined');
        element._path = 'chell.go';
        flush();
        assert.equal(linkEls[0].getAttribute('href'), '42-undefined-undefined');
        assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
        assert.equal(linkEls[2].getAttribute('href'),
            '42-glados.txt-10-PARENT');
        element._path = 'not_a_real_file';
        flush();
        assert.equal(linkEls[0].getAttribute('href'),
            '42-wheatley.md-10-PARENT');
        assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
        assert.equal(linkEls[2].getAttribute('href'), '42-chell.go-10-PARENT');
      });

      test('prev/up/next links with patch range', () => {
        element._changeNum = '42';
        element._patchRange = {
          basePatchNum: 5,
          patchNum: 10,
        };
        element._change = {
          _number: 42,
          revisions: {
            a: {_number: 5, commit: {parents: []}},
            b: {_number: 10, commit: {parents: []}},
          },
        };
        element._files = getFilesFromFileList(
            ['chell.go', 'glados.txt', 'wheatley.md']);
        element._path = 'glados.txt';
        flush();
        const linkEls = element.root.querySelectorAll('.navLink');
        assert.equal(linkEls.length, 3);
        assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-5');
        assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
        assert.equal(linkEls[2].getAttribute('href'), '42-wheatley.md-10-5');
        element._path = 'wheatley.md';
        flush();
        assert.equal(linkEls[0].getAttribute('href'), '42-glados.txt-10-5');
        assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
        assert.equal(linkEls[2].getAttribute('href'), '42-10-5');
        element._path = 'chell.go';
        flush();
        assert.equal(linkEls[0].getAttribute('href'),
            '42-10-5');
        assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
        assert.equal(linkEls[2].getAttribute('href'), '42-glados.txt-10-5');
      });
    });

    test('_handlePatchChange calls navigateToDiff correctly', () => {
      const navigateStub = sinon.stub(GerritNav, 'navigateToDiff');
      element._change = {_number: 321, project: 'foo/bar'};
      element._path = 'path/to/file.txt';

      element._patchRange = {
        basePatchNum: 'PARENT',
        patchNum: 3,
      };

      const detail = {
        basePatchNum: 'PARENT',
        patchNum: 1,
      };

      element.$.rangeSelect.dispatchEvent(
          new CustomEvent('patch-range-change', {detail, bubbles: false}));

      assert(navigateStub.lastCall.calledWithExactly(element._change,
          element._path, 1, 'PARENT'));
    });

    test('_prefs.manual_review is respected', () => {
      const saveReviewedStub = sinon.stub(element, '_saveReviewedState')
          .callsFake(() => Promise.resolve());
      const getReviewedStub = sinon.stub(element, '_getReviewedStatus')
          .callsFake(() => Promise.resolve());

      sinon.stub(element.$.diffHost, 'reload');
      element._loggedIn = true;
      element.params = {
        view: GerritNav.View.DIFF,
        changeNum: '42',
        patchNum: 2,
        basePatchNum: 1,
        path: '/COMMIT_MSG',
      };
      element._patchRange = {
        patchNum: 2,
        basePatchNum: 1,
      };
      element._prefs = {manual_review: true};
      flush();

      assert.isFalse(saveReviewedStub.called);
      assert.isTrue(getReviewedStub.called);

      element._prefs = {};
      flush();

      assert.isTrue(saveReviewedStub.called);
      assert.isTrue(getReviewedStub.calledOnce);
    });

    test('file review status', () => {
      const saveReviewedStub = sinon.stub(element, '_saveReviewedState')
          .callsFake(() => Promise.resolve());
      sinon.stub(element.$.diffHost, 'reload');

      element._loggedIn = true;
      element.params = {
        view: GerritNav.View.DIFF,
        changeNum: '42',
        patchNum: 2,
        basePatchNum: 1,
        path: '/COMMIT_MSG',
      };
      element._patchRange = {
        patchNum: 2,
        basePatchNum: 1,
      };
      element._prefs = {};
      flush();

      const commitMsg = element.root.querySelector(
          'input[type="checkbox"]');

      assert.isTrue(commitMsg.checked);
      MockInteractions.tap(commitMsg);
      assert.isFalse(commitMsg.checked);
      assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(false));

      MockInteractions.tap(commitMsg);
      assert.isTrue(commitMsg.checked);
      assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(true));
      const callCount = saveReviewedStub.callCount;

      element.set('params.view', GerritNav.View.CHANGE);
      flush();

      // saveReviewedState observer observes params, but should not fire when
      // view !== GerritNav.View.DIFF.
      assert.equal(saveReviewedStub.callCount, callCount);
    });

    test('file review status with edit loaded', () => {
      const saveReviewedStub = sinon.stub(element, '_saveReviewedState');

      element._patchRange = {patchNum: EditPatchSetNum};
      flush();

      assert.isTrue(element._editMode);
      element._setReviewed();
      assert.isFalse(saveReviewedStub.called);
    });

    test('hash is determined from params', done => {
      sinon.stub(element.$.diffHost, 'reload');
      sinon.stub(element, '_initLineOfInterestAndCursor');

      element._loggedIn = true;
      element.params = {
        view: GerritNav.View.DIFF,
        changeNum: '42',
        patchNum: 2,
        basePatchNum: 1,
        path: '/COMMIT_MSG',
        hash: 10,
      };

      flush(() => {
        assert.isTrue(element._initLineOfInterestAndCursor.calledOnce);
        done();
      });
    });

    test('diff mode selector correctly toggles the diff', () => {
      const select = element.$.modeSelect;
      const diffDisplay = element.$.diffHost;
      element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};

      // The mode selected in the view state reflects the selected option.
      assert.equal(element._getDiffViewMode(), select.mode);

      // The mode selected in the view state reflects the view rednered in the
      // diff.
      assert.equal(select.mode, diffDisplay.viewMode);

      // We will simulate a user change of the selected mode.
      const newMode = 'UNIFIED_DIFF';

      // Set the mode, and simulate the change event.
      element.set('changeViewState.diffMode', newMode);

      // Make sure the handler was called and the state is still coherent.
      assert.equal(element._getDiffViewMode(), newMode);
      assert.equal(element._getDiffViewMode(), select.mode);
      assert.equal(element._getDiffViewMode(), diffDisplay.viewMode);
    });

    test('diff mode selector initializes from preferences', () => {
      let resolvePrefs;
      const prefsPromise = new Promise(resolve => {
        resolvePrefs = resolve;
      });
      stubRestApi('getPreferences')
          .callsFake(() => prefsPromise);

      // Attach a new gr-diff-view so we can intercept the preferences fetch.
      const view = document.createElement('gr-diff-view');
      blankFixture.instantiate().appendChild(view);
      flush();

      // At this point the diff mode doesn't yet have the user's preference.
      assert.equal(view._getDiffViewMode(), 'SIDE_BY_SIDE');

      // Receive the overriding preference.
      resolvePrefs({default_diff_view: 'UNIFIED'});
      flush();
      assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
    });

    test('diff mode selector should be hidden for binary', done => {
      element._diff = {binary: true, content: []};

      flush(() => {
        const diffModeSelector = element.shadowRoot
            .querySelector('.diffModeSelector');
        assert.isTrue(diffModeSelector.classList.contains('hide'));
        done();
      });
    });

    suite('_commitRange', () => {
      const change = {
        _number: 42,
        revisions: {
          'commit-sha-1': {
            _number: 1,
            commit: {
              parents: [{commit: 'sha-1-parent'}],
            },
          },
          'commit-sha-2': {_number: 2, commit: {parents: []}},
          'commit-sha-3': {_number: 3, commit: {parents: []}},
          'commit-sha-4': {_number: 4, commit: {parents: []}},
          'commit-sha-5': {
            _number: 5,
            commit: {
              parents: [{commit: 'sha-5-parent'}],
            },
          },
        },
      };
      setup(() => {
        sinon.stub(element.$.diffHost, 'reload');
        sinon.stub(element, '_initCursor');
        element._change = change;
        sinon.stub(element, '_getChangeDetail').returns(Promise.resolve(
            change));
      });

      test('uses the patchNum and basePatchNum ', done => {
        element.params = {
          view: GerritNav.View.DIFF,
          changeNum: '42',
          patchNum: 4,
          basePatchNum: 2,
          path: '/COMMIT_MSG',
        };
        element._change = change;
        flush(() => {
          assert.deepEqual(element._commitRange, {
            baseCommit: 'commit-sha-2',
            commit: 'commit-sha-4',
          });
          done();
        });
      });

      test('uses the parent when there is no base patch num ', done => {
        element.params = {
          view: GerritNav.View.DIFF,
          changeNum: '42',
          patchNum: 5,
          path: '/COMMIT_MSG',
        };
        element._change = change;
        flush(() => {
          assert.deepEqual(element._commitRange, {
            commit: 'commit-sha-5',
            baseCommit: 'sha-5-parent',
          });
          done();
        });
      });
    });

    test('_initCursor', () => {
      assert.isNotOk(element.$.cursor.initialLineNumber);

      // Does nothing when params specify no cursor address:
      element._initCursor(false);
      assert.isNotOk(element.$.cursor.initialLineNumber);

      // Does nothing when params specify side but no number:
      element._initCursor(true);
      assert.isNotOk(element.$.cursor.initialLineNumber);

      // Revision hash: specifies lineNum but not side.

      element._focusLineNum = 234;
      element._initCursor(false);
      assert.equal(element.$.cursor.initialLineNumber, 234);
      assert.equal(element.$.cursor.side, 'right');

      // Base hash: specifies lineNum and side.
      element._focusLineNum = 345;
      element._initCursor(true);
      assert.equal(element.$.cursor.initialLineNumber, 345);
      assert.equal(element.$.cursor.side, 'left');

      // Specifies right side:
      element._focusLineNum = 123;
      element._initCursor(false);
      assert.equal(element.$.cursor.initialLineNumber, 123);
      assert.equal(element.$.cursor.side, 'right');
    });

    test('_getLineOfInterest', () => {
      assert.isUndefined(element._getLineOfInterest(false));

      element._focusLineNum = 12;
      let result = element._getLineOfInterest(false);
      assert.equal(result.number, 12);
      assert.isNotOk(result.leftSide);

      result = element._getLineOfInterest(true);
      assert.equal(result.number, 12);
      assert.isOk(result.leftSide);
    });

    test('_onLineSelected', () => {
      const getUrlStub = sinon.stub(GerritNav, 'getUrlForDiffById');
      const replaceStateStub = sinon.stub(history, 'replaceState');
      sinon.stub(element.$.cursor, 'getAddress')
          .returns({number: 123, isLeftSide: false});

      element._changeNum = 321;
      element._change = {_number: 321, project: 'foo/bar'};
      element._patchRange = {
        basePatchNum: 3,
        patchNum: 5,
      };
      const e = {};
      const detail = {number: 123, side: 'right'};

      element._onLineSelected(e, detail);

      assert.isTrue(replaceStateStub.called);
      assert.isTrue(getUrlStub.called);
      assert.isFalse(getUrlStub.lastCall.args[6]);
    });

    test('line selected on left side', () => {
      const getUrlStub = sinon.stub(GerritNav, 'getUrlForDiffById');
      const replaceStateStub = sinon.stub(history, 'replaceState');
      sinon.stub(element.$.cursor, 'getAddress')
          .returns({number: 123, isLeftSide: true});

      element._changeNum = 321;
      element._change = {_number: 321, project: 'foo/bar'};
      element._patchRange = {
        basePatchNum: 3,
        patchNum: 5,
      };
      const e = {};
      const detail = {number: 123, side: 'left'};

      element._onLineSelected(e, detail);

      assert.isTrue(replaceStateStub.called);
      assert.isTrue(getUrlStub.called);
      assert.isTrue(getUrlStub.lastCall.args[6]);
    });

    test('_getDiffViewMode', () => {
      // No user prefs or change view state set.
      assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');

      // User prefs but no change view state set.
      element._userPrefs = {default_diff_view: 'UNIFIED_DIFF'};
      assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');

      // User prefs and change view state set.
      element.changeViewState = {diffMode: 'SIDE_BY_SIDE'};
      assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
    });

    test('_handleToggleDiffMode', () => {
      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
      const e = {preventDefault: () => {}};
      // Initial state.
      assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');

      element._handleToggleDiffMode(e);
      assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');

      element._handleToggleDiffMode(e);
      assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
    });

    suite('_initPatchRange', () => {
      setup(async () => {
        element.params = {
          view: GerritView.DIFF,
          changeNum: '42',
          patchNum: 3,
        };
        await flush();
      });
      test('empty', () => {
        sinon.stub(element, '_getPaths').returns(new Map());
        element._initPatchRange();
        assert.equal(Object.keys(element._commentMap).length, 0);
      });

      test('has paths', () => {
        sinon.stub(element, '_getFiles');
        sinon.stub(element, '_getPaths').returns({
          'path/to/file/one.cpp': [{patch_set: 3, message: 'lorem'}],
          'path-to/file/two.py': [{patch_set: 5, message: 'ipsum'}],
        });
        element._changeNum = '42';
        element._patchRange = {
          basePatchNum: 3,
          patchNum: 5,
        };
        element._initPatchRange();
        assert.deepEqual(Object.keys(element._commentMap),
            ['path/to/file/one.cpp', 'path-to/file/two.py']);
      });
    });

    suite('_computeCommentSkips', () => {
      test('empty file list', () => {
        const commentMap = {
          'path/one.jpg': true,
          'path/three.wav': true,
        };
        const path = 'path/two.m4v';
        const fileList = [];
        const result = element._computeCommentSkips(commentMap, fileList, path);
        assert.isNull(result.previous);
        assert.isNull(result.next);
      });

      test('finds skips', () => {
        const fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav'];
        let path = fileList[1];
        const commentMap = {};
        commentMap[fileList[0]] = true;
        commentMap[fileList[1]] = false;
        commentMap[fileList[2]] = true;

        let result = element._computeCommentSkips(commentMap, fileList, path);
        assert.equal(result.previous, fileList[0]);
        assert.equal(result.next, fileList[2]);

        commentMap[fileList[1]] = true;

        result = element._computeCommentSkips(commentMap, fileList, path);
        assert.equal(result.previous, fileList[0]);
        assert.equal(result.next, fileList[2]);

        path = fileList[0];

        result = element._computeCommentSkips(commentMap, fileList, path);
        assert.isNull(result.previous);
        assert.equal(result.next, fileList[1]);

        path = fileList[2];

        result = element._computeCommentSkips(commentMap, fileList, path);
        assert.equal(result.previous, fileList[1]);
        assert.isNull(result.next);
      });

      suite('skip next/previous', () => {
        let navToChangeStub;
        let navToDiffStub;

        setup(() => {
          navToChangeStub = sinon.stub(element, '_navToChangeView');
          navToDiffStub = sinon.stub(GerritNav, 'navigateToDiff');
          element._files = getFilesFromFileList([
            'path/one.jpg', 'path/two.m4v', 'path/three.wav',
          ]);
          element._patchRange = {patchNum: 2, basePatchNum: 1};
        });

        suite('_moveToPreviousFileWithComment', () => {
          test('no skips', () => {
            element._moveToPreviousFileWithComment();
            assert.isFalse(navToChangeStub.called);
            assert.isFalse(navToDiffStub.called);
          });

          test('no previous', () => {
            const commentMap = {};
            commentMap[element._fileList[0]] = false;
            commentMap[element._fileList[1]] = false;
            commentMap[element._fileList[2]] = true;
            element._commentMap = commentMap;
            element._path = element._fileList[1];

            element._moveToPreviousFileWithComment();
            assert.isTrue(navToChangeStub.calledOnce);
            assert.isFalse(navToDiffStub.called);
          });

          test('w/ previous', () => {
            const commentMap = {};
            commentMap[element._fileList[0]] = true;
            commentMap[element._fileList[1]] = false;
            commentMap[element._fileList[2]] = true;
            element._commentMap = commentMap;
            element._path = element._fileList[1];

            element._moveToPreviousFileWithComment();
            assert.isFalse(navToChangeStub.called);
            assert.isTrue(navToDiffStub.calledOnce);
          });
        });

        suite('_moveToNextFileWithComment', () => {
          test('no skips', () => {
            element._moveToNextFileWithComment();
            assert.isFalse(navToChangeStub.called);
            assert.isFalse(navToDiffStub.called);
          });

          test('no previous', () => {
            const commentMap = {};
            commentMap[element._fileList[0]] = true;
            commentMap[element._fileList[1]] = false;
            commentMap[element._fileList[2]] = false;
            element._commentMap = commentMap;
            element._path = element._fileList[1];

            element._moveToNextFileWithComment();
            assert.isTrue(navToChangeStub.calledOnce);
            assert.isFalse(navToDiffStub.called);
          });

          test('w/ previous', () => {
            const commentMap = {};
            commentMap[element._fileList[0]] = true;
            commentMap[element._fileList[1]] = false;
            commentMap[element._fileList[2]] = true;
            element._commentMap = commentMap;
            element._path = element._fileList[1];

            element._moveToNextFileWithComment();
            assert.isFalse(navToChangeStub.called);
            assert.isTrue(navToDiffStub.calledOnce);
          });
        });
      });
    });

    test('_computeEditMode', () => {
      const callCompute = range => element._computeEditMode({base: range});
      assert.isFalse(callCompute({}));
      assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1}));
      assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1}));
      assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'}));
    });

    test('_computeFileNum', () => {
      assert.equal(element._computeFileNum('/foo',
          [{value: '/foo'}, {value: '/bar'}]), 1);
      assert.equal(element._computeFileNum('/bar',
          [{value: '/foo'}, {value: '/bar'}]), 2);
    });

    test('_computeFileNumClass', () => {
      assert.equal(element._computeFileNumClass(0, []), '');
      assert.equal(element._computeFileNumClass(1,
          [{value: '/foo'}, {value: '/bar'}]), 'show');
    });

    test('_getReviewedStatus', () => {
      const promises = [];
      getReviewedFilesStub.returns(Promise.resolve(['path']));

      promises.push(element._getReviewedStatus(true, null, null, 'path')
          .then(reviewed => assert.isFalse(reviewed)));

      promises.push(element._getReviewedStatus(false, null, null, 'otherPath')
          .then(reviewed => assert.isFalse(reviewed)));

      promises.push(element._getReviewedStatus(false, null, null, 'path')
          .then(reviewed => assert.isFalse(reviewed)));

      promises.push(element._getReviewedStatus(false, 3, 5, 'path')
          .then(reviewed => assert.isTrue(reviewed)));

      return Promise.all(promises);
    });

    test('f open file dropdown', () => {
      assert.isFalse(element.$.dropdown.$.dropdown.opened);
      MockInteractions.pressAndReleaseKeyOn(element, 70, null, 'f');
      flush();
      assert.isTrue(element.$.dropdown.$.dropdown.opened);
    });

    suite('blame', () => {
      test('toggle blame with button', () => {
        const toggleBlame = sinon.stub(
            element.$.diffHost, 'loadBlame')
            .callsFake(() => Promise.resolve());
        MockInteractions.tap(element.$.toggleBlame);
        assert.isTrue(toggleBlame.calledOnce);
      });
      test('toggle blame with shortcut', () => {
        const toggleBlame = sinon.stub(
            element.$.diffHost, 'loadBlame').callsFake(() => Promise.resolve());
        MockInteractions.pressAndReleaseKeyOn(element, 66, null, 'b');
        assert.isTrue(toggleBlame.calledOnce);
      });
    });

    suite('editMode behavior', () => {
      setup(() => {
        element._loggedIn = true;
      });

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

      test('reviewed checkbox', () => {
        sinon.stub(element, '_handlePatchChange');
        element._patchRange = {patchNum: 1};
        // Reviewed checkbox should be shown.
        assert.isTrue(isVisible(element.$.reviewed));
        element.set('_patchRange.patchNum', EditPatchSetNum);
        flush();

        assert.isFalse(isVisible(element.$.reviewed));
      });
    });

    test('_paramsChanged sets in projectLookup', () => {
      sinon.stub(element, '_initLineOfInterestAndCursor');
      const setStub = stubRestApi('setInProjectLookup');
      element._paramsChanged({
        view: GerritNav.View.DIFF,
        changeNum: 101,
        project: 'test-project',
        path: '',
      });
      assert.isTrue(setStub.calledOnce);
      assert.isTrue(setStub.calledWith(101, 'test-project'));
    });

    test('shift+m navigates to next unreviewed file', () => {
      element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
      element._reviewedFiles = new Set(['file1', 'file2']);
      element._path = 'file1';
      const reviewedStub = sinon.stub(element, '_setReviewed');
      const navStub = sinon.stub(element, '_navToFile');
      MockInteractions.pressAndReleaseKeyOn(element, 77, 'shift', 'm');
      flush();

      assert.isTrue(reviewedStub.lastCall.args[0]);
      assert.deepEqual(navStub.lastCall.args, [
        'file1',
        ['file1', 'file3'],
        1,
      ]);
    });

    test('File change should trigger navigateToDiff once', done => {
      element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
      sinon.stub(element, '_initLineOfInterestAndCursor');
      sinon.stub(GerritNav, 'navigateToDiff');

      // Load file1
      element.params = {
        view: GerritNav.View.DIFF,
        patchNum: 1,
        changeNum: 101,
        project: 'test-project',
        path: 'file1',
      };
      element._patchRange = {
        patchNum: 1,
        basePatchNum: 'PARENT',
      };
      element._change = {
        ...createChange(),
        revisions: createRevisions(1),
      };
      flush();
      assert.isTrue(GerritNav.navigateToDiff.notCalled);

      // Switch to file2
      element._handleFileChange({detail: {value: 'file2'}});
      assert.isTrue(GerritNav.navigateToDiff.calledOnce);

      // This is to mock the param change triggered by above navigate
      element.params = {
        view: GerritNav.View.DIFF,
        patchNum: 1,
        changeNum: 101,
        project: 'test-project',
        path: 'file2',
      };
      element._patchRange = {
        patchNum: 1,
        basePatchNum: 'PARENT',
      };

      // No extra call
      assert.isTrue(GerritNav.navigateToDiff.calledOnce);
      done();
    });

    test('_computeDownloadDropdownLinks', () => {
      const downloadLinks = [
        {
          url: '/changes/test~12/revisions/1/patch?zip&path=index.php',
          name: 'Patch',
        },
        {
          url: '/changes/test~12/revisions/1' +
              '/files/index.php/download?parent=1',
          name: 'Left Content',
        },
        {
          url: '/changes/test~12/revisions/1' +
              '/files/index.php/download',
          name: 'Right Content',
        },
      ];

      const side = {
        meta_a: true,
        meta_b: true,
      };

      const base = {
        patchNum: 1,
        basePatchNum: 'PARENT',
      };

      assert.deepEqual(
          element._computeDownloadDropdownLinks(
              'test', 12, base, 'index.php', side),
          downloadLinks);
    });

    test('_computeDownloadDropdownLinks diff returns renamed', () => {
      const downloadLinks = [
        {
          url: '/changes/test~12/revisions/3/patch?zip&path=index.php',
          name: 'Patch',
        },
        {
          url: '/changes/test~12/revisions/2' +
              '/files/index2.php/download',
          name: 'Left Content',
        },
        {
          url: '/changes/test~12/revisions/3' +
              '/files/index.php/download',
          name: 'Right Content',
        },
      ];

      const side = {
        change_type: 'RENAMED',
        meta_a: {
          name: 'index2.php',
        },
        meta_b: true,
      };

      const base = {
        patchNum: 3,
        basePatchNum: 2,
      };

      assert.deepEqual(
          element._computeDownloadDropdownLinks(
              'test', 12, base, 'index.php', side),
          downloadLinks);
    });

    test('_computeDownloadFileLink', () => {
      const base = {
        patchNum: 1,
        basePatchNum: 'PARENT',
      };

      assert.equal(
          element._computeDownloadFileLink(
              'test', 12, base, 'index.php', true),
          '/changes/test~12/revisions/1/files/index.php/download?parent=1');

      assert.equal(
          element._computeDownloadFileLink(
              'test', 12, base, 'index.php', false),
          '/changes/test~12/revisions/1/files/index.php/download');
    });

    test('_computeDownloadPatchLink', () => {
      assert.equal(
          element._computeDownloadPatchLink(
              'test', 12, {patchNum: 1}, 'index.php'),
          '/changes/test~12/revisions/1/patch?zip&path=index.php');
    });
  });

  suite('gr-diff-view tests unmodified files with comments', () => {
    let element;
    setup(() => {
      const changedFiles = {
        'file1.txt': {},
        'a/b/test.c': {},
      };
      stubRestApi('getConfig').returns(Promise.resolve({change: {}}));

      stubRestApi('getProjectConfig').returns(Promise.resolve({}));
      stubRestApi('getDiffChangeDetail').returns(Promise.resolve({}));
      stubRestApi('getChangeFiles').returns(Promise.resolve(changedFiles));
      stubRestApi('saveFileReviewed').returns(Promise.resolve());
      stubRestApi('getDiffComments').returns(Promise.resolve({}));
      stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
      stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
      stubRestApi('getReviewedFiles').returns(
          Promise.resolve([]));
      element = basicFixture.instantiate();
      element._changeNum = '42';
      return element._loadComments();
    });

    test('_getFiles add files with comments without changes', () => {
      const patchChangeRecord = {
        base: {
          basePatchNum: 5,
          patchNum: 10,
        },
      };
      const changeComments = {
        getPaths: sinon.stub().returns({
          'file2.txt': {},
          'file1.txt': {},
        }),
      };
      return element._getFiles(23, patchChangeRecord, changeComments)
          .then(() => {
            assert.deepEqual(element._files, {
              sortedFileList: ['a/b/test.c', 'file1.txt', 'file2.txt'],
              changeFilesByPath: {
                'file1.txt': {},
                'file2.txt': {status: 'U'},
                'a/b/test.c': {},
              },
            });
          });
    });
  });
});

