import { OWNER } from '@ember/-internals/owner';
import { assign } from '@ember/polyfills';
import { window } from '@ember/-internals/browser-environment';
import { run } from '@ember/runloop';
import { get } from '@ember/-internals/metal';
import AutoLocation from '../../lib/location/auto_location';
import { getHistoryPath, getHashPath } from '../../lib/location/auto_location';
import HistoryLocation from '../../lib/location/history_location';
import HashLocation from '../../lib/location/hash_location';
import NoneLocation from '../../lib/location/none_location';
import { buildOwner, moduleFor, AbstractTestCase } from 'internal-test-helpers';

function mockBrowserLocation(overrides, assert) {
  return assign(
    {
      href: 'http://test.com/',
      pathname: '/',
      hash: '',
      search: '',
      replace() {
        assert.ok(false, 'location.replace should not be called during testing');
      },
    },
    overrides
  );
}

function mockBrowserHistory(overrides, assert) {
  return assign(
    {
      pushState() {
        assert.ok(false, 'history.pushState should not be called during testing');
      },
      replaceState() {
        assert.ok(false, 'history.replaceState should not be called during testing');
      },
    },
    overrides
  );
}

function createLocation(location, history) {
  owner = buildOwner();

  owner.register('location:history', HistoryLocation);
  owner.register('location:hash', HashLocation);
  owner.register('location:none', NoneLocation);

  let autolocation = AutoLocation.create({
    [OWNER]: owner,
    location: location,
    history: history,
    global: {},
  });

  return autolocation;
}

let location, owner;

moduleFor(
  'AutoLocation',
  class extends AbstractTestCase {
    teardown() {
      if (owner) {
        run(owner, 'destroy');
        owner = location = undefined;
      }
    }

    ['@test AutoLocation should have the `global`'](assert) {
      let location = AutoLocation.create();

      assert.ok(location.global, 'has a global defined');
      assert.strictEqual(location.global, window, 'has the environments window global');
    }

    ["@test AutoLocation should return concrete implementation's value for `getURL`"](assert) {
      let browserLocation = mockBrowserLocation({}, assert);
      let browserHistory = mockBrowserHistory({}, assert);

      location = createLocation(browserLocation, browserHistory);
      location.detect();

      let concreteImplementation = get(location, 'concreteImplementation');

      concreteImplementation.getURL = function() {
        return '/lincoln/park';
      };

      assert.equal(location.getURL(), '/lincoln/park');
    }

    ['@test AutoLocation should use a HistoryLocation instance when pushStates is supported'](
      assert
    ) {
      let browserLocation = mockBrowserLocation({}, assert);
      let browserHistory = mockBrowserHistory({}, assert);

      location = createLocation(browserLocation, browserHistory);
      location.detect();

      assert.ok(get(location, 'concreteImplementation') instanceof HistoryLocation);
    }

    ['@test AutoLocation should use a HashLocation instance when pushStates are not supported, but hashchange events are and the URL is already in the HashLocation format'](
      assert
    ) {
      let browserLocation = mockBrowserLocation(
        {
          hash: '#/testd',
        },
        assert
      );

      location = createLocation(browserLocation);
      location.global = {
        onhashchange() {},
      };

      location.detect();
      assert.ok(get(location, 'concreteImplementation') instanceof HashLocation);
    }

    ['@test AutoLocation should use a NoneLocation instance when neither history nor hashchange are supported.'](
      assert
    ) {
      location = createLocation(mockBrowserLocation({}, assert));
      location.detect();

      assert.ok(get(location, 'concreteImplementation') instanceof NoneLocation);
    }

    ["@test AutoLocation should use an index path (i.e. '/') without any location.hash as OK for HashLocation"](
      assert
    ) {
      let browserLocation = mockBrowserLocation(
        {
          href: 'http://test.com/',
          pathname: '/',
          hash: '',
          search: '',
          replace() {
            assert.ok(false, 'location.replace should not be called');
          },
        },
        assert
      );

      location = createLocation(browserLocation);
      location.global = {
        onhashchange() {},
      };

      location.detect();

      assert.ok(
        get(location, 'concreteImplementation') instanceof HashLocation,
        'uses a HashLocation'
      );
    }

    ['@test AutoLocation should transform the URL for hashchange-only browsers viewing a HistoryLocation-formatted path'](
      assert
    ) {
      assert.expect(3);

      let browserLocation = mockBrowserLocation(
        {
          hash: '',
          hostname: 'test.com',
          href: 'http://test.com/test',
          pathname: '/test',
          protocol: 'http:',
          port: '',
          search: '',
          replace(path) {
            assert.equal(
              path,
              'http://test.com/#/test',
              'location.replace should be called with normalized HashLocation path'
            );
          },
        },
        assert
      );

      let location = createLocation(browserLocation);
      location.global = {
        onhashchange() {},
      };

      location.detect();

      assert.ok(
        get(location, 'concreteImplementation') instanceof NoneLocation,
        'NoneLocation should be used while we attempt to location.replace()'
      );
      assert.equal(
        get(location, 'cancelRouterSetup'),
        true,
        'cancelRouterSetup should be set so the router knows.'
      );
    }

    ['@test AutoLocation should replace the URL for pushState-supported browsers viewing a HashLocation-formatted url'](
      assert
    ) {
      assert.expect(2);
      let browserLocation = mockBrowserLocation(
        {
          hash: '#/test',
          hostname: 'test.com',
          href: 'http://test.com/#/test',
          pathname: '/',
          protocol: 'http:',
          port: '',
          search: '',
        },
        assert
      );

      let browserHistory = mockBrowserHistory(
        {
          replaceState(state, title, path) {
            assert.equal(
              path,
              '/test',
              'history.replaceState should be called with normalized HistoryLocation url'
            );
          },
        },
        assert
      );

      let location = createLocation(browserLocation, browserHistory);
      location.detect();

      assert.ok(get(location, 'concreteImplementation'), HistoryLocation);
    }

    ['@test AutoLocation requires any rootURL given to end in a trailing forward slash'](assert) {
      let browserLocation = mockBrowserLocation({}, assert);
      let expectedMsg = /rootURL must end with a trailing forward slash e.g. "\/app\/"/;

      location = createLocation(browserLocation);
      location.rootURL = 'app';

      expectAssertion(function() {
        location.detect();
      }, expectedMsg);

      location.rootURL = '/app';
      expectAssertion(function() {
        location.detect();
      }, expectedMsg);

      // Note the trailing whitespace
      location.rootURL = '/app/ ';
      expectAssertion(function() {
        location.detect();
      }, expectedMsg);
    }

    ['@test AutoLocation provides its rootURL to the concreteImplementation'](assert) {
      let browserLocation = mockBrowserLocation(
        {
          pathname: '/some/subdir/derp',
        },
        assert
      );
      let browserHistory = mockBrowserHistory({}, assert);

      location = createLocation(browserLocation, browserHistory);
      location.rootURL = '/some/subdir/';

      location.detect();

      let concreteLocation = get(location, 'concreteImplementation');
      assert.equal(location.rootURL, concreteLocation.rootURL);
    }

    ['@test getHistoryPath() should return a normalized, HistoryLocation-supported path'](assert) {
      let browserLocation = mockBrowserLocation(
        {
          href: 'http://test.com/app/about?foo=bar#foo',
          pathname: '/app/about',
          search: '?foo=bar',
          hash: '#foo',
        },
        assert
      );

      assert.equal(
        getHistoryPath('/app/', browserLocation),
        '/app/about?foo=bar#foo',
        'URLs already in HistoryLocation form should come out the same'
      );

      browserLocation = mockBrowserLocation(
        {
          href: 'http://test.com/app/#/about?foo=bar#foo',
          pathname: '/app/',
          search: '',
          hash: '#/about?foo=bar#foo',
        },
        assert
      );
      assert.equal(
        getHistoryPath('/app/', browserLocation),
        '/app/about?foo=bar#foo',
        'HashLocation formed URLs should be normalized'
      );

      browserLocation = mockBrowserLocation(
        {
          href: 'http://test.com/app/#about?foo=bar#foo',
          pathname: '/app/',
          search: '',
          hash: '#about?foo=bar#foo',
        },
        assert
      );
      assert.equal(
        getHistoryPath('/app', browserLocation),
        '/app/#about?foo=bar#foo',
        "URLs with a hash not following #/ convention shouldn't be normalized as a route"
      );
    }

    ['@test getHashPath() should return a normalized, HashLocation-supported path'](assert) {
      let browserLocation = mockBrowserLocation(
        {
          href: 'http://test.com/app/#/about?foo=bar#foo',
          pathname: '/app/',
          search: '',
          hash: '#/about?foo=bar#foo',
        },
        assert
      );
      assert.equal(
        getHashPath('/app/', browserLocation),
        '/app/#/about?foo=bar#foo',
        'URLs already in HistoryLocation form should come out the same'
      );

      browserLocation = mockBrowserLocation(
        {
          href: 'http://test.com/app/about?foo=bar#foo',
          pathname: '/app/about',
          search: '?foo=bar',
          hash: '#foo',
        },
        assert
      );
      assert.equal(
        getHashPath('/app/', browserLocation),
        '/app/#/about?foo=bar#foo',
        'HistoryLocation formed URLs should be normalized'
      );

      browserLocation = mockBrowserLocation(
        {
          href: 'http://test.com/app/#about?foo=bar#foo',
          pathname: '/app/',
          search: '',
          hash: '#about?foo=bar#foo',
        },
        assert
      );

      assert.equal(
        getHashPath('/app/', browserLocation),
        '/app/#/#about?foo=bar#foo',
        "URLs with a hash not following #/ convention shouldn't be normalized as a route"
      );
    }
  }
);