var TestEnv = require('testenv');
var Mocks = require('mocks');
var Util = require('core/util').default;
var HTTPSocket = require('core/http/http_socket').default;
var Runtime = require('runtime').default;

describe("HTTP.Socket", function() {
  var onOpen, onMessage, onActivity, onClose;
  var hooks;

  var lastXHR;
  var socket;

  var HTTPFactory;

  beforeEach(function() {
    HTTPFactory = require('runtime').default.HTTPFactory;
    jasmine.clock().install();

    spyOn(HTTPFactory, "createXHR").and.callFake(function(method, url) {
      lastXHR = Mocks.getHTTPRequest(method, url);
      return lastXHR;
    });

    if (TestEnv === "web") {
      spyOn(HTTPFactory, "createXDR").and.callFake(function(method, url) {
        lastXHR = Mocks.getHTTPRequest(method, url);
        return lastXHR;
      });
    }

    spyOn(Runtime, "isXHRSupported").and.returnValue(true);
    if (TestEnv === "web") spyOn(Runtime, "isXDRSupported").and.returnValue(false);

    hooks = {
      getReceiveURL: jasmine.createSpy().and.callFake(function(url, session) {
        return url.base + "/" + session + url.queryString;
      }),
      onHeartbeat: jasmine.createSpy(),
      sendHeartbeat: jasmine.createSpy(),
      onFinished: jasmine.createSpy()
    };
    socket = new HTTPSocket(hooks, "http://example.com/pusher");

    onOpen = jasmine.createSpy("onOpen");
    onMessage = jasmine.createSpy("onMessage");
    onActivity = jasmine.createSpy("onActivity");
    onClose = jasmine.createSpy("onClose");

    socket.onopen = onOpen;
    socket.onmessage = onMessage;
    socket.onactivity = onActivity;
    socket.onclose = onClose;
  });

  afterEach(function() {
    socket.close();
    jasmine.clock().uninstall();
  });

  it("should use XHR if it's supported", function() {
    Runtime.isXHRSupported.and.returnValue(true);
    if (TestEnv === "web" ) Runtime.isXDRSupported.and.returnValue(false);

    var socket = new HTTPSocket(hooks, "http://example.com");
    expect(HTTPFactory.createXHR).toHaveBeenCalled();
    socket.close();
  });

  if (TestEnv === "web") {
    it("should use XDR if it's supported", function() {
      Runtime.isXHRSupported.and.returnValue(false);
      Runtime.isXDRSupported.and.returnValue(true);

      var socket = new HTTPSocket(hooks, "http://example.com");
      expect(HTTPFactory.createXDR).toHaveBeenCalled();

      socket.close();
    });
  }

  it("should send a POST request to the URL constructed with getReceiveURL", function() {
    var socket = new HTTPSocket(hooks, "http://example.com/x?arg=val");

    expect(lastXHR.method).toEqual("POST");
    expect(lastXHR.url).toMatch(
      /^http:\/\/example\.com\/x\/[0-9]{1,3}\/[0-9a-z]{8}\?arg=val&t=[0-9]+&n=[0-9]+$/
    );

    socket.close();
  });

  it("should start streaming immediately", function() {
    expect(lastXHR.start).toHaveBeenCalled();
  });

  it("should start streaming from different URLs", function() {
    var socket1 = new HTTPSocket(hooks, "http://example.com");
    var url1 = lastXHR.url;
    var socket2 = new HTTPSocket(hooks, "http://example.com");
    var url2 = lastXHR.url;

    expect(url1).not.toEqual(url2);
  });

  describe("on HTTP request start exception", function() {
    var onError;
    var stream;

    beforeEach(function() {
      // close the default socket
      socket.close();

      HTTPFactory.createXHR.and.callFake(function() {
        stream = Mocks.getHTTPRequest();
        stream.start.and.throwError("start exception");
        return stream;
      });

      onError = jasmine.createSpy("onError");
      onClose = jasmine.createSpy("onClose");

      socket = new HTTPSocket(hooks, "http://example.com");
      socket.onerror = onError;
      socket.onclose = onClose;
    });

    it("should raise an error", function() {
      expect(onError).not.toHaveBeenCalled();

      jasmine.clock().tick(1);

      expect(onError).toHaveBeenCalledWith(new Error('start exception'));
    });

    it("should close itself with code 1006", function() {
      expect(onClose).not.toHaveBeenCalled();
      jasmine.clock().tick(1);
      expect(onClose).toHaveBeenCalledWith({
        code: 1006,
        reason: "Could not start streaming",
        wasClean: false
      });
    });

    it("should unbind all listeners from the stream", function() {
      spyOn(stream, "unbind_all");
      jasmine.clock().tick(1);
      expect(stream.unbind_all).toHaveBeenCalled();
    });

    it("should close the stream", function() {
      jasmine.clock().tick(1);
      expect(stream.close).toHaveBeenCalled();
    });
  });

  describe("#ping", function() {
    it("should call the sendHeartbeat hook", function() {
      expect(hooks.sendHeartbeat).not.toHaveBeenCalledWith(socket);
      socket.ping();
      expect(hooks.sendHeartbeat).toHaveBeenCalledWith(socket);
    });
  });

  describe("#close", function() {
    it("should close itself with supplied code and reason", function() {
      socket.close(2013, "test reason");
      expect(onClose).toHaveBeenCalledWith({
        code: 2013,
        reason: "test reason",
        wasClean: true
      });
    });

    it("should unbind all listeners from the stream", function() {
      spyOn(lastXHR, "unbind_all");
      socket.close(2013, "test reason");
      expect(lastXHR.unbind_all).toHaveBeenCalled();
    });

    it("should close the stream", function() {
      socket.close(2013, "test reason");
      expect(lastXHR.close).toHaveBeenCalled();
    });
  });

  describe("before connecting", function() {
    describe("#send", function() {
      it("should return false", function() {
        expect(socket.send("test")).toEqual(false);
      });

      it("should not trigger any HTTP requests", function() {
        expect(HTTPFactory.createXHR.calls.count()).toEqual(1);
        socket.send("test");
        expect(HTTPFactory.createXHR.calls.count()).toEqual(1);
      });
    });
  });

  describe("when connecting", function() {
    describe("before the open frame", function() {
      it("should ignore heartbeat frames", function() {
        var requestCount = HTTPFactory.createXHR.calls.count();
        lastXHR.emit("chunk", { status: 200, data: "hhhhhhhhhhh" });
        expect(HTTPFactory.createXHR.calls.count()).toEqual(requestCount);
      });
    });

    describe("on the open frame", function() {
      it("should emit an 'open' event", function() {
        lastXHR.emit("chunk", { status: 200, data: "o" });
        expect(onOpen).toHaveBeenCalled();
      });
    });
  });

  describe("after opening without hostname binding", function() {
    beforeEach(function() {
      lastXHR.emit("chunk", { status: 200, data: "hhhhhhhhhhh" });
      lastXHR.emit("chunk", { status: 200, data: "o" });
    });

    describe("#send", function() {
      it("should send an HTTP request to a correct URL", function() {
        expect(HTTPFactory.createXHR.calls.count()).toEqual(1);
        socket.send("test");

        expect(HTTPFactory.createXHR.calls.count()).toEqual(2);
        expect(lastXHR.method).toEqual("POST");
        expect(lastXHR.url).toMatch(
          new RegExp(
            "^http:\\/\\/example\\.com\\/pusher\\/" +
            socket.session +
            "\\/xhr_send\\?t=[0-9]+&n=[0-9]+$"
          )
        );
      });

      it("should send a string payload", function() {
        var data = "test";
        socket.send(data);
        expect(JSON.parse(lastXHR.start.calls.first().args[0])).toEqual([data]);
      });

      it("should send an array payload", function() {
        var data = ["test", 1, { foo: "bar" }];
        socket.send(data);
        expect(JSON.parse(lastXHR.start.calls.first().args[0])).toEqual([data]);
      });

      it("should send an object payload", function() {
        var data = { num: 1, str: "data", arr: [1, 2, 3]};
        socket.send(data);
        expect(JSON.parse(lastXHR.start.calls.first().args[0])).toEqual([data]);
      });

      it("should return true if the request did not raise an exception", function() {
        expect(socket.send("test")).toBe(true);
      });

      it("should return false if the request raised an exception", function() {
        HTTPFactory.createXHR.and.callFake(function() {
          var request = Mocks.getHTTPRequest();
          request.start.and.throwError("exception");
          return request;
        });

        expect(socket.send("test")).toBe(false);
      });

      it("should send messages to different URLs", function() {
        socket.send("test");
        var url1 = lastXHR.url;
        socket.send("test");
        var url2 = lastXHR.url;
        expect(url1).not.toEqual(url2);
      });
    });

    describe("on a single-message frame", function() {
      it("should emit the message if status is 200", function() {
        lastXHR.emit("chunk", { status: 200, data: 'm{"foo": 123}' });
        expect(onMessage).toHaveBeenCalledWith({ data: { foo: 123 } });
      });

      it("should not emit the message if status is not 200", function() {
        lastXHR.emit("chunk", { status: 400, data: 'm[]' });
        expect(onMessage).not.toHaveBeenCalled();
      });
    });

    describe("on a multi-message frame", function() {
      it("should emit all messages if status is 200", function() {
        lastXHR.emit("chunk", { status: 200, data: 'a[1,2,3]' });
        expect(onMessage.calls.first().args[0]).toEqual({ data: 1 });
        expect(onMessage.calls.argsFor(1)[0]).toEqual({ data: 2 });
        expect(onMessage.calls.argsFor(2)[0]).toEqual({ data: 3 });
      });

      it("should not emit any messages if status is not 200", function() {
        lastXHR.emit("chunk", { status: 400, data: 'a[1,2,3]' });
        expect(onMessage).not.toHaveBeenCalled();
      });
    });

    describe("on a heartbeat frame", function() {
      it("should call the onHeartbeat hook", function() {
        lastXHR.emit("chunk", { status: 200, data: 'h' });
        expect(hooks.onHeartbeat).toHaveBeenCalledWith(socket);
      });
    });

    describe("on a close frame", function() {
      it("should call onclose with the code and the reason", function() {
        lastXHR.emit("chunk", { status: 200, data: 'c[1234, "testing"]' });

        expect(onClose).toHaveBeenCalledWith({
          code: 1234,
          reason: "testing",
          wasClean: true
        });
      });

      it("should unbind all listeners from the stream", function() {
        spyOn(lastXHR, "unbind_all");
        lastXHR.emit("chunk", { status: 200, data: 'c[1234, "testing"]' });
        expect(lastXHR.unbind_all).toHaveBeenCalled();
      });

      it("should close the stream", function() {
        lastXHR.emit("chunk", { status: 200, data: 'c[1234, "testing"]' });
        expect(lastXHR.close).toHaveBeenCalled();
      });
    });

    describe("on any chunk", function() {
      it("should call onactivity", function() {
        expect(onActivity).not.toHaveBeenCalled();
        lastXHR.emit("chunk", { status: 200, data: "x" });
        expect(onActivity).toHaveBeenCalled();
      });
    });
  });

  describe("after opening with hostname binding", function() {
    beforeEach(function() {
      lastXHR.emit("chunk", { status: 200, data: "hhhhhhhhhhhhhhhhh" });
      lastXHR.emit("chunk", { status: 200, data: 'o{"hostname":"example.org"}'});
    });

    describe("#send", function() {
      it("should send an HTTP request to the updated host", function() {
        socket.send("test");
        // opening the connection sends the first request
        expect(HTTPFactory.createXHR.calls.count()).toEqual(2);
        expect(lastXHR.method).toEqual("POST");
        expect(lastXHR.url).toMatch(
          new RegExp(
            "^http:\\/\\/example\\.org\\/pusher\\/" +
            socket.session +
            "\\/xhr_send\\?t=[0-9]+&n=[0-9]+$"
          )
        );
      });
    });
  });
});
