/**
 * @license
 * Copyright (C) 2018 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-host.js';
import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {sortComments} from '../../../utils/comment-util.js';
import {Side} from '../../../constants/constants.js';
import {createChange} from '../../../test/test-data-generators.js';

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

suite('gr-diff-host tests', () => {
  let element;

  let getLoggedIn;

  setup(() => {
    getLoggedIn = false;
    stub('gr-rest-api-interface', {
      async getLoggedIn() { return getLoggedIn; },
    });
    element = basicFixture.instantiate();
    element.changeNum = 123;
    element.path = 'some/path';
    sinon.stub(element.reporting, 'time');
    sinon.stub(element.reporting, 'timeEnd');
  });

  suite('plugin layers', () => {
    const pluginLayers = [{annotate: () => {}}, {annotate: () => {}}];
    setup(() => {
      stub('gr-js-api-interface', {
        getDiffLayers() { return pluginLayers; },
      });
      element = basicFixture.instantiate();
      element.changeNum = 123;
      element.path = 'some/path';
    });
    test('plugin layers requested', () => {
      element.patchRange = {};
      element.change = createChange();
      element.reload();
      assert(element.$.jsAPI.getDiffLayers.called);
    });
  });

  suite('handle comment-update', () => {
    setup(() => {
      sinon.stub(element, '_commentsChanged');
      element.comments = {
        meta: {
          changeNum: '42',
          patchRange: {
            basePatchNum: 'PARENT',
            patchNum: 3,
          },
          path: '/path/to/foo',
          projectConfig: {foo: 'bar'},
        },
        left: [
          {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
          {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
          {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
          {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
        ],
        right: [
          {id: 'c1', __commentSide: 'right'},
          {id: 'c2', __commentSide: 'right'},
          {id: 'd1', __draft: true, __commentSide: 'right'},
          {id: 'd2', __draft: true, __commentSide: 'right'},
        ],
      };
    });

    test('creating a draft', () => {
      const comment = {__draft: true, __draftID: 'tempID', side: 'PARENT',
        __commentSide: 'left'};
      element.dispatchEvent(
          new CustomEvent('comment-update', {
            detail: {comment},
            composed: true, bubbles: true,
          }));
      assert.include(element.comments.left, comment);
    });

    test('discarding a draft', () => {
      const draftID = 'tempID';
      const id = 'savedID';
      const comment = {
        __draft: true,
        __draftID: draftID,
        side: 'PARENT',
        __commentSide: 'left',
      };
      const diffCommentsModifiedStub = sinon.stub();
      element.addEventListener('diff-comments-modified',
          diffCommentsModifiedStub);
      element.comments.left.push(comment);
      comment.id = id;
      element.dispatchEvent(
          new CustomEvent('comment-discard', {
            detail: {comment},
            composed: true, bubbles: true,
          }));
      const drafts = element.comments.left
          .filter(item => item.__draftID === draftID);
      assert.equal(drafts.length, 0);
      assert.isTrue(diffCommentsModifiedStub.called);
    });

    test('saving a draft', () => {
      const draftID = 'tempID';
      const id = 'savedID';
      const comment = {
        __draft: true,
        __draftID: draftID,
        side: 'PARENT',
        __commentSide: 'left',
      };
      const diffCommentsModifiedStub = sinon.stub();
      element.addEventListener('diff-comments-modified',
          diffCommentsModifiedStub);
      element.comments.left.push(comment);
      comment.id = id;
      element.dispatchEvent(
          new CustomEvent('comment-save', {
            detail: {comment},
            composed: true, bubbles: true,
          }));
      const drafts = element.comments.left
          .filter(item => item.__draftID === draftID);
      assert.equal(drafts.length, 1);
      assert.equal(drafts[0].id, id);
      assert.isTrue(diffCommentsModifiedStub.called);
    });
  });

  test('remove comment', () => {
    sinon.stub(element, '_commentsChanged');
    element.comments = {
      meta: {
        changeNum: '42',
        patchRange: {
          basePatchNum: 'PARENT',
          patchNum: 3,
        },
        path: '/path/to/foo',
        projectConfig: {foo: 'bar'},
      },
      left: [
        {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
        {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
        {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
        {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
      ],
      right: [
        {id: 'c1', __commentSide: 'right'},
        {id: 'c2', __commentSide: 'right'},
        {id: 'd1', __draft: true, __commentSide: 'right'},
        {id: 'd2', __draft: true, __commentSide: 'right'},
      ],
    };

    // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem
    // to believe that one object deepEquals another even when they do :-/.
    assert.equal(JSON.stringify(element.comments), JSON.stringify({
      meta: {
        changeNum: '42',
        patchRange: {
          basePatchNum: 'PARENT',
          patchNum: 3,
        },
        path: '/path/to/foo',
        projectConfig: {foo: 'bar'},
      },
      left: [
        {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
        {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
        {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
        {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
      ],
      right: [
        {id: 'c1', __commentSide: 'right'},
        {id: 'c2', __commentSide: 'right'},
        {id: 'd1', __draft: true, __commentSide: 'right'},
        {id: 'd2', __draft: true, __commentSide: 'right'},
      ],
    }));

    element._removeComment({id: 'bc2', side: 'PARENT',
      __commentSide: 'left'});
    assert.deepEqual(element.comments, {
      meta: {
        changeNum: '42',
        patchRange: {
          basePatchNum: 'PARENT',
          patchNum: 3,
        },
        path: '/path/to/foo',
        projectConfig: {foo: 'bar'},
      },
      left: [
        {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
        {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
        {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
      ],
      right: [
        {id: 'c1', __commentSide: 'right'},
        {id: 'c2', __commentSide: 'right'},
        {id: 'd1', __draft: true, __commentSide: 'right'},
        {id: 'd2', __draft: true, __commentSide: 'right'},
      ],
    });

    element._removeComment({id: 'd2', __commentSide: 'right'});
    assert.deepEqual(element.comments, {
      meta: {
        changeNum: '42',
        patchRange: {
          basePatchNum: 'PARENT',
          patchNum: 3,
        },
        path: '/path/to/foo',
        projectConfig: {foo: 'bar'},
      },
      left: [
        {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
        {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
        {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
      ],
      right: [
        {id: 'c1', __commentSide: 'right'},
        {id: 'c2', __commentSide: 'right'},
        {id: 'd1', __draft: true, __commentSide: 'right'},
      ],
    });
  });

  test('thread-discard handling', () => {
    const threads = element._createThreads([
      {
        id: 4711,
        __commentSide: 'left',
        updated: '2015-12-20 15:01:20.396000000',
      },
      {
        id: 42,
        __commentSide: 'left',
        updated: '2017-12-20 15:01:20.396000000',
      },
    ]);
    element._parentIndex = 1;
    element.changeNum = 2;
    element.path = 'some/path';
    element.projectName = 'Some project';
    const threadEls = threads.map(
        thread => {
          const threadEl = element._createThreadElement(thread);
          // Polymer 2 doesn't fire ready events and doesn't execute
          // observers if element is not added to the Dom.
          // See https://github.com/Polymer/old-docs-site/issues/2322
          // and https://github.com/Polymer/polymer/issues/4526
          element._attachThreadElement(threadEl);
          return threadEl;
        });
    assert.equal(threadEls.length, 2);
    assert.equal(threadEls[0].comments[0].id, 4711);
    assert.equal(threadEls[1].comments[0].id, 42);
    for (const threadEl of threadEls) {
      element.appendChild(threadEl);
    }

    threadEls[0].dispatchEvent(
        new CustomEvent('thread-discard', {detail: {rootId: 4711}}));
    const attachedThreads = element.queryAllEffectiveChildren(
        'gr-comment-thread');
    assert.equal(attachedThreads.length, 1);
    assert.equal(attachedThreads[0].comments[0].id, 42);
  });

  suite('render reporting', () => {
    test('starts total and content timer on render-start', done => {
      element.dispatchEvent(
          new CustomEvent('render-start', {bubbles: true, composed: true}));
      assert.isTrue(element.reporting.time.calledWithExactly(
          'Diff Total Render'));
      assert.isTrue(element.reporting.time.calledWithExactly(
          'Diff Content Render'));
      done();
    });

    test('ends content timer on render-content', () => {
      element.dispatchEvent(
          new CustomEvent('render-content', {bubbles: true, composed: true}));
      assert.isTrue(element.reporting.timeEnd.calledWithExactly(
          'Diff Content Render'));
    });

    test('ends total and syntax timer after syntax layer', async () => {
      sinon.stub(element.reporting, 'diffViewContentDisplayed');
      let notifySyntaxProcessed;
      sinon.stub(element.$.syntaxLayer, 'process').returns(new Promise(
          resolve => {
            notifySyntaxProcessed = resolve;
          }));
      sinon.stub(element.$.restAPI, 'getDiff').returns(
          Promise.resolve({content: []}));
      element.patchRange = {};
      element.change = createChange();
      element.$.restAPI.getDiffPreferences().then(prefs => {
        element.prefs = prefs;
        return element.reload(true);
      });
      // Multiple cascading microtasks are scheduled.
      await flush();
      notifySyntaxProcessed();
      // Multiple cascading microtasks are scheduled.
      await flush();
      assert.isTrue(element.reporting.timeEnd.calledWithExactly(
          'Diff Total Render'));
      assert.isTrue(element.reporting.timeEnd.calledWithExactly(
          'Diff Syntax Render'));
      assert.isTrue(element.reporting.diffViewContentDisplayed.called);
    });

    test('ends total timer w/ no syntax layer processing', async () => {
      sinon.stub(element.$.restAPI, 'getDiff').returns(
          Promise.resolve({content: []}));
      element.patchRange = {};
      element.change = createChange();
      element.reload();
      // Multiple cascading microtasks are scheduled.
      await flush();
      // Reporting can be called with other parameters (ex. PluginsLoaded),
      // but only 'Diff Total Render' is important in this test.
      assert.equal(
          element.reporting.timeEnd.getCalls()
              .filter(call => call.calledWithExactly('Diff Total Render'))
              .length,
          1);
    });

    test('completes reload promise after syntax layer processing', async () => {
      let notifySyntaxProcessed;
      sinon.stub(element.$.syntaxLayer, 'process').returns(new Promise(
          resolve => {
            notifySyntaxProcessed = resolve;
          }));
      sinon.stub(element.$.restAPI, 'getDiff').returns(
          Promise.resolve({content: []}));
      element.patchRange = {};
      element.change = createChange();
      let reloadComplete = false;
      element.$.restAPI.getDiffPreferences()
          .then(prefs => {
            element.prefs = prefs;
            return element.reload();
          })
          .then(() => {
            reloadComplete = true;
          });
      // Multiple cascading microtasks are scheduled.
      await flush();
      assert.isFalse(reloadComplete);
      notifySyntaxProcessed();
      // Assert after the notification task is processed.
      await flush();
      assert.isTrue(reloadComplete);
    });
  });

  test('reload() cancels before network resolves', () => {
    const cancelStub = sinon.stub(element.$.diff, 'cancel');

    // Stub the network calls into requests that never resolve.
    sinon.stub(element, '_getDiff').callsFake(() => new Promise(() => {}));
    element.patchRange = {};
    element.change = createChange();

    // Needs to be set to something first for it to cancel.
    element.diff = {
      content: [{
        a: ['foo'],
      }],
    };

    element.reload();
    assert.isTrue(cancelStub.called);
  });

  suite('not logged in', () => {
    setup(() => {
      getLoggedIn = false;
      element = basicFixture.instantiate();
      element.changeNum = 123;
      element.change = createChange();
      element.path = 'some/path';
    });

    test('reload() loads files weblinks', () => {
      const weblinksStub = sinon.stub(GerritNav, '_generateWeblinks')
          .returns({name: 'stubb', url: '#s'});
      sinon.stub(element.$.restAPI, 'getDiff').returns(Promise.resolve({
        content: [],
      }));
      element.projectName = 'test-project';
      element.path = 'test-path';
      element.commitRange = {baseCommit: 'test-base', commit: 'test-commit'};
      element.patchRange = {};
      return element.reload().then(() => {
        assert.isTrue(weblinksStub.calledTwice);
        assert.isTrue(weblinksStub.firstCall.calledWith({
          commit: 'test-base',
          file: 'test-path',
          options: {
            weblinks: undefined,
          },
          repo: 'test-project',
          type: GerritNav.WeblinkType.FILE}));
        assert.isTrue(weblinksStub.secondCall.calledWith({
          commit: 'test-commit',
          file: 'test-path',
          options: {
            weblinks: undefined,
          },
          repo: 'test-project',
          type: GerritNav.WeblinkType.FILE}));
        assert.deepEqual(element.filesWeblinks, {
          meta_a: [{name: 'stubb', url: '#s'}],
          meta_b: [{name: 'stubb', url: '#s'}],
        });
      });
    });

    test('prefetch getDiff', done => {
      const diffRestApiStub = sinon.stub(element.$.restAPI, 'getDiff')
          .returns(Promise.resolve({content: []}));
      element.changeNum = 123;
      element.patchRange = {basePatchNum: 1, patchNum: 2};
      element.path = 'file.txt';
      element.prefetchDiff();
      element._getDiff().then(() =>{
        assert.isTrue(diffRestApiStub.calledOnce);
        done();
      });
    });

    test('_getDiff handles null diff responses', done => {
      stub('gr-rest-api-interface', {
        getDiff() { return Promise.resolve(null); },
      });
      element.changeNum = 123;
      element.patchRange = {basePatchNum: 1, patchNum: 2};
      element.path = 'file.txt';
      element._getDiff().then(done);
    });

    test('reload resolves on error', () => {
      const onErrStub = sinon.stub(element, '_handleGetDiffError');
      const error = new Response(null, {ok: false, status: 500});
      sinon.stub(element.$.restAPI, 'getDiff').callsFake(
          (changeNum, basePatchNum, patchNum, path, whitespace, onErr) => {
            onErr(error);
          });
      element.patchRange = {};
      return element.reload().then(() => {
        assert.isTrue(onErrStub.calledOnce);
      });
    });

    suite('_handleGetDiffError', () => {
      let serverErrorStub;
      let pageErrorStub;

      setup(() => {
        serverErrorStub = sinon.stub();
        element.addEventListener('server-error', serverErrorStub);
        pageErrorStub = sinon.stub();
        element.addEventListener('page-error', pageErrorStub);
      });

      test('page error on HTTP-409', () => {
        element._handleGetDiffError({status: 409});
        assert.isTrue(serverErrorStub.calledOnce);
        assert.isFalse(pageErrorStub.called);
        assert.isNotOk(element._errorMessage);
      });

      test('server error on non-HTTP-409', () => {
        element._handleGetDiffError({status: 500});
        assert.isFalse(serverErrorStub.called);
        assert.isTrue(pageErrorStub.calledOnce);
        assert.isNotOk(element._errorMessage);
      });

      test('error message if showLoadFailure', () => {
        element.showLoadFailure = true;
        element._handleGetDiffError({status: 500, statusText: 'Failure!'});
        assert.isFalse(serverErrorStub.called);
        assert.isFalse(pageErrorStub.called);
        assert.equal(element._errorMessage,
            'Encountered error when loading the diff: 500 Failure!');
      });
    });

    suite('image diffs', () => {
      let mockFile1;
      let mockFile2;
      setup(() => {
        mockFile1 = {
          body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
          'wsAAAAAAAAAAAAAAAAA/w==',
          type: 'image/bmp',
        };
        mockFile2 = {
          body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
          'wsAAAAAAAAAAAAA/////w==',
          type: 'image/bmp',
        };
        sinon.stub(element.$.restAPI,
            'getB64FileContents')
            .callsFake(
                (changeId, patchNum, path, opt_parentIndex) => Promise.resolve(
                    opt_parentIndex === 1 ? mockFile1 : mockFile2)
            );

        element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
        element.change = createChange();
        element.comments = {
          left: [],
          right: [],
          meta: {patchRange: element.patchRange},
        };
      });

      test('renders image diffs with same file name', done => {
        const mockDiff = {
          meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
          meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
            lines: 560},
          intraline_status: 'OK',
          change_type: 'MODIFIED',
          diff_header: [
            'diff --git a/carrot.jpg b/carrot.jpg',
            'index 2adc47d..f9c2f2c 100644',
            '--- a/carrot.jpg',
            '+++ b/carrot.jpg',
            'Binary files differ',
          ],
          content: [{skip: 66}],
          binary: true,
        };
        sinon.stub(element.$.restAPI, 'getDiff')
            .returns(Promise.resolve(mockDiff));

        const rendered = () => {
          // Recognizes that it should be an image diff.
          assert.isTrue(element.isImageDiff);
          assert.instanceOf(
              element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);

          // Left image rendered with the parent commit's version of the file.
          const leftImage =
              element.$.diff.$.diffTable.querySelector('td.left img');
          const leftLabel =
              element.$.diff.$.diffTable.querySelector('td.left label');
          const leftLabelContent = leftLabel.querySelector('.label');
          const leftLabelName = leftLabel.querySelector('.name');

          const rightImage =
              element.$.diff.$.diffTable.querySelector('td.right img');
          const rightLabel = element.$.diff.$.diffTable.querySelector(
              'td.right label');
          const rightLabelContent = rightLabel.querySelector('.label');
          const rightLabelName = rightLabel.querySelector('.name');

          assert.isNotOk(rightLabelName);
          assert.isNotOk(leftLabelName);

          let leftLoaded = false;
          let rightLoaded = false;

          leftImage.addEventListener('load', () => {
            assert.isOk(leftImage);
            assert.equal(leftImage.getAttribute('src'),
                'data:image/bmp;base64, ' + mockFile1.body);
            assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
            leftLoaded = true;
            if (rightLoaded) {
              element.removeEventListener('render', rendered);
              done();
            }
          });

          rightImage.addEventListener('load', () => {
            assert.isOk(rightImage);
            assert.equal(rightImage.getAttribute('src'),
                'data:image/bmp;base64, ' + mockFile2.body);
            assert.equal(rightLabelContent.textContent, '1×1 image/bmp');

            rightLoaded = true;
            if (leftLoaded) {
              element.removeEventListener('render', rendered);
              done();
            }
          });
        };

        element.addEventListener('render', rendered);

        element.$.restAPI.getDiffPreferences().then(prefs => {
          element.prefs = prefs;
          element.reload();
        });
      });

      test('renders image diffs with a different file name', done => {
        const mockDiff = {
          meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
          meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
            lines: 560},
          intraline_status: 'OK',
          change_type: 'MODIFIED',
          diff_header: [
            'diff --git a/carrot.jpg b/carrot2.jpg',
            'index 2adc47d..f9c2f2c 100644',
            '--- a/carrot.jpg',
            '+++ b/carrot2.jpg',
            'Binary files differ',
          ],
          content: [{skip: 66}],
          binary: true,
        };
        sinon.stub(element.$.restAPI, 'getDiff')
            .returns(Promise.resolve(mockDiff));

        const rendered = () => {
          // Recognizes that it should be an image diff.
          assert.isTrue(element.isImageDiff);
          assert.instanceOf(
              element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);

          // Left image rendered with the parent commit's version of the file.
          const leftImage =
              element.$.diff.$.diffTable.querySelector('td.left img');
          const leftLabel =
              element.$.diff.$.diffTable.querySelector('td.left label');
          const leftLabelContent = leftLabel.querySelector('.label');
          const leftLabelName = leftLabel.querySelector('.name');

          const rightImage =
              element.$.diff.$.diffTable.querySelector('td.right img');
          const rightLabel = element.$.diff.$.diffTable.querySelector(
              'td.right label');
          const rightLabelContent = rightLabel.querySelector('.label');
          const rightLabelName = rightLabel.querySelector('.name');

          assert.isOk(rightLabelName);
          assert.isOk(leftLabelName);
          assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
          assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);

          let leftLoaded = false;
          let rightLoaded = false;

          leftImage.addEventListener('load', () => {
            assert.isOk(leftImage);
            assert.equal(leftImage.getAttribute('src'),
                'data:image/bmp;base64, ' + mockFile1.body);
            assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
            leftLoaded = true;
            if (rightLoaded) {
              element.removeEventListener('render', rendered);
              done();
            }
          });

          rightImage.addEventListener('load', () => {
            assert.isOk(rightImage);
            assert.equal(rightImage.getAttribute('src'),
                'data:image/bmp;base64, ' + mockFile2.body);
            assert.equal(rightLabelContent.textContent, '1×1 image/bmp');

            rightLoaded = true;
            if (leftLoaded) {
              element.removeEventListener('render', rendered);
              done();
            }
          });
        };

        element.addEventListener('render', rendered);

        element.$.restAPI.getDiffPreferences().then(prefs => {
          element.prefs = prefs;
          element.reload();
        });
      });

      test('renders added image', done => {
        const mockDiff = {
          meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
            lines: 560},
          intraline_status: 'OK',
          change_type: 'ADDED',
          diff_header: [
            'diff --git a/carrot.jpg b/carrot.jpg',
            'index 0000000..f9c2f2c 100644',
            '--- /dev/null',
            '+++ b/carrot.jpg',
            'Binary files differ',
          ],
          content: [{skip: 66}],
          binary: true,
        };
        sinon.stub(element.$.restAPI, 'getDiff')
            .returns(Promise.resolve(mockDiff));

        element.addEventListener('render', () => {
          // Recognizes that it should be an image diff.
          assert.isTrue(element.isImageDiff);
          assert.instanceOf(
              element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);

          const leftImage =
              element.$.diff.$.diffTable.querySelector('td.left img');
          const rightImage =
              element.$.diff.$.diffTable.querySelector('td.right img');

          assert.isNotOk(leftImage);
          assert.isOk(rightImage);
          done();
        });

        element.$.restAPI.getDiffPreferences().then(prefs => {
          element.prefs = prefs;
          element.reload();
        });
      });

      test('renders removed image', done => {
        const mockDiff = {
          meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
            lines: 560},
          intraline_status: 'OK',
          change_type: 'DELETED',
          diff_header: [
            'diff --git a/carrot.jpg b/carrot.jpg',
            'index f9c2f2c..0000000 100644',
            '--- a/carrot.jpg',
            '+++ /dev/null',
            'Binary files differ',
          ],
          content: [{skip: 66}],
          binary: true,
        };
        sinon.stub(element.$.restAPI, 'getDiff')
            .returns(Promise.resolve(mockDiff));

        element.addEventListener('render', () => {
          // Recognizes that it should be an image diff.
          assert.isTrue(element.isImageDiff);
          assert.instanceOf(
              element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);

          const leftImage =
              element.$.diff.$.diffTable.querySelector('td.left img');
          const rightImage =
              element.$.diff.$.diffTable.querySelector('td.right img');

          assert.isOk(leftImage);
          assert.isNotOk(rightImage);
          done();
        });

        element.$.restAPI.getDiffPreferences().then(prefs => {
          element.prefs = prefs;
          element.reload();
        });
      });

      test('does not render disallowed image type', done => {
        const mockDiff = {
          meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
            lines: 560},
          intraline_status: 'OK',
          change_type: 'DELETED',
          diff_header: [
            'diff --git a/carrot.jpg b/carrot.jpg',
            'index f9c2f2c..0000000 100644',
            '--- a/carrot.jpg',
            '+++ /dev/null',
            'Binary files differ',
          ],
          content: [{skip: 66}],
          binary: true,
        };
        mockFile1.type = 'image/jpeg-evil';

        sinon.stub(element.$.restAPI, 'getDiff')
            .returns(Promise.resolve(mockDiff));

        element.addEventListener('render', () => {
          // Recognizes that it should be an image diff.
          assert.isTrue(element.isImageDiff);
          assert.instanceOf(
              element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
          const leftImage =
              element.$.diff.$.diffTable.querySelector('td.left img');
          assert.isNotOk(leftImage);
          done();
        });

        element.$.restAPI.getDiffPreferences().then(prefs => {
          element.prefs = prefs;
          element.reload();
        });
      });
    });
  });

  test('delegates cancel()', () => {
    const stub = sinon.stub(element.$.diff, 'cancel');
    element.patchRange = {};
    element.cancel();
    assert.isTrue(stub.calledOnce);
    assert.equal(stub.lastCall.args.length, 0);
  });

  test('delegates getCursorStops()', () => {
    const returnValue = [document.createElement('b')];
    const stub = sinon.stub(element.$.diff, 'getCursorStops')
        .returns(returnValue);
    assert.equal(element.getCursorStops(), returnValue);
    assert.isTrue(stub.calledOnce);
    assert.equal(stub.lastCall.args.length, 0);
  });

  test('delegates isRangeSelected()', () => {
    const returnValue = true;
    const stub = sinon.stub(element.$.diff, 'isRangeSelected')
        .returns(returnValue);
    assert.equal(element.isRangeSelected(), returnValue);
    assert.isTrue(stub.calledOnce);
    assert.equal(stub.lastCall.args.length, 0);
  });

  test('delegates toggleLeftDiff()', () => {
    const stub = sinon.stub(element.$.diff, 'toggleLeftDiff');
    element.toggleLeftDiff();
    assert.isTrue(stub.calledOnce);
    assert.equal(stub.lastCall.args.length, 0);
  });

  suite('blame', () => {
    setup(() => {
      element = basicFixture.instantiate();
      element.changeNum = 123;
      element.path = 'some/path';
    });

    test('clearBlame', () => {
      element._blame = [];
      const setBlameSpy = sinon.spy(element.$.diff.$.diffBuilder, 'setBlame');
      element.clearBlame();
      assert.isNull(element._blame);
      assert.isTrue(setBlameSpy.calledWithExactly(null));
      assert.equal(element.isBlameLoaded, false);
    });

    test('loadBlame', () => {
      const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
      const showAlertStub = sinon.stub();
      element.addEventListener('show-alert', showAlertStub);
      const getBlameStub = sinon.stub(element.$.restAPI, 'getBlame')
          .returns(Promise.resolve(mockBlame));
      element.changeNum = 42;
      element.patchRange = {patchNum: 5, basePatchNum: 4};
      element.path = 'foo/bar.baz';
      return element.loadBlame().then(() => {
        assert.isTrue(getBlameStub.calledWithExactly(
            42, 5, 'foo/bar.baz', true));
        assert.isFalse(showAlertStub.called);
        assert.equal(element._blame, mockBlame);
        assert.equal(element.isBlameLoaded, true);
      });
    });

    test('loadBlame empty', () => {
      const mockBlame = [];
      const showAlertStub = sinon.stub();
      element.addEventListener('show-alert', showAlertStub);
      sinon.stub(element.$.restAPI, 'getBlame')
          .returns(Promise.resolve(mockBlame));
      element.changeNum = 42;
      element.patchRange = {patchNum: 5, basePatchNum: 4};
      element.path = 'foo/bar.baz';
      return element.loadBlame()
          .then(() => {
            assert.isTrue(false, 'Promise should not resolve');
          })
          .catch(() => {
            assert.isTrue(showAlertStub.calledOnce);
            assert.isNull(element._blame);
            assert.equal(element.isBlameLoaded, false);
          });
    });
  });

  test('getThreadEls() returns .comment-threads', () => {
    const threadEl = document.createElement('div');
    threadEl.className = 'comment-thread';
    element.$.diff.appendChild(threadEl);
    assert.deepEqual(element.getThreadEls(), [threadEl]);
  });

  test('delegates addDraftAtLine(el)', () => {
    const param0 = document.createElement('b');
    const stub = sinon.stub(element.$.diff, 'addDraftAtLine');
    element.addDraftAtLine(param0);
    assert.isTrue(stub.calledOnce);
    assert.equal(stub.lastCall.args.length, 1);
    assert.equal(stub.lastCall.args[0], param0);
  });

  test('delegates clearDiffContent()', () => {
    const stub = sinon.stub(element.$.diff, 'clearDiffContent');
    element.clearDiffContent();
    assert.isTrue(stub.calledOnce);
    assert.equal(stub.lastCall.args.length, 0);
  });

  test('delegates expandAllContext()', () => {
    const stub = sinon.stub(element.$.diff, 'expandAllContext');
    element.expandAllContext();
    assert.isTrue(stub.calledOnce);
    assert.equal(stub.lastCall.args.length, 0);
  });

  test('passes in changeNum', () => {
    element.changeNum = 12345;
    assert.equal(element.$.diff.changeNum, 12345);
  });

  test('passes in noAutoRender', () => {
    const value = true;
    element.noAutoRender = value;
    assert.equal(element.$.diff.noAutoRender, value);
  });

  test('passes in patchRange', () => {
    const value = {patchNum: 'foo', basePatchNum: 'bar'};
    element.patchRange = value;
    assert.equal(element.$.diff.patchRange, value);
  });

  test('passes in path', () => {
    const value = 'some/file/path';
    element.path = value;
    assert.equal(element.$.diff.path, value);
  });

  test('passes in prefs', () => {
    const value = {};
    element.prefs = value;
    assert.equal(element.$.diff.prefs, value);
  });

  test('passes in changeNum', () => {
    element.changeNum = 12345;
    assert.equal(element.$.diff.changeNum, 12345);
  });

  test('passes in displayLine', () => {
    const value = true;
    element.displayLine = value;
    assert.equal(element.$.diff.displayLine, value);
  });

  test('passes in hidden', () => {
    const value = true;
    element.hidden = value;
    assert.equal(element.$.diff.hidden, value);
    assert.isNotNull(element.getAttribute('hidden'));
  });

  test('passes in noRenderOnPrefsChange', () => {
    const value = true;
    element.noRenderOnPrefsChange = value;
    assert.equal(element.$.diff.noRenderOnPrefsChange, value);
  });

  test('passes in lineWrapping', () => {
    const value = true;
    element.lineWrapping = value;
    assert.equal(element.$.diff.lineWrapping, value);
  });

  test('passes in viewMode', () => {
    const value = 'SIDE_BY_SIDE';
    element.viewMode = value;
    assert.equal(element.$.diff.viewMode, value);
  });

  test('passes in lineOfInterest', () => {
    const value = {number: 123, leftSide: true};
    element.lineOfInterest = value;
    assert.equal(element.$.diff.lineOfInterest, value);
  });

  suite('_reportDiff', () => {
    let reportStub;

    setup(() => {
      element = basicFixture.instantiate();
      element.changeNum = 123;
      element.path = 'file.txt';
      element.patchRange = {basePatchNum: 1};
      reportStub = sinon.stub(element.reporting, 'reportInteraction');
    });

    test('null and content-less', () => {
      element._reportDiff(null);
      assert.isFalse(reportStub.called);

      element._reportDiff({});
      assert.isFalse(reportStub.called);
    });

    test('diff w/ no delta', () => {
      const diff = {
        content: [
          {ab: ['foo', 'bar']},
          {ab: ['baz', 'foo']},
        ],
      };
      element._reportDiff(diff);
      assert.isTrue(reportStub.calledOnce);
      assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
      assert.isUndefined(reportStub.lastCall.args[1]);
    });

    test('diff w/ no rebase delta', () => {
      const diff = {
        content: [
          {ab: ['foo', 'bar']},
          {a: ['baz', 'foo']},
          {ab: ['foo', 'bar']},
          {a: ['baz', 'foo'], b: ['bar', 'baz']},
          {ab: ['foo', 'bar']},
          {b: ['baz', 'foo']},
          {ab: ['foo', 'bar']},
        ],
      };
      element._reportDiff(diff);
      assert.isTrue(reportStub.calledOnce);
      assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
      assert.isUndefined(reportStub.lastCall.args[1]);
    });

    test('diff w/ some rebase delta', () => {
      const diff = {
        content: [
          {ab: ['foo', 'bar']},
          {a: ['baz', 'foo'], due_to_rebase: true},
          {ab: ['foo', 'bar']},
          {a: ['baz', 'foo'], b: ['bar', 'baz']},
          {ab: ['foo', 'bar']},
          {b: ['baz', 'foo'], due_to_rebase: true},
          {ab: ['foo', 'bar']},
          {a: ['baz', 'foo']},
        ],
      };
      element._reportDiff(diff);
      assert.isTrue(reportStub.calledOnce);
      assert.isTrue(reportStub.calledWith(
          'rebase-percent-nonzero',
          {percentRebaseDelta: 50}
      ));
    });

    test('diff w/ all rebase delta', () => {
      const diff = {content: [{
        a: ['foo', 'bar'],
        b: ['baz', 'foo'],
        due_to_rebase: true,
      }]};
      element._reportDiff(diff);
      assert.isTrue(reportStub.calledOnce);
      assert.isTrue(reportStub.calledWith(
          'rebase-percent-nonzero',
          {percentRebaseDelta: 100}
      ));
    });

    test('diff against parent event', () => {
      element.patchRange.basePatchNum = 'PARENT';
      const diff = {content: [{
        a: ['foo', 'bar'],
        b: ['baz', 'foo'],
      }]};
      element._reportDiff(diff);
      assert.isTrue(reportStub.calledOnce);
      assert.equal(reportStub.lastCall.args[0], 'diff-against-parent');
      assert.isUndefined(reportStub.lastCall.args[1]);
    });
  });

  test('comments sorting', () => {
    const comments = [
      {
        id: 'new_draft',
        message: 'i do not like either of you',
        __commentSide: 'left',
        __draft: true,
        updated: '2015-12-20 15:01:20.396000000',
      },
      {
        id: 'sallys_confession',
        message: 'i like you, jack',
        updated: '2015-12-23 15:00:20.396000000',
        line: 1,
        __commentSide: 'left',
      }, {
        id: 'jacks_reply',
        message: 'i like you, too',
        updated: '2015-12-24 15:01:20.396000000',
        __commentSide: 'left',
        line: 1,
        in_reply_to: 'sallys_confession',
      },
    ];
    const sortedComments = sortComments(comments);
    assert.equal(sortedComments[0], comments[1]);
    assert.equal(sortedComments[1], comments[2]);
    assert.equal(sortedComments[2], comments[0]);
  });

  test('_createThreads', () => {
    const comments = [
      {
        id: 'sallys_confession',
        message: 'i like you, jack',
        updated: '2015-12-23 15:00:20.396000000',
        line: 1,
        __commentSide: 'left',
      }, {
        id: 'jacks_reply',
        message: 'i like you, too',
        updated: '2015-12-24 15:01:20.396000000',
        __commentSide: 'left',
        line: 1,
        in_reply_to: 'sallys_confession',
      },
      {
        id: 'new_draft',
        message: 'i do not like either of you',
        __commentSide: 'left',
        __draft: true,
        updated: '2015-12-20 15:01:20.396000000',
      },
    ];

    const actualThreads = element._createThreads(comments);

    assert.equal(actualThreads.length, 2);

    assert.equal(actualThreads[0].commentSide, 'left');
    assert.equal(actualThreads[0].comments.length, 2);
    assert.deepEqual(actualThreads[0].comments[0], comments[0]);
    assert.deepEqual(actualThreads[0].comments[1], comments[1]);
    assert.equal(actualThreads[0].patchNum, undefined);
    assert.equal(actualThreads[0].lineNum, 1);

    assert.equal(actualThreads[1].commentSide, 'left');
    assert.equal(actualThreads[1].comments.length, 1);
    assert.deepEqual(actualThreads[1].comments[0], comments[2]);
    assert.equal(actualThreads[1].patchNum, undefined);
    assert.equal(actualThreads[1].lineNum, undefined);
  });

  test('_createThreads inherits patchNum and range', () => {
    const comments = [{
      id: 'betsys_confession',
      message: 'i like you, jack',
      updated: '2015-12-24 15:00:10.396000000',
      range: {
        start_line: 1,
        start_character: 1,
        end_line: 1,
        end_character: 2,
      },
      patch_set: 5,
      __commentSide: 'left',
      line: 1,
    }];

    const expectedThreads = [
      {
        commentSide: 'left',
        comments: [{
          id: 'betsys_confession',
          message: 'i like you, jack',
          updated: '2015-12-24 15:00:10.396000000',
          range: {
            start_line: 1,
            start_character: 1,
            end_line: 1,
            end_character: 2,
          },
          patch_set: 5,
          __commentSide: 'left',
          line: 1,
        }],
        patchNum: 5,
        range: {
          start_line: 1,
          start_character: 1,
          end_line: 1,
          end_character: 2,
        },
        lineNum: 1,
        isOnParent: false,
      },
    ];

    assert.deepEqual(
        element._createThreads(comments),
        expectedThreads);
  });

  test('_createThreads does not thread unrelated comments at same location',
      () => {
        const comments = [
          {
            id: 'sallys_confession',
            message: 'i like you, jack',
            updated: '2015-12-23 15:00:20.396000000',
            __commentSide: 'left',
          }, {
            id: 'jacks_reply',
            message: 'i like you, too',
            updated: '2015-12-24 15:01:20.396000000',
            __commentSide: 'left',
          },
        ];
        assert.equal(element._createThreads(comments).length, 2);
      });

  test('_createThreads derives isOnParent using  side from first comment',
      () => {
        const comments = [
          {
            id: 'sallys_confession',
            message: 'i like you, jack',
            updated: '2015-12-23 15:00:20.396000000',
            __commentSide: 'left',
          }, {
            id: 'jacks_reply',
            message: 'i like you, too',
            updated: '2015-12-24 15:01:20.396000000',
            __commentSide: 'left',
            in_reply_to: 'sallys_confession',
          },
        ];

        assert.equal(element._createThreads(comments)[0].isOnParent, false);

        comments[0].side = 'REVISION';
        assert.equal(element._createThreads(comments)[0].isOnParent, false);

        comments[0].side = 'PARENT';
        assert.equal(element._createThreads(comments)[0].isOnParent, true);
      });

  test('_getOrCreateThread', () => {
    const commentSide = 'left';

    assert.isOk(element._getOrCreateThread('2', 3,
        commentSide, undefined, false));

    let threads = dom(element.$.diff)
        .queryDistributedElements('gr-comment-thread');

    assert.equal(threads.length, 1);
    assert.equal(threads[0].commentSide, commentSide);
    assert.equal(threads[0].range, undefined);
    assert.equal(threads[0].isOnParent, false);
    assert.equal(threads[0].patchNum, 2);

    // Try to fetch a thread with a different range.
    const range = {
      start_line: 1,
      start_character: 1,
      end_line: 1,
      end_character: 3,
    };

    assert.isOk(element._getOrCreateThread(
        '3', 1, commentSide, range, true));

    threads = dom(element.$.diff)
        .queryDistributedElements('gr-comment-thread');

    assert.equal(threads.length, 2);
    assert.equal(threads[1].commentSide, commentSide);
    assert.equal(threads[1].range, range);
    assert.equal(threads[1].isOnParent, true);
    assert.equal(threads[1].patchNum, 3);
  });

  test('thread should use old file path if first created' +
   'on patch set (left) before renaming', () => {
    const commentSide = 'left';
    element.file = {basePath: 'file_renamed.txt', path: element.path};

    assert.isOk(element._getOrCreateThread('2', 3,
        commentSide, undefined, /* isOnParent= */ false));

    const threads = dom(element.$.diff)
        .queryDistributedElements('gr-comment-thread');

    assert.equal(threads.length, 1);
    assert.equal(threads[0].commentSide, commentSide);
    assert.equal(threads[0].path, element.file.basePath);
  });

  test('thread should use new file path if first created' +
   'on patch set (right) after renaming', () => {
    const commentSide = 'right';
    element.file = {basePath: 'file_renamed.txt', path: element.path};

    assert.isOk(element._getOrCreateThread('2', 3,
        commentSide, undefined, /* isOnParent= */ false));

    const threads = dom(element.$.diff)
        .queryDistributedElements('gr-comment-thread');

    assert.equal(threads.length, 1);
    assert.equal(threads[0].commentSide, commentSide);
    assert.equal(threads[0].path, element.file.path);
  });

  test('thread should use new file path if first created' +
   'on patch set (left) but is base', () => {
    const commentSide = 'left';
    element.file = {basePath: 'file_renamed.txt', path: element.path};

    assert.isOk(element._getOrCreateThread('2', 3,
        commentSide, undefined, /* isOnParent= */ true));

    const threads = dom(element.$.diff)
        .queryDistributedElements('gr-comment-thread');

    assert.equal(threads.length, 1);
    assert.equal(threads[0].commentSide, commentSide);
    assert.equal(threads[0].path, element.file.path);
  });

  test('_filterThreadElsForLocation with no threads', () => {
    const line = {beforeNumber: 3, afterNumber: 5};

    const threads = [];
    assert.deepEqual(element._filterThreadElsForLocation(threads, line), []);
    assert.deepEqual(element._filterThreadElsForLocation(threads, line,
        Side.LEFT), []);
    assert.deepEqual(element._filterThreadElsForLocation(threads, line,
        Side.RIGHT), []);
  });

  test('_filterThreadElsForLocation for line comments', () => {
    const line = {beforeNumber: 3, afterNumber: 5};

    const l3 = document.createElement('div');
    l3.setAttribute('line-num', 3);
    l3.setAttribute('comment-side', 'left');

    const l5 = document.createElement('div');
    l5.setAttribute('line-num', 5);
    l5.setAttribute('comment-side', 'left');

    const r3 = document.createElement('div');
    r3.setAttribute('line-num', 3);
    r3.setAttribute('comment-side', 'right');

    const r5 = document.createElement('div');
    r5.setAttribute('line-num', 5);
    r5.setAttribute('comment-side', 'right');

    const threadEls = [l3, l5, r3, r5];
    assert.deepEqual(element._filterThreadElsForLocation(threadEls, line),
        [l3, r5]);
    assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
        Side.LEFT), [l3]);
    assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
        Side.RIGHT), [r5]);
  });

  test('_filterThreadElsForLocation for file comments', () => {
    const line = {beforeNumber: 'FILE', afterNumber: 'FILE'};

    const l = document.createElement('div');
    l.setAttribute('comment-side', 'left');
    l.setAttribute('line-num', 'FILE');

    const r = document.createElement('div');
    r.setAttribute('comment-side', 'right');
    r.setAttribute('line-num', 'FILE');

    const threadEls = [l, r];
    assert.deepEqual(element._filterThreadElsForLocation(threadEls, line),
        [l, r]);
    assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
        Side.BOTH), [l, r]);
    assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
        Side.LEFT), [l]);
    assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
        Side.RIGHT), [r]);
  });

  suite('syntax layer with syntax_highlighting on', () => {
    setup(() => {
      const prefs = {
        line_length: 10,
        show_tabs: true,
        tab_size: 4,
        context: -1,
        syntax_highlighting: true,
      };
      element.patchRange = {};
      element.prefs = prefs;
      element.changeNum = 123;
      element.change = createChange();
      element.path = 'some/path';
    });

    test('gr-diff-host provides syntax highlighting layer to gr-diff', () => {
      element.reload();
      assert.equal(element.$.diff.layers[0], element.$.syntaxLayer);
    });

    test('rendering normal-sized diff does not disable syntax', () => {
      element.diff = {
        content: [{
          a: ['foo'],
        }],
      };
      assert.isTrue(element.$.syntaxLayer.enabled);
    });

    test('rendering large diff disables syntax', () => {
      // Before it renders, set the first diff line to 500 '*' characters.
      element.diff = {
        content: [{
          a: [new Array(501).join('*')],
        }],
      };
      assert.isFalse(element.$.syntaxLayer.enabled);
    });

    test('starts syntax layer processing on render event', async () => {
      sinon.stub(element.$.syntaxLayer, 'process')
          .returns(Promise.resolve());
      sinon.stub(element.$.restAPI, 'getDiff').returns(
          Promise.resolve({content: []}));
      element.reload();
      await flush();
      element.dispatchEvent(
          new CustomEvent('render', {bubbles: true, composed: true}));
      assert.isTrue(element.$.syntaxLayer.process.called);
    });
  });

  suite('syntax layer with syntax_highlighting off', () => {
    setup(() => {
      const prefs = {
        line_length: 10,
        show_tabs: true,
        tab_size: 4,
        context: -1,
      };
      element.diff = {
        content: [{
          a: ['foo'],
        }],
      };
      element.patchRange = {};
      element.change = createChange();
      element.prefs = prefs;
    });

    test('gr-diff-host provides syntax highlighting layer', () => {
      element.reload();
      assert.equal(element.$.diff.layers[0], element.$.syntaxLayer);
    });

    test('syntax layer should be disabled', () => {
      assert.isFalse(element.$.syntaxLayer.enabled);
    });

    test('still disabled for large diff', () => {
      // Before it renders, set the first diff line to 500 '*' characters.
      element.diff = {
        content: [{
          a: [new Array(501).join('*')],
        }],
      };
      assert.isFalse(element.$.syntaxLayer.enabled);
    });
  });

  suite('coverage layer', () => {
    let notifyStub;
    setup(() => {
      notifyStub = sinon.stub();
      stub('gr-js-api-interface', {
        getCoverageAnnotationApis() {
          return Promise.resolve([{
            notify: notifyStub,
            getCoverageProvider() {
              return () => Promise.resolve([
                {
                  type: 'COVERED',
                  side: 'right',
                  code_range: {
                    start_line: 1,
                    end_line: 2,
                  },
                },
                {
                  type: 'NOT_COVERED',
                  side: 'right',
                  code_range: {
                    start_line: 3,
                    end_line: 4,
                  },
                },
              ]);
            },
          }]);
        },
      });
      element = basicFixture.instantiate();
      element.changeNum = 123;
      element.change = createChange();
      element.path = 'some/path';
      const prefs = {
        line_length: 10,
        show_tabs: true,
        tab_size: 4,
        context: -1,
      };
      element.diff = {
        content: [{
          a: ['foo'],
        }],
      };
      element.patchRange = {};
      element.prefs = prefs;
    });

    test('getCoverageAnnotationApis should be called', done => {
      element.reload();
      flush(() => {
        assert.isTrue(element.$.jsAPI.getCoverageAnnotationApis.calledOnce);
        done();
      });
    });

    test('coverageRangeChanged should be called', done => {
      element.reload();
      flush(() => {
        assert.equal(notifyStub.callCount, 2);
        done();
      });
    });
  });

  suite('trailing newlines', () => {
    setup(() => {
    });

    suite('_lastChunkForSide', () => {
      test('deltas', () => {
        const diff = {content: [
          {a: ['foo', 'bar'], b: ['baz']},
          {ab: ['foo', 'bar', 'baz']},
          {b: ['foo']},
        ]};
        assert.equal(element._lastChunkForSide(diff, false), diff.content[2]);
        assert.equal(element._lastChunkForSide(diff, true), diff.content[1]);

        diff.content.push({a: ['foo'], b: ['bar']});
        assert.equal(element._lastChunkForSide(diff, false), diff.content[3]);
        assert.equal(element._lastChunkForSide(diff, true), diff.content[3]);
      });

      test('addition with a undefined', () => {
        const diff = {content: [
          {b: ['foo', 'bar', 'baz']},
        ]};
        assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
        assert.isNull(element._lastChunkForSide(diff, true));
      });

      test('addition with a empty', () => {
        const diff = {content: [
          {a: [], b: ['foo', 'bar', 'baz']},
        ]};
        assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
        assert.isNull(element._lastChunkForSide(diff, true));
      });

      test('deletion with b undefined', () => {
        const diff = {content: [
          {a: ['foo', 'bar', 'baz']},
        ]};
        assert.isNull(element._lastChunkForSide(diff, false));
        assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
      });

      test('deletion with b empty', () => {
        const diff = {content: [
          {a: ['foo', 'bar', 'baz'], b: []},
        ]};
        assert.isNull(element._lastChunkForSide(diff, false));
        assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
      });

      test('empty', () => {
        const diff = {content: []};
        assert.isNull(element._lastChunkForSide(diff, false));
        assert.isNull(element._lastChunkForSide(diff, true));
      });
    });

    suite('_hasTrailingNewlines', () => {
      test('shared no trailing', () => {
        const diff = undefined;
        sinon.stub(element, '_lastChunkForSide')
            .returns({ab: ['foo', 'bar']});
        assert.isFalse(element._hasTrailingNewlines(diff, false));
        assert.isFalse(element._hasTrailingNewlines(diff, true));
      });

      test('delta trailing in right', () => {
        const diff = undefined;
        sinon.stub(element, '_lastChunkForSide')
            .returns({a: ['foo', 'bar'], b: ['baz', '']});
        assert.isTrue(element._hasTrailingNewlines(diff, false));
        assert.isFalse(element._hasTrailingNewlines(diff, true));
      });

      test('addition', () => {
        const diff = undefined;
        sinon.stub(element, '_lastChunkForSide').callsFake((diff, leftSide) => {
          if (leftSide) { return null; }
          return {b: ['foo', '']};
        });
        assert.isTrue(element._hasTrailingNewlines(diff, false));
        assert.isNull(element._hasTrailingNewlines(diff, true));
      });

      test('deletion', () => {
        const diff = undefined;
        sinon.stub(element, '_lastChunkForSide').callsFake((diff, leftSide) => {
          if (!leftSide) { return null; }
          return {a: ['foo']};
        });
        assert.isNull(element._hasTrailingNewlines(diff, false));
        assert.isFalse(element._hasTrailingNewlines(diff, true));
      });
    });
  });
});

