'use strict';
const common = require('../common');
if (!common.hasCrypto)
  common.skip('missing crypto');

const assert = require('assert');
const tls = require('tls');
const fixtures = require('../common/fixtures');

const options = {
  key: fixtures.readKey('agent2-key.pem'),
  cert: fixtures.readKey('agent2-cert.pem')
};

const smallMessage = Buffer.from('hello world');
// Used to test .pause(), so needs to be larger than the internal buffer
const largeMessage = Buffer.alloc(64 * 1024).fill('hello world');

// Test typical usage
tls.createServer(options, common.mustCall(function(socket) {
  this.close();
  socket.end(smallMessage);
})).listen(0, common.mustCall(function() {
  let received = 0;
  const buffers = [];
  const sockBuf = Buffer.alloc(8);
  tls.connect({
    port: this.address().port,
    rejectUnauthorized: false,
    onread: {
      buffer: sockBuf,
      callback: common.mustCallAtLeast((nread, buf) => {
        assert.strictEqual(buf, sockBuf);
        received += nread;
        buffers.push(Buffer.from(buf.slice(0, nread)));
      })
    }
  }).on('data', common.mustNotCall()).on('end', common.mustCall(() => {
    assert.strictEqual(received, smallMessage.length);
    assert.deepStrictEqual(Buffer.concat(buffers), smallMessage);
  }));
}));

// Test Uint8Array support
tls.createServer(options, common.mustCall(function(socket) {
  this.close();
  socket.end(smallMessage);
})).listen(0, common.mustCall(function() {
  let received = 0;
  let incoming = new Uint8Array(0);
  const sockBuf = new Uint8Array(8);
  tls.connect({
    port: this.address().port,
    rejectUnauthorized: false,
    onread: {
      buffer: sockBuf,
      callback: common.mustCallAtLeast((nread, buf) => {
        assert.strictEqual(buf, sockBuf);
        received += nread;
        const newIncoming = new Uint8Array(incoming.length + nread);
        newIncoming.set(incoming);
        newIncoming.set(buf.slice(0, nread), incoming.length);
        incoming = newIncoming;
      })
    }
  }).on('data', common.mustNotCall()).on('end', common.mustCall(() => {
    assert.strictEqual(received, smallMessage.length);
    assert.deepStrictEqual(incoming, new Uint8Array(smallMessage));
  }));
}));

// Test Buffer callback usage
tls.createServer(options, common.mustCall(function(socket) {
  this.close();
  socket.end(smallMessage);
})).listen(0, common.mustCall(function() {
  let received = 0;
  const incoming = [];
  const bufPool = [ Buffer.alloc(2), Buffer.alloc(2), Buffer.alloc(2) ];
  let bufPoolIdx = -1;
  let bufPoolUsage = 0;
  tls.connect({
    port: this.address().port,
    rejectUnauthorized: false,
    onread: {
      buffer: () => {
        ++bufPoolUsage;
        bufPoolIdx = (bufPoolIdx + 1) % bufPool.length;
        return bufPool[bufPoolIdx];
      },
      callback: common.mustCallAtLeast((nread, buf) => {
        assert.strictEqual(buf, bufPool[bufPoolIdx]);
        received += nread;
        incoming.push(Buffer.from(buf.slice(0, nread)));
      })
    }
  }).on('data', common.mustNotCall()).on('end', common.mustCall(() => {
    assert.strictEqual(received, smallMessage.length);
    assert.deepStrictEqual(Buffer.concat(incoming), smallMessage);
    assert.strictEqual(bufPoolUsage, 7);
  }));
}));

// Test Uint8Array callback support
tls.createServer(options, common.mustCall(function(socket) {
  this.close();
  socket.end(smallMessage);
})).listen(0, common.mustCall(function() {
  let received = 0;
  let incoming = new Uint8Array(0);
  const bufPool = [ new Uint8Array(2), new Uint8Array(2), new Uint8Array(2) ];
  let bufPoolIdx = -1;
  let bufPoolUsage = 0;
  tls.connect({
    port: this.address().port,
    rejectUnauthorized: false,
    onread: {
      buffer: () => {
        ++bufPoolUsage;
        bufPoolIdx = (bufPoolIdx + 1) % bufPool.length;
        return bufPool[bufPoolIdx];
      },
      callback: common.mustCallAtLeast((nread, buf) => {
        assert.strictEqual(buf, bufPool[bufPoolIdx]);
        received += nread;
        const newIncoming = new Uint8Array(incoming.length + nread);
        newIncoming.set(incoming);
        newIncoming.set(buf.slice(0, nread), incoming.length);
        incoming = newIncoming;
      }),
    }
  }).on('data', common.mustNotCall()).on('end', common.mustCall(() => {
    assert.strictEqual(received, smallMessage.length);
    assert.deepStrictEqual(incoming, new Uint8Array(smallMessage));
    assert.strictEqual(bufPoolUsage, 7);
  }));
}));

// Test explicit socket pause
tls.createServer(options, common.mustCall(function(socket) {
  this.close();
  // Need larger message here to observe the pause
  socket.end(largeMessage);
})).listen(0, common.mustCall(function() {
  let received = 0;
  const buffers = [];
  const sockBuf = Buffer.alloc(64);
  let pauseScheduled = false;
  const client = tls.connect({
    port: this.address().port,
    rejectUnauthorized: false,
    onread: {
      buffer: sockBuf,
      callback: common.mustCallAtLeast((nread, buf) => {
        assert.strictEqual(buf, sockBuf);
        received += nread;
        buffers.push(Buffer.from(buf.slice(0, nread)));
        if (!pauseScheduled) {
          pauseScheduled = true;
          client.pause();
          setTimeout(() => {
            client.resume();
          }, 100);
        }
      }),
    }
  }).on('data', common.mustNotCall()).on('end', common.mustCall(() => {
    assert.strictEqual(received, largeMessage.length);
    assert.deepStrictEqual(Buffer.concat(buffers), largeMessage);
  }));
}));

// Test implicit socket pause
tls.createServer(options, common.mustCall(function(socket) {
  this.close();
  // Need larger message here to observe the pause
  socket.end(largeMessage);
})).listen(0, common.mustCall(function() {
  let received = 0;
  const buffers = [];
  const sockBuf = Buffer.alloc(64);
  let pauseScheduled = false;
  const client = tls.connect({
    port: this.address().port,
    rejectUnauthorized: false,
    onread: {
      buffer: sockBuf,
      callback: common.mustCallAtLeast((nread, buf) => {
        assert.strictEqual(buf, sockBuf);
        received += nread;
        buffers.push(Buffer.from(buf.slice(0, nread)));
        if (!pauseScheduled) {
          pauseScheduled = true;
          setTimeout(() => {
            client.resume();
          }, 100);
          return false;
        }
        return true;
      }),
    }
  }).on('data', common.mustNotCall()).on('end', common.mustCall(() => {
    assert.strictEqual(received, largeMessage.length);
    assert.deepStrictEqual(Buffer.concat(buffers), largeMessage);
  }));
}));
