/**
 * @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-thread-list.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {SpecialFilePath} from '../../../constants/constants.js';

const basicFixture = fixtureFromElement('gr-thread-list');

suite('gr-thread-list tests', () => {
  let element;

  let threadElements;

  function getVisibleThreads() {
    return [...dom(element.root)
        .querySelectorAll('gr-comment-thread')]
        .filter(e => e.style.display !== 'none');
  }

  setup(done => {
    element = basicFixture.instantiate();
    element.changeNum = 123;
    element.change = {
      project: 'testRepo',
    };
    element.threads = [
      {
        comments: [
          {
            __path: '/COMMIT_MSG',
            author: {
              _account_id: 1000000,
              name: 'user',
              username: 'user',
            },
            patch_set: 4,
            id: 'ecf0b9fa_fe1a5f62',
            line: 5,
            updated: '2018-02-08 18:49:18.000000000',
            message: 'test',
            unresolved: true,
          },
          {
            id: '503008e2_0ab203ee',
            path: '/COMMIT_MSG',
            line: 5,
            in_reply_to: 'ecf0b9fa_fe1a5f62',
            updated: '2018-02-13 22:48:48.018000000',
            message: 'draft',
            unresolved: true,
            __draft: true,
            __draftID: '0.m683trwff68',
            __editing: false,
            patch_set: '2',
          },
        ],
        patchNum: 4,
        path: '/COMMIT_MSG',
        line: 5,
        rootId: 'ecf0b9fa_fe1a5f62',
        start_datetime: '2018-02-08 18:49:18.000000000',
      },
      {
        comments: [
          {
            __path: 'test.txt',
            author: {
              _account_id: 1000000,
              name: 'user',
              username: 'user',
            },
            patch_set: 3,
            id: '09a9fb0a_1484e6cf',
            side: 'PARENT',
            updated: '2018-02-13 22:47:19.000000000',
            message: 'Some comment on another patchset.',
            unresolved: false,
          },
        ],
        patchNum: 3,
        path: 'test.txt',
        rootId: '09a9fb0a_1484e6cf',
        start_datetime: '2018-02-13 22:47:19.000000000',
        commentSide: 'PARENT',
      },
      {
        comments: [
          {
            __path: '/COMMIT_MSG',
            author: {
              _account_id: 1000000,
              name: 'user',
              username: 'user',
            },
            patch_set: 2,
            id: '8caddf38_44770ec1',
            updated: '2018-02-13 22:48:40.000000000',
            message: 'Another unresolved comment',
            unresolved: false,
          },
        ],
        patchNum: 2,
        path: '/COMMIT_MSG',
        rootId: '8caddf38_44770ec1',
        start_datetime: '2018-02-13 22:48:40.000000000',
      },
      {
        comments: [
          {
            __path: '/COMMIT_MSG',
            author: {
              _account_id: 1000000,
              name: 'user',
              username: 'user',
            },
            patch_set: 2,
            id: 'scaddf38_44770ec1',
            line: 4,
            updated: '2018-02-14 22:48:40.000000000',
            message: 'Yet another unresolved comment',
            unresolved: true,
          },
        ],
        patchNum: 2,
        path: '/COMMIT_MSG',
        line: 4,
        rootId: 'scaddf38_44770ec1',
        start_datetime: '2018-02-14 22:48:40.000000000',
      },
      {
        comments: [
          {
            id: 'zcf0b9fa_fe1a5f62',
            path: '/COMMIT_MSG',
            line: 6,
            updated: '2018-02-15 22:48:48.018000000',
            message: 'resolved draft',
            unresolved: false,
            __draft: true,
            __draftID: '0.m683trwff69',
            __editing: false,
            patch_set: '2',
          },
        ],
        patchNum: 4,
        path: '/COMMIT_MSG',
        line: 6,
        rootId: 'zcf0b9fa_fe1a5f62',
        start_datetime: '2018-02-09 18:49:18.000000000',
      },
      {
        comments: [
          {
            id: 'patchset_level_1',
            path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
            updated: '2018-02-15 22:48:48.018000000',
            message: 'patchset comment 1',
            unresolved: false,
            __editing: false,
            patch_set: '2',
          },
        ],
        patchNum: 2,
        path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
        rootId: 'patchset_level_1',
        start_datetime: '2018-02-09 18:49:18.000000000',
      },
      {
        comments: [
          {
            id: 'patchset_level_2',
            path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
            updated: '2018-02-15 22:48:48.018000000',
            message: 'patchset comment 2',
            unresolved: false,
            __editing: false,
            patch_set: '3',
          },
        ],
        patchNum: 3,
        path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
        rootId: 'patchset_level_2',
        start_datetime: '2018-02-09 18:49:18.000000000',
      },
      {
        comments: [
          {
            __path: '/COMMIT_MSG',
            author: {
              _account_id: 1000000,
              name: 'user',
              username: 'user',
            },
            patch_set: 4,
            id: 'rc1',
            line: 5,
            updated: '2019-02-08 18:49:18.000000000',
            message: 'test',
            unresolved: true,
            robot_id: 'rc1',
          },
        ],
        patchNum: 4,
        path: '/COMMIT_MSG',
        line: 5,
        rootId: 'rc1',
        start_datetime: '2019-02-08 18:49:18.000000000',
      },
      {
        comments: [
          {
            __path: '/COMMIT_MSG',
            author: {
              _account_id: 1000000,
              name: 'user',
              username: 'user',
            },
            patch_set: 4,
            id: 'rc2',
            line: 7,
            updated: '2019-03-08 18:49:18.000000000',
            message: 'test',
            unresolved: true,
            robot_id: 'rc2',
          },
          {
            __path: '/COMMIT_MSG',
            author: {
              _account_id: 1000000,
              name: 'user',
              username: 'user',
            },
            patch_set: 4,
            id: 'c2_1',
            line: 5,
            updated: '2019-03-08 18:49:18.000000000',
            message: 'test',
            unresolved: true,
          },
        ],
        patchNum: 4,
        path: '/COMMIT_MSG',
        line: 7,
        rootId: 'rc2',
        start_datetime: '2019-03-08 18:49:18.000000000',
      },
    ];

    // use flush to render all (bypass initial-count set on dom-repeat)
    flush(() => {
      threadElements = dom(element.root)
          .querySelectorAll('gr-comment-thread');
      done();
    });
  });

  test('draft toggle only appears when logged in', () => {
    assert.equal(getComputedStyle(element.shadowRoot
        .querySelector('.draftToggle')).display,
    'none');
    element.loggedIn = true;
    assert.notEqual(getComputedStyle(element.shadowRoot
        .querySelector('.draftToggle')).display,
    'none');
  });

  test('show all threads by default', () => {
    assert.equal(dom(element.root)
        .querySelectorAll('gr-comment-thread').length, element.threads.length);
    assert.equal(getVisibleThreads().length, element.threads.length);
  });

  test('show unresolved threads if unresolvedOnly is set', done => {
    element.unresolvedOnly = true;
    flush();
    const unresolvedThreads = element.threads.filter(t => t.comments.some(
        c => c.unresolved
    ));
    assert.equal(getVisibleThreads().length, unresolvedThreads.length);
    done();
  });

  test('showing file name takes visible threads into account', () => {
    assert.equal(element._isFirstThreadWithFileName(element._sortedThreads,
        element._sortedThreads[2], element.unresolvedOnly, element._draftsOnly,
        element.onlyShowRobotCommentsWithHumanReply), true);
    element.unresolvedOnly = true;
    assert.equal(element._isFirstThreadWithFileName(element._sortedThreads,
        element._sortedThreads[2], element.unresolvedOnly, element._draftsOnly,
        element.onlyShowRobotCommentsWithHumanReply), false);
  });

  test('onlyShowRobotCommentsWithHumanReply ', () => {
    element.onlyShowRobotCommentsWithHumanReply = true;
    flush();
    assert.equal(
        getVisibleThreads().length,
        element.threads.length - 1);
    assert.isNotOk(getVisibleThreads().find(th => th.rootId === 'rc1'));
  });

  suite('_compareThreads', () => {
    test('patchset comes before any other file', () => {
      const t1 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS}};
      const t2 = {thread: {path: SpecialFilePath.COMMIT_MESSAGE}};

      t1.patchNum = t2.patchNum = 1;
      t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
      assert.equal(element._compareThreads(t1, t2), -1);
      assert.equal(element._compareThreads(t2, t1), 1);

      // assigning values to properties such that t2 should come first
      t1.patchNum = 1;
      t2.patchNum = 2;
      t1.unresolved = t1.hasDraft = false;
      t2.unresolved = t2.unresolved = true;
      assert.equal(element._compareThreads(t1, t2), -1);
      assert.equal(element._compareThreads(t2, t1), 1);
    });

    test('file path is compared lexicographically', () => {
      const t1 = {thread: {path: 'a.txt'}};
      const t2 = {thread: {path: 'b.txt'}};
      t1.patchNum = t2.patchNum = 1;
      t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
      assert.equal(element._compareThreads(t1, t2), -1);
      assert.equal(element._compareThreads(t2, t1), 1);

      t1.patchNum = 1;
      t2.patchNum = 2;
      t1.unresolved = t1.hasDraft = false;
      t2.unresolved = t2.unresolved = true;
      assert.equal(element._compareThreads(t1, t2), -1);
      assert.equal(element._compareThreads(t2, t1), 1);
    });

    test('patchset comments sorted by reverse patchset', () => {
      const t1 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
        patchNum: 1}};
      const t2 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
        patchNum: 2}};
      t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
      assert.equal(element._compareThreads(t1, t2), 1);
      assert.equal(element._compareThreads(t2, t1), -1);

      t1.unresolved = t1.hasDraft = false;
      t2.unresolved = t2.unresolved = true;
      assert.equal(element._compareThreads(t1, t2), 1);
      assert.equal(element._compareThreads(t2, t1), -1);
    });

    test('patchset comments with same patchset picks unresolved first', () => {
      const t1 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
        patchNum: 1}, unresolved: true};
      const t2 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
        patchNum: 1}, unresolved: false};
      t1.hasDraft = t2.hasDraft = false;
      assert.equal(element._compareThreads(t1, t2), -1);
      assert.equal(element._compareThreads(t2, t1), 1);
    });

    test('file level comment before line', () => {
      const t1 = {thread: {path: 'a.txt', line: 2}};
      const t2 = {thread: {path: 'a.txt'}};
      t1.patchNum = t2.patchNum = 1;
      t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
      assert.equal(element._compareThreads(t1, t2), 1);
      assert.equal(element._compareThreads(t2, t1), -1);

      // give preference to t1 in unresolved/draft properties
      t1.unresolved = t1.hasDraft = true;
      t2.unresolved = t2.unresolved = false;
      assert.equal(element._compareThreads(t1, t2), 1);
      assert.equal(element._compareThreads(t2, t1), -1);
    });

    test('comments sorted by line', () => {
      const t1 = {thread: {path: 'a.txt', line: 2}};
      const t2 = {thread: {path: 'a.txt', line: 3}};
      t1.patchNum = t2.patchNum = 1;
      t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
      assert.equal(element._compareThreads(t1, t2), -1);
      assert.equal(element._compareThreads(t2, t1), 1);

      t1.unresolved = t1.hasDraft = false;
      t2.unresolved = t2.unresolved = true;
      assert.equal(element._compareThreads(t1, t2), -1);
      assert.equal(element._compareThreads(t2, t1), 1);
    });

    test('comments on same line sorted by reverse patchset', () => {
      const t1 = {thread: {path: 'a.txt', line: 2, patchNum: 1}};
      const t2 = {thread: {path: 'a.txt', line: 2, patchNum: 2}};
      t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
      assert.equal(element._compareThreads(t1, t2), 1);
      assert.equal(element._compareThreads(t2, t1), -1);

      // give preference to t1 in unresolved/draft properties
      t1.unresolved = t1.hasDraft = true;
      t2.unresolved = t2.unresolved = false;
      assert.equal(element._compareThreads(t1, t2), 1);
      assert.equal(element._compareThreads(t2, t1), -1);
    });

    test('comments on same line & patchset sorted by unresolved first',
        () => {
          const t1 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
            unresolved: true};
          const t2 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
            unresolved: false};
          t1.patchNum = t2.patchNum = 1;
          assert.equal(element._compareThreads(t1, t2), -1);
          assert.equal(element._compareThreads(t2, t1), 1);

          t2.hasDraft = true;
          t1.hasDraft = false;
          assert.equal(element._compareThreads(t1, t2), -1);
          assert.equal(element._compareThreads(t2, t1), 1);
        });

    test('comments on same line & patchset & unresolved sorted by draft',
        () => {
          const t1 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
            unresolved: true, hasDraft: false};
          const t2 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
            unresolved: true, hasDraft: true};
          t1.patchNum = t2.patchNum = 1;
          assert.equal(element._compareThreads(t1, t2), 1);
          assert.equal(element._compareThreads(t2, t1), -1);
        });
  });

  test('_computeSortedThreads', () => {
    assert.equal(element._sortedThreads.length, 9);
    const expectedSortedRootIds = [
      'patchset_level_2', // Posted on Patchset 3
      'patchset_level_1', // Posted on Patchset 2
      '8caddf38_44770ec1', // File level on COMMIT_MSG
      'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
      'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
      'rc1', // Line 5 on COMMIT_MESSAGE without drafts
      'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
      'rc2', // Line 7 on COMMIT_MSG
      '09a9fb0a_1484e6cf', // File level on test.txt
    ];
    element._sortedThreads.forEach((thread, index) => {
      assert.equal(thread.rootId, expectedSortedRootIds[index]);
    });
  });

  test('thread removal and sort again', () => {
    threadElements[1].dispatchEvent(
        new CustomEvent('thread-discard', {
          detail: {rootId: 'rc2'},
          composed: true, bubbles: true,
        }));
    flush();
    assert.equal(element._sortedThreads.length, 8);
    const expectedSortedRootIds = [
      'patchset_level_2',
      'patchset_level_1',
      '8caddf38_44770ec1', // File level on COMMIT_MSG
      'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
      'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
      'rc1', // Line 5 on COMMIT_MESSAGE without drafts
      'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
      '09a9fb0a_1484e6cf', // File level on test.txt
    ];
    element._sortedThreads.forEach((thread, index) => {
      assert.equal(thread.rootId, expectedSortedRootIds[index]);
    });
  });

  test('modification on thread shold not trigger sort again', () => {
    const currentSortedThreads = [...element._sortedThreads];
    for (const thread of currentSortedThreads) {
      thread.comments = [...thread.comments];
    }
    const modifiedThreads = [...element.threads];
    modifiedThreads[5] = {...modifiedThreads[5]};
    modifiedThreads[5].comments = [...modifiedThreads[5].comments, {
      ...modifiedThreads[5].comments[0],
      unresolved: false,
    }];
    element.threads = modifiedThreads;
    assert.notDeepEqual(currentSortedThreads, element._sortedThreads);

    // exact same order as in _computeSortedThreads
    const expectedSortedRootIds = [
      'patchset_level_2',
      'patchset_level_1',
      '8caddf38_44770ec1', // File level on COMMIT_MSG
      'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
      'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
      'rc1', // Line 5 on COMMIT_MESSAGE without drafts
      'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
      'rc2', // Line 7 on COMMIT_MSG
      '09a9fb0a_1484e6cf', // File level on test.txt
    ];
    element._sortedThreads.forEach((thread, index) => {
      assert.equal(thread.rootId, expectedSortedRootIds[index]);
    });
  });

  test('reset sortedThreads when threads set to undefiend', () => {
    element.threads = undefined;
    assert.deepEqual(element._sortedThreads, []);
  });

  test('non-equal length of sortThreads and threads' +
    ' should trigger sort again', () => {
    const modifiedThreads = [...element.threads];
    const currentSortedThreads = [...element._sortedThreads];
    element._sortedThreads = [];
    element.threads = modifiedThreads;
    assert.deepEqual(currentSortedThreads, element._sortedThreads);

    // exact same order as in _computeSortedThreads
    const expectedSortedRootIds = [
      'patchset_level_2',
      'patchset_level_1',
      '8caddf38_44770ec1', // File level on COMMIT_MSG
      'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
      'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
      'rc1', // Line 5 on COMMIT_MESSAGE without drafts
      'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
      'rc2', // Line 7 on COMMIT_MSG
      '09a9fb0a_1484e6cf', // File level on test.txt
    ];
    element._sortedThreads.forEach((thread, index) => {
      assert.equal(thread.rootId, expectedSortedRootIds[index]);
    });
  });

  test('toggle unresolved shows all comments', () => {
    MockInteractions.tap(element.shadowRoot.querySelector(
        '#unresolvedToggle'));
    flush();
    assert.equal(getVisibleThreads().length, 4);
  });

  test('toggle drafts only shows threads with draft comments', () => {
    MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle'));
    flush();
    assert.equal(getVisibleThreads().length, 2);
  });

  test('toggle drafts and unresolved should ignore comments in editing', () => {
    const modifiedThreads = [...element.threads];
    modifiedThreads[5] = {...modifiedThreads[5]};
    modifiedThreads[5].comments = [...modifiedThreads[5].comments];
    modifiedThreads[5].comments.push({
      ...modifiedThreads[5].comments[0],
      __editing: true,
    });
    element.threads = modifiedThreads;
    MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle'));
    MockInteractions.tap(element.shadowRoot.querySelector(
        '#unresolvedToggle'));
    flush();
    assert.equal(getVisibleThreads().length, 2);
  });

  test('toggle drafts and unresolved only shows threads with drafts and ' +
      'publicly unresolved ', () => {
    MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle'));
    MockInteractions.tap(element.shadowRoot.querySelector(
        '#unresolvedToggle'));
    flush();
    assert.equal(getVisibleThreads().length, 1);
  });

  test('modification events are consumed and displatched', () => {
    sinon.spy(element, '_handleCommentsChanged');
    const dispatchSpy = sinon.stub();
    element.addEventListener('thread-list-modified', dispatchSpy);
    threadElements[0].dispatchEvent(
        new CustomEvent('thread-changed', {
          detail: {
            rootId: 'ecf0b9fa_fe1a5f62', path: '/COMMIT_MSG'},
          composed: true, bubbles: true,
        }));
    assert.isTrue(element._handleCommentsChanged.called);
    assert.isTrue(dispatchSpy.called);
    assert.equal(dispatchSpy.lastCall.args[0].detail.rootId,
        'ecf0b9fa_fe1a5f62');
    assert.equal(dispatchSpy.lastCall.args[0].detail.path, '/COMMIT_MSG');
  });

  suite('hideToggleButtons', () => {
    setup(done => {
      element.hideToggleButtons = true;
      flush(() => {
        done();
      });
    });

    test('toggle buttons are hidden', () => {
      assert.equal(element.shadowRoot.querySelector('.header').style.display,
          'none');
    });
  });

  suite('empty thread', () => {
    setup(done => {
      element.threads = [];
      flush(() => {
        done();
      });
    });

    test('default empty message should show', () => {
      assert.isTrue(
          element.shadowRoot.querySelector('#threads').textContent.trim()
              .includes('No comments.'));
    });
  });
});

