// Covers TCPWRAP and related TCPCONNECTWRAP
'use strict';

const common = require('../common');
if (!common.hasIPv6)
  common.skip('IPv6 support required');

const assert = require('assert');
const tick = require('../common/tick');
const initHooks = require('./init-hooks');
const { checkInvocations } = require('./hook-checks');
const net = require('net');

let tcp1, tcp2;
let tcpserver;
let tcpconnect;

const hooks = initHooks();
hooks.enable();

const server = net
  .createServer(common.mustCall(onconnection))
  .on('listening', common.mustCall(onlistening));

// Calling server.listen creates a TCPWRAP synchronously
{
  server.listen(0);
  const tcpsservers = hooks.activitiesOfTypes('TCPSERVERWRAP');
  const tcpconnects = hooks.activitiesOfTypes('TCPCONNECTWRAP');
  assert.strictEqual(tcpsservers.length, 1);
  assert.strictEqual(tcpconnects.length, 0);
  tcpserver = tcpsservers[0];
  assert.strictEqual(tcpserver.type, 'TCPSERVERWRAP');
  assert.strictEqual(typeof tcpserver.uid, 'number');
  assert.strictEqual(typeof tcpserver.triggerAsyncId, 'number');
  checkInvocations(tcpserver, { init: 1 }, 'when calling server.listen');
}

// Calling net.connect creates another TCPWRAP synchronously
{
  net.connect(
    { port: server.address().port, host: '::1' },
    common.mustCall(onconnected));
  const tcps = hooks.activitiesOfTypes('TCPWRAP');
  assert.strictEqual(tcps.length, 1);
  process.nextTick(() => {
    const tcpconnects = hooks.activitiesOfTypes('TCPCONNECTWRAP');
    assert.strictEqual(tcpconnects.length, 1);
  });

  tcp1 = tcps[0];
  assert.strictEqual(tcps.length, 1);
  assert.strictEqual(tcp1.type, 'TCPWRAP');
  assert.strictEqual(typeof tcp1.uid, 'number');
  assert.strictEqual(typeof tcp1.triggerAsyncId, 'number');

  checkInvocations(tcpserver, { init: 1 },
                   'tcpserver when client is connecting');
  checkInvocations(tcp1, { init: 1 }, 'tcp1 when client is connecting');
}

function onlistening() {
  assert.strictEqual(hooks.activitiesOfTypes('TCPWRAP').length, 1);
}

// Depending on timing we see client: onconnected or server: onconnection first
// Therefore we can't depend on any ordering, but when we see a connection for
// the first time we assign the tcpconnectwrap.
function ontcpConnection(serverConnection) {
  if (tcpconnect != null) {
    // When client receives connection first ('onconnected') and the server
    // second then we see an 'after' here, otherwise not
    const expected = serverConnection ?
      { init: 1, before: 1, after: 1 } :
      { init: 1, before: 1 };
    checkInvocations(
      tcpconnect, expected,
      'tcpconnect: when both client and server received connection');
    return;
  }

  // only focusing on TCPCONNECTWRAP here
  const tcpconnects = hooks.activitiesOfTypes('TCPCONNECTWRAP');
  assert.strictEqual(tcpconnects.length, 1);
  tcpconnect = tcpconnects[0];
  assert.strictEqual(tcpconnect.type, 'TCPCONNECTWRAP');
  assert.strictEqual(typeof tcpconnect.uid, 'number');
  assert.strictEqual(typeof tcpconnect.triggerAsyncId, 'number');
  // When client receives connection first ('onconnected'), we 'before' has
  // been invoked at this point already, otherwise it only was 'init'ed
  const expected = serverConnection ? { init: 1 } : { init: 1, before: 1 };
  checkInvocations(tcpconnect, expected,
                   'tcpconnect: when tcp connection is established');
}

let serverConnected = false;
function onconnected() {
  ontcpConnection(false);
  // In the case that the client connects before the server TCPWRAP 'before'
  // and 'after' weren't invoked yet. Also @see ontcpConnection.
  const expected = serverConnected ?
    { init: 1, before: 1, after: 1 } :
    { init: 1 };
  checkInvocations(tcpserver, expected, 'tcpserver when client connects');
  checkInvocations(tcp1, { init: 1 }, 'tcp1 when client connects');
}

function onconnection(c) {
  serverConnected = true;
  ontcpConnection(true);

  const tcps = hooks.activitiesOfTypes([ 'TCPWRAP' ]);
  const tcpconnects = hooks.activitiesOfTypes('TCPCONNECTWRAP');
  assert.strictEqual(tcps.length, 2);
  assert.strictEqual(tcpconnects.length, 1);
  tcp2 = tcps[1];
  assert.strictEqual(tcp2.type, 'TCPWRAP');
  assert.strictEqual(typeof tcp2.uid, 'number');
  assert.strictEqual(typeof tcp2.triggerAsyncId, 'number');

  checkInvocations(tcpserver, { init: 1, before: 1 },
                   'tcpserver when server receives connection');
  checkInvocations(tcp1, { init: 1 }, 'tcp1 when server receives connection');
  checkInvocations(tcp2, { init: 1 }, 'tcp2 when server receives connection');

  c.end();
  this.close(common.mustCall(onserverClosed));
}

function onserverClosed() {
  checkInvocations(tcpserver, { init: 1, before: 1, after: 1, destroy: 1 },
                   'tcpserver when server is closed');
  setImmediate(() => {
    checkInvocations(tcp1, { init: 1, before: 2, after: 2, destroy: 1 },
                     'tcp1 after server is closed');
  });
  checkInvocations(tcp2, { init: 1, before: 1, after: 1 },
                   'tcp2 synchronously when server is closed');

  tick(2, () => {
    checkInvocations(tcp2, { init: 1, before: 2, after: 2, destroy: 1 },
                     'tcp2 when server is closed');
    checkInvocations(tcpconnect, { init: 1, before: 1, after: 1, destroy: 1 },
                     'tcpconnect when server is closed');
  });
}

process.on('exit', onexit);

function onexit() {
  hooks.disable();
  hooks.sanityCheck([ 'TCPWRAP', 'TCPSERVERWRAP', 'TCPCONNECTWRAP' ]);

  checkInvocations(tcpserver, { init: 1, before: 1, after: 1, destroy: 1 },
                   'tcpserver when process exits');
  checkInvocations(
    tcp1, { init: 1, before: 2, after: 2, destroy: 1 },
    'tcp1 when process exits');
  checkInvocations(
    tcp2, { init: 1, before: 2, after: 2, destroy: 1 },
    'tcp2 when process exits');
  checkInvocations(
    tcpconnect, { init: 1, before: 1, after: 1, destroy: 1 },
    'tcpconnect when process exits');
}
