/**
 * @license
 * Copyright 2017 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import '../../../test/common-test-setup';
import './gr-router';
import {page, PageContext} from '../../../utils/page-wrapper-utils';
import {
  stubBaseUrl,
  stubRestApi,
  addListenerForTest,
  waitEventLoop,
} from '../../../test/test-utils';
import {GrRouter, routerToken, _testOnly_RoutePattern} from './gr-router';
import {GerritView} from '../../../services/router/router-model';
import {
  BasePatchSetNum,
  GroupId,
  NumericChangeId,
  PARENT,
  RepoName,
  RevisionPatchSetNum,
  UrlEncodedCommentId,
} from '../../../types/common';
import {AppElementParams} from '../../gr-app-types';
import {assert} from '@open-wc/testing';
import {AdminChildView} from '../../../models/views/admin';
import {RepoDetailView} from '../../../models/views/repo';
import {GroupDetailView} from '../../../models/views/group';
import {EditViewState} from '../../../models/views/edit';
import {ChangeViewState} from '../../../models/views/change';
import {PatchRangeParams} from '../../../utils/url-util';
import {DependencyRequestEvent} from '../../../models/dependency';

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

  setup(() => {
    document.dispatchEvent(
      new DependencyRequestEvent(routerToken, x => (router = x))
    );
  });

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

    url = '';
    hash = router.getHashFromCanonicalPath(url);
    assert.equal(hash, '');

    url = '/foo#bar';
    hash = router.getHashFromCanonicalPath(url);
    assert.equal(hash, 'bar');

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

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

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

      actual = router.parseLineAddress('foobar');
      assert.isNull(actual);

      actual = router.parseLineAddress('foo123');
      assert.isNull(actual);

      actual = router.parseLineAddress('123bar');
      assert.isNull(actual);
    });

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

      actual = router.parseLineAddress('a4');
      assert.isOk(actual);
      assert.equal(actual!.lineNum, 4);
      assert.isTrue(actual!.leftSide);

      actual = router.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: any = {};
    const doesNotRequireAuth: any = {};
    sinon.stub(page, 'start');
    sinon.stub(page, 'base');
    sinon
      .stub(router, 'mapRoute')
      .callsFake((_pattern, methodName, _method, usesAuth) => {
        if (usesAuth) {
          requiresAuth[methodName] = true;
        } else {
          doesNotRequireAuth[methodName] = true;
        }
      });
    router.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',
      'handleCommentsRoute',
      'handleDiffRoute',
      'handleDefaultRoute',
      'handleChangeLegacyRoute',
      'handleDocumentationRedirectRoute',
      'handleDocumentationSearchRoute',
      'handleDocumentationSearchRedirectRoute',
      'handleLegacyLinenum',
      'handleImproperlyEncodedPlusRoute',
      'handlePassThroughRoute',
      'handleProjectDashboardRoute',
      'handleLegacyProjectDashboardRoute',
      'handleProjectsOldRoute',
      'handleRepoAccessRoute',
      'handleRepoDashboardsRoute',
      'handleRepoGeneralRoute',
      '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', () => {
    stubRestApi('getLoggedIn').returns(Promise.resolve(true));
    const ctx = {
      save() {},
      handled: true,
      canonicalPath: '',
      path: '',
      querystring: '',
      pathname: '',
      state: '',
      title: '',
      hash: '',
      params: {test: 'test'},
    };
    const redirectStub = sinon.stub(router, 'redirectToLogin');
    return router.redirectIfNotLoggedIn(ctx).then(() => {
      assert.isFalse(redirectStub.called);
    });
  });

  test('redirectIfNotLoggedIn while logged out', () => {
    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
    const redirectStub = sinon.stub(router, 'redirectToLogin');
    const ctx = {
      save() {},
      handled: true,
      canonicalPath: '',
      path: '',
      querystring: '',
      pathname: '',
      state: '',
      title: '',
      hash: '',
      params: {test: 'test'},
    };
    return new Promise(resolve => {
      router
        .redirectIfNotLoggedIn(ctx)
        .then(() => {
          assert.isTrue(false, 'Should never execute');
        })
        .catch(() => {
          assert.isTrue(redirectStub.calledOnce);
          resolve(Promise.resolve());
        });
    });
  });

  suite('param normalization', () => {
    suite('normalizePatchRangeParams', () => {
      test('range n..n normalizes to n', () => {
        const params: PatchRangeParams = {
          basePatchNum: 4 as BasePatchSetNum,
          patchNum: 4 as RevisionPatchSetNum,
        };
        router.normalizePatchRangeParams(params);
        assert.equal(params.basePatchNum, PARENT);
        assert.equal(params.patchNum, 4 as RevisionPatchSetNum);
      });

      test('range n.. normalizes to n', () => {
        const params: PatchRangeParams = {basePatchNum: 4 as BasePatchSetNum};
        router.normalizePatchRangeParams(params);
        assert.equal(params.basePatchNum, PARENT);
        assert.equal(params.patchNum, 4 as RevisionPatchSetNum);
      });
    });
  });

  suite('route handlers', () => {
    let redirectStub: sinon.SinonStub;
    let setStateStub: sinon.SinonStub;
    let handlePassThroughRoute: sinon.SinonStub;

    // Simple route handlers are direct mappings from parsed route ctx to a
    // new set of app.params. This test helper asserts that passing `ctx`
    // into `methodName` results in setting the params specified in `params`.
    function assertctxToParams(
      ctx: PageContext,
      methodName: string,
      params: AppElementParams
    ) {
      (router as any)[methodName](ctx);
      assert.deepEqual(setStateStub.lastCall.args[0], params);
    }

    function createPageContext(): PageContext {
      return {
        canonicalPath: '',
        path: '',
        querystring: '',
        pathname: '',
        hash: '',
        params: {},
      };
    }

    setup(() => {
      redirectStub = sinon.stub(router, 'redirect');
      setStateStub = sinon.stub(router, 'setState');
      handlePassThroughRoute = sinon.stub(router, 'handlePassThroughRoute');
    });

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

    test('handleAgreementsRoute', () => {
      router.handleAgreementsRoute();
      assert.isTrue(redirectStub.calledOnce);
      assert.equal(redirectStub.lastCall.args[0], '/settings/#Agreements');
    });

    test('handleNewAgreementsRoute', () => {
      router.handleNewAgreementsRoute();
      assert.isTrue(setStateStub.calledOnce);
      assert.equal(setStateStub.lastCall.args[0].view, GerritView.AGREEMENTS);
    });

    test('handleSettingsLegacyRoute', () => {
      const ctx = {...createPageContext(), params: {0: 'my-token'}};
      assertctxToParams(ctx, 'handleSettingsLegacyRoute', {
        view: GerritView.SETTINGS,
        emailToken: 'my-token',
      });
    });

    test('handleSettingsLegacyRoute with +', () => {
      const ctx = {...createPageContext(), params: {0: 'my-token test'}};
      assertctxToParams(ctx, 'handleSettingsLegacyRoute', {
        view: GerritView.SETTINGS,
        emailToken: 'my-token+test',
      });
    });

    test('handleSettingsRoute', () => {
      const ctx = createPageContext();
      assertctxToParams(ctx, 'handleSettingsRoute', {
        view: GerritView.SETTINGS,
      });
    });

    test('handleDefaultRoute on first load', () => {
      const spy = sinon.spy();
      addListenerForTest(document, 'page-error', spy);
      router.handleDefaultRoute();
      assert.isTrue(spy.calledOnce);
      assert.equal(spy.lastCall.args[0].detail.response.status, 404);
    });

    test('handleDefaultRoute after internal navigation', () => {
      let onExit: Function | null = null;
      const onRegisteringExit = (
        _match: string | RegExp,
        _onExit: Function
      ) => {
        onExit = _onExit;
      };
      sinon.stub(page, 'exit').callsFake(onRegisteringExit);
      sinon.stub(page, 'start');
      sinon.stub(page, 'base');
      router.startRouter();

      router.handleDefaultRoute();

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

      router.handleDefaultRoute();
      assert.isTrue(handlePassThroughRoute.calledOnce);
    });

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

      sinon.stub(router, 'getHashFromCanonicalPath').returns('foo');
      router.handleImproperlyEncodedPlusRoute(params);
      assert.equal(redirectStub.lastCall.args[0], '/c/test/+/42#foo');
    });

    test('handleQueryRoute', () => {
      const ctx: PageContext = {
        ...createPageContext(),
        params: {0: 'project:foo/bar/baz'},
      };
      assertctxToParams(ctx, 'handleQueryRoute', {
        view: GerritView.SEARCH,
        query: 'project:foo/bar/baz',
        offset: undefined,
      });

      ctx.params[1] = '123';
      ctx.params[2] = '123';
      assertctxToParams(ctx, 'handleQueryRoute', {
        view: GerritView.SEARCH,
        query: 'project:foo/bar/baz',
        offset: '123',
      });
    });

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

    test('handleChangeIdQueryRoute', () => {
      const ctx = {
        ...createPageContext(),
        params: {0: 'I0123456789abcdef0123456789abcdef01234567'},
      };
      assertctxToParams(ctx, 'handleChangeIdQueryRoute', {
        view: GerritView.SEARCH,
        query: 'I0123456789abcdef0123456789abcdef01234567',
      });
    });

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

      test('no param', () => {
        const ctx = createPageContext();
        router.handleRegisterRoute(ctx);
        assert.isTrue(redirectStub.calledWithExactly('/'));
        assert.isTrue(setStateStub.calledOnce);
        assert.isTrue(setStateStub.lastCall.args[0].justRegistered);
      });

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

    suite('handleRootRoute', () => {
      test('closes for closeAfterLogin', () => {
        const ctx = {...createPageContext(), querystring: 'closeAfterLogin'};
        const closeStub = sinon.stub(window, 'close');
        const result = router.handleRootRoute(ctx);
        assert.isNotOk(result);
        assert.isTrue(closeStub.called);
        assert.isFalse(redirectStub.called);
      });

      test('redirects to dashboard if logged in', () => {
        const ctx = {...createPageContext(), canonicalPath: '/', path: '/'};
        const result = router.handleRootRoute(ctx);
        assert.isOk(result);
        return result!.then(() => {
          assert.isTrue(redirectStub.calledWithExactly('/dashboard/self'));
        });
      });

      test('redirects to open changes if not logged in', () => {
        stubRestApi('getLoggedIn').returns(Promise.resolve(false));
        const ctx = {...createPageContext(), canonicalPath: '/', path: '/'};
        const result = router.handleRootRoute(ctx);
        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 ctx = {
            ...createPageContext(),
            canonicalPath: '/#/foo/bar/baz',
            hash: '/foo/bar/baz',
          };
          const result = router.handleRootRoute(ctx);
          assert.isNotOk(result);
          assert.isTrue(redirectStub.called);
          assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz'));
        });

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

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

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

        test('normalizes /VE/ settings hash-paths', () => {
          const ctx = {
            ...createPageContext(),
            canonicalPath: '/#/VE/foo/bar',
            hash: '/VE/foo/bar',
          };
          const result = router.handleRootRoute(ctx);
          assert.isNotOk(result);
          assert.isTrue(redirectStub.called);
          assert.isTrue(redirectStub.calledWithExactly('/settings/VE/foo/bar'));
        });

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

    suite('handleDashboardRoute', () => {
      let redirectToLoginStub: sinon.SinonStub;

      setup(() => {
        redirectToLoginStub = sinon.stub(router, 'redirectToLogin');
      });

      test('own dashboard but signed out redirects to login', () => {
        stubRestApi('getLoggedIn').returns(Promise.resolve(false));
        const ctx = {
          ...createPageContext(),
          canonicalPath: '/dashboard/',
          params: {0: 'seLF'},
        };
        return router.handleDashboardRoute(ctx).then(() => {
          assert.isTrue(redirectToLoginStub.calledOnce);
          assert.isFalse(redirectStub.called);
          assert.isFalse(setStateStub.called);
        });
      });

      test('non-self dashboard but signed out does not redirect', () => {
        stubRestApi('getLoggedIn').returns(Promise.resolve(false));
        const ctx = {
          ...createPageContext(),
          canonicalPath: '/dashboard/',
          params: {0: 'foo'},
        };
        return router.handleDashboardRoute(ctx).then(() => {
          assert.isFalse(redirectToLoginStub.called);
          assert.isFalse(setStateStub.called);
          assert.isTrue(redirectStub.calledOnce);
          assert.equal(redirectStub.lastCall.args[0], '/q/owner:foo');
        });
      });

      test('dashboard while signed in sets params', () => {
        const ctx = {
          ...createPageContext(),
          canonicalPath: '/dashboard/',
          params: {0: 'foo'},
        };
        return router.handleDashboardRoute(ctx).then(() => {
          assert.isFalse(redirectToLoginStub.called);
          assert.isFalse(redirectStub.called);
          assert.isTrue(setStateStub.calledOnce);
          assert.deepEqual(setStateStub.lastCall.args[0], {
            view: GerritView.DASHBOARD,
            user: 'foo',
          });
        });
      });
    });

    suite('handleCustomDashboardRoute', () => {
      let redirectToLoginStub: sinon.SinonStub;

      setup(() => {
        redirectToLoginStub = sinon.stub(router, 'redirectToLogin');
      });

      test('no user specified', () => {
        const ctx: PageContext = {
          ...createPageContext(),
          canonicalPath: '/dashboard/',
          params: {0: ''},
          querystring: '',
        };
        return router.handleCustomDashboardRoute(ctx).then(() => {
          assert.isFalse(setStateStub.called);
          assert.isTrue(redirectStub.called);
          assert.equal(redirectStub.lastCall.args[0], '/dashboard/self');
        });
      });

      test('custom dashboard without title', () => {
        const ctx: PageContext = {
          ...createPageContext(),
          canonicalPath: '/dashboard/',
          params: {0: ''},
          querystring: '?a=b&c&d=e',
        };
        return router.handleCustomDashboardRoute(ctx).then(() => {
          assert.isFalse(redirectStub.called);
          assert.isTrue(setStateStub.calledOnce);
          assert.deepEqual(setStateStub.lastCall.args[0], {
            view: GerritView.DASHBOARD,
            user: 'self',
            sections: [
              {name: 'a', query: 'b'},
              {name: 'd', query: 'e'},
            ],
            title: 'Custom Dashboard',
          });
        });
      });

      test('custom dashboard with title', () => {
        const ctx: PageContext = {
          ...createPageContext(),
          canonicalPath: '/dashboard/',
          params: {0: ''},
          querystring: '?a=b&c&d=&=e&title=t',
        };
        return router.handleCustomDashboardRoute(ctx).then(() => {
          assert.isFalse(redirectToLoginStub.called);
          assert.isFalse(redirectStub.called);
          assert.isTrue(setStateStub.calledOnce);
          assert.deepEqual(setStateStub.lastCall.args[0], {
            view: GerritView.DASHBOARD,
            user: 'self',
            sections: [{name: 'a', query: 'b'}],
            title: 't',
          });
        });
      });

      test('custom dashboard with foreach', () => {
        const ctx: PageContext = {
          ...createPageContext(),
          canonicalPath: '/dashboard/',
          params: {0: ''},
          querystring: '?a=b&c&d=&=e&foreach=is:open',
        };
        return router.handleCustomDashboardRoute(ctx).then(() => {
          assert.isFalse(redirectToLoginStub.called);
          assert.isFalse(redirectStub.called);
          assert.isTrue(setStateStub.calledOnce);
          assert.deepEqual(setStateStub.lastCall.args[0], {
            view: GerritView.DASHBOARD,
            user: 'self',
            sections: [{name: 'a', query: 'is:open b'}],
            title: 'Custom Dashboard',
          });
        });
      });
    });

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

      test('handleGroupAuditLogRoute', () => {
        const ctx = {...createPageContext(), params: {0: '1234'}};
        assertctxToParams(ctx, 'handleGroupAuditLogRoute', {
          view: GerritView.GROUP,
          detail: GroupDetailView.LOG,
          groupId: '1234' as GroupId,
        });
      });

      test('handleGroupMembersRoute', () => {
        const ctx = {...createPageContext(), params: {0: '1234'}};
        assertctxToParams(ctx, 'handleGroupMembersRoute', {
          view: GerritView.GROUP,
          detail: GroupDetailView.MEMBERS,
          groupId: '1234' as GroupId,
        });
      });

      test('handleGroupListOffsetRoute', () => {
        const ctx = createPageContext();
        assertctxToParams(ctx, 'handleGroupListOffsetRoute', {
          view: GerritView.ADMIN,
          adminView: AdminChildView.GROUPS,
          offset: 0,
          filter: null,
          openCreateModal: false,
        });

        ctx.params[1] = '42';
        assertctxToParams(ctx, 'handleGroupListOffsetRoute', {
          view: GerritView.ADMIN,
          adminView: AdminChildView.GROUPS,
          offset: '42',
          filter: null,
          openCreateModal: false,
        });

        ctx.hash = 'create';
        assertctxToParams(ctx, 'handleGroupListOffsetRoute', {
          view: GerritView.ADMIN,
          adminView: AdminChildView.GROUPS,
          offset: '42',
          filter: null,
          openCreateModal: true,
        });
      });

      test('handleGroupListFilterOffsetRoute', () => {
        const ctx = {
          ...createPageContext(),
          params: {filter: 'foo', offset: '42'},
        };
        assertctxToParams(ctx, 'handleGroupListFilterOffsetRoute', {
          view: GerritView.ADMIN,
          adminView: AdminChildView.GROUPS,
          offset: '42',
          filter: 'foo',
        });
      });

      test('handleGroupListFilterRoute', () => {
        const ctx = {...createPageContext(), params: {filter: 'foo'}};
        assertctxToParams(ctx, 'handleGroupListFilterRoute', {
          view: GerritView.ADMIN,
          adminView: AdminChildView.GROUPS,
          filter: 'foo',
        });
      });

      test('handleGroupRoute', () => {
        const ctx = {...createPageContext(), params: {0: '4321'}};
        assertctxToParams(ctx, 'handleGroupRoute', {
          view: GerritView.GROUP,
          groupId: '4321' as GroupId,
        });
      });
    });

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

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

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

      test('handleRepoRoute', () => {
        const ctx = {...createPageContext(), path: '/admin/repos/test'};
        router.handleRepoRoute(ctx);
        assert.isTrue(redirectStub.calledOnce);
        assert.equal(
          redirectStub.lastCall.args[0],
          '/admin/repos/test,general'
        );
      });

      test('handleRepoGeneralRoute', () => {
        const ctx = {...createPageContext(), params: {0: '4321'}};
        assertctxToParams(ctx, 'handleRepoGeneralRoute', {
          view: GerritView.REPO,
          detail: RepoDetailView.GENERAL,
          repo: '4321' as RepoName,
        });
      });

      test('handleRepoCommandsRoute', () => {
        const ctx = {...createPageContext(), params: {0: '4321'}};
        assertctxToParams(ctx, 'handleRepoCommandsRoute', {
          view: GerritView.REPO,
          detail: RepoDetailView.COMMANDS,
          repo: '4321' as RepoName,
        });
      });

      test('handleRepoAccessRoute', () => {
        const ctx = {...createPageContext(), params: {0: '4321'}};
        assertctxToParams(ctx, 'handleRepoAccessRoute', {
          view: GerritView.REPO,
          detail: RepoDetailView.ACCESS,
          repo: '4321' as RepoName,
        });
      });

      suite('branch list routes', () => {
        test('handleBranchListOffsetRoute', () => {
          const ctx: PageContext = {
            ...createPageContext(),
            params: {0: '4321'},
          };
          assertctxToParams(ctx, 'handleBranchListOffsetRoute', {
            view: GerritView.REPO,
            detail: RepoDetailView.BRANCHES,
            repo: '4321' as RepoName,
            offset: 0,
            filter: null,
          });

          ctx.params[2] = '42';
          assertctxToParams(ctx, 'handleBranchListOffsetRoute', {
            view: GerritView.REPO,
            detail: RepoDetailView.BRANCHES,
            repo: '4321' as RepoName,
            offset: '42',
            filter: null,
          });
        });

        test('handleBranchListFilterOffsetRoute', () => {
          const ctx = {
            ...createPageContext(),
            params: {repo: '4321', filter: 'foo', offset: '42'},
          };
          assertctxToParams(ctx, 'handleBranchListFilterOffsetRoute', {
            view: GerritView.REPO,
            detail: RepoDetailView.BRANCHES,
            repo: '4321' as RepoName,
            offset: '42',
            filter: 'foo',
          });
        });

        test('handleBranchListFilterRoute', () => {
          const ctx = {
            ...createPageContext(),
            params: {repo: '4321', filter: 'foo'},
          };
          assertctxToParams(ctx, 'handleBranchListFilterRoute', {
            view: GerritView.REPO,
            detail: RepoDetailView.BRANCHES,
            repo: '4321' as RepoName,
            filter: 'foo',
          });
        });
      });

      suite('tag list routes', () => {
        test('handleTagListOffsetRoute', () => {
          const ctx = {...createPageContext(), params: {0: '4321'}};
          assertctxToParams(ctx, 'handleTagListOffsetRoute', {
            view: GerritView.REPO,
            detail: RepoDetailView.TAGS,
            repo: '4321' as RepoName,
            offset: 0,
            filter: null,
          });
        });

        test('handleTagListFilterOffsetRoute', () => {
          const ctx = {
            ...createPageContext(),
            params: {repo: '4321', filter: 'foo', offset: '42'},
          };
          assertctxToParams(ctx, 'handleTagListFilterOffsetRoute', {
            view: GerritView.REPO,
            detail: RepoDetailView.TAGS,
            repo: '4321' as RepoName,
            offset: '42',
            filter: 'foo',
          });
        });

        test('handleTagListFilterRoute', () => {
          const ctx: PageContext = {
            ...createPageContext(),
            params: {repo: '4321'},
          };
          assertctxToParams(ctx, 'handleTagListFilterRoute', {
            view: GerritView.REPO,
            detail: RepoDetailView.TAGS,
            repo: '4321' as RepoName,
            filter: null,
          });

          ctx.params.filter = 'foo';
          assertctxToParams(ctx, 'handleTagListFilterRoute', {
            view: GerritView.REPO,
            detail: RepoDetailView.TAGS,
            repo: '4321' as RepoName,
            filter: 'foo',
          });
        });
      });

      suite('repo list routes', () => {
        test('handleRepoListOffsetRoute', () => {
          const ctx = createPageContext();
          assertctxToParams(ctx, 'handleRepoListOffsetRoute', {
            view: GerritView.ADMIN,
            adminView: AdminChildView.REPOS,
            offset: 0,
            filter: null,
            openCreateModal: false,
          });

          ctx.params[1] = '42';
          assertctxToParams(ctx, 'handleRepoListOffsetRoute', {
            view: GerritView.ADMIN,
            adminView: AdminChildView.REPOS,
            offset: '42',
            filter: null,
            openCreateModal: false,
          });

          ctx.hash = 'create';
          assertctxToParams(ctx, 'handleRepoListOffsetRoute', {
            view: GerritView.ADMIN,
            adminView: AdminChildView.REPOS,
            offset: '42',
            filter: null,
            openCreateModal: true,
          });
        });

        test('handleRepoListFilterOffsetRoute', () => {
          const ctx = {
            ...createPageContext(),
            params: {filter: 'foo', offset: '42'},
          };
          assertctxToParams(ctx, 'handleRepoListFilterOffsetRoute', {
            view: GerritView.ADMIN,
            adminView: AdminChildView.REPOS,
            offset: '42',
            filter: 'foo',
          });
        });

        test('handleRepoListFilterRoute', () => {
          const ctx = createPageContext();
          assertctxToParams(ctx, 'handleRepoListFilterRoute', {
            view: GerritView.ADMIN,
            adminView: AdminChildView.REPOS,
            filter: null,
          });

          ctx.params.filter = 'foo';
          assertctxToParams(ctx, 'handleRepoListFilterRoute', {
            view: GerritView.ADMIN,
            adminView: AdminChildView.REPOS,
            filter: 'foo',
          });
        });
      });
    });

    suite('plugin routes', () => {
      test('handlePluginListOffsetRoute', () => {
        const ctx = createPageContext();
        assertctxToParams(ctx, 'handlePluginListOffsetRoute', {
          view: GerritView.ADMIN,
          adminView: AdminChildView.PLUGINS,
          offset: 0,
          filter: null,
        });

        ctx.params[1] = '42';
        assertctxToParams(ctx, 'handlePluginListOffsetRoute', {
          view: GerritView.ADMIN,
          adminView: AdminChildView.PLUGINS,
          offset: '42',
          filter: null,
        });
      });

      test('handlePluginListFilterOffsetRoute', () => {
        const ctx = {
          ...createPageContext(),
          params: {filter: 'foo', offset: '42'},
        };
        assertctxToParams(ctx, 'handlePluginListFilterOffsetRoute', {
          view: GerritView.ADMIN,
          adminView: AdminChildView.PLUGINS,
          offset: '42',
          filter: 'foo',
        });
      });

      test('handlePluginListFilterRoute', () => {
        const ctx = createPageContext();
        assertctxToParams(ctx, 'handlePluginListFilterRoute', {
          view: GerritView.ADMIN,
          adminView: AdminChildView.PLUGINS,
          filter: null,
        });

        ctx.params.filter = 'foo';
        assertctxToParams(ctx, 'handlePluginListFilterRoute', {
          view: GerritView.ADMIN,
          adminView: AdminChildView.PLUGINS,
          filter: 'foo',
        });
      });

      test('handlePluginListRoute', () => {
        const ctx = createPageContext();
        assertctxToParams(ctx, 'handlePluginListRoute', {
          view: GerritView.ADMIN,
          adminView: AdminChildView.PLUGINS,
        });
      });
    });

    suite('change/diff routes', () => {
      test('handleChangeNumberLegacyRoute', () => {
        const ctx = {...createPageContext(), params: {0: '12345'}};
        router.handleChangeNumberLegacyRoute(ctx);
        assert.isTrue(redirectStub.calledOnce);
        assert.isTrue(redirectStub.calledWithExactly('/c/12345'));
      });

      test('handleChangeLegacyRoute', async () => {
        stubRestApi('getFromProjectLookup').returns(
          Promise.resolve('project' as RepoName)
        );
        const ctx = {
          ...createPageContext(),
          params: {0: '1234', 1: 'comment/6789'},
        };
        router.handleChangeLegacyRoute(ctx);
        await waitEventLoop();
        assert.isTrue(
          redirectStub.calledWithExactly('/c/project/+/1234' + '/comment/6789')
        );
      });

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

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

      suite('handleChangeRoute', () => {
        function makeParams(_path: string, _hash: string): PageContext {
          return {
            ...createPageContext(),
            params: {
              0: 'foo/bar', // 0 Project
              1: '1234', // 1 Change number
              2: '', // 2 Unused
              3: '', // 3 Unused
              4: '4', // 4 Base patch number
              5: '', // 5 Unused
              6: '7', // 6 Patch number
            },
          };
        }

        setup(() => {
          stubRestApi('setInProjectLookup');
        });

        test('change view', () => {
          const ctx = makeParams('', '');
          assertctxToParams(ctx, 'handleChangeRoute', {
            view: GerritView.CHANGE,
            project: 'foo/bar' as RepoName,
            changeNum: 1234 as NumericChangeId,
            basePatchNum: 4 as BasePatchSetNum,
            patchNum: 7 as RevisionPatchSetNum,
          });
          assert.isFalse(redirectStub.called);
        });

        test('params', () => {
          const ctx = makeParams('', '');
          const queryMap = new URLSearchParams();
          queryMap.set('tab', 'checks');
          queryMap.set('filter', 'fff');
          queryMap.set('select', 'sss');
          queryMap.set('attempt', '1');
          queryMap.set('checksRunsSelected', 'asdf,qwer');
          queryMap.set('checksResultsFilter', 'asdf.*qwer');
          ctx.querystring = queryMap.toString();
          assertctxToParams(ctx, 'handleChangeRoute', {
            view: GerritView.CHANGE,
            project: 'foo/bar' as RepoName,
            changeNum: 1234 as NumericChangeId,
            basePatchNum: 4 as BasePatchSetNum,
            patchNum: 7 as RevisionPatchSetNum,
            attempt: 1,
            filter: 'fff',
            tab: 'checks',
            checksRunsSelected: new Set(['asdf', 'qwer']),
            checksResultsFilter: 'asdf.*qwer',
          });
        });
      });

      suite('handleDiffRoute', () => {
        function makeParams(path: string, hash: string): PageContext {
          return {
            ...createPageContext(),
            hash,
            params: {
              0: 'foo/bar', // 0 Project
              1: '1234', // 1 Change number
              2: '', // 2 Unused
              3: '', // 3 Unused
              4: '4', // 4 Base patch number
              5: '', // 5 Unused
              6: '7', // 6 Patch number
              7: '', // 7 Unused,
              8: path, // 8 Diff path
            },
          };
        }

        setup(() => {
          stubRestApi('setInProjectLookup');
        });

        test('diff view', () => {
          const ctx = makeParams('foo/bar/baz', 'b44');
          assertctxToParams(ctx, 'handleDiffRoute', {
            view: GerritView.DIFF,
            project: 'foo/bar' as RepoName,
            changeNum: 1234 as NumericChangeId,
            basePatchNum: 4 as BasePatchSetNum,
            patchNum: 7 as RevisionPatchSetNum,
            path: 'foo/bar/baz',
            leftSide: true,
            lineNum: 44,
          });
          assert.isFalse(redirectStub.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
          ]);
          assertctxToParams(
            {params: groups!.slice(1)} as any,
            'handleCommentRoute',
            {
              project: 'gerrit' as RepoName,
              changeNum: 264833 as NumericChangeId,
              commentId: '00049681_f34fd6a9' as UrlEncodedCommentId,
              commentLink: true,
              view: GerritView.DIFF,
            }
          );
        });

        test('comments route', () => {
          const url = '/c/gerrit/+/264833/comments/00049681_f34fd6a9/';
          const groups = url.match(_testOnly_RoutePattern.COMMENTS_TAB);
          assert.deepEqual(groups!.slice(1), [
            'gerrit', // project
            '264833', // changeNum
            '00049681_f34fd6a9', // commentId
          ]);
          assertctxToParams(
            {params: groups!.slice(1)} as any,
            'handleCommentsRoute',
            {
              project: 'gerrit' as RepoName,
              changeNum: 264833 as NumericChangeId,
              commentId: '00049681_f34fd6a9' as UrlEncodedCommentId,
              view: GerritView.CHANGE,
            }
          );
        });
      });

      test('handleDiffEditRoute', () => {
        stubRestApi('setInProjectLookup');
        const ctx = {
          ...createPageContext(),
          hash: '',
          params: {
            0: 'foo/bar', // 0 Project
            1: '1234', // 1 Change number
            2: '3', // 2 Patch num
            3: 'foo/bar/baz', // 3 File path
          },
        };
        const appParams: EditViewState = {
          project: 'foo/bar' as RepoName,
          changeNum: 1234 as NumericChangeId,
          view: GerritView.EDIT,
          path: 'foo/bar/baz',
          patchNum: 3 as RevisionPatchSetNum,
          lineNum: 0,
        };

        router.handleDiffEditRoute(ctx);
        assert.isFalse(redirectStub.called);
        assert.deepEqual(setStateStub.lastCall.args[0], appParams);
      });

      test('handleDiffEditRoute with lineNum', () => {
        stubRestApi('setInProjectLookup');
        const ctx = {
          ...createPageContext(),
          hash: '4',
          params: {
            0: 'foo/bar', // 0 Project
            1: '1234', // 1 Change number
            2: '3', // 2 Patch num
            3: 'foo/bar/baz', // 3 File path
          },
        };
        const appParams: EditViewState = {
          project: 'foo/bar' as RepoName,
          changeNum: 1234 as NumericChangeId,
          view: GerritView.EDIT,
          path: 'foo/bar/baz',
          patchNum: 3 as RevisionPatchSetNum,
          lineNum: 4,
        };

        router.handleDiffEditRoute(ctx);
        assert.isFalse(redirectStub.called);
        assert.deepEqual(setStateStub.lastCall.args[0], appParams);
      });

      test('handleChangeEditRoute', () => {
        stubRestApi('setInProjectLookup');
        const ctx = {
          ...createPageContext(),
          params: {
            0: 'foo/bar', // 0 Project
            1: '1234', // 1 Change number
            2: '',
            3: '3', // 3 Patch num
          },
        };
        const appParams: ChangeViewState = {
          project: 'foo/bar' as RepoName,
          changeNum: 1234 as NumericChangeId,
          view: GerritView.CHANGE,
          patchNum: 3 as RevisionPatchSetNum,
          edit: true,
        };

        router.handleChangeEditRoute(ctx);
        assert.isFalse(redirectStub.called);
        assert.deepEqual(setStateStub.lastCall.args[0], appParams);
      });
    });

    test('handlePluginScreen', () => {
      const ctx = {...createPageContext(), params: {0: 'foo', 1: 'bar'}};
      assertctxToParams(ctx, 'handlePluginScreen', {
        view: GerritView.PLUGIN_SCREEN,
        plugin: 'foo',
        screen: 'bar',
      });
      assert.isFalse(redirectStub.called);
    });
  });
});
