'use strict';

// Testcase to check reporting of uv handles.
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const path = require('path');
if (common.isIBMi)
  common.skip('IBMi does not support fs.watch()');

// This is quite similar to common.PIPE except that it uses an extended prefix
// of "\\?\pipe" on windows.
const PIPE = (() => {
  const localRelative = path.relative(process.cwd(), `${tmpdir.path}/`);
  const pipePrefix = common.isWindows ? '\\\\?\\pipe\\' : localRelative;
  const pipeName = `node-test.${process.pid}.sock`;
  return path.join(pipePrefix, pipeName);
})();

function createFsHandle(childData) {
  const fs = require('fs');
  // Watching files should result in fs_event/fs_poll uv handles.
  let watcher;
  try {
    watcher = fs.watch(__filename);
  } catch {
    // fs.watch() unavailable
  }
  fs.watchFile(__filename, () => {});
  childData.skip_fs_watch = watcher === undefined;

  return () => {
    if (watcher) watcher.close();
    fs.unwatchFile(__filename);
  };
}

function createChildProcessHandle(childData) {
  const spawn = require('child_process').spawn;
  // Child should exist when this returns as child_process.pid must be set.
  const cp = spawn(process.execPath,
                   ['-e', "process.stdin.on('data', (x) => " +
          'console.log(x.toString()));']);
  childData.pid = cp.pid;

  return () => {
    cp.kill();
  };
}

function createTimerHandle() {
  const timeout = setInterval(() => {}, 1000);
  // Make sure the timer doesn't keep the test alive and let
  // us check we detect unref'd handles correctly.
  timeout.unref();
  return () => {
    clearInterval(timeout);
  };
}

function createTcpHandle(childData) {
  const http = require('http');

  return new Promise((resolve) => {
    // Simple server/connection to create tcp uv handles.
    const server = http.createServer((req, res) => {
      req.on('end', () => {
        resolve(() => {
          res.writeHead(200, { 'Content-Type': 'text/plain' });
          res.end();
          server.close();
        });
      });
      req.resume();
    });
    server.listen(() => {
      childData.tcp_address = server.address();
      http.get({ port: server.address().port });
    });
  });
}

function createUdpHandle(childData) {
  // Datagram socket for udp uv handles.
  const dgram = require('dgram');
  const udpSocket = dgram.createSocket('udp4');
  const connectedUdpSocket = dgram.createSocket('udp4');

  return new Promise((resolve) => {
    udpSocket.bind({}, common.mustCall(() => {
      connectedUdpSocket.connect(udpSocket.address().port);

      childData.udp_address = udpSocket.address();
      resolve(() => {
        connectedUdpSocket.close();
        udpSocket.close();
      });
    }));
  });
}

function createNamedPipeHandle(childData) {
  const net = require('net');
  const sockPath = PIPE;
  return new Promise((resolve) => {
    const server = net.createServer((socket) => {
      childData.pipe_sock_path = server.address();
      resolve(() => {
        socket.end();
        server.close();
      });
    });
    server.listen(
      sockPath,
      () => {
        net.connect(sockPath, (socket) => {});
      });
  });
}

async function child() {
  // Exit on loss of parent process
  const exit = () => process.exit(2);
  process.on('disconnect', exit);

  const childData = {};
  const disposes = await Promise.all([
    createFsHandle(childData),
    createChildProcessHandle(childData),
    createTimerHandle(childData),
    createTcpHandle(childData),
    createUdpHandle(childData),
    createNamedPipeHandle(childData),
  ]);
  process.send(childData);

  // Generate the report while the connection is active.
  console.log(JSON.stringify(process.report.getReport(), null, 2));

  // Tidy up to allow process to exit cleanly.
  disposes.forEach((it) => {
    it();
  });
  process.removeListener('disconnect', exit);
}

if (process.argv[2] === 'child') {
  child();
} else {
  const helper = require('../common/report.js');
  const fork = require('child_process').fork;
  const assert = require('assert');
  tmpdir.refresh();
  const options = { encoding: 'utf8', silent: true, cwd: tmpdir.path };
  const child = fork(__filename, ['child'], options);
  let child_data;
  child.on('message', (data) => { child_data = data; });
  let stderr = '';
  child.stderr.on('data', (chunk) => { stderr += chunk; });
  let stdout = '';
  const report_msg = 'Report files were written: unexpectedly';
  child.stdout.on('data', (chunk) => { stdout += chunk; });
  child.on('exit', common.mustCall((code, signal) => {
    assert.strictEqual(stderr.trim(), '');
    assert.strictEqual(code, 0, 'Process exited unexpectedly with code: ' +
                       `${code}`);
    assert.strictEqual(signal, null, 'Process should have exited cleanly,' +
                       ` but did not: ${signal}`);

    const reports = helper.findReports(child.pid, tmpdir.path);
    assert.deepStrictEqual(reports, [], report_msg, reports);

    // Test libuv handle key order
    {
      const get_libuv = /"libuv":\s\[([\s\S]*?)\]/g;
      const get_handle_inner = /{([\s\S]*?),*?}/g;
      const libuv_handles_str = get_libuv.exec(stdout)[1];
      const libuv_handles_array = libuv_handles_str.match(get_handle_inner);
      for (const i of libuv_handles_array) {
        // Exclude nested structure
        if (i.includes('type')) {
          const handle_keys = i.match(/(".*"):/g);
          assert(handle_keys[0], 'type');
          assert(handle_keys[1], 'is_active');
        }
      }
    }

    const report = JSON.parse(stdout);
    const prefix = common.isWindows ? '\\\\?\\' : '';
    const expected_filename = `${prefix}${__filename}`;
    const found_tcp = [];
    const found_udp = [];
    const found_named_pipe = [];
    // Functions are named to aid debugging when they are not called.
    const validators = {
      fs_event: common.mustCall(function fs_event_validator(handle) {
        if (!child_data.skip_fs_watch) {
          assert.strictEqual(handle.filename, expected_filename);
          assert(handle.is_referenced);
        }
      }),
      fs_poll: common.mustCall(function fs_poll_validator(handle) {
        assert.strictEqual(handle.filename, expected_filename);
        assert(handle.is_referenced);
      }),
      loop: common.mustCall(function loop_validator(handle) {
        assert.strictEqual(typeof handle.loopIdleTimeSeconds, 'number');
      }),
      pipe: common.mustCallAtLeast(function pipe_validator(handle) {
        assert(handle.is_referenced);
        // Pipe handles. The report should contain three pipes:
        // 1. The server's listening pipe.
        // 2. The inbound pipe making the request.
        // 3. The outbound pipe sending the response.
        //
        // There is no way to distinguish inbound and outbound in a cross
        // platform manner, so we just check inbound here.
        const sockPath = child_data.pipe_sock_path;
        if (handle.localEndpoint === sockPath) {
          if (handle.writable === false) {
            found_named_pipe.push('listening');
          }
        } else if (handle.remoteEndpoint === sockPath) {
          found_named_pipe.push('inbound');
        }
      }),
      process: common.mustCall(function process_validator(handle) {
        assert.strictEqual(handle.pid, child_data.pid);
        assert(handle.is_referenced);
      }),
      tcp: common.mustCall(function tcp_validator(handle) {
        // TCP handles. The report should contain three sockets:
        // 1. The server's listening socket.
        // 2. The inbound socket making the request.
        // 3. The outbound socket sending the response.
        const port = child_data.tcp_address.port;
        if (handle.localEndpoint.port === port) {
          if (handle.remoteEndpoint === null) {
            found_tcp.push('listening');
          } else {
            found_tcp.push('inbound');
          }
        } else if (handle.remoteEndpoint.port === port) {
          found_tcp.push('outbound');
        }
        assert(handle.is_referenced);
      }, 3),
      timer: common.mustCallAtLeast(function timer_validator(handle) {
        assert(!handle.is_referenced);
        assert.strictEqual(handle.repeat, 0);
      }),
      udp: common.mustCall(function udp_validator(handle) {
        if (handle.remoteEndpoint === null) {
          assert.strictEqual(handle.localEndpoint.port,
                             child_data.udp_address.port);
          found_udp.push('unconnected');
        } else {
          assert.strictEqual(handle.remoteEndpoint.port,
                             child_data.udp_address.port);
          found_udp.push('connected');
        }
        assert(handle.is_referenced);
        assert.strictEqual(handle.writeQueueSize, 0);
        assert.strictEqual(handle.writeQueueCount, 0);
      }, 2),
    };

    for (const entry of report.libuv) {
      if (validators[entry.type]) validators[entry.type](entry);
    }
    for (const socket of ['listening', 'inbound', 'outbound']) {
      assert(found_tcp.includes(socket), `${socket} TCP socket was not found`);
    }
    for (const socket of ['connected', 'unconnected']) {
      assert(found_udp.includes(socket), `${socket} UDP socket was not found`);
    }
    for (const socket of ['listening', 'inbound']) {
      assert(found_named_pipe.includes(socket), `${socket} named pipe socket was not found`);
    }

    // Common report tests.
    helper.validateContent(stdout);
  }));
}
