'use strict';

/*
 * The goal of this test is to make sure that errors thrown within domains
 * are handled correctly. It checks that the process' 'uncaughtException' event
 * is emitted when appropriate, and not emitted when it shouldn't. It also
 * checks that the proper domain error handlers are called when they should
 * be called, and not called when they shouldn't.
 */

const common = require('../common');
const assert = require('assert');
const domain = require('domain');
const child_process = require('child_process');

const tests = [];

function test1() {
  /*
   * Throwing from an async callback from within a domain that doesn't have
   * an error handler must result in emitting the process' uncaughtException
   * event.
   */
  const d = domain.create();
  d.run(function() {
    setTimeout(function onTimeout() {
      throw new Error('boom!');
    }, 1);
  });
}

tests.push({
  fn: test1,
  expectedMessages: ['uncaughtException']
});

function test2() {
  /*
   * Throwing from from within a domain that doesn't have an error handler must
   * result in emitting the process' uncaughtException event.
   */
  const d2 = domain.create();
  d2.run(function() {
    throw new Error('boom!');
  });
}

tests.push({
  fn: test2,
  expectedMessages: ['uncaughtException']
});

function test3() {
  /*
   * This test creates two nested domains: d3 and d4. d4 doesn't register an
   * error handler, but d3 does. The error is handled by the d3 domain and thus
   * an 'uncaughtException' event should _not_ be emitted.
   */
  const d3 = domain.create();
  const d4 = domain.create();

  d3.on('error', function onErrorInD3Domain() {
    process.send('errorHandledByDomain');
  });

  d3.run(function() {
    d4.run(function() {
      throw new Error('boom!');
    });
  });
}

tests.push({
  fn: test3,
  expectedMessages: ['errorHandledByDomain']
});

function test4() {
  /*
   * This test creates two nested domains: d5 and d6. d6 doesn't register an
   * error handler. When the timer's callback is called, because async
   * operations like timer callbacks are bound to the domain that was active
   * at the time of their creation, and because both d5 and d6 domains have
   * exited by the time the timer's callback is called, its callback runs with
   * only d6 on the domains stack. Since d6 doesn't register an error handler,
   * the process' uncaughtException event should be emitted.
   */
  const d5 = domain.create();
  const d6 = domain.create();

  d5.on('error', function onErrorInD2Domain() {
    process.send('errorHandledByDomain');
  });

  d5.run(function() {
    d6.run(function() {
      setTimeout(function onTimeout() {
        throw new Error('boom!');
      }, 1);
    });
  });
}

tests.push({
  fn: test4,
  expectedMessages: ['uncaughtException']
});

function test5() {
  /*
   * This test creates two nested domains: d7 and d8. d8 _does_ register an
   * error handler, so throwing within that domain should not emit an uncaught
   * exception.
   */
  const d7 = domain.create();
  const d8 = domain.create();

  d8.on('error', function onErrorInD3Domain() {
    process.send('errorHandledByDomain');
  });

  d7.run(function() {
    d8.run(function() {
      throw new Error('boom!');
    });
  });
}
tests.push({
  fn: test5,
  expectedMessages: ['errorHandledByDomain']
});

function test6() {
  /*
   * This test creates two nested domains: d9 and d10. d10 _does_ register an
   * error handler, so throwing within that domain in an async callback should
   * _not_ emit an uncaught exception.
   */
  const d9 = domain.create();
  const d10 = domain.create();

  d10.on('error', function onErrorInD2Domain() {
    process.send('errorHandledByDomain');
  });

  d9.run(function() {
    d10.run(function() {
      setTimeout(function onTimeout() {
        throw new Error('boom!');
      }, 1);
    });
  });
}

tests.push({
  fn: test6,
  expectedMessages: ['errorHandledByDomain']
});

if (process.argv[2] === 'child') {
  const testIndex = process.argv[3];
  process.on('uncaughtException', function onUncaughtException() {
    process.send('uncaughtException');
  });

  tests[testIndex].fn();
} else {
  // Run each test's function in a child process. Listen on
  // messages sent by each child process and compare expected
  // messages defined for each test with the actual received messages.
  tests.forEach(function doTest(test, testIndex) {
    const testProcess = child_process.fork(__filename, ['child', testIndex]);

    testProcess.on('message', function onMsg(msg) {
      if (test.messagesReceived === undefined)
        test.messagesReceived = [];

      test.messagesReceived.push(msg);
    });

    testProcess.on('disconnect', common.mustCall(function onExit() {
      // Make sure that all expected messages were sent from the
      // child process
      test.expectedMessages.forEach(function(expectedMessage) {
        const msgs = test.messagesReceived;
        if (msgs === undefined || !msgs.includes(expectedMessage)) {
          assert.fail(`test ${test.fn.name} should have sent message: ${
            expectedMessage} but didn't`);
        }
      });

      if (test.messagesReceived) {
        test.messagesReceived.forEach(function(receivedMessage) {
          if (!test.expectedMessages.includes(receivedMessage)) {
            assert.fail(`test ${test.fn.name} should not have sent message: ${
              receivedMessage} but did`);
          }
        });
      }
    }));
  });
}
