/* global createHttpBackend: false, createMockXhr: false, MockXhr: false */
'use strict';

describe('$httpBackend', function() {

  var $backend, $browser, $jsonpCallbacks,
      xhr, fakeDocument, callback;

  beforeEach(inject(function($injector) {

    $browser = $injector.get('$browser');

    fakeDocument = {
      $$scripts: [],
      createElement: jasmine.createSpy('createElement').and.callFake(function() {
        // Return a proper script element...
        return window.document.createElement(arguments[0]);
      }),
      body: {
        appendChild: jasmine.createSpy('body.appendChild').and.callFake(function(script) {
          fakeDocument.$$scripts.push(script);
        }),
        removeChild: jasmine.createSpy('body.removeChild').and.callFake(function(script) {
          var index = fakeDocument.$$scripts.indexOf(script);
          if (index !== -1) {
            fakeDocument.$$scripts.splice(index, 1);
          }
        })
      }
    };

    $jsonpCallbacks = {
      createCallback: function(url) {
        $jsonpCallbacks[url] = function(data) {
          $jsonpCallbacks[url].called = true;
          $jsonpCallbacks[url].data = data;
        };
        return url;
      },
      wasCalled: function(callbackPath) {
        return $jsonpCallbacks[callbackPath].called;
      },
      getResponse: function(callbackPath) {
        return $jsonpCallbacks[callbackPath].data;
      },
      removeCallback: function(callbackPath) {
        delete $jsonpCallbacks[callbackPath];
      }
    };

    $backend = createHttpBackend($browser, createMockXhr, $browser.defer, $jsonpCallbacks, fakeDocument);
    callback = jasmine.createSpy('done');
  }));


  it('should do basics - open async xhr and send data', function() {
    $backend('GET', '/some-url', 'some-data', noop);
    xhr = MockXhr.$$lastInstance;

    expect(xhr.$$method).toBe('GET');
    expect(xhr.$$url).toBe('/some-url');
    expect(xhr.$$data).toBe('some-data');
    expect(xhr.$$async).toBe(true);
  });

  it('should pass null to send if no body is set', function() {
    $backend('GET', '/some-url', undefined, noop);
    xhr = MockXhr.$$lastInstance;

    expect(xhr.$$data).toBe(null);
  });

  it('should pass the correct falsy value to send if falsy body is set (excluding undefined, NaN)',
    function() {
      var values = [false, 0, '', null];
      angular.forEach(values, function(value) {
        $backend('GET', '/some-url', value, noop);
        xhr = MockXhr.$$lastInstance;

        expect(xhr.$$data).toBe(value);
      });
    }
  );

  it('should pass NaN to send if NaN body is set', function() {
    $backend('GET', '/some-url', NaN, noop);
    xhr = MockXhr.$$lastInstance;

    expect(isNaN(xhr.$$data)).toEqual(true);
  });

  it('should call completion function with xhr.statusText if present', function() {
    callback.and.callFake(function(status, response, headers, statusText) {
      expect(statusText).toBe('OK');
    });

    $backend('GET', '/some-url', null, callback);
    xhr = MockXhr.$$lastInstance;
    xhr.statusText = 'OK';
    xhr.onload();
    expect(callback).toHaveBeenCalledOnce();
  });

  it('should call completion function with empty string if not present', function() {
    callback.and.callFake(function(status, response, headers, statusText) {
      expect(statusText).toBe('');
    });

    $backend('GET', '/some-url', null, callback);
    xhr = MockXhr.$$lastInstance;
    xhr.onload();
    expect(callback).toHaveBeenCalledOnce();
  });


  it('should normalize IE\'s 1223 status code into 204', function() {
    callback.and.callFake(function(status) {
      expect(status).toBe(204);
    });

    $backend('GET', 'URL', null, callback);
    xhr = MockXhr.$$lastInstance;

    xhr.status = 1223;
    xhr.onload();

    expect(callback).toHaveBeenCalledOnce();
  });

  it('should set only the requested headers', function() {
    $backend('POST', 'URL', null, noop, {'X-header1': 'value1', 'X-header2': 'value2'});
    xhr = MockXhr.$$lastInstance;

    expect(xhr.$$reqHeaders).toEqual({
      'X-header1': 'value1',
      'X-header2': 'value2'
    });
  });

  it('should set requested headers even if they have falsy values', function() {
    $backend('POST', 'URL', null, noop, {
      'X-header1': 0,
      'X-header2': '',
      'X-header3': false,
      'X-header4': undefined
    });

    xhr = MockXhr.$$lastInstance;

    expect(xhr.$$reqHeaders).toEqual({
      'X-header1': 0,
      'X-header2': '',
      'X-header3': false
    });
  });

  it('should not try to read response data when request is aborted', function() {
    callback.and.callFake(function(status, response, headers, statusText) {
      expect(status).toBe(-1);
      expect(response).toBe(null);
      expect(headers).toBe(null);
      expect(statusText).toBe('');
    });
    $backend('GET', '/url', null, callback, {}, 2000);
    xhr = MockXhr.$$lastInstance;
    spyOn(xhr, 'abort');

    $browser.defer.flush();
    expect(xhr.abort).toHaveBeenCalledOnce();

    xhr.status = 0;
    xhr.onabort();
    expect(callback).toHaveBeenCalledOnce();
  });

  it('should complete the request on timeout', function() {
    callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
      expect(status).toBe(-1);
      expect(response).toBe(null);
      expect(headers).toBe(null);
      expect(statusText).toBe('');
      expect(xhrStatus).toBe('timeout');
    });
    $backend('GET', '/url', null, callback, {});
    xhr = MockXhr.$$lastInstance;

    expect(callback).not.toHaveBeenCalled();

    xhr.ontimeout();
    expect(callback).toHaveBeenCalledOnce();
  });

  it('should complete the request on abort', function() {
    callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
      expect(status).toBe(-1);
      expect(response).toBe(null);
      expect(headers).toBe(null);
      expect(statusText).toBe('');
      expect(xhrStatus).toBe('abort');
    });
    $backend('GET', '/url', null, callback, {});
    xhr = MockXhr.$$lastInstance;

    expect(callback).not.toHaveBeenCalled();

    xhr.onabort();
    expect(callback).toHaveBeenCalledOnce();
  });

  it('should complete the request on error', function() {
    callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
      expect(status).toBe(-1);
      expect(response).toBe(null);
      expect(headers).toBe(null);
      expect(statusText).toBe('');
      expect(xhrStatus).toBe('error');
    });
    $backend('GET', '/url', null, callback, {});
    xhr = MockXhr.$$lastInstance;

    expect(callback).not.toHaveBeenCalled();

    xhr.onerror();
    expect(callback).toHaveBeenCalledOnce();
  });

  it('should complete the request on success', function() {
    callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
      expect(status).toBe(200);
      expect(response).toBe('response');
      expect(headers).toBe('');
      expect(statusText).toBe('');
      expect(xhrStatus).toBe('complete');
    });
    $backend('GET', '/url', null, callback, {});
    xhr = MockXhr.$$lastInstance;

    expect(callback).not.toHaveBeenCalled();

    xhr.statusText = '';
    xhr.response = 'response';
    xhr.status = 200;
    xhr.onload();
    expect(callback).toHaveBeenCalledOnce();
  });

  it('should abort request on numerical timeout', function() {
    callback.and.callFake(function(status, response) {
      expect(status).toBe(-1);
    });

    $backend('GET', '/url', null, callback, {}, 2000);
    xhr = MockXhr.$$lastInstance;
    spyOn(xhr, 'abort');

    expect($browser.deferredFns[0].time).toBe(2000);

    $browser.defer.flush();
    expect(xhr.abort).toHaveBeenCalledOnce();

    xhr.status = 0;
    xhr.onabort();
    expect(callback).toHaveBeenCalledOnce();
  });


  it('should abort request on $timeout promise resolution', inject(function($timeout) {
    callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
      expect(status).toBe(-1);
      expect(xhrStatus).toBe('timeout');
    });

    $backend('GET', '/url', null, callback, {}, $timeout(noop, 2000));
    xhr = MockXhr.$$lastInstance;
    spyOn(xhr, 'abort');

    $timeout.flush();
    expect(xhr.abort).toHaveBeenCalledOnce();

    xhr.status = 0;
    xhr.onabort();
    expect(callback).toHaveBeenCalledOnce();
  }));


  it('should not abort resolved request on timeout promise resolution', inject(function($timeout) {
    callback.and.callFake(function(status, response) {
      expect(status).toBe(200);
    });

    $backend('GET', '/url', null, callback, {}, $timeout(noop, 2000));
    xhr = MockXhr.$$lastInstance;
    spyOn(xhr, 'abort');

    xhr.status = 200;
    xhr.onload();
    expect(callback).toHaveBeenCalledOnce();

    $timeout.flush();
    expect(xhr.abort).not.toHaveBeenCalled();
  }));


  it('should abort request on canceler promise resolution', inject(function($q, $browser) {
    var canceler = $q.defer();

    callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
      expect(status).toBe(-1);
      expect(xhrStatus).toBe('abort');
    });

    $backend('GET', '/url', null, callback, {}, canceler.promise);
    xhr = MockXhr.$$lastInstance;

    canceler.resolve();
    $browser.defer.flush();

    expect(callback).toHaveBeenCalledOnce();
  }));


  it('should cancel timeout on completion', function() {
    callback.and.callFake(function(status, response) {
      expect(status).toBe(200);
    });

    $backend('GET', '/url', null, callback, {}, 2000);
    xhr = MockXhr.$$lastInstance;
    spyOn(xhr, 'abort');

    expect($browser.deferredFns[0].time).toBe(2000);

    xhr.status = 200;
    xhr.onload();
    expect(callback).toHaveBeenCalledOnce();

    expect($browser.deferredFns.length).toBe(0);
    expect(xhr.abort).not.toHaveBeenCalled();
  });


  it('should call callback with xhrStatus "abort" on explicit xhr.abort() when $timeout is set', inject(function($timeout) {
    callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
      expect(status).toBe(-1);
      expect(xhrStatus).toBe('abort');
    });

    $backend('GET', '/url', null, callback, {}, $timeout(noop, 2000));
    xhr = MockXhr.$$lastInstance;
    spyOn(xhr, 'abort').and.callThrough();

    xhr.abort();

    expect(callback).toHaveBeenCalledOnce();
  }));


  it('should set withCredentials', function() {
    $backend('GET', '/some.url', null, callback, {}, null, true);
    expect(MockXhr.$$lastInstance.withCredentials).toBe(true);
  });


  it('should call $xhrFactory with method and url', function() {
    var mockXhrFactory = jasmine.createSpy('mockXhrFactory').and.callFake(createMockXhr);
    $backend = createHttpBackend($browser, mockXhrFactory, $browser.defer, $jsonpCallbacks, fakeDocument);
    $backend('GET', '/some-url', 'some-data', noop);
    expect(mockXhrFactory).toHaveBeenCalledWith('GET', '/some-url');
  });


  it('should set up event listeners', function() {
    var progressFn = function() {};
    var uploadProgressFn = function() {};
    $backend('GET', '/url', null, callback, {}, null, null, null,
        {progress: progressFn}, {progress: uploadProgressFn});
    xhr = MockXhr.$$lastInstance;
    expect(xhr.$$events.progress[0]).toBe(progressFn);
    expect(xhr.upload.$$events.progress[0]).toBe(uploadProgressFn);
  });


  describe('responseType', function() {

    it('should set responseType and return xhr.response', function() {
      $backend('GET', '/whatever', null, callback, {}, null, null, 'blob');

      var xhrInstance = MockXhr.$$lastInstance;
      expect(xhrInstance.responseType).toBe('blob');

      callback.and.callFake(function(status, response) {
        expect(response).toBe(xhrInstance.response);
      });

      xhrInstance.response = {some: 'object'};
      xhrInstance.onload();

      expect(callback).toHaveBeenCalledOnce();
    });


    it('should read responseText if response was not defined', function() {
      //  old browsers like IE9, don't support responseType, so they always respond with responseText

      $backend('GET', '/whatever', null, callback, {}, null, null, 'blob');

      var xhrInstance = MockXhr.$$lastInstance;
      var responseText = '{"some": "object"}';
      expect(xhrInstance.responseType).toBe('blob');

      callback.and.callFake(function(status, response) {
        expect(response).toBe(responseText);
      });

      xhrInstance.responseText = responseText;
      xhrInstance.onload();

      expect(callback).toHaveBeenCalledOnce();
    });
  });


  describe('JSONP', function() {

    var SCRIPT_URL = /([^?]*)\?cb=(.*)/;


    it('should add script tag for JSONP request', function() {
      callback.and.callFake(function(status, response) {
        expect(status).toBe(200);
        expect(response).toBe('some-data');
      });

      $backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback);
      expect(fakeDocument.$$scripts.length).toBe(1);

      var script = fakeDocument.$$scripts.shift(),
          url = script.src.match(SCRIPT_URL);

      expect(url[1]).toBe('http://example.org/path');
      $jsonpCallbacks[url[2]]('some-data');
      browserTrigger(script, 'load');

      expect(callback).toHaveBeenCalledOnce();
    });


    it('should clean up the callback and remove the script', function() {
      spyOn($jsonpCallbacks, 'removeCallback').and.callThrough();

      $backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback);
      expect(fakeDocument.$$scripts.length).toBe(1);


      var script = fakeDocument.$$scripts.shift(),
          callbackId = script.src.match(SCRIPT_URL)[2];

      $jsonpCallbacks[callbackId]('some-data');
      browserTrigger(script, 'load');

      expect($jsonpCallbacks.removeCallback).toHaveBeenCalledOnceWith(callbackId);
      expect(fakeDocument.body.removeChild).toHaveBeenCalledOnceWith(script);
    });


    it('should set url to current location if not specified or empty string', function() {
      $backend('JSONP', undefined, null, callback);
      expect(fakeDocument.$$scripts[0].src).toBe($browser.url());
      fakeDocument.$$scripts.shift();

      $backend('JSONP', '', null, callback);
      expect(fakeDocument.$$scripts[0].src).toBe($browser.url());
    });


    it('should abort request on timeout and remove JSONP callback', function() {
      spyOn($jsonpCallbacks, 'removeCallback').and.callThrough();

      callback.and.callFake(function(status, response) {
        expect(status).toBe(-1);
      });

      $backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback, null, 2000);
      expect(fakeDocument.$$scripts.length).toBe(1);
      expect($browser.deferredFns[0].time).toBe(2000);

      var script = fakeDocument.$$scripts.shift(),
        callbackId = script.src.match(SCRIPT_URL)[2];

      $browser.defer.flush();
      expect(fakeDocument.$$scripts.length).toBe(0);
      expect(callback).toHaveBeenCalledOnce();

      expect($jsonpCallbacks.removeCallback).toHaveBeenCalledOnceWith(callbackId);
    });


    // TODO(vojta): test whether it fires "async-start"
    // TODO(vojta): test whether it fires "async-end" on both success and error
  });


  describe('protocols that return 0 status code', function() {

    function respond(status, content) {
      xhr = MockXhr.$$lastInstance;
      xhr.status = status;
      xhr.responseText = content;
      xhr.onload();
    }

    beforeEach(function() {
      $backend = createHttpBackend($browser, createMockXhr);
    });


    it('should convert 0 to 200 if content and file protocol', function() {
      $backend('GET', 'file:///whatever/index.html', null, callback);
      respond(0, 'SOME CONTENT');

      expect(callback).toHaveBeenCalled();
      expect(callback.calls.mostRecent().args[0]).toBe(200);
    });

    it('should convert 0 to 200 if content for protocols other than file', function() {
      $backend('GET', 'someProtocol:///whatever/index.html', null, callback);
      respond(0, 'SOME CONTENT');

      expect(callback).toHaveBeenCalled();
      expect(callback.calls.mostRecent().args[0]).toBe(200);
    });

    it('should convert 0 to 404 if no content and file protocol', function() {
      $backend('GET', 'file:///whatever/index.html', null, callback);
      respond(0, '');

      expect(callback).toHaveBeenCalled();
      expect(callback.calls.mostRecent().args[0]).toBe(404);
    });

    it('should not convert 0 to 404 if no content for protocols other than file', function() {
      $backend('GET', 'someProtocol:///whatever/index.html', null, callback);
      respond(0, '');

      expect(callback).toHaveBeenCalled();
      expect(callback.calls.mostRecent().args[0]).toBe(0);
    });

    it('should convert 0 to 404 if no content - relative url', function() {
      /* global urlParsingNode: true */
      var originalUrlParsingNode = urlParsingNode;

      //temporarily overriding the DOM element to pretend that the test runs origin with file:// protocol
      urlParsingNode = {
        hash: '#/C:/',
        host: '',
        hostname: '',
        href: 'file:///C:/base#!/C:/foo',
        pathname: '/C:/foo',
        port: '',
        protocol: 'file:',
        search: '',
        setAttribute: angular.noop
      };

      try {

        $backend('GET', '/whatever/index.html', null, callback);
        respond(0, '');

        expect(callback).toHaveBeenCalled();
        expect(callback.calls.mostRecent().args[0]).toBe(404);

      } finally {
        urlParsingNode = originalUrlParsingNode;
      }
    });

    it('should return original backend status code if different from 0', function() {
      // request to http://
      $backend('POST', 'http://rest_api/create_whatever', null, callback);
      respond(201, '');

      expect(callback).toHaveBeenCalled();
      expect(callback.calls.mostRecent().args[0]).toBe(201);


      // request to file://
      $backend('POST', 'file://rest_api/create_whatever', null, callback);
      respond(201, '');

      expect(callback).toHaveBeenCalled();
      expect(callback.calls.mostRecent().args[0]).toBe(201);

      // request to file:// with HTTP status >= 300
      $backend('POST', 'file://rest_api/create_whatever', null, callback);
      respond(503, '');

      expect(callback).toHaveBeenCalled();
      expect(callback.calls.mostRecent().args[0]).toBe(503);
    });
  });
});

