/**
 * @license
 * Copyright (C) 2017 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-router.js';
import {page} from '../../../utils/page-wrapper-utils.js';
import {GerritNav} from '../gr-navigation/gr-navigation.js';
import {stubBaseUrl} from '../../../test/test-utils.js';
import {_testOnly_RoutePattern} from './gr-router.js';

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

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

  setup(() => {
    element = basicFixture.instantiate();
  });

  test('_firstCodeBrowserWeblink', () => {
    assert.deepEqual(element._firstCodeBrowserWeblink([
      {name: 'gitweb'},
      {name: 'gitiles'},
      {name: 'browse'},
      {name: 'test'}]), {name: 'gitiles'});

    assert.deepEqual(element._firstCodeBrowserWeblink([
      {name: 'gitweb'},
      {name: 'test'}]), {name: 'gitweb'});
  });

  test('_getBrowseCommitWeblink', () => {
    const browserLink = {name: 'browser', url: 'browser/url'};
    const link = {name: 'test', url: 'test/url'};
    const weblinks = [browserLink, link];
    const config = {gerrit: {primary_weblink_name: browserLink.name}};
    sinon.stub(element, '_firstCodeBrowserWeblink').returns(link);

    assert.deepEqual(element._getBrowseCommitWeblink(weblinks, config),
        browserLink);

    assert.deepEqual(element._getBrowseCommitWeblink(weblinks, {}), link);
  });

  test('_getChangeWeblinks', () => {
    const link = {name: 'test', url: 'test/url'};
    const browserLink = {name: 'browser', url: 'browser/url'};
    const mapLinksToConfig = weblinks => { return {options: {weblinks}}; };
    sinon.stub(element, '_getBrowseCommitWeblink').returns(browserLink);

    assert.deepEqual(
        element._getChangeWeblinks(mapLinksToConfig([link, browserLink]))[0],
        {name: 'test', url: 'test/url'});

    assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0],
        {name: 'test', url: 'test/url'});

    link.url = 'https://' + link.url;
    assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0],
        {name: 'test', url: 'https://test/url'});
  });

  test('_getHashFromCanonicalPath', () => {
    let url = '/foo/bar';
    let hash = element._getHashFromCanonicalPath(url);
    assert.equal(hash, '');

    url = '';
    hash = element._getHashFromCanonicalPath(url);
    assert.equal(hash, '');

    url = '/foo#bar';
    hash = element._getHashFromCanonicalPath(url);
    assert.equal(hash, 'bar');

    url = '/foo#bar#baz';
    hash = element._getHashFromCanonicalPath(url);
    assert.equal(hash, 'bar#baz');

    url = '#foo#bar#baz';
    hash = element._getHashFromCanonicalPath(url);
    assert.equal(hash, 'foo#bar#baz');
  });

  suite('_parseLineAddress', () => {
    test('returns null for empty and invalid hashes', () => {
      let actual = element._parseLineAddress('');
      assert.isNull(actual);

      actual = element._parseLineAddress('foobar');
      assert.isNull(actual);

      actual = element._parseLineAddress('foo123');
      assert.isNull(actual);

      actual = element._parseLineAddress('123bar');
      assert.isNull(actual);
    });

    test('parses correctly', () => {
      let actual = element._parseLineAddress('1234');
      assert.isOk(actual);
      assert.equal(actual.lineNum, 1234);
      assert.isFalse(actual.leftSide);

      actual = element._parseLineAddress('a4');
      assert.isOk(actual);
      assert.equal(actual.lineNum, 4);
      assert.isTrue(actual.leftSide);

      actual = element._parseLineAddress('b77');
      assert.isOk(actual);
      assert.equal(actual.lineNum, 77);
      assert.isTrue(actual.leftSide);
    });
  });

  test('_startRouter requires auth for the right handlers', () => {
    // This test encodes the lists of route handler methods that gr-router
    // automatically checks for authentication before triggering.

    const requiresAuth = {};
    const doesNotRequireAuth = {};
    sinon.stub(GerritNav, 'setup');
    sinon.stub(page, 'start');
    sinon.stub(page, 'base');
    sinon.stub(element, '_mapRoute').callsFake(
        (pattern, methodName, usesAuth) => {
          if (usesAuth) {
            requiresAuth[methodName] = true;
          } else {
            doesNotRequireAuth[methodName] = true;
          }
        });
    element._startRouter();

    const actualRequiresAuth = Object.keys(requiresAuth);
    actualRequiresAuth.sort();
    const actualDoesNotRequireAuth = Object.keys(doesNotRequireAuth);
    actualDoesNotRequireAuth.sort();

    const shouldRequireAutoAuth = [
      '_handleAgreementsRoute',
      '_handleChangeEditRoute',
      '_handleCreateGroupRoute',
      '_handleCreateProjectRoute',
      '_handleDiffEditRoute',
      '_handleGroupAuditLogRoute',
      '_handleGroupInfoRoute',
      '_handleGroupListFilterOffsetRoute',
      '_handleGroupListFilterRoute',
      '_handleGroupListOffsetRoute',
      '_handleGroupMembersRoute',
      '_handleGroupRoute',
      '_handleGroupSelfRedirectRoute',
      '_handleNewAgreementsRoute',
      '_handlePluginListFilterOffsetRoute',
      '_handlePluginListFilterRoute',
      '_handlePluginListOffsetRoute',
      '_handlePluginListRoute',
      '_handleRepoCommandsRoute',
      '_handleSettingsLegacyRoute',
      '_handleSettingsRoute',
    ];
    assert.deepEqual(actualRequiresAuth, shouldRequireAutoAuth);

    const unauthenticatedHandlers = [
      '_handleBranchListFilterOffsetRoute',
      '_handleBranchListFilterRoute',
      '_handleBranchListOffsetRoute',
      '_handleChangeIdQueryRoute',
      '_handleChangeNumberLegacyRoute',
      '_handleChangeRoute',
      '_handleCommentRoute',
      '_handleDiffRoute',
      '_handleDefaultRoute',
      '_handleChangeLegacyRoute',
      '_handleDiffLegacyRoute',
      '_handleDocumentationRedirectRoute',
      '_handleDocumentationSearchRoute',
      '_handleDocumentationSearchRedirectRoute',
      '_handleLegacyLinenum',
      '_handleImproperlyEncodedPlusRoute',
      '_handlePassThroughRoute',
      '_handleProjectDashboardRoute',
      '_handleLegacyProjectDashboardRoute',
      '_handleProjectsOldRoute',
      '_handleRepoAccessRoute',
      '_handleRepoDashboardsRoute',
      '_handleRepoListFilterOffsetRoute',
      '_handleRepoListFilterRoute',
      '_handleRepoListOffsetRoute',
      '_handleRepoRoute',
      '_handleQueryLegacySuffixRoute',
      '_handleQueryRoute',
      '_handleRegisterRoute',
      '_handleTagListFilterOffsetRoute',
      '_handleTagListFilterRoute',
      '_handleTagListOffsetRoute',
      '_handlePluginScreen',
    ];

    // Handler names that check authentication themselves, and thus don't need
    // it performed for them.
    const selfAuthenticatingHandlers = [
      '_handleDashboardRoute',
      '_handleCustomDashboardRoute',
      '_handleRootRoute',
    ];

    const shouldNotRequireAuth = unauthenticatedHandlers
        .concat(selfAuthenticatingHandlers);
    shouldNotRequireAuth.sort();
    assert.deepEqual(actualDoesNotRequireAuth, shouldNotRequireAuth);
  });

  test('_redirectIfNotLoggedIn while logged in', () => {
    sinon.stub(element.$.restAPI, 'getLoggedIn')
        .returns(Promise.resolve(true));
    const data = {canonicalPath: ''};
    const redirectStub = sinon.stub(element, '_redirectToLogin');
    return element._redirectIfNotLoggedIn(data).then(() => {
      assert.isFalse(redirectStub.called);
    });
  });

  test('_redirectIfNotLoggedIn while logged out', () => {
    sinon.stub(element.$.restAPI, 'getLoggedIn')
        .returns(Promise.resolve(false));
    const redirectStub = sinon.stub(element, '_redirectToLogin');
    const data = {canonicalPath: ''};
    return new Promise(resolve => {
      element._redirectIfNotLoggedIn(data)
          .then(() => {
            assert.isTrue(false, 'Should never execute');
          })
          .catch(() => {
            assert.isTrue(redirectStub.calledOnce);
            resolve();
          });
    });
  });

  suite('generateUrl', () => {
    test('search', () => {
      let params = {
        view: GerritNav.View.SEARCH,
        owner: 'a%b',
        project: 'c%d',
        branch: 'e%f',
        topic: 'g%h',
        statuses: ['op%en'],
      };
      assert.equal(element._generateUrl(params),
          '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
          'topic:"g%2525h"+status:op%2525en');

      params.offset = 100;
      assert.equal(element._generateUrl(params),
          '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
          'topic:"g%2525h"+status:op%2525en,100');
      delete params.offset;

      // The presence of the query param overrides other params.
      params.query = 'foo$bar';
      assert.equal(element._generateUrl(params), '/q/foo%2524bar');

      params.offset = 100;
      assert.equal(element._generateUrl(params), '/q/foo%2524bar,100');

      params = {
        view: GerritNav.View.SEARCH,
        statuses: ['a', 'b', 'c'],
      };
      assert.equal(element._generateUrl(params),
          '/q/(status:a OR status:b OR status:c)');
    });

    test('change', () => {
      const params = {
        view: GerritNav.View.CHANGE,
        changeNum: '1234',
        project: 'test',
      };
      const paramsWithQuery = {
        view: GerritNav.View.CHANGE,
        changeNum: '1234',
        project: 'test',
        querystring: 'revert&foo=bar',
      };

      assert.equal(element._generateUrl(params), '/c/test/+/1234');
      assert.equal(element._generateUrl(paramsWithQuery),
          '/c/test/+/1234?revert&foo=bar');

      params.patchNum = 10;
      assert.equal(element._generateUrl(params), '/c/test/+/1234/10');
      paramsWithQuery.patchNum = 10;
      assert.equal(element._generateUrl(paramsWithQuery),
          '/c/test/+/1234/10?revert&foo=bar');

      params.basePatchNum = 5;
      assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10');
      paramsWithQuery.basePatchNum = 5;
      assert.equal(element._generateUrl(paramsWithQuery),
          '/c/test/+/1234/5..10?revert&foo=bar');

      params.messageHash = '#123';
      assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123');
    });

    test('change with repo name encoding', () => {
      const params = {
        view: GerritNav.View.CHANGE,
        changeNum: '1234',
        project: 'x+/y+/z+/w',
      };
      assert.equal(element._generateUrl(params),
          '/c/x%252B/y%252B/z%252B/w/+/1234');
    });

    test('diff', () => {
      const params = {
        view: GerritNav.View.DIFF,
        changeNum: '42',
        path: 'x+y/path.cpp',
        patchNum: 12,
      };
      assert.equal(element._generateUrl(params),
          '/c/42/12/x%252By/path.cpp');

      params.project = 'test';
      assert.equal(element._generateUrl(params),
          '/c/test/+/42/12/x%252By/path.cpp');

      params.basePatchNum = 6;
      assert.equal(element._generateUrl(params),
          '/c/test/+/42/6..12/x%252By/path.cpp');

      params.path = 'foo bar/my+file.txt%';
      params.patchNum = 2;
      delete params.basePatchNum;
      assert.equal(element._generateUrl(params),
          '/c/test/+/42/2/foo+bar/my%252Bfile.txt%2525');

      params.path = 'file.cpp';
      params.lineNum = 123;
      assert.equal(element._generateUrl(params),
          '/c/test/+/42/2/file.cpp#123');

      params.leftSide = true;
      assert.equal(element._generateUrl(params),
          '/c/test/+/42/2/file.cpp#b123');
    });

    test('diff with repo name encoding', () => {
      const params = {
        view: GerritNav.View.DIFF,
        changeNum: '42',
        path: 'x+y/path.cpp',
        patchNum: 12,
        project: 'x+/y',
      };
      assert.equal(element._generateUrl(params),
          '/c/x%252B/y/+/42/12/x%252By/path.cpp');
    });

    test('edit', () => {
      const params = {
        view: GerritNav.View.EDIT,
        changeNum: '42',
        project: 'test',
        path: 'x+y/path.cpp',
      };
      assert.equal(element._generateUrl(params),
          '/c/test/+/42/x%252By/path.cpp,edit');
    });

    test('_getPatchRangeExpression', () => {
      const params = {};
      let actual = element._getPatchRangeExpression(params);
      assert.equal(actual, '');

      params.patchNum = 4;
      actual = element._getPatchRangeExpression(params);
      assert.equal(actual, '4');

      params.basePatchNum = 2;
      actual = element._getPatchRangeExpression(params);
      assert.equal(actual, '2..4');

      delete params.patchNum;
      actual = element._getPatchRangeExpression(params);
      assert.equal(actual, '2..');
    });

    suite('dashboard', () => {
      test('self dashboard', () => {
        const params = {
          view: GerritNav.View.DASHBOARD,
        };
        assert.equal(element._generateUrl(params), '/dashboard/self');
      });

      test('user dashboard', () => {
        const params = {
          view: GerritNav.View.DASHBOARD,
          user: 'user',
        };
        assert.equal(element._generateUrl(params), '/dashboard/user');
      });

      test('custom self dashboard, no title', () => {
        const params = {
          view: GerritNav.View.DASHBOARD,
          sections: [
            {name: 'section 1', query: 'query 1'},
            {name: 'section 2', query: 'query 2'},
          ],
        };
        assert.equal(
            element._generateUrl(params),
            '/dashboard/?section%201=query%201&section%202=query%202');
      });

      test('custom repo dashboard', () => {
        const params = {
          view: GerritNav.View.DASHBOARD,
          sections: [
            {name: 'section 1', query: 'query 1 ${project}'},
            {name: 'section 2', query: 'query 2 ${repo}'},
          ],
          repo: 'repo-name',
        };
        assert.equal(
            element._generateUrl(params),
            '/dashboard/?section%201=query%201%20repo-name&' +
            'section%202=query%202%20repo-name');
      });

      test('custom user dashboard, with title', () => {
        const params = {
          view: GerritNav.View.DASHBOARD,
          user: 'user',
          sections: [{name: 'name', query: 'query'}],
          title: 'custom dashboard',
        };
        assert.equal(
            element._generateUrl(params),
            '/dashboard/user?name=query&title=custom%20dashboard');
      });

      test('repo dashboard', () => {
        const params = {
          view: GerritNav.View.DASHBOARD,
          repo: 'gerrit/repo',
          dashboard: 'default:main',
        };
        assert.equal(
            element._generateUrl(params),
            '/p/gerrit/repo/+/dashboard/default:main');
      });

      test('project dashboard (legacy)', () => {
        const params = {
          view: GerritNav.View.DASHBOARD,
          project: 'gerrit/project',
          dashboard: 'default:main',
        };
        assert.equal(
            element._generateUrl(params),
            '/p/gerrit/project/+/dashboard/default:main');
      });
    });

    suite('groups', () => {
      test('group info', () => {
        const params = {
          view: GerritNav.View.GROUP,
          groupId: 1234,
        };
        assert.equal(element._generateUrl(params), '/admin/groups/1234');
      });

      test('group members', () => {
        const params = {
          view: GerritNav.View.GROUP,
          groupId: 1234,
          detail: 'members',
        };
        assert.equal(element._generateUrl(params),
            '/admin/groups/1234,members');
      });

      test('group audit log', () => {
        const params = {
          view: GerritNav.View.GROUP,
          groupId: 1234,
          detail: 'log',
        };
        assert.equal(element._generateUrl(params),
            '/admin/groups/1234,audit-log');
      });
    });
  });

  suite('param normalization', () => {
    let projectLookupStub;
    let generateUrlStub;

    setup(() => {
      projectLookupStub = sinon
          .stub(element.$.restAPI, 'getFromProjectLookup');
      generateUrlStub = sinon.stub(element, '_generateUrl');
    });

    suite('_normalizeLegacyRouteParams', () => {
      let rangeStub;
      let redirectStub;
      let show404Stub;

      setup(() => {
        rangeStub = sinon.stub(element, '_normalizePatchRangeParams')
            .returns(Promise.resolve());
        redirectStub = sinon.stub(element, '_redirect');
        show404Stub = sinon.stub(element, '_show404');
      });

      test('w/o changeNum', () => {
        projectLookupStub.returns(Promise.resolve('foo/bar'));
        const params = {};
        return element._normalizeLegacyRouteParams(params).then(() => {
          assert.isFalse(generateUrlStub.calledOnce);
          assert.isFalse(projectLookupStub.called);
          assert.isFalse(rangeStub.called);
          assert.isFalse(redirectStub.called);
          assert.isFalse(show404Stub.called);
        });
      });

      test('w/ changeNum', () => {
        projectLookupStub.returns(Promise.resolve('foo/bar'));
        const params = {changeNum: 1234};

        return element._normalizeLegacyRouteParams(params).then(() => {
          assert.isTrue(generateUrlStub.calledOnce);
          const updatedParams = generateUrlStub.lastCall.args[0];
          assert.isTrue(projectLookupStub.called);
          assert.isTrue(rangeStub.called);
          assert.equal(updatedParams.project, 'foo/bar');
          assert.isTrue(redirectStub.calledOnce);
          assert.isFalse(show404Stub.called);
        });
      });

      test('halts on project lookup failure', () => {
        projectLookupStub.returns(Promise.resolve(undefined));
        const params = {changeNum: 1234};
        return element._normalizeLegacyRouteParams(params).then(() => {
          assert.isFalse(generateUrlStub.calledOnce);
          assert.isTrue(projectLookupStub.called);
          assert.isFalse(rangeStub.called);
          assert.isFalse(redirectStub.called);
          assert.isTrue(show404Stub.calledOnce);
        });
      });
    });

    suite('_normalizePatchRangeParams', () => {
      test('range n..n normalizes to n', () => {
        const params = {basePatchNum: 4, patchNum: 4};
        const needsRedirect = element._normalizePatchRangeParams(params);
        assert.isTrue(needsRedirect);
        assert.isNotOk(params.basePatchNum);
        assert.equal(params.patchNum, 4);
      });

      test('range n.. normalizes to n', () => {
        const params = {basePatchNum: 4};
        const needsRedirect = element._normalizePatchRangeParams(params);
        assert.isFalse(needsRedirect);
        assert.isNotOk(params.basePatchNum);
        assert.equal(params.patchNum, 4);
      });
    });
  });

  suite('route handlers', () => {
    let redirectStub;
    let setParamsStub;
    let handlePassThroughRoute;

    // Simple route handlers are direct mappings from parsed route data to a
    // new set of app.params. This test helper asserts that passing `data`
    // into `methodName` results in setting the params specified in `params`.
    function assertDataToParams(data, methodName, params) {
      element[methodName](data);
      assert.deepEqual(setParamsStub.lastCall.args[0], params);
    }

    setup(() => {
      redirectStub = sinon.stub(element, '_redirect');
      setParamsStub = sinon.stub(element, '_setParams');
      handlePassThroughRoute = sinon.stub(element, '_handlePassThroughRoute');
    });

    test('_handleLegacyProjectDashboardRoute', () => {
      const params = {0: 'gerrit/project', 1: 'dashboard:main'};
      element._handleLegacyProjectDashboardRoute({params});
      assert.isTrue(redirectStub.calledOnce);
      assert.equal(redirectStub.lastCall.args[0],
          '/p/gerrit/project/+/dashboard/dashboard:main');
    });

    test('_handleAgreementsRoute', () => {
      const data = {params: {}};
      element._handleAgreementsRoute(data);
      assert.isTrue(redirectStub.calledOnce);
      assert.equal(redirectStub.lastCall.args[0], '/settings/#Agreements');
    });

    test('_handleNewAgreementsRoute', () => {
      element._handleNewAgreementsRoute({params: {}});
      assert.isTrue(setParamsStub.calledOnce);
      assert.equal(setParamsStub.lastCall.args[0].view,
          GerritNav.View.AGREEMENTS);
    });

    test('_handleSettingsLegacyRoute', () => {
      const data = {params: {0: 'my-token'}};
      assertDataToParams(data, '_handleSettingsLegacyRoute', {
        view: GerritNav.View.SETTINGS,
        emailToken: 'my-token',
      });
    });

    test('_handleSettingsLegacyRoute with +', () => {
      const data = {params: {0: 'my-token test'}};
      assertDataToParams(data, '_handleSettingsLegacyRoute', {
        view: GerritNav.View.SETTINGS,
        emailToken: 'my-token+test',
      });
    });

    test('_handleSettingsRoute', () => {
      const data = {};
      assertDataToParams(data, '_handleSettingsRoute', {
        view: GerritNav.View.SETTINGS,
      });
    });

    test('_handleDefaultRoute on first load', () => {
      const appElementStub = {dispatchEvent: sinon.stub()};
      element._appElement = () => appElementStub;
      element._handleDefaultRoute();
      assert.isTrue(appElementStub.dispatchEvent.calledOnce);
      assert.equal(
          appElementStub.dispatchEvent.lastCall.args[0].detail.response.status,
          404);
    });

    test('_handleDefaultRoute after internal navigation', () => {
      let onExit = null;
      const onRegisteringExit = (match, _onExit) => {
        onExit = _onExit;
      };
      sinon.stub(page, 'exit').callsFake( onRegisteringExit);
      sinon.stub(GerritNav, 'setup');
      sinon.stub(page, 'start');
      sinon.stub(page, 'base');
      element._startRouter();

      const appElementStub = {dispatchEvent: sinon.stub()};
      element._appElement = () => appElementStub;
      element._handleDefaultRoute();

      onExit('', () => {}); // we left page;

      element._handleDefaultRoute();
      assert.isTrue(handlePassThroughRoute.calledOnce);
    });

    test('_handleImproperlyEncodedPlusRoute', () => {
      // Regression test for Issue 7100.
      element._handleImproperlyEncodedPlusRoute(
          {canonicalPath: '/c/test/%20/42', params: ['test', '42']});
      assert.isTrue(redirectStub.calledOnce);
      assert.equal(
          redirectStub.lastCall.args[0],
          '/c/test/+/42');

      sinon.stub(element, '_getHashFromCanonicalPath').returns('foo');
      element._handleImproperlyEncodedPlusRoute(
          {canonicalPath: '/c/test/%20/42', params: ['test', '42']});
      assert.equal(
          redirectStub.lastCall.args[0],
          '/c/test/+/42#foo');
    });

    test('_handleQueryRoute', () => {
      const data = {params: ['project:foo/bar/baz']};
      assertDataToParams(data, '_handleQueryRoute', {
        view: GerritNav.View.SEARCH,
        query: 'project:foo/bar/baz',
        offset: undefined,
      });

      data.params.push(',123', '123');
      assertDataToParams(data, '_handleQueryRoute', {
        view: GerritNav.View.SEARCH,
        query: 'project:foo/bar/baz',
        offset: '123',
      });
    });

    test('_handleQueryLegacySuffixRoute', () => {
      element._handleQueryLegacySuffixRoute({path: '/q/foo+bar,n,z'});
      assert.isTrue(redirectStub.calledOnce);
      assert.equal(redirectStub.lastCall.args[0], '/q/foo+bar');
    });

    test('_handleQueryRoute', () => {
      const data = {params: ['project:foo/bar/baz']};
      assertDataToParams(data, '_handleQueryRoute', {
        view: GerritNav.View.SEARCH,
        query: 'project:foo/bar/baz',
        offset: undefined,
      });

      data.params.push(',123', '123');
      assertDataToParams(data, '_handleQueryRoute', {
        view: GerritNav.View.SEARCH,
        query: 'project:foo/bar/baz',
        offset: '123',
      });
    });

    test('_handleChangeIdQueryRoute', () => {
      const data = {params: ['I0123456789abcdef0123456789abcdef01234567']};
      assertDataToParams(data, '_handleChangeIdQueryRoute', {
        view: GerritNav.View.SEARCH,
        query: 'I0123456789abcdef0123456789abcdef01234567',
      });
    });

    suite('_handleRegisterRoute', () => {
      test('happy path', () => {
        const ctx = {params: ['/foo/bar']};
        element._handleRegisterRoute(ctx);
        assert.isTrue(redirectStub.calledWithExactly('/foo/bar'));
        assert.isTrue(setParamsStub.calledOnce);
        assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
      });

      test('no param', () => {
        const ctx = {params: ['']};
        element._handleRegisterRoute(ctx);
        assert.isTrue(redirectStub.calledWithExactly('/'));
        assert.isTrue(setParamsStub.calledOnce);
        assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
      });

      test('prevent redirect', () => {
        const ctx = {params: ['/register']};
        element._handleRegisterRoute(ctx);
        assert.isTrue(redirectStub.calledWithExactly('/'));
        assert.isTrue(setParamsStub.calledOnce);
        assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
      });
    });

    suite('_handleRootRoute', () => {
      test('closes for closeAfterLogin', () => {
        const data = {querystring: 'closeAfterLogin', canonicalPath: ''};
        const closeStub = sinon.stub(window, 'close');
        const result = element._handleRootRoute(data);
        assert.isNotOk(result);
        assert.isTrue(closeStub.called);
        assert.isFalse(redirectStub.called);
      });

      test('redirects to dashboard if logged in', () => {
        sinon.stub(element.$.restAPI, 'getLoggedIn')
            .returns(Promise.resolve(true));
        const data = {
          canonicalPath: '/', path: '/', querystring: '', hash: '',
        };
        const result = element._handleRootRoute(data);
        assert.isOk(result);
        return result.then(() => {
          assert.isTrue(redirectStub.calledWithExactly('/dashboard/self'));
        });
      });

      test('redirects to open changes if not logged in', () => {
        sinon.stub(element.$.restAPI, 'getLoggedIn')
            .returns(Promise.resolve(false));
        const data = {
          canonicalPath: '/', path: '/', querystring: '', hash: '',
        };
        const result = element._handleRootRoute(data);
        assert.isOk(result);
        return result.then(() => {
          assert.isTrue(
              redirectStub.calledWithExactly('/q/status:open+-is:wip'));
        });
      });

      suite('GWT hash-path URLs', () => {
        test('redirects hash-path URLs', () => {
          const data = {
            canonicalPath: '/#/foo/bar/baz',
            hash: '/foo/bar/baz',
            querystring: '',
          };
          const result = element._handleRootRoute(data);
          assert.isNotOk(result);
          assert.isTrue(redirectStub.called);
          assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz'));
        });

        test('redirects hash-path URLs w/o leading slash', () => {
          const data = {
            canonicalPath: '/#foo/bar/baz',
            querystring: '',
            hash: 'foo/bar/baz',
          };
          const result = element._handleRootRoute(data);
          assert.isNotOk(result);
          assert.isTrue(redirectStub.called);
          assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz'));
        });

        test('normalizes "/ /" in hash to "/+/"', () => {
          const data = {
            canonicalPath: '/#/foo/bar/+/123/4',
            querystring: '',
            hash: '/foo/bar/ /123/4',
          };
          const result = element._handleRootRoute(data);
          assert.isNotOk(result);
          assert.isTrue(redirectStub.called);
          assert.isTrue(redirectStub.calledWithExactly('/foo/bar/+/123/4'));
        });

        test('prepends baseurl to hash-path', () => {
          const data = {
            canonicalPath: '/#/foo/bar',
            querystring: '',
            hash: '/foo/bar',
          };
          stubBaseUrl('/baz');
          const result = element._handleRootRoute(data);
          assert.isNotOk(result);
          assert.isTrue(redirectStub.called);
          assert.isTrue(redirectStub.calledWithExactly('/baz/foo/bar'));
        });

        test('normalizes /VE/ settings hash-paths', () => {
          const data = {
            canonicalPath: '/#/VE/foo/bar',
            querystring: '',
            hash: '/VE/foo/bar',
          };
          const result = element._handleRootRoute(data);
          assert.isNotOk(result);
          assert.isTrue(redirectStub.called);
          assert.isTrue(redirectStub.calledWithExactly(
              '/settings/VE/foo/bar'));
        });

        test('does not drop "inner hashes"', () => {
          const data = {
            canonicalPath: '/#/foo/bar#baz',
            querystring: '',
            hash: '/foo/bar',
          };
          const result = element._handleRootRoute(data);
          assert.isNotOk(result);
          assert.isTrue(redirectStub.called);
          assert.isTrue(redirectStub.calledWithExactly('/foo/bar#baz'));
        });
      });
    });

    suite('_handleDashboardRoute', () => {
      let redirectToLoginStub;

      setup(() => {
        redirectToLoginStub = sinon.stub(element, '_redirectToLogin');
      });

      test('own dashboard but signed out redirects to login', () => {
        sinon.stub(element.$.restAPI, 'getLoggedIn')
            .returns(Promise.resolve(false));
        const data = {canonicalPath: '/dashboard/', params: {0: 'seLF'}};
        return element._handleDashboardRoute(data, '').then(() => {
          assert.isTrue(redirectToLoginStub.calledOnce);
          assert.isFalse(redirectStub.called);
          assert.isFalse(setParamsStub.called);
        });
      });

      test('non-self dashboard but signed out does not redirect', () => {
        sinon.stub(element.$.restAPI, 'getLoggedIn')
            .returns(Promise.resolve(false));
        const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
        return element._handleDashboardRoute(data, '').then(() => {
          assert.isFalse(redirectToLoginStub.called);
          assert.isFalse(setParamsStub.called);
          assert.isTrue(redirectStub.calledOnce);
          assert.equal(redirectStub.lastCall.args[0], '/q/owner:foo');
        });
      });

      test('dashboard while signed in sets params', () => {
        sinon.stub(element.$.restAPI, 'getLoggedIn')
            .returns(Promise.resolve(true));
        const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
        return element._handleDashboardRoute(data, '').then(() => {
          assert.isFalse(redirectToLoginStub.called);
          assert.isFalse(redirectStub.called);
          assert.isTrue(setParamsStub.calledOnce);
          assert.deepEqual(setParamsStub.lastCall.args[0], {
            view: GerritNav.View.DASHBOARD,
            user: 'foo',
          });
        });
      });
    });

    suite('_handleCustomDashboardRoute', () => {
      let redirectToLoginStub;

      setup(() => {
        redirectToLoginStub = sinon.stub(element, '_redirectToLogin');
      });

      test('no user specified', () => {
        const data = {canonicalPath: '/dashboard/', params: {0: ''}};
        return element._handleCustomDashboardRoute(data, '').then(() => {
          assert.isFalse(setParamsStub.called);
          assert.isTrue(redirectStub.called);
          assert.equal(redirectStub.lastCall.args[0], '/dashboard/self');
        });
      });

      test('custom dashboard without title', () => {
        const data = {canonicalPath: '/dashboard/', params: {0: ''}};
        return element._handleCustomDashboardRoute(data, '?a=b&c&d=e')
            .then(() => {
              assert.isFalse(redirectStub.called);
              assert.isTrue(setParamsStub.calledOnce);
              assert.deepEqual(setParamsStub.lastCall.args[0], {
                view: GerritNav.View.DASHBOARD,
                user: 'self',
                sections: [
                  {name: 'a', query: 'b'},
                  {name: 'd', query: 'e'},
                ],
                title: 'Custom Dashboard',
              });
            });
      });

      test('custom dashboard with title', () => {
        const data = {canonicalPath: '/dashboard/', params: {0: ''}};
        return element._handleCustomDashboardRoute(data,
            '?a=b&c&d=&=e&title=t')
            .then(() => {
              assert.isFalse(redirectToLoginStub.called);
              assert.isFalse(redirectStub.called);
              assert.isTrue(setParamsStub.calledOnce);
              assert.deepEqual(setParamsStub.lastCall.args[0], {
                view: GerritNav.View.DASHBOARD,
                user: 'self',
                sections: [
                  {name: 'a', query: 'b'},
                ],
                title: 't',
              });
            });
      });

      test('custom dashboard with foreach', () => {
        const data = {canonicalPath: '/dashboard/', params: {0: ''}};
        return element._handleCustomDashboardRoute(data,
            '?a=b&c&d=&=e&foreach=is:open')
            .then(() => {
              assert.isFalse(redirectToLoginStub.called);
              assert.isFalse(redirectStub.called);
              assert.isTrue(setParamsStub.calledOnce);
              assert.deepEqual(setParamsStub.lastCall.args[0], {
                view: GerritNav.View.DASHBOARD,
                user: 'self',
                sections: [
                  {name: 'a', query: 'is:open b'},
                ],
                title: 'Custom Dashboard',
              });
            });
      });
    });

    suite('group routes', () => {
      test('_handleGroupInfoRoute', () => {
        const data = {params: {0: 1234}};
        element._handleGroupInfoRoute(data);
        assert.isTrue(redirectStub.calledOnce);
        assert.equal(redirectStub.lastCall.args[0], '/admin/groups/1234');
      });

      test('_handleGroupAuditLogRoute', () => {
        const data = {params: {0: 1234}};
        assertDataToParams(data, '_handleGroupAuditLogRoute', {
          view: GerritNav.View.GROUP,
          detail: 'log',
          groupId: 1234,
        });
      });

      test('_handleGroupMembersRoute', () => {
        const data = {params: {0: 1234}};
        assertDataToParams(data, '_handleGroupMembersRoute', {
          view: GerritNav.View.GROUP,
          detail: 'members',
          groupId: 1234,
        });
      });

      test('_handleGroupListOffsetRoute', () => {
        const data = {params: {}};
        assertDataToParams(data, '_handleGroupListOffsetRoute', {
          view: GerritNav.View.ADMIN,
          adminView: 'gr-admin-group-list',
          offset: 0,
          filter: null,
          openCreateModal: false,
        });

        data.params[1] = 42;
        assertDataToParams(data, '_handleGroupListOffsetRoute', {
          view: GerritNav.View.ADMIN,
          adminView: 'gr-admin-group-list',
          offset: 42,
          filter: null,
          openCreateModal: false,
        });

        data.hash = 'create';
        assertDataToParams(data, '_handleGroupListOffsetRoute', {
          view: GerritNav.View.ADMIN,
          adminView: 'gr-admin-group-list',
          offset: 42,
          filter: null,
          openCreateModal: true,
        });
      });

      test('_handleGroupListFilterOffsetRoute', () => {
        const data = {params: {filter: 'foo', offset: 42}};
        assertDataToParams(data, '_handleGroupListFilterOffsetRoute', {
          view: GerritNav.View.ADMIN,
          adminView: 'gr-admin-group-list',
          offset: 42,
          filter: 'foo',
        });
      });

      test('_handleGroupListFilterRoute', () => {
        const data = {params: {filter: 'foo'}};
        assertDataToParams(data, '_handleGroupListFilterRoute', {
          view: GerritNav.View.ADMIN,
          adminView: 'gr-admin-group-list',
          filter: 'foo',
        });
      });

      test('_handleGroupRoute', () => {
        const data = {params: {0: 4321}};
        assertDataToParams(data, '_handleGroupRoute', {
          view: GerritNav.View.GROUP,
          groupId: 4321,
        });
      });
    });

    suite('repo routes', () => {
      test('_handleProjectsOldRoute', () => {
        const data = {params: {}};
        element._handleProjectsOldRoute(data);
        assert.isTrue(redirectStub.calledOnce);
        assert.equal(redirectStub.lastCall.args[0], '/admin/repos/');
      });

      test('_handleProjectsOldRoute test', () => {
        const data = {params: {1: 'test'}};
        element._handleProjectsOldRoute(data);
        assert.isTrue(redirectStub.calledOnce);
        assert.equal(redirectStub.lastCall.args[0], '/admin/repos/test');
      });

      test('_handleProjectsOldRoute test,branches', () => {
        const data = {params: {1: 'test,branches'}};
        element._handleProjectsOldRoute(data);
        assert.isTrue(redirectStub.calledOnce);
        assert.equal(
            redirectStub.lastCall.args[0], '/admin/repos/test,branches');
      });

      test('_handleRepoRoute', () => {
        const data = {params: {0: 4321}};
        assertDataToParams(data, '_handleRepoRoute', {
          view: GerritNav.View.REPO,
          repo: 4321,
        });
      });

      test('_handleRepoCommandsRoute', () => {
        const data = {params: {0: 4321}};
        assertDataToParams(data, '_handleRepoCommandsRoute', {
          view: GerritNav.View.REPO,
          detail: GerritNav.RepoDetailView.COMMANDS,
          repo: 4321,
        });
      });

      test('_handleRepoAccessRoute', () => {
        const data = {params: {0: 4321}};
        assertDataToParams(data, '_handleRepoAccessRoute', {
          view: GerritNav.View.REPO,
          detail: GerritNav.RepoDetailView.ACCESS,
          repo: 4321,
        });
      });

      suite('branch list routes', () => {
        test('_handleBranchListOffsetRoute', () => {
          const data = {params: {0: 4321}};
          assertDataToParams(data, '_handleBranchListOffsetRoute', {
            view: GerritNav.View.REPO,
            detail: GerritNav.RepoDetailView.BRANCHES,
            repo: 4321,
            offset: 0,
            filter: null,
          });

          data.params[2] = 42;
          assertDataToParams(data, '_handleBranchListOffsetRoute', {
            view: GerritNav.View.REPO,
            detail: GerritNav.RepoDetailView.BRANCHES,
            repo: 4321,
            offset: 42,
            filter: null,
          });
        });

        test('_handleBranchListFilterOffsetRoute', () => {
          const data = {params: {repo: 4321, filter: 'foo', offset: 42}};
          assertDataToParams(data, '_handleBranchListFilterOffsetRoute', {
            view: GerritNav.View.REPO,
            detail: GerritNav.RepoDetailView.BRANCHES,
            repo: 4321,
            offset: 42,
            filter: 'foo',
          });
        });

        test('_handleBranchListFilterRoute', () => {
          const data = {params: {repo: 4321, filter: 'foo'}};
          assertDataToParams(data, '_handleBranchListFilterRoute', {
            view: GerritNav.View.REPO,
            detail: GerritNav.RepoDetailView.BRANCHES,
            repo: 4321,
            filter: 'foo',
          });
        });
      });

      suite('tag list routes', () => {
        test('_handleTagListOffsetRoute', () => {
          const data = {params: {0: 4321}};
          assertDataToParams(data, '_handleTagListOffsetRoute', {
            view: GerritNav.View.REPO,
            detail: GerritNav.RepoDetailView.TAGS,
            repo: 4321,
            offset: 0,
            filter: null,
          });
        });

        test('_handleTagListFilterOffsetRoute', () => {
          const data = {params: {repo: 4321, filter: 'foo', offset: 42}};
          assertDataToParams(data, '_handleTagListFilterOffsetRoute', {
            view: GerritNav.View.REPO,
            detail: GerritNav.RepoDetailView.TAGS,
            repo: 4321,
            offset: 42,
            filter: 'foo',
          });
        });

        test('_handleTagListFilterRoute', () => {
          const data = {params: {repo: 4321}};
          assertDataToParams(data, '_handleTagListFilterRoute', {
            view: GerritNav.View.REPO,
            detail: GerritNav.RepoDetailView.TAGS,
            repo: 4321,
            filter: null,
          });

          data.params.filter = 'foo';
          assertDataToParams(data, '_handleTagListFilterRoute', {
            view: GerritNav.View.REPO,
            detail: GerritNav.RepoDetailView.TAGS,
            repo: 4321,
            filter: 'foo',
          });
        });
      });

      suite('repo list routes', () => {
        test('_handleRepoListOffsetRoute', () => {
          const data = {params: {}};
          assertDataToParams(data, '_handleRepoListOffsetRoute', {
            view: GerritNav.View.ADMIN,
            adminView: 'gr-repo-list',
            offset: 0,
            filter: null,
            openCreateModal: false,
          });

          data.params[1] = 42;
          assertDataToParams(data, '_handleRepoListOffsetRoute', {
            view: GerritNav.View.ADMIN,
            adminView: 'gr-repo-list',
            offset: 42,
            filter: null,
            openCreateModal: false,
          });

          data.hash = 'create';
          assertDataToParams(data, '_handleRepoListOffsetRoute', {
            view: GerritNav.View.ADMIN,
            adminView: 'gr-repo-list',
            offset: 42,
            filter: null,
            openCreateModal: true,
          });
        });

        test('_handleRepoListFilterOffsetRoute', () => {
          const data = {params: {filter: 'foo', offset: 42}};
          assertDataToParams(data, '_handleRepoListFilterOffsetRoute', {
            view: GerritNav.View.ADMIN,
            adminView: 'gr-repo-list',
            offset: 42,
            filter: 'foo',
          });
        });

        test('_handleRepoListFilterRoute', () => {
          const data = {params: {}};
          assertDataToParams(data, '_handleRepoListFilterRoute', {
            view: GerritNav.View.ADMIN,
            adminView: 'gr-repo-list',
            filter: null,
          });

          data.params.filter = 'foo';
          assertDataToParams(data, '_handleRepoListFilterRoute', {
            view: GerritNav.View.ADMIN,
            adminView: 'gr-repo-list',
            filter: 'foo',
          });
        });
      });
    });

    suite('plugin routes', () => {
      test('_handlePluginListOffsetRoute', () => {
        const data = {params: {}};
        assertDataToParams(data, '_handlePluginListOffsetRoute', {
          view: GerritNav.View.ADMIN,
          adminView: 'gr-plugin-list',
          offset: 0,
          filter: null,
        });

        data.params[1] = 42;
        assertDataToParams(data, '_handlePluginListOffsetRoute', {
          view: GerritNav.View.ADMIN,
          adminView: 'gr-plugin-list',
          offset: 42,
          filter: null,
        });
      });

      test('_handlePluginListFilterOffsetRoute', () => {
        const data = {params: {filter: 'foo', offset: 42}};
        assertDataToParams(data, '_handlePluginListFilterOffsetRoute', {
          view: GerritNav.View.ADMIN,
          adminView: 'gr-plugin-list',
          offset: 42,
          filter: 'foo',
        });
      });

      test('_handlePluginListFilterRoute', () => {
        const data = {params: {}};
        assertDataToParams(data, '_handlePluginListFilterRoute', {
          view: GerritNav.View.ADMIN,
          adminView: 'gr-plugin-list',
          filter: null,
        });

        data.params.filter = 'foo';
        assertDataToParams(data, '_handlePluginListFilterRoute', {
          view: GerritNav.View.ADMIN,
          adminView: 'gr-plugin-list',
          filter: 'foo',
        });
      });

      test('_handlePluginListRoute', () => {
        const data = {params: {}};
        assertDataToParams(data, '_handlePluginListRoute', {
          view: GerritNav.View.ADMIN,
          adminView: 'gr-plugin-list',
        });
      });
    });

    suite('change/diff routes', () => {
      test('_handleChangeNumberLegacyRoute', () => {
        const data = {params: {0: 12345}};
        element._handleChangeNumberLegacyRoute(data);
        assert.isTrue(redirectStub.calledOnce);
        assert.isTrue(redirectStub.calledWithExactly('/c/12345'));
      });

      test('_handleChangeLegacyRoute', () => {
        const normalizeRouteStub = sinon.stub(element,
            '_normalizeLegacyRouteParams');
        const ctx = {
          params: [
            1234, // 0 Change number
            null, // 1 Unused
            null, // 2 Unused
            6, // 3 Base patch number
            null, // 4 Unused
            9, // 5 Patch number
          ],
          querystring: '',
        };
        element._handleChangeLegacyRoute(ctx);
        assert.isTrue(normalizeRouteStub.calledOnce);
        assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
          changeNum: 1234,
          basePatchNum: 6,
          patchNum: 9,
          view: GerritNav.View.CHANGE,
          querystring: '',
        });
      });

      test('_handleDiffLegacyRoute', () => {
        const normalizeRouteStub = sinon.stub(element,
            '_normalizeLegacyRouteParams');
        const ctx = {
          params: [
            1234, // 0 Change number
            null, // 1 Unused
            3, // 2 Base patch number
            null, // 3 Unused
            8, // 4 Patch number
            'foo/bar', // 5 Diff path
          ],
          path: '/c/1234/3..8/foo/bar',
          hash: 'b123',
        };
        element._handleDiffLegacyRoute(ctx);
        assert.isFalse(redirectStub.called);
        assert.isTrue(normalizeRouteStub.calledOnce);
        assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
          changeNum: 1234,
          basePatchNum: 3,
          patchNum: 8,
          view: GerritNav.View.DIFF,
          path: 'foo/bar',
          lineNum: 123,
          leftSide: true,
        });
      });

      test('_handleLegacyLinenum w/ @321', () => {
        const ctx = {path: '/c/1234/3..8/foo/bar@321'};
        element._handleLegacyLinenum(ctx);
        assert.isTrue(redirectStub.calledOnce);
        assert.isTrue(redirectStub.calledWithExactly(
            '/c/1234/3..8/foo/bar#321'));
      });

      test('_handleLegacyLinenum w/ @b123', () => {
        const ctx = {path: '/c/1234/3..8/foo/bar@b123'};
        element._handleLegacyLinenum(ctx);
        assert.isTrue(redirectStub.calledOnce);
        assert.isTrue(redirectStub.calledWithExactly(
            '/c/1234/3..8/foo/bar#b123'));
      });

      suite('_handleChangeRoute', () => {
        let normalizeRangeStub;

        function makeParams(path, hash) {
          return {
            params: [
              'foo/bar', // 0 Project
              1234, // 1 Change number
              null, // 2 Unused
              null, // 3 Unused
              4, // 4 Base patch number
              null, // 5 Unused
              7, // 6 Patch number
            ],
            queryMap: new Map(),
          };
        }

        setup(() => {
          normalizeRangeStub = sinon.stub(element,
              '_normalizePatchRangeParams');
          sinon.stub(element.$.restAPI, 'setInProjectLookup');
        });

        test('needs redirect', () => {
          normalizeRangeStub.returns(true);
          sinon.stub(element, '_generateUrl').returns('foo');
          const ctx = makeParams(null, '');
          element._handleChangeRoute(ctx);
          assert.isTrue(normalizeRangeStub.called);
          assert.isFalse(setParamsStub.called);
          assert.isTrue(redirectStub.calledOnce);
          assert.isTrue(redirectStub.calledWithExactly('foo'));
        });

        test('change view', () => {
          normalizeRangeStub.returns(false);
          sinon.stub(element, '_generateUrl').returns('foo');
          const ctx = makeParams(null, '');
          assertDataToParams(ctx, '_handleChangeRoute', {
            view: GerritNav.View.CHANGE,
            project: 'foo/bar',
            changeNum: 1234,
            basePatchNum: 4,
            patchNum: 7,
            queryMap: new Map(),
          });
          assert.isFalse(redirectStub.called);
          assert.isTrue(normalizeRangeStub.called);
        });
      });

      suite('_handleDiffRoute', () => {
        let normalizeRangeStub;

        function makeParams(path, hash) {
          return {
            params: [
              'foo/bar', // 0 Project
              1234, // 1 Change number
              null, // 2 Unused
              null, // 3 Unused
              4, // 4 Base patch number
              null, // 5 Unused
              7, // 6 Patch number
              null, // 7 Unused,
              path, // 8 Diff path
            ],
            hash,
          };
        }

        setup(() => {
          normalizeRangeStub = sinon.stub(element,
              '_normalizePatchRangeParams');
          sinon.stub(element.$.restAPI, 'setInProjectLookup');
        });

        test('needs redirect', () => {
          normalizeRangeStub.returns(true);
          sinon.stub(element, '_generateUrl').returns('foo');
          const ctx = makeParams(null, '');
          element._handleDiffRoute(ctx);
          assert.isTrue(normalizeRangeStub.called);
          assert.isFalse(setParamsStub.called);
          assert.isTrue(redirectStub.calledOnce);
          assert.isTrue(redirectStub.calledWithExactly('foo'));
        });

        test('diff view', () => {
          normalizeRangeStub.returns(false);
          sinon.stub(element, '_generateUrl').returns('foo');
          const ctx = makeParams('foo/bar/baz', 'b44');
          assertDataToParams(ctx, '_handleDiffRoute', {
            view: GerritNav.View.DIFF,
            project: 'foo/bar',
            changeNum: 1234,
            basePatchNum: 4,
            patchNum: 7,
            path: 'foo/bar/baz',
            leftSide: true,
            lineNum: 44,
          });
          assert.isFalse(redirectStub.called);
          assert.isTrue(normalizeRangeStub.called);
        });

        test('comment route', () => {
          const url = '/c/gerrit/+/264833/comment/00049681_f34fd6a9/';
          const groups = url.match(_testOnly_RoutePattern.COMMENT);
          assert.deepEqual(groups.slice(1), [
            'gerrit', // project
            '264833', // changeNum
            '00049681_f34fd6a9', // commentId
          ]);
          assertDataToParams({params: groups.slice(1)}, '_handleCommentRoute', {
            project: 'gerrit',
            changeNum: '264833',
            commentId: '00049681_f34fd6a9',
            commentLink: true,
            view: GerritNav.View.DIFF,
          });
        });
      });

      test('_handleDiffEditRoute', () => {
        const normalizeRangeSpy =
            sinon.spy(element, '_normalizePatchRangeParams');
        sinon.stub(element.$.restAPI, 'setInProjectLookup');
        const ctx = {
          params: [
            'foo/bar', // 0 Project
            1234, // 1 Change number
            3, // 2 Patch num
            'foo/bar/baz', // 3 File path
          ],
        };
        const appParams = {
          project: 'foo/bar',
          changeNum: 1234,
          view: GerritNav.View.EDIT,
          path: 'foo/bar/baz',
          patchNum: 3,
          lineNum: undefined,
        };

        element._handleDiffEditRoute(ctx);
        assert.isFalse(redirectStub.called);
        assert.isTrue(normalizeRangeSpy.calledOnce);
        assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
        assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
        assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
      });

      test('_handleDiffEditRoute with lineNum', () => {
        const normalizeRangeSpy =
            sinon.spy(element, '_normalizePatchRangeParams');
        sinon.stub(element.$.restAPI, 'setInProjectLookup');
        const ctx = {
          params: [
            'foo/bar', // 0 Project
            1234, // 1 Change number
            3, // 2 Patch num
            'foo/bar/baz', // 3 File path
          ],
          hash: 4,
        };
        const appParams = {
          project: 'foo/bar',
          changeNum: 1234,
          view: GerritNav.View.EDIT,
          path: 'foo/bar/baz',
          patchNum: 3,
          lineNum: 4,
        };

        element._handleDiffEditRoute(ctx);
        assert.isFalse(redirectStub.called);
        assert.isTrue(normalizeRangeSpy.calledOnce);
        assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
        assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
        assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
      });

      test('_handleChangeEditRoute', () => {
        const normalizeRangeSpy =
            sinon.spy(element, '_normalizePatchRangeParams');
        sinon.stub(element.$.restAPI, 'setInProjectLookup');
        const ctx = {
          params: [
            'foo/bar', // 0 Project
            1234, // 1 Change number
            null,
            3, // 3 Patch num
          ],
        };
        const appParams = {
          project: 'foo/bar',
          changeNum: 1234,
          view: GerritNav.View.CHANGE,
          patchNum: 3,
          edit: true,
        };

        element._handleChangeEditRoute(ctx);
        assert.isFalse(redirectStub.called);
        assert.isTrue(normalizeRangeSpy.calledOnce);
        assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
        assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
        assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
      });
    });

    test('_handlePluginScreen', () => {
      const ctx = {params: ['foo', 'bar']};
      assertDataToParams(ctx, '_handlePluginScreen', {
        view: GerritNav.View.PLUGIN_SCREEN,
        plugin: 'foo',
        screen: 'bar',
      });
      assert.isFalse(redirectStub.called);
    });
  });

  suite('_parseQueryString', () => {
    test('empty queries', () => {
      assert.deepEqual(element._parseQueryString(''), []);
      assert.deepEqual(element._parseQueryString('?'), []);
      assert.deepEqual(element._parseQueryString('??'), []);
      assert.deepEqual(element._parseQueryString('&&&'), []);
    });

    test('url decoding', () => {
      assert.deepEqual(element._parseQueryString('+'), [[' ', '']]);
      assert.deepEqual(element._parseQueryString('???+%3d+'), [[' = ', '']]);
      assert.deepEqual(
          element._parseQueryString('%6e%61%6d%65=%76%61%6c%75%65'),
          [['name', 'value']]);
    });

    test('multiple parameters', () => {
      assert.deepEqual(
          element._parseQueryString('a=b&c=d&e=f'),
          [['a', 'b'], ['c', 'd'], ['e', 'f']]);
      assert.deepEqual(
          element._parseQueryString('&a=b&&&e=f&c'),
          [['a', 'b'], ['e', 'f'], ['c', '']]);
    });
  });
});

