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

import '../../../test/common-test-setup-karma.js';
import './gr-rest-api-interface.js';
import {mockPromise} from '../../../test/test-utils.js';
import {GrReviewerUpdatesParser} from './gr-reviewer-updates-parser.js';
import {ListChangesOption} from '../../../utils/change-util.js';
import {appContext} from '../../../services/app-context.js';

const basicFixture = fixtureFromElement('gr-rest-api-interface');

suite('gr-rest-api-interface tests', () => {
  let element;

  let ctr = 0;
  let originalCanonicalPath;

  setup(() => {
    // Modify CANONICAL_PATH to effectively reset cache.
    ctr += 1;
    originalCanonicalPath = window.CANONICAL_PATH;
    window.CANONICAL_PATH = `test${ctr}`;

    const testJSON = ')]}\'\n{"hello": "bonjour"}';
    sinon.stub(window, 'fetch').returns(Promise.resolve({
      ok: true,
      text() {
        return Promise.resolve(testJSON);
      },
    }));
    // fake auth
    sinon.stub(appContext.authService, 'authCheck')
        .returns(Promise.resolve(true));
    element = basicFixture.instantiate();
    element._projectLookup = {};
  });

  teardown(() => {
    window.CANONICAL_PATH = originalCanonicalPath;
  });

  test('parent diff comments are properly grouped', () => {
    sinon.stub(element._restApiHelper, 'fetchJSON')
        .callsFake(() => Promise.resolve({
          '/COMMIT_MSG': [],
          'sieve.go': [
            {
              updated: '2017-02-03 22:32:28.000000000',
              message: 'this isn’t quite right',
            },
            {
              side: 'PARENT',
              message: 'how did this work in the first place?',
              updated: '2017-02-03 22:33:28.000000000',
            },
          ],
        }));
    return element._getDiffComments('42', '', 'PARENT', 1, 'sieve.go').then(
        obj => {
          assert.equal(obj.baseComments.length, 1);
          assert.deepEqual(obj.baseComments[0], {
            side: 'PARENT',
            message: 'how did this work in the first place?',
            path: 'sieve.go',
            updated: '2017-02-03 22:33:28.000000000',
          });
          assert.equal(obj.comments.length, 1);
          assert.deepEqual(obj.comments[0], {
            message: 'this isn’t quite right',
            path: 'sieve.go',
            updated: '2017-02-03 22:32:28.000000000',
          });
        });
  });

  test('_setRange', () => {
    const comments = [
      {
        id: 1,
        side: 'PARENT',
        message: 'how did this work in the first place?',
        updated: '2017-02-03 22:32:28.000000000',
        range: {
          start_line: 1,
          start_character: 1,
          end_line: 2,
          end_character: 1,
        },
      },
      {
        id: 2,
        in_reply_to: 1,
        message: 'this isn’t quite right',
        updated: '2017-02-03 22:33:28.000000000',
      },
    ];
    const expectedResult = {
      id: 2,
      in_reply_to: 1,
      message: 'this isn’t quite right',
      updated: '2017-02-03 22:33:28.000000000',
      range: {
        start_line: 1,
        start_character: 1,
        end_line: 2,
        end_character: 1,
      },
    };
    const comment = comments[1];
    assert.deepEqual(element._setRange(comments, comment), expectedResult);
  });

  test('_setRanges', () => {
    const comments = [
      {
        id: 3,
        in_reply_to: 2,
        message: 'this isn’t quite right either',
        updated: '2017-02-03 22:34:28.000000000',
      },
      {
        id: 2,
        in_reply_to: 1,
        message: 'this isn’t quite right',
        updated: '2017-02-03 22:33:28.000000000',
      },
      {
        id: 1,
        side: 'PARENT',
        message: 'how did this work in the first place?',
        updated: '2017-02-03 22:32:28.000000000',
        range: {
          start_line: 1,
          start_character: 1,
          end_line: 2,
          end_character: 1,
        },
      },
    ];
    const expectedResult = [
      {
        id: 1,
        side: 'PARENT',
        message: 'how did this work in the first place?',
        updated: '2017-02-03 22:32:28.000000000',
        range: {
          start_line: 1,
          start_character: 1,
          end_line: 2,
          end_character: 1,
        },
      },
      {
        id: 2,
        in_reply_to: 1,
        message: 'this isn’t quite right',
        updated: '2017-02-03 22:33:28.000000000',
        range: {
          start_line: 1,
          start_character: 1,
          end_line: 2,
          end_character: 1,
        },
      },
      {
        id: 3,
        in_reply_to: 2,
        message: 'this isn’t quite right either',
        updated: '2017-02-03 22:34:28.000000000',
        range: {
          start_line: 1,
          start_character: 1,
          end_line: 2,
          end_character: 1,
        },
      },
    ];
    assert.deepEqual(element._setRanges(comments), expectedResult);
  });

  test('differing patch diff comments are properly grouped', () => {
    sinon.stub(element, 'getFromProjectLookup')
        .returns(Promise.resolve('test'));
    sinon.stub(element._restApiHelper, 'fetchJSON').callsFake(request => {
      const url = request.url;
      if (url === '/changes/test~42/revisions/1') {
        return Promise.resolve({
          '/COMMIT_MSG': [],
          'sieve.go': [
            {
              message: 'this isn’t quite right',
              updated: '2017-02-03 22:32:28.000000000',
            },
            {
              side: 'PARENT',
              message: 'how did this work in the first place?',
              updated: '2017-02-03 22:33:28.000000000',
            },
          ],
        });
      } else if (url === '/changes/test~42/revisions/2') {
        return Promise.resolve({
          '/COMMIT_MSG': [],
          'sieve.go': [
            {
              message: 'What on earth are you thinking, here?',
              updated: '2017-02-03 22:32:28.000000000',
            },
            {
              side: 'PARENT',
              message: 'Yeah not sure how this worked either?',
              updated: '2017-02-03 22:33:28.000000000',
            },
            {
              message: '¯\\_(ツ)_/¯',
              updated: '2017-02-04 22:33:28.000000000',
            },
          ],
        });
      }
    });
    return element._getDiffComments('42', '', 1, 2, 'sieve.go').then(
        obj => {
          assert.equal(obj.baseComments.length, 1);
          assert.deepEqual(obj.baseComments[0], {
            message: 'this isn’t quite right',
            path: 'sieve.go',
            updated: '2017-02-03 22:32:28.000000000',
          });
          assert.equal(obj.comments.length, 2);
          assert.deepEqual(obj.comments[0], {
            message: 'What on earth are you thinking, here?',
            path: 'sieve.go',
            updated: '2017-02-03 22:32:28.000000000',
          });
          assert.deepEqual(obj.comments[1], {
            message: '¯\\_(ツ)_/¯',
            path: 'sieve.go',
            updated: '2017-02-04 22:33:28.000000000',
          });
        });
  });

  test('server error', () => {
    const getResponseObjectStub = sinon.stub(element, 'getResponseObject');
    window.fetch.returns(Promise.resolve({ok: false}));
    const serverErrorEventPromise = new Promise(resolve => {
      element.addEventListener('server-error', resolve);
    });

    return Promise.all([element._restApiHelper.fetchJSON({}).then(response => {
      assert.isUndefined(response);
      assert.isTrue(getResponseObjectStub.notCalled);
    }), serverErrorEventPromise]);
  });

  test('legacy n,z key in change url is replaced', async () => {
    sinon.stub(element, 'getConfig').callsFake(async () => {
      return {};
    });
    const stub = sinon.stub(element._restApiHelper, 'fetchJSON')
        .returns(Promise.resolve([]));
    await element.getChanges(1, null, 'n,z');
    assert.equal(stub.lastCall.args[0].params.S, 0);
  });

  test('saveDiffPreferences invalidates cache line', () => {
    const cacheKey = '/accounts/self/preferences.diff';
    const sendStub = sinon.stub(element._restApiHelper, 'send');
    element._cache.set(cacheKey, {tab_size: 4});
    element.saveDiffPreferences({tab_size: 8});
    assert.isTrue(sendStub.called);
    assert.isFalse(element._restApiHelper._cache.has(cacheKey));
  });

  test('getAccount when resp is null does not add to cache', async () => {
    const cacheKey = '/accounts/self/detail';
    const stub = sinon.stub(element._restApiHelper, 'fetchCacheURL')
        .callsFake(() => Promise.resolve());

    await element.getAccount();
    assert.isTrue(stub.called);
    assert.isFalse(element._restApiHelper._cache.has(cacheKey));

    element._restApiHelper._cache.set(cacheKey, 'fake cache');
    stub.lastCall.args[0].errFn();
  });

  test('getAccount does not add to cache when status is 403', async () => {
    const cacheKey = '/accounts/self/detail';
    const stub = sinon.stub(element._restApiHelper, 'fetchCacheURL')
        .callsFake(() => Promise.resolve());

    await element.getAccount();
    assert.isTrue(stub.called);
    assert.isFalse(element._restApiHelper._cache.has(cacheKey));

    element._cache.set(cacheKey, 'fake cache');
    stub.lastCall.args[0].errFn({status: 403});
  });

  test('getAccount when resp is successful', async () => {
    const cacheKey = '/accounts/self/detail';
    const stub = sinon.stub(element._restApiHelper, 'fetchCacheURL').callsFake(
        () => Promise.resolve());

    await element.getAccount();

    element._restApiHelper._cache.set(cacheKey, 'fake cache');
    assert.isTrue(stub.called);
    assert.equal(element._restApiHelper._cache.get(cacheKey), 'fake cache');
    stub.lastCall.args[0].errFn({});
  });

  const preferenceSetup = function(testJSON, loggedIn, smallScreen) {
    sinon.stub(element, 'getLoggedIn')
        .callsFake(() => Promise.resolve(loggedIn));
    sinon.stub(element, '_isNarrowScreen').callsFake(() => smallScreen);
    sinon.stub(
        element._restApiHelper,
        'fetchCacheURL')
        .callsFake(() => Promise.resolve(testJSON));
  };

  test('getPreferences returns correctly on small screens logged in',
      () => {
        const testJSON = {diff_view: 'SIDE_BY_SIDE'};
        const loggedIn = true;
        const smallScreen = true;

        preferenceSetup(testJSON, loggedIn, smallScreen);

        return element.getPreferences().then(obj => {
          assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
          assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
        });
      });

  test('getPreferences returns correctly on small screens not logged in',
      () => {
        const testJSON = {diff_view: 'SIDE_BY_SIDE'};
        const loggedIn = false;
        const smallScreen = true;

        preferenceSetup(testJSON, loggedIn, smallScreen);
        return element.getPreferences().then(obj => {
          assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
          assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
        });
      });

  test('getPreferences returns correctly on larger screens logged in',
      () => {
        const testJSON = {diff_view: 'UNIFIED_DIFF'};
        const loggedIn = true;
        const smallScreen = false;

        preferenceSetup(testJSON, loggedIn, smallScreen);

        return element.getPreferences().then(obj => {
          assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
          assert.equal(obj.diff_view, 'UNIFIED_DIFF');
        });
      });

  test('getPreferences returns correctly on larger screens not logged in',
      () => {
        const testJSON = {diff_view: 'UNIFIED_DIFF'};
        const loggedIn = false;
        const smallScreen = false;

        preferenceSetup(testJSON, loggedIn, smallScreen);

        return element.getPreferences().then(obj => {
          assert.equal(obj.default_diff_view, 'SIDE_BY_SIDE');
          assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
        });
      });

  test('savPreferences normalizes download scheme', () => {
    const sendStub = sinon.stub(element._restApiHelper, 'send');
    element.savePreferences({download_scheme: 'HTTP'});
    assert.isTrue(sendStub.called);
    assert.equal(sendStub.lastCall.args[0].body.download_scheme, 'http');
  });

  test('getDiffPreferences returns correct defaults', () => {
    sinon.stub(element, 'getLoggedIn').callsFake(() => Promise.resolve(false));

    return element.getDiffPreferences().then(obj => {
      assert.equal(obj.auto_hide_diff_table_header, true);
      assert.equal(obj.context, 10);
      assert.equal(obj.cursor_blink_rate, 0);
      assert.equal(obj.font_size, 12);
      assert.equal(obj.ignore_whitespace, 'IGNORE_NONE');
      assert.equal(obj.intraline_difference, true);
      assert.equal(obj.line_length, 100);
      assert.equal(obj.line_wrapping, false);
      assert.equal(obj.show_line_endings, true);
      assert.equal(obj.show_tabs, true);
      assert.equal(obj.show_whitespace_errors, true);
      assert.equal(obj.syntax_highlighting, true);
      assert.equal(obj.tab_size, 8);
      assert.equal(obj.theme, 'DEFAULT');
    });
  });

  test('saveDiffPreferences set show_tabs to false', () => {
    const sendStub = sinon.stub(element._restApiHelper, 'send');
    element.saveDiffPreferences({show_tabs: false});
    assert.isTrue(sendStub.called);
    assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
  });

  test('getEditPreferences returns correct defaults', () => {
    sinon.stub(element, 'getLoggedIn').callsFake(() => Promise.resolve(false));

    return element.getEditPreferences().then(obj => {
      assert.equal(obj.auto_close_brackets, false);
      assert.equal(obj.cursor_blink_rate, 0);
      assert.equal(obj.hide_line_numbers, false);
      assert.equal(obj.hide_top_menu, false);
      assert.equal(obj.indent_unit, 2);
      assert.equal(obj.indent_with_tabs, false);
      assert.equal(obj.key_map_type, 'DEFAULT');
      assert.equal(obj.line_length, 100);
      assert.equal(obj.line_wrapping, false);
      assert.equal(obj.match_brackets, true);
      assert.equal(obj.show_base, false);
      assert.equal(obj.show_tabs, true);
      assert.equal(obj.show_whitespace_errors, true);
      assert.equal(obj.syntax_highlighting, true);
      assert.equal(obj.tab_size, 8);
      assert.equal(obj.theme, 'DEFAULT');
    });
  });

  test('saveEditPreferences set show_tabs to false', () => {
    const sendStub = sinon.stub(element._restApiHelper, 'send');
    element.saveEditPreferences({show_tabs: false});
    assert.isTrue(sendStub.called);
    assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
  });

  test('confirmEmail', () => {
    const sendStub = sinon.spy(element._restApiHelper, 'send');
    element.confirmEmail('foo');
    assert.isTrue(sendStub.calledOnce);
    assert.equal(sendStub.lastCall.args[0].method, 'PUT');
    assert.equal(sendStub.lastCall.args[0].url,
        '/config/server/email.confirm');
    assert.deepEqual(sendStub.lastCall.args[0].body, {token: 'foo'});
  });

  test('setAccountStatus', () => {
    const sendStub = sinon.stub(element._restApiHelper, 'send')
        .returns(Promise.resolve('OOO'));
    element._cache.set('/accounts/self/detail', {});
    return element.setAccountStatus('OOO').then(() => {
      assert.isTrue(sendStub.calledOnce);
      assert.equal(sendStub.lastCall.args[0].method, 'PUT');
      assert.equal(sendStub.lastCall.args[0].url,
          '/accounts/self/status');
      assert.deepEqual(sendStub.lastCall.args[0].body,
          {status: 'OOO'});
      assert.deepEqual(
          element._restApiHelper._cache.get('/accounts/self/detail'),
          {status: 'OOO'});
    });
  });

  suite('draft comments', () => {
    test('_sendDiffDraftRequest pending requests tracked', () => {
      const obj = element._pendingRequests;
      sinon.stub(element, '_getChangeURLAndSend')
          .callsFake(() => mockPromise());
      assert.notOk(element.hasPendingDiffDrafts());

      element._sendDiffDraftRequest(null, null, null, {});
      assert.equal(obj.sendDiffDraft.length, 1);
      assert.isTrue(!!element.hasPendingDiffDrafts());

      element._sendDiffDraftRequest(null, null, null, {});
      assert.equal(obj.sendDiffDraft.length, 2);
      assert.isTrue(!!element.hasPendingDiffDrafts());

      for (const promise of obj.sendDiffDraft) {
        promise.resolve();
      }

      return element.awaitPendingDiffDrafts().then(() => {
        assert.equal(obj.sendDiffDraft.length, 0);
        assert.isFalse(!!element.hasPendingDiffDrafts());
      });
    });

    suite('_failForCreate200', () => {
      test('_sendDiffDraftRequest checks for 200 on create', () => {
        const sendPromise = Promise.resolve();
        sinon.stub(element, '_getChangeURLAndSend').returns(sendPromise);
        const failStub = sinon.stub(element, '_failForCreate200')
            .returns(Promise.resolve());
        return element._sendDiffDraftRequest('PUT', 123, 4, {}).then(() => {
          assert.isTrue(failStub.calledOnce);
          assert.isTrue(failStub.calledWithExactly(sendPromise));
        });
      });

      test('_sendDiffDraftRequest no checks for 200 on non create', () => {
        sinon.stub(element, '_getChangeURLAndSend')
            .returns(Promise.resolve());
        const failStub = sinon.stub(element, '_failForCreate200')
            .returns(Promise.resolve());
        return element._sendDiffDraftRequest('PUT', 123, 4, {id: '123'})
            .then(() => {
              assert.isFalse(failStub.called);
            });
      });

      test('_failForCreate200 fails on 200', () => {
        const result = {
          ok: true,
          status: 200,
          headers: {
            entries: () => [
              ['Set-CoOkiE', 'secret'],
              ['Innocuous', 'hello'],
            ],
          },
        };
        return element._failForCreate200(Promise.resolve(result))
            .then(() => {
              assert.fail('Error expected.');
            })
            .catch(e => {
              assert.isOk(e);
              assert.include(e.message, 'Saving draft resulted in HTTP 200');
              assert.include(e.message, 'hello');
              assert.notInclude(e.message, 'secret');
            });
      });

      test('_failForCreate200 does not fail on 201', () => {
        const result = {
          ok: true,
          status: 201,
          headers: {entries: () => []},
        };
        return element._failForCreate200(Promise.resolve(result));
      });
    });
  });

  test('saveChangeEdit', () => {
    element._projectLookup = {1: 'test'};
    const change_num = '1';
    const file_name = 'index.php';
    const file_contents = '<?php';
    sinon.stub(element._restApiHelper, 'send').returns(
        Promise.resolve([change_num, file_name, file_contents]));
    sinon.stub(element, 'getResponseObject')
        .returns(Promise.resolve([change_num, file_name, file_contents]));
    element._cache.set('/changes/' + change_num + '/edit/' + file_name, {});
    return element.saveChangeEdit(change_num, file_name, file_contents)
        .then(() => {
          assert.isTrue(element._restApiHelper.send.calledOnce);
          assert.equal(element._restApiHelper.send.lastCall.args[0].method,
              'PUT');
          assert.equal(element._restApiHelper.send.lastCall.args[0].url,
              '/changes/test~1/edit/' + file_name);
          assert.equal(element._restApiHelper.send.lastCall.args[0].body,
              file_contents);
        });
  });

  test('putChangeCommitMessage', () => {
    element._projectLookup = {1: 'test'};
    const change_num = '1';
    const message = 'this is a commit message';
    sinon.stub(element._restApiHelper, 'send').returns(
        Promise.resolve([change_num, message]));
    sinon.stub(element, 'getResponseObject')
        .returns(Promise.resolve([change_num, message]));
    element._cache.set('/changes/' + change_num + '/message', {});
    return element.putChangeCommitMessage(change_num, message).then(() => {
      assert.isTrue(element._restApiHelper.send.calledOnce);
      assert.equal(element._restApiHelper.send.lastCall.args[0].method, 'PUT');
      assert.equal(element._restApiHelper.send.lastCall.args[0].url,
          '/changes/test~1/message');
      assert.deepEqual(element._restApiHelper.send.lastCall.args[0].body,
          {message});
    });
  });

  test('deleteChangeCommitMessage', () => {
    element._projectLookup = {1: 'test'};
    const change_num = '1';
    const messageId = 'abc';
    sinon.stub(element._restApiHelper, 'send').returns(
        Promise.resolve([change_num, messageId]));
    sinon.stub(element, 'getResponseObject')
        .returns(Promise.resolve([change_num, messageId]));
    return element.deleteChangeCommitMessage(change_num, messageId).then(() => {
      assert.isTrue(element._restApiHelper.send.calledOnce);
      assert.equal(
          element._restApiHelper.send.lastCall.args[0].method,
          'DELETE'
      );
      assert.equal(element._restApiHelper.send.lastCall.args[0].url,
          '/changes/test~1/messages/abc');
    });
  });

  test('startWorkInProgress', () => {
    const sendStub = sinon.stub(element, '_getChangeURLAndSend')
        .returns(Promise.resolve('ok'));
    element.startWorkInProgress('42');
    assert.isTrue(sendStub.calledOnce);
    assert.equal(sendStub.lastCall.args[0].changeNum, '42');
    assert.equal(sendStub.lastCall.args[0].method, 'POST');
    assert.isNotOk(sendStub.lastCall.args[0].patchNum);
    assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
    assert.deepEqual(sendStub.lastCall.args[0].body, {});

    element.startWorkInProgress('42', 'revising...');
    assert.isTrue(sendStub.calledTwice);
    assert.equal(sendStub.lastCall.args[0].changeNum, '42');
    assert.equal(sendStub.lastCall.args[0].method, 'POST');
    assert.isNotOk(sendStub.lastCall.args[0].patchNum);
    assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
    assert.deepEqual(sendStub.lastCall.args[0].body,
        {message: 'revising...'});
  });

  test('startReview', () => {
    const sendStub = sinon.stub(element, '_getChangeURLAndSend')
        .returns(Promise.resolve({}));
    element.startReview('42', {message: 'Please review.'});
    assert.isTrue(sendStub.calledOnce);
    assert.equal(sendStub.lastCall.args[0].changeNum, '42');
    assert.equal(sendStub.lastCall.args[0].method, 'POST');
    assert.isNotOk(sendStub.lastCall.args[0].patchNum);
    assert.equal(sendStub.lastCall.args[0].endpoint, '/ready');
    assert.deepEqual(sendStub.lastCall.args[0].body,
        {message: 'Please review.'});
  });

  test('deleteComment', () => {
    const sendStub = sinon.stub(element, '_getChangeURLAndSend')
        .returns(Promise.resolve('some response'));
    return element.deleteComment('foo', 'bar', '01234', 'removal reason')
        .then(response => {
          assert.equal(response, 'some response');
          assert.isTrue(sendStub.calledOnce);
          assert.equal(sendStub.lastCall.args[0].changeNum, 'foo');
          assert.equal(sendStub.lastCall.args[0].method, 'POST');
          assert.equal(sendStub.lastCall.args[0].patchNum, 'bar');
          assert.equal(sendStub.lastCall.args[0].endpoint,
              '/comments/01234/delete');
          assert.deepEqual(sendStub.lastCall.args[0].body,
              {reason: 'removal reason'});
        });
  });

  test('createRepo encodes name', () => {
    const sendStub = sinon.stub(element._restApiHelper, 'send')
        .returns(Promise.resolve());
    return element.createRepo({name: 'x/y'}).then(() => {
      assert.isTrue(sendStub.calledOnce);
      assert.equal(sendStub.lastCall.args[0].url, '/projects/x%2Fy');
    });
  });

  test('queryChangeFiles', () => {
    const fetchStub = sinon.stub(element, '_getChangeURLAndFetch')
        .returns(Promise.resolve());
    return element.queryChangeFiles('42', 'edit', 'test/path.js').then(() => {
      assert.equal(fetchStub.lastCall.args[0].changeNum, '42');
      assert.equal(fetchStub.lastCall.args[0].endpoint,
          '/files?q=test%2Fpath.js');
      assert.equal(fetchStub.lastCall.args[0].revision, 'edit');
    });
  });

  test('normal use', () => {
    const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';

    assert.equal(element._getReposUrl('test', 25),
        '/projects/?n=26&S=0&query=test');

    assert.equal(element._getReposUrl(null, 25),
        `/projects/?n=26&S=0&query=${defaultQuery}`);

    assert.equal(element._getReposUrl('test', 25, 25),
        '/projects/?n=26&S=25&query=test');
  });

  test('invalidateReposCache', () => {
    const url = '/projects/?n=26&S=0&query=test';

    element._cache.set(url, {});

    element.invalidateReposCache();

    assert.isUndefined(element._sharedFetchPromises[url]);

    assert.isFalse(element._cache.has(url));
  });

  test('invalidateAccountsCache', () => {
    const url = '/accounts/self/detail';

    element._cache.set(url, {});

    element.invalidateAccountsCache();

    assert.isUndefined(element._sharedFetchPromises[url]);

    assert.isFalse(element._cache.has(url));
  });

  suite('getRepos', () => {
    const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';
    let fetchCacheURLStub;
    setup(() => {
      fetchCacheURLStub =
          sinon.stub(element._restApiHelper, 'fetchCacheURL');
    });

    test('normal use', () => {
      element.getRepos('test', 25);
      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
          '/projects/?n=26&S=0&query=test');

      element.getRepos(null, 25);
      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
          `/projects/?n=26&S=0&query=${defaultQuery}`);

      element.getRepos('test', 25, 25);
      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
          '/projects/?n=26&S=25&query=test');
    });

    test('with blank', () => {
      element.getRepos('test/test', 25);
      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
          '/projects/?n=26&S=0&query=inname%3Atest%20AND%20inname%3Atest');
    });

    test('with hyphen', () => {
      element.getRepos('foo-bar', 25);
      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
          '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
    });

    test('with leading hyphen', () => {
      element.getRepos('-bar', 25);
      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
          '/projects/?n=26&S=0&query=inname%3Abar');
    });

    test('with trailing hyphen', () => {
      element.getRepos('foo-bar-', 25);
      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
          '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
    });

    test('with underscore', () => {
      element.getRepos('foo_bar', 25);
      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
          '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
    });

    test('with underscore', () => {
      element.getRepos('foo_bar', 25);
      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
          '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
    });

    test('hyphen only', () => {
      element.getRepos('-', 25);
      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
          `/projects/?n=26&S=0&query=${defaultQuery}`);
    });
  });

  test('_getGroupsUrl normal use', () => {
    assert.equal(element._getGroupsUrl('test', 25),
        '/groups/?n=26&S=0&m=test');

    assert.equal(element._getGroupsUrl(null, 25),
        '/groups/?n=26&S=0');

    assert.equal(element._getGroupsUrl('test', 25, 25),
        '/groups/?n=26&S=25&m=test');
  });

  test('invalidateGroupsCache', () => {
    const url = '/groups/?n=26&S=0&m=test';

    element._cache.set(url, {});

    element.invalidateGroupsCache();

    assert.isUndefined(element._sharedFetchPromises[url]);

    assert.isFalse(element._cache.has(url));
  });

  suite('getGroups', () => {
    let fetchCacheURLStub;
    setup(() => {
      fetchCacheURLStub =
          sinon.stub(element._restApiHelper, 'fetchCacheURL');
    });

    test('normal use', () => {
      element.getGroups('test', 25);
      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
          '/groups/?n=26&S=0&m=test');

      element.getGroups(null, 25);
      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
          '/groups/?n=26&S=0');

      element.getGroups('test', 25, 25);
      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
          '/groups/?n=26&S=25&m=test');
    });

    test('regex', () => {
      element.getGroups('^test.*', 25);
      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
          '/groups/?n=26&S=0&r=%5Etest.*');

      element.getGroups('^test.*', 25, 25);
      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
          '/groups/?n=26&S=25&r=%5Etest.*');
    });
  });

  test('gerrit auth is used', () => {
    sinon.stub(appContext.authService, 'fetch').returns(Promise.resolve());
    element._restApiHelper.fetchJSON({url: 'foo'});
    assert(appContext.authService.fetch.called);
  });

  test('getSuggestedAccounts does not return _fetchJSON', () => {
    const _fetchJSONSpy = sinon.spy(element._restApiHelper, 'fetchJSON');
    return element.getSuggestedAccounts().then(accts => {
      assert.isFalse(_fetchJSONSpy.called);
      assert.equal(accts.length, 0);
    });
  });

  test('_fetchJSON gets called by getSuggestedAccounts', () => {
    const _fetchJSONStub = sinon.stub(element._restApiHelper, 'fetchJSON')
        .callsFake(() => Promise.resolve());
    return element.getSuggestedAccounts('own').then(() => {
      assert.deepEqual(_fetchJSONStub.lastCall.args[0].params, {
        q: 'own',
        suggest: null,
      });
    });
  });

  suite('getChangeDetail', () => {
    suite('change detail options', () => {
      setup(() => {
        sinon.stub(element, '_getChangeDetail').callsFake(
            async (changeNum, options) => {
              return {changeNum, options};
            });
      });

      test('signed pushes disabled', async () => {
        sinon.stub(element, 'getConfig').callsFake(async () => {
          return {};
        });
        const {changeNum, options} = await element.getChangeDetail(123);
        assert.strictEqual(123, changeNum);
        assert.isNotOk(
            parseInt(options, 16) & (1 << ListChangesOption.PUSH_CERTIFICATES));
      });

      test('signed pushes enabled', async () => {
        sinon.stub(element, 'getConfig').callsFake(async () => {
          return {receive: {enable_signed_push: true}};
        });
        const {changeNum, options} = await element.getChangeDetail(123);
        assert.strictEqual(123, changeNum);
        assert.ok(
            parseInt(options, 16) & (1 << ListChangesOption.PUSH_CERTIFICATES));
      });
    });

    test('GrReviewerUpdatesParser.parse is used', () => {
      sinon.stub(GrReviewerUpdatesParser, 'parse').returns(
          Promise.resolve('foo'));
      return element.getChangeDetail(42).then(result => {
        assert.isTrue(GrReviewerUpdatesParser.parse.calledOnce);
        assert.equal(result, 'foo');
      });
    });

    test('_getChangeDetail passes params to ETags decorator', () => {
      const changeNum = 4321;
      element._projectLookup[changeNum] = 'test';
      const expectedUrl =
          window.CANONICAL_PATH + '/changes/test~4321/detail?O=516714';
      sinon.stub(element._etags, 'getOptions');
      sinon.stub(element._etags, 'collect');
      return element._getChangeDetail(changeNum, '516714').then(() => {
        assert.isTrue(element._etags.getOptions.calledWithExactly(
            expectedUrl));
        assert.equal(element._etags.collect.lastCall.args[0], expectedUrl);
      });
    });

    test('_getChangeDetail calls errFn on 500', () => {
      const errFn = sinon.stub();
      sinon.stub(element, 'getChangeActionURL')
          .returns(Promise.resolve(''));
      sinon.stub(element._restApiHelper, 'fetchRawJSON')
          .returns(Promise.resolve({ok: false, status: 500}));
      return element._getChangeDetail(123, '516714', errFn).then(() => {
        assert.isTrue(errFn.called);
      });
    });

    test('_getChangeDetail populates _projectLookup', () => {
      sinon.stub(element, 'getChangeActionURL')
          .returns(Promise.resolve(''));
      sinon.stub(element._restApiHelper, 'fetchRawJSON')
          .returns(Promise.resolve({ok: true}));

      const mockResponse = {_number: 1, project: 'test'};
      sinon.stub(element._restApiHelper, 'readResponsePayload')
          .returns(Promise.resolve({
            parsed: mockResponse,
            raw: JSON.stringify(mockResponse),
          }));
      return element._getChangeDetail(1, '516714').then(() => {
        assert.equal(Object.keys(element._projectLookup).length, 1);
        assert.equal(element._projectLookup[1], 'test');
      });
    });

    suite('_getChangeDetail ETag cache', () => {
      let requestUrl;
      let mockResponseSerial;
      let collectSpy;

      setup(() => {
        requestUrl = '/foo/bar';
        const mockResponse = {foo: 'bar', baz: 42};
        mockResponseSerial = element.JSON_PREFIX +
            JSON.stringify(mockResponse);
        sinon.stub(element._restApiHelper, 'urlWithParams')
            .returns(requestUrl);
        sinon.stub(element, 'getChangeActionURL')
            .returns(Promise.resolve(requestUrl));
        collectSpy = sinon.spy(element._etags, 'collect');
      });

      test('contributes to cache', () => {
        const getPayloadSpy = sinon.spy(element._etags, 'getCachedPayload');
        sinon.stub(element._restApiHelper, 'fetchRawJSON')
            .returns(Promise.resolve({
              text: () => Promise.resolve(mockResponseSerial),
              status: 200,
              ok: true,
            }));

        return element._getChangeDetail(123, '516714').then(detail => {
          assert.isFalse(getPayloadSpy.called);
          assert.isTrue(collectSpy.calledOnce);
          const cachedResponse = element._etags.getCachedPayload(requestUrl);
          assert.equal(cachedResponse, mockResponseSerial);
        });
      });

      test('uses cache on HTTP 304', () => {
        const getPayloadStub = sinon.stub(element._etags, 'getCachedPayload');
        getPayloadStub.returns(mockResponseSerial);
        sinon.stub(element._restApiHelper, 'fetchRawJSON')
            .returns(Promise.resolve({
              text: () => Promise.resolve(''),
              status: 304,
              ok: true,
            }));

        return element._getChangeDetail(123, '').then(detail => {
          assert.isFalse(collectSpy.called);
          assert.isTrue(getPayloadStub.calledOnce);
        });
      });
    });
  });

  test('setInProjectLookup', () => {
    element.setInProjectLookup('test', 'project');
    assert.deepEqual(element._projectLookup, {test: 'project'});
  });

  suite('getFromProjectLookup', () => {
    test('getChange fails', () => {
      sinon.stub(element, 'getChange')
          .returns(Promise.resolve(null));
      return element.getFromProjectLookup().then(val => {
        assert.strictEqual(val, undefined);
        assert.deepEqual(element._projectLookup, {});
      });
    });

    test('getChange succeeds, no project', () => {
      sinon.stub(element, 'getChange').returns(Promise.resolve(null));
      return element.getFromProjectLookup().then(val => {
        assert.strictEqual(val, undefined);
        assert.deepEqual(element._projectLookup, {});
      });
    });

    test('getChange succeeds with project', () => {
      sinon.stub(element, 'getChange')
          .returns(Promise.resolve({project: 'project'}));
      return element.getFromProjectLookup('test').then(val => {
        assert.equal(val, 'project');
        assert.deepEqual(element._projectLookup, {test: 'project'});
      });
    });
  });

  suite('getChanges populates _projectLookup', () => {
    test('multiple queries', () => {
      sinon.stub(element._restApiHelper, 'fetchJSON')
          .returns(Promise.resolve([
            [
              {_number: 1, project: 'test'},
              {_number: 2, project: 'test'},
            ], [
              {_number: 3, project: 'test/test'},
            ],
          ]));
      // When opt_query instanceof Array, _fetchJSON returns
      // Array<Array<Object>>.
      return element.getChanges(null, []).then(() => {
        assert.equal(Object.keys(element._projectLookup).length, 3);
        assert.equal(element._projectLookup[1], 'test');
        assert.equal(element._projectLookup[2], 'test');
        assert.equal(element._projectLookup[3], 'test/test');
      });
    });

    test('no query', () => {
      sinon.stub(element._restApiHelper, 'fetchJSON')
          .returns(Promise.resolve([
            {_number: 1, project: 'test'},
            {_number: 2, project: 'test'},
            {_number: 3, project: 'test/test'},
          ]));

      // When opt_query !instanceof Array, _fetchJSON returns
      // Array<Object>.
      return element.getChanges().then(() => {
        assert.equal(Object.keys(element._projectLookup).length, 3);
        assert.equal(element._projectLookup[1], 'test');
        assert.equal(element._projectLookup[2], 'test');
        assert.equal(element._projectLookup[3], 'test/test');
      });
    });
  });

  test('_getChangeURLAndFetch', () => {
    element._projectLookup = {1: 'test'};
    const fetchStub = sinon.stub(element._restApiHelper, 'fetchJSON')
        .returns(Promise.resolve());
    const req = {changeNum: 1, endpoint: '/test', revision: 1};
    return element._getChangeURLAndFetch(req).then(() => {
      assert.equal(fetchStub.lastCall.args[0].url,
          '/changes/test~1/revisions/1/test');
    });
  });

  test('_getChangeURLAndSend', () => {
    element._projectLookup = {1: 'test'};
    const sendStub = sinon.stub(element._restApiHelper, 'send')
        .returns(Promise.resolve());

    const req = {
      changeNum: 1,
      method: 'POST',
      patchNum: 1,
      endpoint: '/test',
    };
    return element._getChangeURLAndSend(req).then(() => {
      assert.isTrue(sendStub.calledOnce);
      assert.equal(sendStub.lastCall.args[0].method, 'POST');
      assert.equal(sendStub.lastCall.args[0].url,
          '/changes/test~1/revisions/1/test');
    });
  });

  suite('reading responses', () => {
    test('_readResponsePayload', () => {
      const mockObject = {foo: 'bar', baz: 'foo'};
      const serial = element.JSON_PREFIX + JSON.stringify(mockObject);
      const mockResponse = {text: () => Promise.resolve(serial)};
      return element._restApiHelper.readResponsePayload(mockResponse)
          .then(payload => {
            assert.deepEqual(payload.parsed, mockObject);
            assert.equal(payload.raw, serial);
          });
    });

    test('_parsePrefixedJSON', () => {
      const obj = {x: 3, y: {z: 4}, w: 23};
      const serial = element.JSON_PREFIX + JSON.stringify(obj);
      const result = element._restApiHelper.parsePrefixedJSON(serial);
      assert.deepEqual(result, obj);
    });
  });

  test('setChangeTopic', () => {
    const sendSpy = sinon.spy(element, '_getChangeURLAndSend');
    return element.setChangeTopic(123, 'foo-bar').then(() => {
      assert.isTrue(sendSpy.calledOnce);
      assert.deepEqual(sendSpy.lastCall.args[0].body, {topic: 'foo-bar'});
    });
  });

  test('setChangeHashtag', () => {
    const sendSpy = sinon.spy(element, '_getChangeURLAndSend');
    return element.setChangeHashtag(123, 'foo-bar').then(() => {
      assert.isTrue(sendSpy.calledOnce);
      assert.equal(sendSpy.lastCall.args[0].body, 'foo-bar');
    });
  });

  test('generateAccountHttpPassword', () => {
    const sendSpy = sinon.spy(element._restApiHelper, 'send');
    return element.generateAccountHttpPassword().then(() => {
      assert.isTrue(sendSpy.calledOnce);
      assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true});
    });
  });

  suite('getChangeFiles', () => {
    test('patch only', () => {
      const fetchStub = sinon.stub(element, '_getChangeURLAndFetch')
          .returns(Promise.resolve());
      const range = {basePatchNum: 'PARENT', patchNum: 2};
      return element.getChangeFiles(123, range).then(() => {
        assert.isTrue(fetchStub.calledOnce);
        assert.equal(fetchStub.lastCall.args[0].revision, 2);
        assert.isNotOk(fetchStub.lastCall.args[0].params);
      });
    });

    test('simple range', () => {
      const fetchStub = sinon.stub(element, '_getChangeURLAndFetch')
          .returns(Promise.resolve());
      const range = {basePatchNum: 4, patchNum: 5};
      return element.getChangeFiles(123, range).then(() => {
        assert.isTrue(fetchStub.calledOnce);
        assert.equal(fetchStub.lastCall.args[0].revision, 5);
        assert.isOk(fetchStub.lastCall.args[0].params);
        assert.equal(fetchStub.lastCall.args[0].params.base, 4);
        assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
      });
    });

    test('parent index', () => {
      const fetchStub = sinon.stub(element, '_getChangeURLAndFetch')
          .returns(Promise.resolve());
      const range = {basePatchNum: -3, patchNum: 5};
      return element.getChangeFiles(123, range).then(() => {
        assert.isTrue(fetchStub.calledOnce);
        assert.equal(fetchStub.lastCall.args[0].revision, 5);
        assert.isOk(fetchStub.lastCall.args[0].params);
        assert.isNotOk(fetchStub.lastCall.args[0].params.base);
        assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
      });
    });
  });

  suite('getDiff', () => {
    test('patchOnly', () => {
      const fetchStub = sinon.stub(element, '_getChangeURLAndFetch')
          .returns(Promise.resolve());
      return element.getDiff(123, 'PARENT', 2, 'foo/bar.baz').then(() => {
        assert.isTrue(fetchStub.calledOnce);
        assert.equal(fetchStub.lastCall.args[0].revision, 2);
        assert.isOk(fetchStub.lastCall.args[0].params);
        assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
        assert.isNotOk(fetchStub.lastCall.args[0].params.base);
      });
    });

    test('simple range', () => {
      const fetchStub = sinon.stub(element, '_getChangeURLAndFetch')
          .returns(Promise.resolve());
      return element.getDiff(123, 4, 5, 'foo/bar.baz').then(() => {
        assert.isTrue(fetchStub.calledOnce);
        assert.equal(fetchStub.lastCall.args[0].revision, 5);
        assert.isOk(fetchStub.lastCall.args[0].params);
        assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
        assert.equal(fetchStub.lastCall.args[0].params.base, 4);
      });
    });

    test('parent index', () => {
      const fetchStub = sinon.stub(element, '_getChangeURLAndFetch')
          .returns(Promise.resolve());
      return element.getDiff(123, -3, 5, 'foo/bar.baz').then(() => {
        assert.isTrue(fetchStub.calledOnce);
        assert.equal(fetchStub.lastCall.args[0].revision, 5);
        assert.isOk(fetchStub.lastCall.args[0].params);
        assert.isNotOk(fetchStub.lastCall.args[0].params.base);
        assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
      });
    });
  });

  test('getDashboard', () => {
    const fetchCacheURLStub = sinon.stub(element._restApiHelper,
        'fetchCacheURL');
    element.getDashboard('gerrit/project', 'default:main');
    assert.isTrue(fetchCacheURLStub.calledOnce);
    assert.equal(
        fetchCacheURLStub.lastCall.args[0].url,
        '/projects/gerrit%2Fproject/dashboards/default%3Amain');
  });

  test('getFileContent', () => {
    sinon.stub(element, '_getChangeURLAndSend')
        .returns(Promise.resolve({
          ok: 'true',
          headers: {
            get(header) {
              if (header === 'X-FYI-Content-Type') {
                return 'text/java';
              }
            },
          },
        }));

    sinon.stub(element, 'getResponseObject')
        .returns(Promise.resolve('new content'));

    const edit = element.getFileContent('1', 'tst/path', 'EDIT').then(res => {
      assert.deepEqual(res,
          {content: 'new content', type: 'text/java', ok: true});
    });

    const normal = element.getFileContent('1', 'tst/path', '3').then(res => {
      assert.deepEqual(res,
          {content: 'new content', type: 'text/java', ok: true});
    });

    return Promise.all([edit, normal]);
  });

  test('getFileContent suppresses 404s', () => {
    const res = {status: 404};
    const spy = sinon.spy();
    element.addEventListener('server-error', spy);
    sinon.stub(appContext.authService, 'fetch').returns(Promise.resolve(res));
    sinon.stub(element, '_changeBaseURL').returns(Promise.resolve(''));
    return element.getFileContent('1', 'tst/path', '1')
        .then(() => {
          flush();
          assert.isFalse(spy.called);

          res.status = 500;
          return element.getFileContent('1', 'tst/path', '1');
        })
        .then(() => {
          assert.isTrue(spy.called);
          assert.notEqual(spy.lastCall.args[0].detail.res.status, 404);
        });
  });

  test('getChangeFilesOrEditFiles is edit-sensitive', () => {
    const fn = element.getChangeOrEditFiles.bind(element);
    const getChangeFilesStub = sinon.stub(element, 'getChangeFiles')
        .returns(Promise.resolve({}));
    const getChangeEditFilesStub = sinon.stub(element, 'getChangeEditFiles')
        .returns(Promise.resolve({}));

    return fn('1', {patchNum: 'edit'}).then(() => {
      assert.isTrue(getChangeEditFilesStub.calledOnce);
      assert.isFalse(getChangeFilesStub.called);
      return fn('1', {patchNum: '1'}).then(() => {
        assert.isTrue(getChangeEditFilesStub.calledOnce);
        assert.isTrue(getChangeFilesStub.calledOnce);
      });
    });
  });

  test('_fetch forwards request and logs', () => {
    const logStub = sinon.stub(element._restApiHelper, '_logCall');
    const response = {status: 404, text: sinon.stub()};
    const url = 'my url';
    const fetchOptions = {method: 'DELETE'};
    sinon.stub(element.authService, 'fetch').returns(Promise.resolve(response));
    const startTime = 123;
    sinon.stub(Date, 'now').returns(startTime);
    const req = {url, fetchOptions};
    return element._restApiHelper.fetch(req).then(() => {
      assert.isTrue(logStub.calledOnce);
      assert.isTrue(logStub.calledWith(req, startTime, response.status));
      assert.isFalse(response.text.called);
    });
  });

  test('_logCall only reports requests with anonymized URLss', () => {
    sinon.stub(Date, 'now').returns(200);
    const handler = sinon.stub();
    element.addEventListener('rpc-log', handler);

    element._restApiHelper._logCall({url: 'url'}, 100, 200);
    assert.isFalse(handler.called);

    element._restApiHelper
        ._logCall({url: 'url', anonymizedUrl: 'not url'}, 100, 200);
    flush();
    assert.isTrue(handler.calledOnce);
  });

  test('saveChangeStarred', async () => {
    sinon.stub(element, 'getFromProjectLookup')
        .returns(Promise.resolve('test'));
    const sendStub =
        sinon.stub(element._restApiHelper, 'send').returns(Promise.resolve());

    await element.saveChangeStarred(123, true);
    assert.isTrue(sendStub.calledOnce);
    assert.deepEqual(sendStub.lastCall.args[0], {
      method: 'PUT',
      url: '/accounts/self/starred.changes/test~123',
      anonymizedUrl: '/accounts/self/starred.changes/*',
    });

    await element.saveChangeStarred(456, false);
    assert.isTrue(sendStub.calledTwice);
    assert.deepEqual(sendStub.lastCall.args[0], {
      method: 'DELETE',
      url: '/accounts/self/starred.changes/test~456',
      anonymizedUrl: '/accounts/self/starred.changes/*',
    });
  });
});

