
/*!
 * socket.io-node
 * Copyright(c) 2011 LearnBoost <dev@learnboost.com>
 * MIT Licensed
 */

/**
 * Test dependencies.
 */

var sio = require('../')
  , should = require('./common')
  , qs = require('querystring')
  , HTTPClient = should.HTTPClient
  , parser = sio.parser
  , ports = 15500;

/**
 * HTTPClient for jsonp-polling transport.
 */

function JSONPPolling (port) {
  HTTPClient.call(this, port);
};

/**
 * Inhertis from HTTPClient.
 */

JSONPPolling.prototype.__proto__ = HTTPClient.prototype;

/**
 * Performs a json-p (cross domain) handshake
 *
 * @api public
 */

JSONPPolling.prototype.handshake = function (opts, fn) {
  if ('function' == typeof opts) {
    fn = opts;
    opts = {};
  }

  var self = this;

  return this.get(
      '/socket.io/{protocol}?jsonp=0'
    , opts
    , function (res, data) {
        var head = 'io.j[0]('
          , foot = ');';

        data.substr(0, head.length).should.eql(head);
        data.substr(-foot.length).should.eql(foot);
        data = data.slice(head.length, data.length - foot.length);

        var parts = JSON.parse(data).split(':');

        if (opts.ignoreConnect) {
          return fn && fn.apply(null, parts);
        }

        // expect connect packet right after handshake
        self.get(
            '/socket.io/{protocol}/jsonp-polling/' + parts[0]
          , function (res, msgs) {
              res.statusCode.should.eql(200);

              msgs.should.have.length(1);
              msgs[0].should.eql({ type: 'connect', endpoint: '', qs: '' });

              fn && fn.apply(null, parts);
            }
        );
      }
  );
};

/**
 * Override GET requests.
 *
 * @api public
 */

JSONPPolling.prototype.get = function (path, opts, fn) {
  if ('function' == typeof opts) {
    fn = opts;
    opts = {};
  }

  opts = opts || {};

  opts.parse = function (data) {
    var head = 'io.j[0]('
      , foot = ');';

    if (~path.indexOf('?i=1')) {
      head = 'io.j[1](';
    }

    data.substr(0, head.length).should.eql(head);
    data.substr(-foot.length).should.eql(foot);

    data = data.substr(head.length, data.length - head.length - foot.length);

    return JSON.parse(data);
  };

  return HTTPClient.prototype.get.call(this, path, opts, fn);
};

/**
 * Issue an encoded POST request
 *
 * @api private
 */

JSONPPolling.prototype.post = function (path, data, opts, fn) {
  if ('function' == typeof opts) {
    fn = opts;
    opts = {};
  }

  opts = opts || {};
  opts.method = 'POST';
  opts.data = qs.stringify({ d: data });

  return this.request(path, opts, fn);
};

/**
 * Create client for this transport.
 *
 * @api public
 */

function client (port) {
  return new JSONPPolling(port);
};

/**
 * Test.
 */

module.exports = {

  'test jsonp handshake': function (done) {
    var cl = client(++ports)
      , io = create(cl);

    io.configure(function () {
      io.set('close timeout', .05);
      io.set('polling duration', 0);
    });

    function finish () {
      cl.end();
      io.server.close();
      done();
    };

    cl.handshake(function (sid) {
      var total = 2;

      cl.get('/socket.io/{protocol}/jsonp-polling/tobi', function (res, msgs) {
        res.statusCode.should.eql(200);

        msgs.should.have.length(1);
        msgs[0].should.eql({
            type: 'error'
          , reason: 'client not handshaken'
          , endpoint: ''
          , advice: 'reconnect'
        });

        --total || finish();
      });

      cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
        res.statusCode.should.eql(200);
        msgs.should.have.length(1);
        msgs[0].should.eql({ type: 'noop', endpoint: '' });
        --total || finish();
      });
    });
  },

  'test the connection event': function (done) {
    var cl = client(++ports)
      , io = create(cl)
      , sid;

    io.configure(function () {
      io.set('polling duration', 0);
      io.set('close timeout', .05);
    });

    io.sockets.on('connection', function (socket) {
      socket.id.should.eql(sid);

      socket.on('disconnect', function () {
        cl.end();
        io.server.close();
        done();
      });
    });

    cl.handshake({ ignoreConnect: true }, function (sessid) {
      sid = sessid;

      cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
        res.statusCode.should.eql(200);
        msgs.should.have.length(1);
        msgs[0].type.should.eql('connect');
      });
    });
  },

  'test the disconnection event after a close timeout': function (done) {
    var cl = client(++ports)
      , io = create(cl)
      , sid;

    io.configure(function () {
      io.set('close timeout', 0);
    });

    io.sockets.on('connection', function (socket) {
      socket.id.should.eql(sid);

      socket.on('disconnect', function () {
        io.server.close();
        done();
      });
    });

    cl.handshake({ ignoreConnect: true }, function (sessid) {
      sid = sessid;

      cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
        res.statusCode.should.eql(200);
        msgs.should.have.length(1);
        msgs[0].type.should.eql('connect');

        setTimeout(function () {
          cl.end();
        }, 10);
      });
    });
  },

  'test the disconnection event when the client sends ?disconnect req': function (done) {
    var cl = client(++ports)
      , io = create(cl)
      , disconnected = false
      , sid;

    io.configure(function () {
      io.set('close timeout', .1);
    });

    io.sockets.on('connection', function (socket) {
      socket.on('disconnect', function () {
        disconnected = true;
      });
    });

    cl.handshake({ ignoreConnect: true }, function (sessid) {
      sid = sessid;

      cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
        res.statusCode.should.eql(200);
        msgs.should.have.length(1);
        msgs[0].type.should.eql('connect');

        cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
          msgs.should.have.length(1);
          msgs[0].should.eql({ type: 'disconnect', endpoint: '' });
          disconnected.should.be.true;
          io.server.close();
          cl.end();
          done();
        });

        // with the new http bits in node 0.5, there's no guarantee that
        // the previous request is actually dispatched (and received) before the following
        // reset call is sent. to not waste more time on a workaround, a timeout is added.
        setTimeout(function() {
          cl.get('/socket.io/{protocol}/jsonp-polling/' + sid + '/?disconnect');
        }, 500);
      });
    });
  },

  'test the disconnection event booting a client': function (done) {
    var cl = client(++ports)
      , io = create(cl)
      , forced = false;

    io.sockets.on('connection', function (socket) {
      socket.on('disconnect', function () {
        io.server.close();
        done();
      });

      cl.end();
      socket.disconnect();
      forced = true;
    });

    cl.handshake(function (sid) {
      cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
        msgs.should.have.length(1);
        msgs[0].should.eql({ type: 'disconnect', endpoint: '' });

        forced.should.be.true;
      });
    });
  },

  'test the disconnection event with client disconnect packet': function (done) {
    var cl = client(++ports)
      , io = create(cl)
      , sid;

    io.sockets.on('connection', function (client) {
      cl.post(
          '/socket.io/{protocol}/jsonp-polling/' + sid
        , JSON.stringify(parser.encodePacket({ type: 'disconnect' }))
        , function (res, data) {
            res.statusCode.should.eql(200);
            data.should.eql('1');
          }
      );

      client.on('disconnect', function () {
        cl.end();
        io.server.close();
        done();
      });
    });

    cl.handshake({ ignoreConnect: true }, function (sessid) {
      sid = sessid;

      cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
        res.statusCode.should.eql(200);
        msgs.should.have.length(1);
        msgs[0].type.should.eql('connect');
      });
    });
  },

  'test sending back data': function (done) {
    var cl = client(++ports)
      , io = create(cl);

    io.configure(function () {
      io.set('polling duration', .05);
      io.set('close timeout', .05);
    });

    io.sockets.on('connection', function (socket) {
      socket.send('woot');

      socket.on('disconnect', function () {
        cl.end();
        io.server.close();
        done();
      });
    });

    cl.handshake(function (sid) {
      cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, packs) {
        packs.should.have.length(1);
        packs[0].type.should.eql('message');
        packs[0].data.should.eql('woot');
      });
    });
  },

  'test sending a batch of messages': function (done) {
    var cl = client(++ports)
      , io = create(cl)
      , sid;

    io.configure(function () {
      io.set('close timeout', .05);
    });

    io.sockets.on('connection', function (socket) {
      var messages = 0;

      cl.post(
          '/socket.io/{protocol}/jsonp-polling/' + sid
        , JSON.stringify(parser.encodePayload([
              parser.encodePacket({ type: 'message', data: 'a' })
            , parser.encodePacket({ type: 'message', data: 'b' })
            , parser.encodePacket({ type: 'disconnect' })
          ]))
        , function (res, data) {
            res.statusCode.should.eql(200);
            data.should.eql('1');
          }
      );

      socket.on('message', function (data) {
        messages++;

        if (messages == 1)
          data.should.eql('a');

        if (messages == 2)
          data.should.eql('b');
      });

      socket.on('disconnect', function () {
        messages.should.eql(2);
        cl.end();
        io.server.close();
        done();
      });
    });

    cl.handshake({ ignoreConnect: true }, function (sessid) {
      sid = sessid;

      cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
        res.statusCode.should.eql(200);
        msgs.should.have.length(1);
        msgs[0].type.should.eql('connect');
      });
    });
  },

  'test message buffering between a response and a request': function (done) {
    var cl = client(++ports)
      , io = create(cl)
      , messages = false
      , tobi;

    io.configure(function () {
      io.set('polling duration', .1);
      io.set('close timeout', .2);
    });

    io.sockets.on('connection', function (socket) {
      tobi = function () {
        socket.send('a');
        socket.send('b');
        socket.send('c');
      };

      socket.on('disconnect', function () {
        messages.should.be.true;

        cl.end();
        io.server.close();
        done();
      });
    });

    cl.handshake(function (sid) {
      cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
        msgs.should.have.length(1);
        msgs[0].should.eql({ type: 'noop', endpoint: '' });

        tobi();

        cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
          msgs.should.have.length(3);
          msgs[0].should.eql({ type: 'message', endpoint: '', data: 'a' });
          msgs[1].should.eql({ type: 'message', endpoint: '', data: 'b' });
          msgs[2].should.eql({ type: 'message', endpoint: '', data: 'c' });
          messages = true;
        });
      })
    });
  },

  'test connecting to a specific endpoint': function (done) {
    var cl = client(++ports)
      , io = create(cl)
      , connectMessage = false
      , sid;

    io.configure(function () {
      io.set('polling duration', 0);
      io.set('close timeout', .05);
    });

    io.of('/woot').on('connection', function (socket) {
      connectMessage.should.be.true;

      socket.on('disconnect', function () {
        cl.end();
        io.server.close();
        done();
      });
    });

    cl.handshake(function (sid) {
      cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, data) {
        cl.get('/socket.io/{protocol}/jsonp-polling/' + sid);

        connectMessage = true;

        cl.post(
            '/socket.io/{protocol}/jsonp-polling/' + sid
          , JSON.stringify(parser.encodePacket({ type: 'connect', endpoint: '/woot' }))
          , function (res, data) {
              res.statusCode.should.eql(200);
              data.should.eql('1');
            }
        );
      });
    });
  },

  'test that connecting doesnt connect to defined endpoints': function (done) {
    var cl = client(++ports)
      , io = create(cl)
      , tobiConnected = false
      , mainConnected = false
      , sid;

    io.configure(function () {
      io.set('polling duration', .05);
      io.set('close timeout', .05);
    });

    io.sockets.on('connection', function (socket) {
      mainConnected = true;

      socket.on('disconnect', function () {
        tobiConnected.should.be.false;
        cl.end();
        io.server.close();
        done();
      });
    });

    io.of('/tobi').on('connection', function () {
      tobiConnected = true;
    });

    cl.handshake(function (sid) {
      cl.get('/socket.io/{protocol}/jsonp-polling/' + sid);
    });
  },

  'test disconnecting a specific endpoint': function (done) {
    var cl = client(++ports)
      , io = create(cl)
      , wootDisconnected = false
      , mainDisconnected = false
      , checked = false;

    io.configure(function () {
      io.set('polling duration', 0);
      io.set('close timeout', .05);
    });

    io.sockets.on('connection', function (socket) {
      socket.on('message', function (data) {
        data.should.eql('ferret');
        mainDisconnected.should.be.false;
        wootDisconnected.should.be.true;
        checked = true;
      });

      socket.on('disconnect', function () {
        mainDisconnected = true;
        checked.should.be.true;
        cl.end();
        io.server.close();
        done();
      });
    });

    io.of('/woot').on('connection', function (socket) {
      socket.on('disconnect', function () {
        wootDisconnected = true;
      });
    });

    cl.handshake(function (sid) {
      cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function () {
        cl.post(
            '/socket.io/{protocol}/jsonp-polling/' + sid
          , JSON.stringify(parser.encodePacket({ type: 'connect', endpoint: '/woot' }))
          , function (res, data) {
              res.statusCode.should.eql(200);
              data.should.eql('1');

              cl.post(
                  '/socket.io/{protocol}/jsonp-polling/' + sid
                , JSON.stringify(parser.encodePacket({ type: 'disconnect', endpoint: '/woot' }))
                , function (res, data) {
                    res.statusCode.should.eql(200);
                    data.should.eql('1');

                    cl.post(
                        '/socket.io/{protocol}/jsonp-polling/' + sid
                      , JSON.stringify(parser.encodePacket({ type: 'message', data: 'ferret' }))
                      , function (res, data) {
                          res.statusCode.should.eql(200);
                          data.should.eql('1');
                        }
                    );
                  }
              );
            }
        );
      });
    });
  },

  'test that disconnecting disconnects all endpoints': function (done) {
    var cl = client(++ports)
      , io = create(cl)
      , aDisconnected = false
      , bDisconnected = false;

    io.configure(function () {
      io.set('polling duration', .2);
      io.set('close timeout', .5);
    });

    io.sockets.on('connection', function (socket) {
      socket.on('disconnect', function () {
        setTimeout(function () {
          aDisconnected.should.be.true;
          bDisconnected.should.be.true;
          cl.end();
          io.server.close();
          done();
        }, 50);
      });
    });

    io.of('/a').on('connection', function (socket) {
      socket.on('disconnect', function (msg) {
        aDisconnected = true;
      });
    });

    io.of('/b').on('connection', function (socket) {
      socket.on('disconnect', function (msg) {
        bDisconnected = true;
      });
    });

    cl.handshake(function (sid) {
      cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
        res.statusCode.should.eql(200);
        msgs.should.have.length(1);
        msgs[0].should.eql({ type: 'noop', endpoint: '' });

        cl.post(
            '/socket.io/{protocol}/jsonp-polling/' + sid
          , JSON.stringify(parser.encodePacket({ type: 'connect', endpoint: '/a' }))
          , function (res, data) {
              res.statusCode.should.eql(200);
              data.should.eql('1');
            }
        );

        cl.post(
            '/socket.io/{protocol}/jsonp-polling/' + sid
          , JSON.stringify(parser.encodePacket({ type: 'connect', endpoint: '/b' }))
          , function (res, data) {
              res.statusCode.should.eql(200);
              data.should.eql('1');
            }
        );
      });
    });
  },

  'test messaging a specific endpoint': function (done) {
    var cl = client(++ports)
      , io = create(cl)
      , messaged = true
      , aMessaged = false
      , bMessaged = false;

    io.configure(function () {
      io.set('polling duration', 0);
      io.set('close timeout', .05);
    });

    io.sockets.on('connection', function (socket) {
      socket.on('message', function (msg) {
        msg.should.eql('');
        messaged = true;
      });

      socket.on('disconnect', function () {
        messaged.should.be.true;
        aMessaged.should.be.true;
        bMessaged.should.be.true;
        cl.end();
        io.server.close();
        done();
      });
    });

    io.of('/a').on('connection', function (socket) {
      socket.on('message', function (msg) {
        msg.should.eql('a');
        aMessaged = true;
      });
    });

    io.of('/b').on('connection', function (socket) {
      socket.on('message', function (msg) {
        msg.should.eql('b');
        bMessaged = true;
      });
    });

    cl.handshake(function (sid) {
      cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, data) {
        cl.post(
            '/socket.io/{protocol}/jsonp-polling/' + sid
          , JSON.stringify(parser.encodePacket({ type: 'message', data: '' }))
          , function (res, data) {
              res.statusCode.should.eql(200);
              data.should.eql('1');
            }
        );

        cl.post(
            '/socket.io/{protocol}/jsonp-polling/' + sid
          , JSON.stringify(parser.encodePacket({ type: 'connect', endpoint: '/a' }))
          , function (res, data) {
              res.statusCode.should.eql(200);
              data.should.eql('1');

              cl.post(
                  '/socket.io/{protocol}/jsonp-polling/' + sid
                , JSON.stringify(parser.encodePacket({ type: 'message', endpoint: '/a', data: 'a' }))
                , function (res, data) {
                    res.statusCode.should.eql(200);
                    data.should.eql('1');
                  }
              );
            }
        );

        cl.post(
            '/socket.io/{protocol}/jsonp-polling/' + sid
          , JSON.stringify(parser.encodePacket({ type: 'connect', endpoint: '/b' }))
          , function (res, data) {
              res.statusCode.should.eql(200);
              data.should.eql('1');

              cl.post(
                  '/socket.io/{protocol}/jsonp-polling/' + sid
                , JSON.stringify(parser.encodePacket({ type: 'message', endpoint: '/b', data: 'b' }))
                , function (res, data) {
                    res.statusCode.should.eql(200);
                    data.should.eql('1');
                  }
              );
            }
        );
      });
    });
  }

};
