import Controller from '@ember/controller';
import { dasherize } from '@ember/string';
import { RSVP, Object as EmberObject, A as emberA } from 'ember-runtime';
import { run } from '@ember/runloop';
import { peekMeta } from 'ember-meta';
import { get, computed } from 'ember-metal';
import { Route } from 'ember-routing';

import { QueryParamTestCase, moduleFor, getTextOf } from 'internal-test-helpers';

moduleFor(
  'Query Params - main',
  class extends QueryParamTestCase {
    refreshModelWhileLoadingTest(loadingReturn) {
      let assert = this.assert;

      assert.expect(9);

      let appModelCount = 0;
      let promiseResolve;

      this.add(
        'route:application',
        Route.extend({
          queryParams: {
            appomg: {
              defaultValue: 'applol',
            },
          },
          model(/* params */) {
            appModelCount++;
          },
        })
      );

      this.setSingleQPController('index', 'omg', undefined, {
        omg: undefined,
      });

      let actionName = typeof loadingReturn !== 'undefined' ? 'loading' : 'ignore';
      let indexModelCount = 0;
      this.add(
        'route:index',
        Route.extend({
          queryParams: {
            omg: {
              refreshModel: true,
            },
          },
          actions: {
            [actionName]: function() {
              return loadingReturn;
            },
          },
          model(params) {
            indexModelCount++;
            if (indexModelCount === 2) {
              assert.deepEqual(params, { omg: 'lex' });
              return new RSVP.Promise(function(resolve) {
                promiseResolve = resolve;
                return;
              });
            } else if (indexModelCount === 3) {
              assert.deepEqual(
                params,
                { omg: 'hello' },
                "Model hook reruns even if the previous one didn't finish"
              );
            }
          },
        })
      );

      return this.visit('/').then(() => {
        assert.equal(appModelCount, 1, 'appModelCount is 1');
        assert.equal(indexModelCount, 1);

        let indexController = this.getController('index');
        this.setAndFlush(indexController, 'omg', 'lex');

        assert.equal(appModelCount, 1, 'appModelCount is 1');
        assert.equal(indexModelCount, 2);

        this.setAndFlush(indexController, 'omg', 'hello');
        assert.equal(appModelCount, 1, 'appModelCount is 1');
        assert.equal(indexModelCount, 3);

        run(function() {
          promiseResolve();
        });

        assert.equal(get(indexController, 'omg'), 'hello', 'At the end last value prevails');
      });
    }

    ["@test No replaceURL occurs on startup because default values don't show up in URL"](assert) {
      assert.expect(1);

      this.setSingleQPController('index');

      return this.visitAndAssert('/');
    }

    ['@test Calling transitionTo does not lose query params already on the activeTransition'](
      assert
    ) {
      assert.expect(2);

      this.router.map(function() {
        this.route('parent', function() {
          this.route('child');
          this.route('sibling');
        });
      });

      this.add(
        'route:parent.child',
        Route.extend({
          afterModel() {
            this.transitionTo('parent.sibling');
          },
        })
      );

      this.setSingleQPController('parent');

      return this.visit('/parent/child?foo=lol').then(() => {
        this.assertCurrentPath(
          '/parent/sibling?foo=lol',
          'redirected to the sibling route, instead of child route'
        );
        assert.equal(
          this.getController('parent').get('foo'),
          'lol',
          'controller has value from the active transition'
        );
      });
    }

    ['@test Single query params can be set on the controller and reflected in the url'](assert) {
      assert.expect(3);

      this.router.map(function() {
        this.route('home', { path: '/' });
      });

      this.setSingleQPController('home');

      return this.visitAndAssert('/').then(() => {
        let controller = this.getController('home');

        this.setAndFlush(controller, 'foo', '456');
        this.assertCurrentPath('/?foo=456');

        this.setAndFlush(controller, 'foo', '987');
        this.assertCurrentPath('/?foo=987');
      });
    }

    ['@test Query params can map to different url keys configured on the controller'](assert) {
      assert.expect(6);

      this.add(
        'controller:index',
        Controller.extend({
          queryParams: [{ foo: 'other_foo', bar: { as: 'other_bar' } }],
          foo: 'FOO',
          bar: 'BAR',
        })
      );

      return this.visitAndAssert('/').then(() => {
        let controller = this.getController('index');

        this.setAndFlush(controller, 'foo', 'LEX');
        this.assertCurrentPath('/?other_foo=LEX', "QP mapped correctly without 'as'");

        this.setAndFlush(controller, 'foo', 'WOO');
        this.assertCurrentPath('/?other_foo=WOO', "QP updated correctly without 'as'");

        this.transitionTo('/?other_foo=NAW');
        assert.equal(controller.get('foo'), 'NAW', 'QP managed correctly on URL transition');

        this.setAndFlush(controller, 'bar', 'NERK');
        this.assertCurrentPath('/?other_bar=NERK&other_foo=NAW', "QP mapped correctly with 'as'");

        this.setAndFlush(controller, 'bar', 'NUKE');
        this.assertCurrentPath('/?other_bar=NUKE&other_foo=NAW', "QP updated correctly with 'as'");
      });
    }

    ['@test Routes have a private overridable serializeQueryParamKey hook'](assert) {
      assert.expect(2);

      this.add(
        'route:index',
        Route.extend({
          serializeQueryParamKey: dasherize,
        })
      );

      this.setSingleQPController('index', 'funTimes', '');

      return this.visitAndAssert('/').then(() => {
        let controller = this.getController('index');

        this.setAndFlush(controller, 'funTimes', 'woot');
        this.assertCurrentPath('/?fun-times=woot');
      });
    }

    ['@test Can override inherited QP behavior by specifying queryParams as a computed property'](
      assert
    ) {
      assert.expect(3);

      this.setSingleQPController('index', 'a', 0, {
        queryParams: computed(function() {
          return ['c'];
        }),
        c: true,
      });

      return this.visitAndAssert('/').then(() => {
        let indexController = this.getController('index');

        this.setAndFlush(indexController, 'a', 1);
        this.assertCurrentPath('/', 'QP did not update due to being overriden');

        this.setAndFlush(indexController, 'c', false);
        this.assertCurrentPath('/?c=false', 'QP updated with overridden param');
      });
    }

    ['@test Can concatenate inherited QP behavior by specifying queryParams as an array'](assert) {
      assert.expect(3);

      this.setSingleQPController('index', 'a', 0, {
        queryParams: ['c'],
        c: true,
      });

      return this.visitAndAssert('/').then(() => {
        let indexController = this.getController('index');

        this.setAndFlush(indexController, 'a', 1);
        this.assertCurrentPath('/?a=1', 'Inherited QP did update');

        this.setAndFlush(indexController, 'c', false);
        this.assertCurrentPath('/?a=1&c=false', 'New QP did update');
      });
    }

    ['@test model hooks receives query params'](assert) {
      assert.expect(2);

      this.setSingleQPController('index');

      this.add(
        'route:index',
        Route.extend({
          model(params) {
            assert.deepEqual(params, { foo: 'bar' });
          },
        })
      );

      return this.visitAndAssert('/');
    }

    ['@test model hooks receives query params with dynamic segment params'](assert) {
      assert.expect(2);

      this.router.map(function() {
        this.route('index', { path: '/:id' });
      });

      this.setSingleQPController('index');

      this.add(
        'route:index',
        Route.extend({
          model(params) {
            assert.deepEqual(params, { foo: 'bar', id: 'baz' });
          },
        })
      );

      return this.visitAndAssert('/baz');
    }

    ['@test model hooks receives query params (overridden by incoming url value)'](assert) {
      assert.expect(2);

      this.router.map(function() {
        this.route('index', { path: '/:id' });
      });

      this.setSingleQPController('index');

      this.add(
        'route:index',
        Route.extend({
          model(params) {
            assert.deepEqual(params, { foo: 'baz', id: 'boo' });
          },
        })
      );

      return this.visitAndAssert('/boo?foo=baz');
    }

    ['@test error is thrown if dynamic segment and query param have same name'](assert) {
      assert.expect(1);

      this.router.map(function() {
        this.route('index', { path: '/:foo' });
      });

      this.setSingleQPController('index');

      expectAssertion(() => {
        this.visitAndAssert('/boo?foo=baz');
      }, `The route 'index' has both a dynamic segment and query param with name 'foo'. Please rename one to avoid collisions.`);
    }

    ['@test query params have been set by the time setupController is called'](assert) {
      assert.expect(2);

      this.setSingleQPController('application');

      this.add(
        'route:application',
        Route.extend({
          setupController(controller) {
            assert.equal(
              controller.get('foo'),
              'YEAH',
              "controller's foo QP property set before setupController called"
            );
          },
        })
      );

      return this.visitAndAssert('/?foo=YEAH');
    }

    ['@test mapped query params have been set by the time setupController is called'](assert) {
      assert.expect(2);

      this.setSingleQPController('application', { faz: 'foo' });

      this.add(
        'route:application',
        Route.extend({
          setupController(controller) {
            assert.equal(
              controller.get('faz'),
              'YEAH',
              "controller's foo QP property set before setupController called"
            );
          },
        })
      );

      return this.visitAndAssert('/?foo=YEAH');
    }

    ['@test Route#paramsFor fetches query params with default value'](assert) {
      assert.expect(2);

      this.router.map(function() {
        this.route('index', { path: '/:something' });
      });

      this.setSingleQPController('index');

      this.add(
        'route:index',
        Route.extend({
          model(/* params, transition */) {
            assert.deepEqual(
              this.paramsFor('index'),
              { something: 'baz', foo: 'bar' },
              'could retrieve params for index'
            );
          },
        })
      );

      return this.visitAndAssert('/baz');
    }

    ['@test Route#paramsFor fetches query params with non-default value'](assert) {
      assert.expect(2);

      this.router.map(function() {
        this.route('index', { path: '/:something' });
      });

      this.setSingleQPController('index');

      this.add(
        'route:index',
        Route.extend({
          model(/* params, transition */) {
            assert.deepEqual(
              this.paramsFor('index'),
              { something: 'baz', foo: 'boo' },
              'could retrieve params for index'
            );
          },
        })
      );

      return this.visitAndAssert('/baz?foo=boo');
    }

    ['@test Route#paramsFor fetches default falsy query params'](assert) {
      assert.expect(2);

      this.router.map(function() {
        this.route('index', { path: '/:something' });
      });

      this.setSingleQPController('index', 'foo', false);

      this.add(
        'route:index',
        Route.extend({
          model(/* params, transition */) {
            assert.deepEqual(
              this.paramsFor('index'),
              { something: 'baz', foo: false },
              'could retrieve params for index'
            );
          },
        })
      );

      return this.visitAndAssert('/baz');
    }

    ['@test Route#paramsFor fetches non-default falsy query params'](assert) {
      assert.expect(2);

      this.router.map(function() {
        this.route('index', { path: '/:something' });
      });

      this.setSingleQPController('index', 'foo', true);

      this.add(
        'route:index',
        Route.extend({
          model(/* params, transition */) {
            assert.deepEqual(
              this.paramsFor('index'),
              { something: 'baz', foo: false },
              'could retrieve params for index'
            );
          },
        })
      );

      return this.visitAndAssert('/baz?foo=false');
    }

    ['@test model hook can query prefix-less application params'](assert) {
      assert.expect(4);

      this.setSingleQPController('application', 'appomg', 'applol');
      this.setSingleQPController('index', 'omg', 'lol');

      this.add(
        'route:application',
        Route.extend({
          model(params) {
            assert.deepEqual(params, { appomg: 'applol' });
          },
        })
      );

      this.add(
        'route:index',
        Route.extend({
          model(params) {
            assert.deepEqual(params, { omg: 'lol' });
            assert.deepEqual(this.paramsFor('application'), {
              appomg: 'applol',
            });
          },
        })
      );

      return this.visitAndAssert('/');
    }

    ['@test model hook can query prefix-less application params (overridden by incoming url value)'](
      assert
    ) {
      assert.expect(4);

      this.setSingleQPController('application', 'appomg', 'applol');
      this.setSingleQPController('index', 'omg', 'lol');

      this.add(
        'route:application',
        Route.extend({
          model(params) {
            assert.deepEqual(params, { appomg: 'appyes' });
          },
        })
      );

      this.add(
        'route:index',
        Route.extend({
          model(params) {
            assert.deepEqual(params, { omg: 'yes' });
            assert.deepEqual(this.paramsFor('application'), {
              appomg: 'appyes',
            });
          },
        })
      );

      return this.visitAndAssert('/?appomg=appyes&omg=yes');
    }

    ['@test can opt into full transition by setting refreshModel in route queryParams'](assert) {
      assert.expect(7);

      this.setSingleQPController('application', 'appomg', 'applol');
      this.setSingleQPController('index', 'omg', 'lol');

      let appModelCount = 0;
      this.add(
        'route:application',
        Route.extend({
          model(/* params, transition */) {
            appModelCount++;
          },
        })
      );

      let indexModelCount = 0;
      this.add(
        'route:index',
        Route.extend({
          queryParams: {
            omg: {
              refreshModel: true,
            },
          },
          model(params) {
            indexModelCount++;

            if (indexModelCount === 1) {
              assert.deepEqual(params, { omg: 'lol' }, 'params are correct on first pass');
            } else if (indexModelCount === 2) {
              assert.deepEqual(params, { omg: 'lex' }, 'params are correct on second pass');
            }
          },
        })
      );

      return this.visitAndAssert('/').then(() => {
        assert.equal(appModelCount, 1, 'app model hook ran');
        assert.equal(indexModelCount, 1, 'index model hook ran');

        let indexController = this.getController('index');
        this.setAndFlush(indexController, 'omg', 'lex');

        assert.equal(appModelCount, 1, 'app model hook did not run again');
        assert.equal(indexModelCount, 2, 'index model hook ran again due to refreshModel');
      });
    }

    ['@test refreshModel and replace work together'](assert) {
      assert.expect(8);

      this.setSingleQPController('application', 'appomg', 'applol');
      this.setSingleQPController('index', 'omg', 'lol');

      let appModelCount = 0;
      this.add(
        'route:application',
        Route.extend({
          model(/* params */) {
            appModelCount++;
          },
        })
      );

      let indexModelCount = 0;
      this.add(
        'route:index',
        Route.extend({
          queryParams: {
            omg: {
              refreshModel: true,
              replace: true,
            },
          },
          model(params) {
            indexModelCount++;

            if (indexModelCount === 1) {
              assert.deepEqual(params, { omg: 'lol' }, 'params are correct on first pass');
            } else if (indexModelCount === 2) {
              assert.deepEqual(params, { omg: 'lex' }, 'params are correct on second pass');
            }
          },
        })
      );

      return this.visitAndAssert('/').then(() => {
        assert.equal(appModelCount, 1, 'app model hook ran');
        assert.equal(indexModelCount, 1, 'index model hook ran');

        let indexController = this.getController('index');
        this.expectedReplaceURL = '/?omg=lex';
        this.setAndFlush(indexController, 'omg', 'lex');

        assert.equal(appModelCount, 1, 'app model hook did not run again');
        assert.equal(indexModelCount, 2, 'index model hook ran again due to refreshModel');
      });
    }

    ['@test multiple QP value changes only cause a single model refresh'](assert) {
      assert.expect(2);

      this.setSingleQPController('index', 'alex', 'lol');
      this.setSingleQPController('index', 'steely', 'lel');

      let refreshCount = 0;
      this.add(
        'route:index',
        Route.extend({
          queryParams: {
            alex: {
              refreshModel: true,
            },
            steely: {
              refreshModel: true,
            },
          },
          refresh() {
            refreshCount++;
          },
        })
      );

      return this.visitAndAssert('/').then(() => {
        let indexController = this.getController('index');
        run(indexController, 'setProperties', {
          alex: 'fran',
          steely: 'david',
        });
        assert.equal(refreshCount, 1, 'index refresh hook only run once');
      });
    }

    ['@test refreshModel does not cause a second transition during app boot '](assert) {
      assert.expect(1);

      this.setSingleQPController('application', 'appomg', 'applol');
      this.setSingleQPController('index', 'omg', 'lol');

      this.add(
        'route:index',
        Route.extend({
          queryParams: {
            omg: {
              refreshModel: true,
            },
          },
          refresh() {
            assert.ok(false);
          },
        })
      );

      return this.visitAndAssert('/?appomg=hello&omg=world');
    }

    ['@test queryParams are updated when a controller property is set and the route is refreshed. Issue #13263  '](
      assert
    ) {
      this.addTemplate(
        'application',
        '<button id="test-button" {{action \'increment\'}}>Increment</button><span id="test-value">{{foo}}</span>{{outlet}}'
      );

      this.setSingleQPController('application', 'foo', 1, {
        actions: {
          increment() {
            this.incrementProperty('foo');
            this.send('refreshRoute');
          },
        },
      });

      this.add(
        'route:application',
        Route.extend({
          actions: {
            refreshRoute() {
              this.refresh();
            },
          },
        })
      );

      return this.visitAndAssert('/').then(() => {
        assert.equal(getTextOf(document.getElementById('test-value')), '1');

        run(document.getElementById('test-button'), 'click');
        assert.equal(getTextOf(document.getElementById('test-value')), '2');
        this.assertCurrentPath('/?foo=2');

        run(document.getElementById('test-button'), 'click');
        assert.equal(getTextOf(document.getElementById('test-value')), '3');
        this.assertCurrentPath('/?foo=3');
      });
    }

    ["@test Use Ember.get to retrieve query params 'refreshModel' configuration"](assert) {
      assert.expect(7);

      this.setSingleQPController('application', 'appomg', 'applol');
      this.setSingleQPController('index', 'omg', 'lol');

      let appModelCount = 0;
      this.add(
        'route:application',
        Route.extend({
          model(/* params */) {
            appModelCount++;
          },
        })
      );

      let indexModelCount = 0;
      this.add(
        'route:index',
        Route.extend({
          queryParams: EmberObject.create({
            unknownProperty() {
              return { refreshModel: true };
            },
          }),
          model(params) {
            indexModelCount++;

            if (indexModelCount === 1) {
              assert.deepEqual(params, { omg: 'lol' });
            } else if (indexModelCount === 2) {
              assert.deepEqual(params, { omg: 'lex' });
            }
          },
        })
      );

      return this.visitAndAssert('/').then(() => {
        assert.equal(appModelCount, 1);
        assert.equal(indexModelCount, 1);

        let indexController = this.getController('index');
        this.setAndFlush(indexController, 'omg', 'lex');

        assert.equal(appModelCount, 1);
        assert.equal(indexModelCount, 2);
      });
    }

    ['@test can use refreshModel even with URL changes that remove QPs from address bar'](assert) {
      assert.expect(4);

      this.setSingleQPController('index', 'omg', 'lol');

      let indexModelCount = 0;
      this.add(
        'route:index',
        Route.extend({
          queryParams: {
            omg: {
              refreshModel: true,
            },
          },
          model(params) {
            indexModelCount++;

            let data;
            if (indexModelCount === 1) {
              data = 'foo';
            } else if (indexModelCount === 2) {
              data = 'lol';
            }

            assert.deepEqual(params, { omg: data }, 'index#model receives right data');
          },
        })
      );

      return this.visitAndAssert('/?omg=foo').then(() => {
        this.transitionTo('/');

        let indexController = this.getController('index');
        assert.equal(indexController.get('omg'), 'lol');
      });
    }

    ['@test can opt into a replace query by specifying replace:true in the Route config hash'](
      assert
    ) {
      assert.expect(2);

      this.setSingleQPController('application', 'alex', 'matchneer');

      this.add(
        'route:application',
        Route.extend({
          queryParams: {
            alex: {
              replace: true,
            },
          },
        })
      );

      return this.visitAndAssert('/').then(() => {
        let appController = this.getController('application');
        this.expectedReplaceURL = '/?alex=wallace';
        this.setAndFlush(appController, 'alex', 'wallace');
      });
    }

    ['@test Route query params config can be configured using property name instead of URL key'](
      assert
    ) {
      assert.expect(2);

      this.add(
        'controller:application',
        Controller.extend({
          queryParams: [{ commitBy: 'commit_by' }],
        })
      );

      this.add(
        'route:application',
        Route.extend({
          queryParams: {
            commitBy: {
              replace: true,
            },
          },
        })
      );

      return this.visitAndAssert('/').then(() => {
        let appController = this.getController('application');
        this.expectedReplaceURL = '/?commit_by=igor_seb';
        this.setAndFlush(appController, 'commitBy', 'igor_seb');
      });
    }

    ['@test An explicit replace:false on a changed QP always wins and causes a pushState'](assert) {
      assert.expect(3);

      this.add(
        'controller:application',
        Controller.extend({
          queryParams: ['alex', 'steely'],
          alex: 'matchneer',
          steely: 'dan',
        })
      );

      this.add(
        'route:application',
        Route.extend({
          queryParams: {
            alex: {
              replace: true,
            },
            steely: {
              replace: false,
            },
          },
        })
      );

      return this.visit('/').then(() => {
        let appController = this.getController('application');
        this.expectedPushURL = '/?alex=wallace&steely=jan';
        run(appController, 'setProperties', { alex: 'wallace', steely: 'jan' });

        this.expectedPushURL = '/?alex=wallace&steely=fran';
        run(appController, 'setProperties', { steely: 'fran' });

        this.expectedReplaceURL = '/?alex=sriracha&steely=fran';
        run(appController, 'setProperties', { alex: 'sriracha' });
      });
    }

    ['@test can opt into full transition by setting refreshModel in route queryParams when transitioning from child to parent'](
      assert
    ) {
      this.addTemplate('parent', '{{outlet}}');
      this.addTemplate(
        'parent.child',
        "{{link-to 'Parent' 'parent' (query-params foo='change') id='parent-link'}}"
      );

      this.router.map(function() {
        this.route('parent', function() {
          this.route('child');
        });
      });

      let parentModelCount = 0;
      this.add(
        'route:parent',
        Route.extend({
          model() {
            parentModelCount++;
          },
          queryParams: {
            foo: {
              refreshModel: true,
            },
          },
        })
      );

      this.setSingleQPController('parent', 'foo', 'abc');

      return this.visit('/parent/child?foo=lol').then(() => {
        assert.equal(parentModelCount, 1);

        run(document.getElementById('parent-link'), 'click');
        assert.equal(parentModelCount, 2);
      });
    }

    ["@test Use Ember.get to retrieve query params 'replace' configuration"](assert) {
      assert.expect(2);

      this.setSingleQPController('application', 'alex', 'matchneer');

      this.add(
        'route:application',
        Route.extend({
          queryParams: EmberObject.create({
            unknownProperty(/* keyName */) {
              // We are simulating all qps requiring refresh
              return { replace: true };
            },
          }),
        })
      );

      return this.visitAndAssert('/').then(() => {
        let appController = this.getController('application');
        this.expectedReplaceURL = '/?alex=wallace';
        this.setAndFlush(appController, 'alex', 'wallace');
      });
    }

    ['@test can override incoming QP values in setupController'](assert) {
      assert.expect(3);

      this.router.map(function() {
        this.route('about');
      });

      this.setSingleQPController('index', 'omg', 'lol');

      this.add(
        'route:index',
        Route.extend({
          setupController(controller) {
            assert.ok(true, 'setupController called');
            controller.set('omg', 'OVERRIDE');
          },
          actions: {
            queryParamsDidChange() {
              assert.ok(false, "queryParamsDidChange shouldn't fire");
            },
          },
        })
      );

      return this.visitAndAssert('/about').then(() => {
        this.transitionTo('index');
        this.assertCurrentPath('/?omg=OVERRIDE');
      });
    }

    ['@test can override incoming QP array values in setupController'](assert) {
      assert.expect(3);

      this.router.map(function() {
        this.route('about');
      });

      this.setSingleQPController('index', 'omg', ['lol']);

      this.add(
        'route:index',
        Route.extend({
          setupController(controller) {
            assert.ok(true, 'setupController called');
            controller.set('omg', ['OVERRIDE']);
          },
          actions: {
            queryParamsDidChange() {
              assert.ok(false, "queryParamsDidChange shouldn't fire");
            },
          },
        })
      );

      return this.visitAndAssert('/about').then(() => {
        this.transitionTo('index');
        this.assertCurrentPath('/?omg=' + encodeURIComponent(JSON.stringify(['OVERRIDE'])));
      });
    }

    ['@test URL transitions that remove QPs still register as QP changes'](assert) {
      assert.expect(2);

      this.setSingleQPController('index', 'omg', 'lol');

      return this.visit('/?omg=borf').then(() => {
        let indexController = this.getController('index');
        assert.equal(indexController.get('omg'), 'borf');

        this.transitionTo('/');
        assert.equal(indexController.get('omg'), 'lol');
      });
    }

    ['@test Subresource naming style is supported'](assert) {
      assert.expect(5);

      this.router.map(function() {
        this.route('abc.def', { path: '/abcdef' }, function() {
          this.route('zoo');
        });
      });

      this.addTemplate(
        'application',
        "{{link-to 'A' 'abc.def' (query-params foo='123') id='one'}}{{link-to 'B' 'abc.def.zoo' (query-params foo='123' bar='456') id='two'}}{{outlet}}"
      );

      this.setSingleQPController('abc.def', 'foo', 'lol');
      this.setSingleQPController('abc.def.zoo', 'bar', 'haha');

      return this.visitAndAssert('/').then(() => {
        assert.equal(this.$('#one').attr('href'), '/abcdef?foo=123');
        assert.equal(this.$('#two').attr('href'), '/abcdef/zoo?bar=456&foo=123');

        run(this.$('#one'), 'click');
        this.assertCurrentPath('/abcdef?foo=123');

        run(this.$('#two'), 'click');
        this.assertCurrentPath('/abcdef/zoo?bar=456&foo=123');
      });
    }

    ['@test transitionTo supports query params']() {
      this.setSingleQPController('index', 'foo', 'lol');

      return this.visitAndAssert('/').then(() => {
        this.transitionTo({ queryParams: { foo: 'borf' } });
        this.assertCurrentPath('/?foo=borf', 'shorthand supported');

        this.transitionTo({ queryParams: { 'index:foo': 'blaf' } });
        this.assertCurrentPath('/?foo=blaf', 'longform supported');

        this.transitionTo({ queryParams: { 'index:foo': false } });
        this.assertCurrentPath('/?foo=false', 'longform supported (bool)');

        this.transitionTo({ queryParams: { foo: false } });
        this.assertCurrentPath('/?foo=false', 'shorhand supported (bool)');
      });
    }

    ['@test transitionTo supports query params (multiple)']() {
      this.add(
        'controller:index',
        Controller.extend({
          queryParams: ['foo', 'bar'],
          foo: 'lol',
          bar: 'wat',
        })
      );

      return this.visitAndAssert('/').then(() => {
        this.transitionTo({ queryParams: { foo: 'borf' } });
        this.assertCurrentPath('/?foo=borf', 'shorthand supported');

        this.transitionTo({ queryParams: { 'index:foo': 'blaf' } });
        this.assertCurrentPath('/?foo=blaf', 'longform supported');

        this.transitionTo({ queryParams: { 'index:foo': false } });
        this.assertCurrentPath('/?foo=false', 'longform supported (bool)');

        this.transitionTo({ queryParams: { foo: false } });
        this.assertCurrentPath('/?foo=false', 'shorhand supported (bool)');
      });
    }

    ["@test setting controller QP to empty string doesn't generate null in URL"](assert) {
      assert.expect(1);

      this.setSingleQPController('index', 'foo', '123');

      return this.visit('/').then(() => {
        let controller = this.getController('index');

        this.expectedPushURL = '/?foo=';
        this.setAndFlush(controller, 'foo', '');
      });
    }

    ["@test setting QP to empty string doesn't generate null in URL"](assert) {
      assert.expect(1);

      this.add(
        'route:index',
        Route.extend({
          queryParams: {
            foo: {
              defaultValue: '123',
            },
          },
        })
      );

      return this.visit('/').then(() => {
        let controller = this.getController('index');

        this.expectedPushURL = '/?foo=';
        this.setAndFlush(controller, 'foo', '');
      });
    }

    ['@test A default boolean value deserializes QPs as booleans rather than strings'](assert) {
      assert.expect(3);

      this.setSingleQPController('index', 'foo', false);

      this.add(
        'route:index',
        Route.extend({
          model(params) {
            assert.equal(params.foo, true, 'model hook received foo as boolean true');
          },
        })
      );

      return this.visit('/?foo=true').then(() => {
        let controller = this.getController('index');
        assert.equal(controller.get('foo'), true);

        this.transitionTo('/?foo=false');
        assert.equal(controller.get('foo'), false);
      });
    }

    ['@test Query param without value are empty string'](assert) {
      assert.expect(1);

      this.add(
        'controller:index',
        Controller.extend({
          queryParams: ['foo'],
          foo: '',
        })
      );

      return this.visit('/?foo=').then(() => {
        let controller = this.getController('index');
        assert.equal(controller.get('foo'), '');
      });
    }

    ['@test Array query params can be set'](assert) {
      assert.expect(2);

      this.router.map(function() {
        this.route('home', { path: '/' });
      });

      this.setSingleQPController('home', 'foo', []);

      return this.visit('/').then(() => {
        let controller = this.getController('home');

        this.setAndFlush(controller, 'foo', [1, 2]);
        this.assertCurrentPath('/?foo=%5B1%2C2%5D');

        this.setAndFlush(controller, 'foo', [3, 4]);
        this.assertCurrentPath('/?foo=%5B3%2C4%5D');
      });
    }

    ['@test (de)serialization: arrays'](assert) {
      assert.expect(4);

      this.setSingleQPController('index', 'foo', [1]);

      return this.visitAndAssert('/').then(() => {
        this.transitionTo({ queryParams: { foo: [2, 3] } });
        this.assertCurrentPath('/?foo=%5B2%2C3%5D', 'shorthand supported');
        this.transitionTo({ queryParams: { 'index:foo': [4, 5] } });
        this.assertCurrentPath('/?foo=%5B4%2C5%5D', 'longform supported');
        this.transitionTo({ queryParams: { foo: [] } });
        this.assertCurrentPath('/?foo=%5B%5D', 'longform supported');
      });
    }

    ['@test Url with array query param sets controller property to array'](assert) {
      assert.expect(1);

      this.setSingleQPController('index', 'foo', '');

      return this.visit('/?foo[]=1&foo[]=2&foo[]=3').then(() => {
        let controller = this.getController('index');
        assert.deepEqual(controller.get('foo'), ['1', '2', '3']);
      });
    }

    ['@test Array query params can be pushed/popped'](assert) {
      assert.expect(17);

      this.router.map(function() {
        this.route('home', { path: '/' });
      });

      this.setSingleQPController('home', 'foo', emberA());

      return this.visitAndAssert('/').then(() => {
        let controller = this.getController('home');

        run(controller.foo, 'pushObject', 1);
        this.assertCurrentPath('/?foo=%5B1%5D');
        assert.deepEqual(controller.foo, [1]);

        run(controller.foo, 'popObject');
        this.assertCurrentPath('/');
        assert.deepEqual(controller.foo, []);

        run(controller.foo, 'pushObject', 1);
        this.assertCurrentPath('/?foo=%5B1%5D');
        assert.deepEqual(controller.foo, [1]);

        run(controller.foo, 'popObject');
        this.assertCurrentPath('/');
        assert.deepEqual(controller.foo, []);

        run(controller.foo, 'pushObject', 1);
        this.assertCurrentPath('/?foo=%5B1%5D');
        assert.deepEqual(controller.foo, [1]);

        run(controller.foo, 'pushObject', 2);
        this.assertCurrentPath('/?foo=%5B1%2C2%5D');
        assert.deepEqual(controller.foo, [1, 2]);

        run(controller.foo, 'popObject');
        this.assertCurrentPath('/?foo=%5B1%5D');
        assert.deepEqual(controller.foo, [1]);

        run(controller.foo, 'unshiftObject', 'lol');
        this.assertCurrentPath('/?foo=%5B%22lol%22%2C1%5D');
        assert.deepEqual(controller.foo, ['lol', 1]);
      });
    }

    ["@test Overwriting with array with same content shouldn't refire update"](assert) {
      assert.expect(4);

      this.router.map(function() {
        this.route('home', { path: '/' });
      });

      let modelCount = 0;
      this.add(
        'route:home',
        Route.extend({
          model() {
            modelCount++;
          },
        })
      );

      this.setSingleQPController('home', 'foo', emberA([1]));

      return this.visitAndAssert('/').then(() => {
        assert.equal(modelCount, 1);

        let controller = this.getController('home');
        this.setAndFlush(controller, 'model', emberA([1]));

        assert.equal(modelCount, 1);
        this.assertCurrentPath('/');
      });
    }

    ['@test Defaulting to params hash as the model should not result in that params object being watched'](
      assert
    ) {
      assert.expect(1);

      this.router.map(function() {
        this.route('other');
      });

      // This causes the params hash, which is returned as a route's
      // model if no other model could be resolved given the provided
      // params (and no custom model hook was defined), to be watched,
      // unless we return a copy of the params hash.
      this.setSingleQPController('application', 'woot', 'wat');

      this.add(
        'route:other',
        Route.extend({
          model(p, trans) {
            let m = peekMeta(trans.params.application);
            assert.ok(m === undefined, "A meta object isn't constructed for this params POJO");
          },
        })
      );

      return this.visit('/').then(() => {
        this.transitionTo('other');
      });
    }

    ['@test Setting bound query param property to null or undefined does not serialize to url'](
      assert
    ) {
      assert.expect(9);

      this.router.map(function() {
        this.route('home');
      });

      this.setSingleQPController('home', 'foo', [1, 2]);

      return this.visitAndAssert('/home').then(() => {
        var controller = this.getController('home');

        assert.deepEqual(controller.get('foo'), [1, 2]);
        this.assertCurrentPath('/home');

        this.setAndFlush(controller, 'foo', emberA([1, 3]));
        this.assertCurrentPath('/home?foo=%5B1%2C3%5D');

        return this.transitionTo('/home').then(() => {
          assert.deepEqual(controller.get('foo'), [1, 2]);
          this.assertCurrentPath('/home');

          this.setAndFlush(controller, 'foo', null);
          this.assertCurrentPath('/home', 'Setting property to null');

          this.setAndFlush(controller, 'foo', emberA([1, 3]));
          this.assertCurrentPath('/home?foo=%5B1%2C3%5D');

          this.setAndFlush(controller, 'foo', undefined);
          this.assertCurrentPath('/home', 'Setting property to undefined');
        });
      });
    }

    ['@test {{link-to}} with null or undefined QPs does not get serialized into url'](assert) {
      assert.expect(3);

      this.addTemplate(
        'home',
        "{{link-to 'Home' 'home' (query-params foo=nullValue) id='null-link'}}{{link-to 'Home' 'home' (query-params foo=undefinedValue) id='undefined-link'}}"
      );

      this.router.map(function() {
        this.route('home');
      });

      this.setSingleQPController('home', 'foo', [], {
        nullValue: null,
        undefinedValue: undefined,
      });

      return this.visitAndAssert('/home').then(() => {
        assert.equal(this.$('#null-link').attr('href'), '/home');
        assert.equal(this.$('#undefined-link').attr('href'), '/home');
      });
    }

    ["@test A child of a resource route still defaults to parent route's model even if the child route has a query param"](
      assert
    ) {
      assert.expect(2);

      this.setSingleQPController('index', 'woot', undefined, {
        woot: undefined,
      });

      this.add(
        'route:application',
        Route.extend({
          model(/* p, trans */) {
            return { woot: true };
          },
        })
      );

      this.add(
        'route:index',
        Route.extend({
          setupController(controller, model) {
            assert.deepEqual(
              model,
              { woot: true },
              'index route inherited model route from parent route'
            );
          },
        })
      );

      return this.visitAndAssert('/');
    }

    ['@test opting into replace does not affect transitions between routes'](assert) {
      assert.expect(5);

      this.addTemplate(
        'application',
        "{{link-to 'Foo' 'foo' id='foo-link'}}{{link-to 'Bar' 'bar' id='bar-no-qp-link'}}{{link-to 'Bar' 'bar' (query-params raytiley='isthebest') id='bar-link'}}{{outlet}}"
      );

      this.router.map(function() {
        this.route('foo');
        this.route('bar');
      });

      this.setSingleQPController('bar', 'raytiley', 'israd');

      this.add(
        'route:bar',
        Route.extend({
          queryParams: {
            raytiley: {
              replace: true,
            },
          },
        })
      );

      return this.visit('/').then(() => {
        let controller = this.getController('bar');

        this.expectedPushURL = '/foo';
        run(document.getElementById('foo-link'), 'click');

        this.expectedPushURL = '/bar';
        run(document.getElementById('bar-no-qp-link'), 'click');

        this.expectedReplaceURL = '/bar?raytiley=woot';
        this.setAndFlush(controller, 'raytiley', 'woot');

        this.expectedPushURL = '/foo';
        run(document.getElementById('foo-link'), 'click');

        this.expectedPushURL = '/bar?raytiley=isthebest';
        run(document.getElementById('bar-link'), 'click');
      });
    }

    ["@test undefined isn't serialized or deserialized into a string"](assert) {
      assert.expect(4);

      this.router.map(function() {
        this.route('example');
      });

      this.addTemplate(
        'application',
        "{{link-to 'Example' 'example' (query-params foo=undefined) id='the-link'}}"
      );

      this.setSingleQPController('example', 'foo', undefined, {
        foo: undefined,
      });

      this.add(
        'route:example',
        Route.extend({
          model(params) {
            assert.deepEqual(params, { foo: undefined });
          },
        })
      );

      return this.visitAndAssert('/').then(() => {
        assert.equal(
          this.$('#the-link').attr('href'),
          '/example',
          'renders without undefined qp serialized'
        );

        return this.transitionTo('example', {
          queryParams: { foo: undefined },
        }).then(() => {
          this.assertCurrentPath('/example');
        });
      });
    }

    ['@test when refreshModel is true and loading hook is undefined, model hook will rerun when QPs change even if previous did not finish']() {
      return this.refreshModelWhileLoadingTest();
    }

    ['@test when refreshModel is true and loading hook returns false, model hook will rerun when QPs change even if previous did not finish']() {
      return this.refreshModelWhileLoadingTest(false);
    }

    ['@test when refreshModel is true and loading hook returns true, model hook will rerun when QPs change even if previous did not finish']() {
      return this.refreshModelWhileLoadingTest(true);
    }

    ["@test warn user that Route's queryParams configuration must be an Object, not an Array"](
      assert
    ) {
      assert.expect(1);

      this.add(
        'route:application',
        Route.extend({
          queryParams: [{ commitBy: { replace: true } }],
        })
      );

      expectAssertion(() => {
        this.visit('/');
      }, 'You passed in `[{"commitBy":{"replace":true}}]` as the value for `queryParams` but `queryParams` cannot be an Array');
    }

    ['@test handle route names that clash with Object.prototype properties'](assert) {
      assert.expect(1);

      this.router.map(function() {
        this.route('constructor');
      });

      this.add(
        'route:constructor',
        Route.extend({
          queryParams: {
            foo: {
              defaultValue: '123',
            },
          },
        })
      );

      return this.visit('/').then(() => {
        this.transitionTo('constructor', { queryParams: { foo: '999' } });
        let controller = this.getController('constructor');
        assert.equal(get(controller, 'foo'), '999');
      });
    }
  }
);
