'use strict';
describe('$location', function() {
  var url;
  afterEach(function() {
    // link rewriting used in html5 mode on legacy browsers binds to document.onClick, so we need
    // to clean this up after each test.
    jqLite(document).unbind('click');
  });
  describe('NewUrl', function() {
    beforeEach(function() {
      url = new LocationHtml5Url('http://www.domain.com:9877/');
      url.$$parse('http://www.domain.com:9877/path/b?search=a&b=c&d#hash');
    });
    it('should provide common getters', function() {
      expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#hash');
      expect(url.protocol()).toBe('http');
      expect(url.host()).toBe('www.domain.com');
      expect(url.port()).toBe(9877);
      expect(url.path()).toBe('/path/b');
      expect(url.search()).toEqual({search: 'a', b: 'c', d: true});
      expect(url.hash()).toBe('hash');
      expect(url.url()).toBe('/path/b?search=a&b=c&d#hash');
    });
    it('path() should change path', function() {
      url.path('/new/path');
      expect(url.path()).toBe('/new/path');
      expect(url.absUrl()).toBe('http://www.domain.com:9877/new/path?search=a&b=c&d#hash');
    });
    it('search() should accept string', function() {
      url.search('x=y&c');
      expect(url.search()).toEqual({x: 'y', c: true});
      expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?x=y&c#hash');
    });
    it('search() should accept object', function() {
      url.search({one: 1, two: true});
      expect(url.search()).toEqual({one: 1, two: true});
      expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?one=1&two#hash');
    });
    it('search() should change single parameter', function() {
      url.search({id: 'old', preserved: true});
      url.search('id', 'new');
      expect(url.search()).toEqual({id: 'new', preserved: true});
    });
    it('search() should remove single parameter', function() {
      url.search({id: 'old', preserved: true});
      url.search('id', null);
      expect(url.search()).toEqual({preserved: true});
    });
    it('hash() should change hash fragment', function() {
      url.hash('new-hash');
      expect(url.hash()).toBe('new-hash');
      expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#new-hash');
    });
    it('url() should change the path, search and hash', function() {
      url.url('/some/path?a=b&c=d#hhh');
      expect(url.url()).toBe('/some/path?a=b&c=d#hhh');
      expect(url.absUrl()).toBe('http://www.domain.com:9877/some/path?a=b&c=d#hhh');
      expect(url.path()).toBe('/some/path');
      expect(url.search()).toEqual({a: 'b', c: 'd'});
      expect(url.hash()).toBe('hhh');
    });
    it('url() should change only hash when no search and path specified', function() {
      url.url('#some-hash');
      expect(url.hash()).toBe('some-hash');
      expect(url.url()).toBe('/path/b?search=a&b=c&d#some-hash');
      expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#some-hash');
    });
    it('url() should change only search and hash when no path specified', function() {
      url.url('?a=b');
      expect(url.search()).toEqual({a: 'b'});
      expect(url.hash()).toBe('');
      expect(url.path()).toBe('/path/b');
    });
    it('url() should reset search and hash when only path specified', function() {
      url.url('/new/path');
      expect(url.path()).toBe('/new/path');
      expect(url.search()).toEqual({});
      expect(url.hash()).toBe('');
    });
    it('replace should set $$replace flag and return itself', function() {
      expect(url.$$replace).toBe(false);
      url.replace();
      expect(url.$$replace).toBe(true);
      expect(url.replace()).toBe(url);
    });
    it('should parse new url', function() {
      url = new LocationHtml5Url('http://host.com/');
      url.$$parse('http://host.com/base');
      expect(url.path()).toBe('/base');
      url = new LocationHtml5Url('http://host.com/');
      url.$$parse('http://host.com/base#');
      expect(url.path()).toBe('/base');
    });
    it('should prefix path with forward-slash', function() {
      url = new LocationHtml5Url('http://server/');
      url.path('b');
      expect(url.path()).toBe('/b');
      expect(url.absUrl()).toBe('http://server/b');
    });
    it('should set path to forward-slash when empty', function() {
      url = new LocationHtml5Url('http://server/');
      url.$$parse('http://server/')
      expect(url.path()).toBe('/');
      expect(url.absUrl()).toBe('http://server/');
    });
    it('setters should return Url object to allow chaining', function() {
      expect(url.path('/any')).toBe(url);
      expect(url.search('')).toBe(url);
      expect(url.hash('aaa')).toBe(url);
      expect(url.url('/some')).toBe(url);
    });
    it('should not preserve old properties when parsing new url', function() {
      url.$$parse('http://www.domain.com:9877/a');
      expect(url.path()).toBe('/a');
      expect(url.search()).toEqual({});
      expect(url.hash()).toBe('');
      expect(url.absUrl()).toBe('http://www.domain.com:9877/a');
    });
    it('should not rewrite when hashbang url is not given', function() {
      initService(true, '!', true);
      inject(
        initBrowser('http://domain.com/base/a/b', '/base'),
        function($rootScope, $location, $browser) {
          expect($browser.url()).toBe('http://domain.com/base/a/b');
        }
      );
    });
    it('should prepend path with basePath', function() {
      url = new LocationHtml5Url('http://server/base/');
      url.$$parse('http://server/base/abc?a');
      expect(url.path()).toBe('/abc');
      expect(url.search()).toEqual({a: true});
      url.path('/new/path');
      expect(url.absUrl()).toBe('http://server/base/new/path?a');
    });
    it('should throw error when invalid server url given', function() {
      url = new LocationHtml5Url('http://server.org/base/abc', '/base');
      expect(function() {
        url.$$parse('http://other.server.org/path#/path');
      }).toThrow('[NgErr21] $location error! Invalid url "http://other.server.org/path#/path", missing path prefix "http://server.org/base/".');
    });
    it('should throw error when invalid base url given', function() {
      url = new LocationHtml5Url('http://server.org/base/abc', '/base');
      expect(function() {
        url.$$parse('http://server.org/path#/path');
      }).toThrow('[NgErr21] $location error! Invalid url "http://server.org/path#/path", missing path prefix "http://server.org/base/".');
    });
    describe('encoding', function() {
      it('should encode special characters', function() {
        url.path('/a <>#');
        url.search({'i j': '<>#'});
        url.hash('<>#');
        expect(url.path()).toBe('/a <>#');
        expect(url.search()).toEqual({'i j': '<>#'});
        expect(url.hash()).toBe('<>#');
        expect(url.absUrl()).toBe('http://www.domain.com:9877/a%20%3C%3E%23?i%20j=%3C%3E%23#%3C%3E%23');
      });
      it('should not encode !$:@', function() {
        url.path('/!$:@');
        url.search('');
        url.hash('!$:@');
        expect(url.absUrl()).toBe('http://www.domain.com:9877/!$:@#!$:@');
      });
      it('should decode special characters', function() {
        url = new LocationHtml5Url('http://host.com/');
        url.$$parse('http://host.com/a%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23');
        expect(url.path()).toBe('/a <>#');
        expect(url.search()).toEqual({'i j': '<>#'});
        expect(url.hash()).toBe('x <>#');
      });
    });
  });
  describe('HashbangUrl', function() {
    beforeEach(function() {
      url = new LocationHashbangUrl('http://www.server.org:1234/base', '#!');
      url.$$parse('http://www.server.org:1234/base#!/path?a=b&c#hash');
    });
    it('should parse hashband url into path and search', function() {
      expect(url.protocol()).toBe('http');
      expect(url.host()).toBe('www.server.org');
      expect(url.port()).toBe(1234);
      expect(url.path()).toBe('/path');
      expect(url.search()).toEqual({a: 'b', c: true});
      expect(url.hash()).toBe('hash');
    });
    it('absUrl() should return hashbang url', function() {
      expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/path?a=b&c#hash');
      url.path('/new/path');
      url.search({one: 1});
      url.hash('hhh');
      expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/new/path?one=1#hhh');
    });
    it('should preserve query params in base', function() {
      url = new LocationHashbangUrl('http://www.server.org:1234/base?base=param', '#');
      url.$$parse('http://www.server.org:1234/base?base=param#/path?a=b&c#hash');
      expect(url.absUrl()).toBe('http://www.server.org:1234/base?base=param#/path?a=b&c#hash');
      url.path('/new/path');
      url.search({one: 1});
      url.hash('hhh');
      expect(url.absUrl()).toBe('http://www.server.org:1234/base?base=param#/new/path?one=1#hhh');
    });
    it('should prefix path with forward-slash', function() {
      url = new LocationHashbangUrl('http://host.com/base', '#');
      url.$$parse('http://host.com/base#path');
      expect(url.path()).toBe('/path');
      expect(url.absUrl()).toBe('http://host.com/base#/path');
      url.path('wrong');
      expect(url.path()).toBe('/wrong');
      expect(url.absUrl()).toBe('http://host.com/base#/wrong');
    });
    it('should set path to forward-slash when empty', function() {
      url = new LocationHashbangUrl('http://server/base', '#!');
      url.$$parse('http://server/base');
      url.path('aaa');
      expect(url.path()).toBe('/aaa');
      expect(url.absUrl()).toBe('http://server/base#!/aaa');
    });
    it('should not preserve old properties when parsing new url', function() {
      url.$$parse('http://www.server.org:1234/base#!/');
      expect(url.path()).toBe('/');
      expect(url.search()).toEqual({});
      expect(url.hash()).toBe('');
      expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/');
    });
    it('should throw error when invalid server url given', function() {
      expect(function() {
        url.$$parse('http://server.org/path#/path');
      }).toThrow('[NgErr22] $location error! Invalid url "http://server.org/path#/path", does not start with "http://www.server.org:1234/base".');
    });
    it('should throw error when invalid hashbang prefix given', function() {
      expect(function() {
        url.$$parse('http://www.server.org:1234/base#/path');
      }).toThrow('[NgErr49] $location error! Invalid url "http://www.server.org:1234/base#/path", missing hash prefix "#!".');
    });
    describe('encoding', function() {
      it('should encode special characters', function() {
        url.path('/a <>#');
        url.search({'i j': '<>#'});
        url.hash('<>#');
        expect(url.path()).toBe('/a <>#');
        expect(url.search()).toEqual({'i j': '<>#'});
        expect(url.hash()).toBe('<>#');
        expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/a%20%3C%3E%23?i%20j=%3C%3E%23#%3C%3E%23');
      });
      it('should not encode !$:@', function() {
        url.path('/!$:@');
        url.search('');
        url.hash('!$:@');
        expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/!$:@#!$:@');
      });
      it('should decode special characters', function() {
        url = new LocationHashbangUrl('http://host.com/a', '#');
        url.$$parse('http://host.com/a#/%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23');
        expect(url.path()).toBe('/ <>#');
        expect(url.search()).toEqual({'i j': '<>#'});
        expect(url.hash()).toBe('x <>#');
      });
      it('should return decoded characters for search specified in URL', function() {
        var locationUrl = new LocationHtml5Url('http://host.com/');
        locationUrl.$$parse('http://host.com/?q=1%2F2%203');
        expect(locationUrl.search()).toEqual({'q': '1/2 3'});
      });
      it('should return decoded characters for search specified with setter', function() {
        var locationUrl = new LocationHtml5Url('http://host.com/');
        locationUrl.$$parse('http://host.com/')
        locationUrl.search('q', '1/2 3');
        expect(locationUrl.search()).toEqual({'q': '1/2 3'});
      });
    });
  });
  function initService(html5Mode, hashPrefix, supportHistory) {
    return module(function($provide, $locationProvider){
      $locationProvider.html5Mode(html5Mode);
      $locationProvider.hashPrefix(hashPrefix);
      $provide.value('$sniffer', {history: supportHistory});
    });
  }
  function initBrowser(url, basePath) {
    return function($browser){
      $browser.url(url);
      $browser.$$baseHref = basePath;
    };
  }
  describe('wiring', function() {
    beforeEach(initService(false, '!', true));
    beforeEach(inject(initBrowser('http://new.com/a/b#!', 'http://new.com/a/b')));
    it('should update $location when browser url changes', inject(function($browser, $location) {
      spyOn($location, '$$parse').andCallThrough();
      $browser.url('http://new.com/a/b#!/aaa');
      $browser.poll();
      expect($location.absUrl()).toBe('http://new.com/a/b#!/aaa');
      expect($location.path()).toBe('/aaa');
      expect($location.$$parse).toHaveBeenCalledOnce();
    }));
    // location.href = '...' fires hashchange event synchronously, so it might happen inside $apply
    it('should not $apply when browser url changed inside $apply', inject(
        function($rootScope, $browser, $location) {
      var OLD_URL = $browser.url(),
          NEW_URL = 'http://new.com/a/b#!/new';
      $rootScope.$apply(function() {
        $browser.url(NEW_URL);
        $browser.poll(); // simulate firing event from browser
        expect($location.absUrl()).toBe(OLD_URL); // should be async
      });
      expect($location.absUrl()).toBe(NEW_URL);
    }));
    // location.href = '...' fires hashchange event synchronously, so it might happen inside $digest
    it('should not $apply when browser url changed inside $digest', inject(
        function($rootScope, $browser, $location) {
      var OLD_URL = $browser.url(),
          NEW_URL = 'http://new.com/a/b#!/new',
          notRunYet = true;
      $rootScope.$watch(function() {
        if (notRunYet) {
          notRunYet = false;
          $browser.url(NEW_URL);
          $browser.poll(); // simulate firing event from browser
          expect($location.absUrl()).toBe(OLD_URL); // should be async
        }
      });
      $rootScope.$digest();
      expect($location.absUrl()).toBe(NEW_URL);
    }));
    it('should update browser when $location changes', inject(function($rootScope, $browser, $location) {
      var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
      $location.path('/new/path');
      expect($browserUrl).not.toHaveBeenCalled();
      $rootScope.$apply();
      expect($browserUrl).toHaveBeenCalledOnce();
      expect($browser.url()).toBe('http://new.com/a/b#!/new/path');
    }));
    it('should update browser only once per $apply cycle', inject(function($rootScope, $browser, $location) {
      var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
      $location.path('/new/path');
      $rootScope.$watch(function() {
        $location.search('a=b');
      });
      $rootScope.$apply();
      expect($browserUrl).toHaveBeenCalledOnce();
      expect($browser.url()).toBe('http://new.com/a/b#!/new/path?a=b');
    }));
    it('should replace browser url when url was replaced at least once',
        inject(function($rootScope, $location, $browser) {
      var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
      $location.path('/n/url').replace();
      $rootScope.$apply();
      expect($browserUrl).toHaveBeenCalledOnce();
      expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b#!/n/url', true]);
      expect($location.$$replace).toBe(false);
    }));
    it('should always reset replace flag after running watch', inject(function($rootScope, $location) {
      // init watches
      $location.url('/initUrl');
      $rootScope.$apply();
      // changes url but resets it before digest
      $location.url('/newUrl').replace().url('/initUrl');
      $rootScope.$apply();
      expect($location.$$replace).toBe(false);
      // set the url to the old value
      $location.url('/newUrl').replace();
      $rootScope.$apply();
      expect($location.$$replace).toBe(false);
      // doesn't even change url only calls replace()
      $location.replace();
      $rootScope.$apply();
      expect($location.$$replace).toBe(false);
    }));
    it('should update the browser if changed from within a watcher', inject(function($rootScope, $location, $browser) {
      $rootScope.$watch(function() { return true; }, function() {
        $location.path('/changed');
      });
      $rootScope.$digest();
      expect($browser.url()).toBe('http://new.com/a/b#!/changed');
    }));
  });
  // html5 history is disabled
  describe('disabled history', function() {
    it('should use hashbang url with hash prefix', function() {
      initService(false, '!');
      inject(
        initBrowser('http://domain.com/base/index.html#!/a/b', '/base/index.html'),
        function($rootScope, $location, $browser) {
          expect($browser.url()).toBe('http://domain.com/base/index.html#!/a/b');
          $location.path('/new');
          $location.search({a: true});
          $rootScope.$apply();
          expect($browser.url()).toBe('http://domain.com/base/index.html#!/new?a');
        }
      );
    });
    it('should use hashbang url without hash prefix', function() {
      initService(false, '');
      inject(
        initBrowser('http://domain.com/base/index.html#/a/b', '/base/index.html'),
        function($rootScope, $location, $browser) {
          expect($browser.url()).toBe('http://domain.com/base/index.html#/a/b');
          $location.path('/new');
          $location.search({a: true});
          $rootScope.$apply();
          expect($browser.url()).toBe('http://domain.com/base/index.html#/new?a');
        }
      );
    });
  });
  // html5 history enabled, but not supported by browser
  describe('history on old browser', function() {
    afterEach(inject(function($rootElement){
      dealoc($rootElement);
    }));
    it('should use hashbang url with hash prefix', function() {
      initService(true, '!!', false);
      inject(
        initBrowser('http://domain.com/base/index.html#!!/a/b', '/base/index.html'),
        function($rootScope, $location,  $browser) {
          expect($browser.url()).toBe('http://domain.com/base/index.html#!!/a/b');
          $location.path('/new');
          $location.search({a: true});
          $rootScope.$apply();
          expect($browser.url()).toBe('http://domain.com/base/index.html#!!/new?a');
        }
      );
    });
    it('should redirect to hashbang url when new url given', function() {
      initService(true, '!');
      inject(
        initBrowser('http://domain.com/base/new-path/index.html', '/base/index.html'),
        function($browser, $location) {
          expect($browser.url()).toBe('http://domain.com/base/index.html#!/new-path/index.html');
        }
      );
    });
   it('should correctly convert html5 url with path matching basepath to hashbang url', function () {
      initService(true, '!', false);
      inject(
        initBrowser('http://domain.com/base/index.html', '/base/index.html'),
        function($browser, $location) {
          expect($browser.url()).toBe('http://domain.com/base/index.html#!/index.html');
        }
      );
    });
  });
  // html5 history enabled and supported by browser
  describe('history on new browser', function() {
    afterEach(inject(function($rootElement){
      dealoc($rootElement);
    }));
    it('should use new url', function() {
      initService(true, '', true);
      inject(
        initBrowser('http://domain.com/base/old/index.html#a', '/base/index.html'),
        function($rootScope, $location, $browser) {
          expect($browser.url()).toBe('http://domain.com/base/old/index.html#a');
          $location.path('/new');
          $location.search({a: true});
          $rootScope.$apply();
          expect($browser.url()).toBe('http://domain.com/base/new?a#a');
        }
      );
    });
    it('should rewrite when hashbang url given', function() {
      initService(true, '!', true);
      inject(
        initBrowser('http://domain.com/base/index.html#!/a/b', '/base/index.html'),
        function($rootScope, $location, $browser) {
          expect($browser.url()).toBe('http://domain.com/base/a/b');
          $location.path('/new');
          $location.hash('abc');
          $rootScope.$apply();
          expect($browser.url()).toBe('http://domain.com/base/new#abc');
          expect($location.path()).toBe('/new');
        }
      );
    });
    it('should rewrite when hashbang url given (without hash prefix)', function() {
      initService(true, '', true);
      inject(
        initBrowser('http://domain.com/base/index.html#/a/b', '/base/index.html'),
        function($rootScope, $location, $browser) {
          expect($browser.url()).toBe('http://domain.com/base/a/b');
          expect($location.path()).toBe('/a/b');
        }
      );
    });
  });
  describe('SERVER_MATCH', function() {
    it('should parse basic url', function() {
      var match = SERVER_MATCH.exec('http://www.angularjs.org/path?search#hash?x=x');
      expect(match[1]).toBe('http');
      expect(match[3]).toBe('www.angularjs.org');
    });
    it('should parse file://', function() {
      var match = SERVER_MATCH.exec('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html');
      expect(match[1]).toBe('file');
      expect(match[3]).toBe('');
      expect(match[5]).toBeFalsy();
    });
    it('should parse url with "-" in host', function() {
      var match = SERVER_MATCH.exec('http://a-b1.c-d.09/path');
      expect(match[1]).toBe('http');
      expect(match[3]).toBe('a-b1.c-d.09');
      expect(match[5]).toBeFalsy();
    });
    it('should parse host without "/" at the end', function() {
      var match = SERVER_MATCH.exec('http://host.org');
      expect(match[3]).toBe('host.org');
      match = SERVER_MATCH.exec('http://host.org#');
      expect(match[3]).toBe('host.org');
      match = SERVER_MATCH.exec('http://host.org?');
      expect(match[3]).toBe('host.org');
    });
    it('should parse chrome extension urls', function() {
      var match = SERVER_MATCH.exec('chrome-extension://jjcldkdmokihdaomalanmlohibnoplog/index.html?foo#bar');
      expect(match[1]).toBe('chrome-extension');
      expect(match[3]).toBe('jjcldkdmokihdaomalanmlohibnoplog');
    });
    it('should parse FFOS app:// urls', function() {
      var match = SERVER_MATCH.exec('app://{d0419af1-8b42-41c5-96f4-ef4179e52315}/path');
      expect(match[1]).toBe('app');
      expect(match[3]).toBe('{d0419af1-8b42-41c5-96f4-ef4179e52315}');
      expect(match[5]).toBeFalsy();
      expect(match[6]).toBe('/path');
      expect(match[8]).toBeFalsy();
      match = SERVER_MATCH.exec('app://}foo{')
      expect(match).toBe(null);
    });
  });
  describe('PATH_MATCH', function() {
    it('should parse just path', function() {
      var match = PATH_MATCH.exec('/path');
      expect(match[1]).toBe('/path');
    });
    it('should parse path with search', function() {
      var match = PATH_MATCH.exec('/ppp/a?a=b&c');
      expect(match[1]).toBe('/ppp/a');
      expect(match[3]).toBe('a=b&c');
    });
    it('should parse path with hash', function() {
      var match = PATH_MATCH.exec('/ppp/a#abc?');
      expect(match[1]).toBe('/ppp/a');
      expect(match[5]).toBe('abc?');
    });
    it('should parse path with both search and hash', function() {
      var match = PATH_MATCH.exec('/ppp/a?a=b&c#abc/d?');
      expect(match[3]).toBe('a=b&c');
    });
  });
  describe('link rewriting', function() {
    var root, link, originalBrowser, lastEventPreventDefault;
    function configureService(linkHref, html5Mode, supportHist, attrs, content) {
      module(function($provide, $locationProvider) {
        attrs = attrs ? ' ' + attrs + ' ' : '';
        // fake the base behavior
        if (linkHref[0] == '/') {
          linkHref = 'http://host.com' + linkHref;
        } else if(!linkHref.match(/:\/\//)) {
          linkHref = 'http://host.com/base/' + linkHref;
        }
        link = jqLite('' + content + '')[0];
        $provide.value('$sniffer', {history: supportHist});
        $locationProvider.html5Mode(html5Mode);
        $locationProvider.hashPrefix('!');
        return function($rootElement, $document) {
          $rootElement.append(link);
          root = $rootElement[0];
          // we need to do this otherwise we can't simulate events
          $document.find('body').append($rootElement);
        };
      });
    }
    function initBrowser() {
      return function($browser){
        $browser.url('http://host.com/base');
        $browser.$$baseHref = '/base/index.html';
      };
    }
    function initLocation() {
      return function($browser, $location, $rootElement) {
        originalBrowser = $browser.url();
        // we have to prevent the default operation, as we need to test absolute links (http://...)
        // and navigating to these links would kill jstd
        $rootElement.bind('click', function(e) {
          lastEventPreventDefault = e.isDefaultPrevented();
          e.preventDefault();
        });
      };
    }
    function expectRewriteTo($browser, url) {
      expect(lastEventPreventDefault).toBe(true);
      expect($browser.url()).toBe(url);
    }
    function expectNoRewrite($browser) {
      expect(lastEventPreventDefault).toBe(false);
      expect($browser.url()).toBe(originalBrowser);
    }
    afterEach(function() {
      dealoc(root);
      dealoc(document.body);
    });
    it('should rewrite rel link to new url when history enabled on new browser', function() {
      configureService('link?a#b', true, true);
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          browserTrigger(link, 'click');
          expectRewriteTo($browser, 'http://host.com/base/link?a#b');
        }
      );
    });
    it('should do nothing if already on the same URL', function() {
      configureService('/base/', true, true);
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          browserTrigger(link, 'click');
          expectRewriteTo($browser, 'http://host.com/base/');
          jqLite(link).attr('href', 'http://host.com/base/foo');
          browserTrigger(link, 'click');
          expectRewriteTo($browser, 'http://host.com/base/foo');
          jqLite(link).attr('href', 'http://host.com/base/');
          browserTrigger(link, 'click');
          expectRewriteTo($browser, 'http://host.com/base/');
          jqLite(link).
              attr('href', 'http://host.com/base/foo').
              bind('click', function(e) { e.preventDefault(); });
          browserTrigger(link, 'click');
          expect($browser.url()).toBe('http://host.com/base/');
        }
      );
    });
    it('should rewrite abs link to new url when history enabled on new browser', function() {
      configureService('/base/link?a#b', true, true);
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          browserTrigger(link, 'click');
          expectRewriteTo($browser, 'http://host.com/base/link?a#b');
        }
      );
    });
    it('should rewrite rel link to hashbang url when history enabled on old browser', function() {
      configureService('link?a#b', true, false);
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          browserTrigger(link, 'click');
          expectRewriteTo($browser, 'http://host.com/base/index.html#!/link?a#b');
        }
      );
    });
    it('should rewrite abs link to hashbang url when history enabled on old browser', function() {
      configureService('/base/link?a#b', true, false);
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          browserTrigger(link, 'click');
          expectRewriteTo($browser, 'http://host.com/base/index.html#!/link?a#b');
        }
      );
    });
    it('should not rewrite full url links do different domain', function() {
      configureService('http://www.dot.abc/a?b=c', true);
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          browserTrigger(link, 'click');
          expectNoRewrite($browser);
        }
      );
    });
    it('should not rewrite links with target="_blank"', function() {
      configureService('/a?b=c', true, true, 'target="_blank"');
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          browserTrigger(link, 'click');
          expectNoRewrite($browser);
        }
      );
    });
    it('should not rewrite links with target specified', function() {
      configureService('/a?b=c', true, true, 'target="some-frame"');
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          browserTrigger(link, 'click');
          expectNoRewrite($browser);
        }
      );
    });
    it('should rewrite full url links to same domain and base path', function() {
      configureService('http://host.com/base/new', true);
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          browserTrigger(link, 'click');
          expectRewriteTo($browser, 'http://host.com/base/index.html#!/new');
        }
      );
    });
    it('should rewrite when clicked span inside link', function() {
      configureService('some/link', true, true, '', 'link');
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          var span = jqLite(link).find('span');
          browserTrigger(span, 'click');
          expectRewriteTo($browser, 'http://host.com/base/some/link');
        }
      );
    });
    it('should not rewrite when link to different base path when history enabled on new browser',
        function() {
      configureService('/other_base/link', true, true);
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          browserTrigger(link, 'click');
          expectNoRewrite($browser);
        }
      );
    });
    it('should not rewrite when link to different base path when history enabled on old browser',
        function() {
      configureService('/other_base/link', true, false);
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          browserTrigger(link, 'click');
          expectNoRewrite($browser);
        }
      );
    });
    it('should not rewrite when link to different base path when history disabled', function() {
      configureService('/other_base/link', false);
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          browserTrigger(link, 'click');
          expectNoRewrite($browser);
        }
      );
    });
    it('should not rewrite when full link to different base path when history enabled on new browser',
        function() {
      configureService('http://host.com/other_base/link', true, true);
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          browserTrigger(link, 'click');
          expectNoRewrite($browser);
        }
      );
    });
    it('should not rewrite when full link to different base path when history enabled on old browser',
        function() {
      configureService('http://host.com/other_base/link', true, false);
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          browserTrigger(link, 'click');
          expectNoRewrite($browser);
        }
      );
    });
    it('should not rewrite when full link to different base path when history disabled', function() {
      configureService('http://host.com/other_base/link', false);
      inject(
        initBrowser(),
        initLocation(),
        function($browser) {
          browserTrigger(link, 'click');
          expectNoRewrite($browser);
        }
      );
    });
    // don't run next tests on IE<9, as browserTrigger does not simulate pressed keys
    if (!(msie < 9)) {
      it('should not rewrite when clicked with ctrl pressed', function() {
        configureService('/a?b=c', true, true);
        inject(
          initBrowser(),
          initLocation(),
          function($browser) {
            browserTrigger(link, 'click', ['ctrl']);
            expectNoRewrite($browser);
          }
        );
      });
      it('should not rewrite when clicked with meta pressed', function() {
        configureService('/a?b=c', true, true);
        inject(
          initBrowser(),
          initLocation(),
          function($browser) {
            browserTrigger(link, 'click', ['meta']);
            expectNoRewrite($browser);
          }
        );
      });
    }
    it('should not mess up hash urls when clicking on links in hashbang mode', function() {
      var base;
      module(function() {
        return function($browser) {
          window.location.hash = 'someHash';
          base = window.location.href
          $browser.url(base);
          base = base.split('#')[0];
        }
      });
      inject(function($rootScope, $compile, $browser, $rootElement, $document, $location) {
        // we need to do this otherwise we can't simulate events
        $document.find('body').append($rootElement);
        var element = $compile('v1v2')($rootScope);
        $rootElement.append(element);
        var av1 = $rootElement.find('a').eq(0);
        var av2 = $rootElement.find('a').eq(1);
        browserTrigger(av1, 'click');
        expect($browser.url()).toEqual(base + '#/view1');
        browserTrigger(av2, 'click');
        expect($browser.url()).toEqual(base + '#/view2');
        $rootElement.remove();
      });
    });
    it('should not mess up hash urls when clicking on links in hashbang mode with a prefix',
        function() {
      var base;
      module(function($locationProvider) {
        return function($browser) {
          window.location.hash = '!someHash';
          $browser.url(base = window.location.href);
          base = base.split('#')[0];
          $locationProvider.hashPrefix('!');
        }
      });
      inject(function($rootScope, $compile, $browser, $rootElement, $document, $location) {
        // we need to do this otherwise we can't simulate events
        $document.find('body').append($rootElement);
        var element = $compile('v1v2')($rootScope);
        $rootElement.append(element);
        var av1 = $rootElement.find('a').eq(0);
        var av2 = $rootElement.find('a').eq(1);
        browserTrigger(av1, 'click');
        expect($browser.url()).toEqual(base + '#!/view1');
        browserTrigger(av2, 'click');
        expect($browser.url()).toEqual(base + '#!/view2');
      });
    });
    it('should not intercept clicks outside the current hash prefix', function() {
      var base, clickHandler;
      module(function($provide) {
        $provide.value('$rootElement', {
          bind: function(event, handler) {
            expect(event).toEqual('click');
            clickHandler = handler;
          },
          unbind: noop
        });
        return function($browser) {
          $browser.url(base = 'http://server/');
        }
      });
      inject(function($location) {
        // make IE happy
        jqLite(window.document.body).html('link');
        var event = {
          target: jqLite(window.document.body).find('a')[0],
          preventDefault: jasmine.createSpy('preventDefault')
        };
        clickHandler(event);
        expect(event.preventDefault).not.toHaveBeenCalled();
      });
    });
    it('should not intercept hash link clicks outside the app base url space', function() {
      var base, clickHandler;
      module(function($provide) {
        $provide.value('$rootElement', {
          bind: function(event, handler) {
            expect(event).toEqual('click');
            clickHandler = handler;
          },
          unbind: angular.noop
        });
        return function($browser) {
          $browser.url(base = 'http://server/');
        }
      });
      inject(function($rootScope, $compile, $browser, $rootElement, $document, $location) {
        // make IE happy
        jqLite(window.document.body).html('link');
        var event = {
          target: jqLite(window.document.body).find('a')[0],
          preventDefault: jasmine.createSpy('preventDefault')
        };
        clickHandler(event);
        expect(event.preventDefault).not.toHaveBeenCalled();
      });
    });
    // regression https://github.com/angular/angular.js/issues/1058
    it('should not throw if element was removed', inject(function($document, $rootElement, $location) {
      // we need to do this otherwise we can't simulate events
      $document.find('body').append($rootElement);
      $rootElement.html('');
      var button = $rootElement.find('button');
      button.bind('click', function() {
        button.remove();
      });
      browserTrigger(button, 'click');
    }));
  });
  describe('location cancellation', function() {
    it('should fire $before/afterLocationChange event', inject(function($location, $browser, $rootScope, $log) {
      expect($browser.url()).toEqual('http://server/');
      $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
        $log.info('before', newUrl, oldUrl, $browser.url());
      });
      $rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) {
        $log.info('after', newUrl, oldUrl, $browser.url());
      });
      expect($location.url()).toEqual('');
      $location.url('/somePath');
      expect($location.url()).toEqual('/somePath');
      expect($browser.url()).toEqual('http://server/');
      expect($log.info.logs).toEqual([]);
      $rootScope.$apply();
      expect($log.info.logs.shift()).
          toEqual(['before', 'http://server/#/somePath', 'http://server/', 'http://server/']);
      expect($log.info.logs.shift()).
          toEqual(['after', 'http://server/#/somePath', 'http://server/', 'http://server/#/somePath']);
      expect($location.url()).toEqual('/somePath');
      expect($browser.url()).toEqual('http://server/#/somePath');
    }));
    it('should allow $locationChangeStart event cancellation', inject(function($location, $browser, $rootScope, $log) {
      expect($browser.url()).toEqual('http://server/');
      expect($location.url()).toEqual('');
      $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
        $log.info('before', newUrl, oldUrl, $browser.url());
        event.preventDefault();
      });
      $rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) {
        throw Error('location should have been canceled');
      });
      expect($location.url()).toEqual('');
      $location.url('/somePath');
      expect($location.url()).toEqual('/somePath');
      expect($browser.url()).toEqual('http://server/');
      expect($log.info.logs).toEqual([]);
      $rootScope.$apply();
      expect($log.info.logs.shift()).
          toEqual(['before', 'http://server/#/somePath', 'http://server/', 'http://server/']);
      expect($log.info.logs[1]).toBeUndefined();
      expect($location.url()).toEqual('');
      expect($browser.url()).toEqual('http://server/');
    }));
    it ('should fire $locationChangeSuccess event when change from browser location bar',
      inject(function($log, $location, $browser, $rootScope) {
        $rootScope.$apply(); // clear initial $locationChangeStart
        expect($browser.url()).toEqual('http://server/');
        expect($location.url()).toEqual('');
        $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
          $log.info('start', newUrl, oldUrl);
        });
        $rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) {
          $log.info('after', newUrl, oldUrl);
        });
        $browser.url('http://server/#/somePath');
        $browser.poll();
        expect($log.info.logs.shift()).
          toEqual(['start', 'http://server/#/somePath', 'http://server/']);
        expect($log.info.logs.shift()).
          toEqual(['after', 'http://server/#/somePath', 'http://server/']);
      })
    );
   it('should listen on click events on href and prevent browser default in hashbang mode', function() {
      module(function() {
        return function($rootElement, $compile, $rootScope) {
          $rootElement.html('link');
          $compile($rootElement)($rootScope);
          jqLite(document.body).append($rootElement);
        }
      });
      inject(function($location, $rootScope, $browser, $rootElement) {
        var log = '',
            link = $rootElement.find('a');
        $rootScope.$on('$locationChangeStart', function(event) {
          event.preventDefault();
          log += '$locationChangeStart';
        });
        $rootScope.$on('$locationChangeSuccess', function() {
          throw new Error('after cancellation in hashbang mode');
        });
        browserTrigger(link, 'click');
        expect(log).toEqual('$locationChangeStart');
        expect($browser.url()).toEqual('http://server/');
        dealoc($rootElement);
      });
    });
    it('should listen on click events on href and prevent browser default in html5 mode', function() {
      module(function($locationProvider) {
        $locationProvider.html5Mode(true);
        return function($rootElement, $compile, $rootScope) {
          $rootElement.html('link');
          $compile($rootElement)($rootScope);
          jqLite(document.body).append($rootElement);
        }
      });
      inject(function($location, $rootScope, $browser, $rootElement) {
        var log = '',
            link = $rootElement.find('a'),
            browserUrlBefore = $browser.url();
        $rootScope.$on('$locationChangeStart', function(event) {
          event.preventDefault();
          log += '$locationChangeStart';
        });
        $rootScope.$on('$locationChangeSuccess', function() {
          throw new Error('after cancellation in html5 mode');
        });
        browserTrigger(link, 'click');
        expect(log).toEqual('$locationChangeStart');
        expect($browser.url()).toBe(browserUrlBefore);
        dealoc($rootElement);
      });
    });
  });
  describe('LocationHtml5Url', function() {
    var location, locationIndex;
    beforeEach(function() {
      location = new LocationHtml5Url('http://server/pre/', 'http://server/pre/path');
      locationIndex = new LocationHtml5Url('http://server/pre/index.html', 'http://server/pre/path');
    });
    it('should rewrite URL', function() {
      expect(location.$$rewrite('http://other')).toEqual(undefined);
      expect(location.$$rewrite('http://server/pre')).toEqual('http://server/pre/');
      expect(location.$$rewrite('http://server/pre/')).toEqual('http://server/pre/');
      expect(location.$$rewrite('http://server/pre/otherPath')).toEqual('http://server/pre/otherPath');
      expect(locationIndex.$$rewrite('http://server/pre')).toEqual('http://server/pre/');
      expect(locationIndex.$$rewrite('http://server/pre/')).toEqual('http://server/pre/');
      expect(locationIndex.$$rewrite('http://server/pre/otherPath')).toEqual('http://server/pre/otherPath');
    });
  });
  describe('LocationHashbangUrl', function() {
    var location;
    beforeEach(function() {
      location = new LocationHashbangUrl('http://server/pre/', 'http://server/pre/#/path');
    });
    it('should rewrite URL', function() {
      expect(location.$$rewrite('http://other')).toEqual(undefined);
      expect(location.$$rewrite('http://server/pre/')).toEqual('http://server/pre/');
      expect(location.$$rewrite('http://server/pre/#otherPath')).toEqual('http://server/pre/#otherPath');
      expect(location.$$rewrite('javascript:void(0)')).toEqual(undefined);
    });
  });
  describe('LocationHashbangInHtml5Url', function() {
    var location, locationIndex;
    beforeEach(function() {
      location = new LocationHashbangInHtml5Url('http://server/pre/', '#!');
      locationIndex = new LocationHashbangInHtml5Url('http://server/pre/index.html', '#!');
    });
    it('should rewrite URL', function() {
      expect(location.$$rewrite('http://other')).toEqual(undefined);
      expect(location.$$rewrite('http://server/pre')).toEqual('http://server/pre/');
      expect(location.$$rewrite('http://server/pre/')).toEqual('http://server/pre/');
      expect(location.$$rewrite('http://server/pre/otherPath')).toEqual('http://server/pre/#!otherPath');
      expect(locationIndex.$$rewrite('http://server/pre')).toEqual('http://server/pre/');
      expect(locationIndex.$$rewrite('http://server/pre/')).toEqual(undefined);
      expect(locationIndex.$$rewrite('http://server/pre/otherPath')).toEqual('http://server/pre/index.html#!otherPath');
    });
  });
});