'use strict';
const common = require('../common');

// This test checks that the semantics of `util.callbackify` are as described in
// the API docs

const assert = require('assert');
const { callbackify } = require('util');
const { join } = require('path');
const { execFile } = require('child_process');
const fixtureDir = join(common.fixturesDir, 'uncaught-exceptions');
const values = [
  'hello world',
  null,
  undefined,
  false,
  0,
  {},
  { key: 'value' },
  Symbol('I am a symbol'),
  function ok() {},
  ['array', 'with', 4, 'values'],
  new Error('boo')
];

{
  // Test that the resolution value is passed as second argument to callback
  for (const value of values) {
    // Test and `async function`
    async function asyncFn() {
      return await Promise.resolve(value);
    }

    const cbAsyncFn = callbackify(asyncFn);
    cbAsyncFn(common.mustCall((err, ret) => {
      assert.ifError(err);
      assert.strictEqual(ret, value);
    }));

    // Test Promise factory
    function promiseFn() {
      return Promise.resolve(value);
    }

    const cbPromiseFn = callbackify(promiseFn);
    cbPromiseFn(common.mustCall((err, ret) => {
      assert.ifError(err);
      assert.strictEqual(ret, value);
    }));

    // Test Thenable
    function thenableFn() {
      return {
        then(onRes, onRej) {
          onRes(value);
        }
      };
    }

    const cbThenableFn = callbackify(thenableFn);
    cbThenableFn(common.mustCall((err, ret) => {
      assert.ifError(err);
      assert.strictEqual(ret, value);
    }));
  }
}

{
  // Test that rejection reason is passed as first argument to callback
  for (const value of values) {
    // Test an `async function`
    async function asyncFn() {
      return await Promise.reject(value);
    }

    const cbAsyncFn = callbackify(asyncFn);
    cbAsyncFn(common.mustCall((err, ret) => {
      assert.strictEqual(ret, undefined);
      if (err instanceof Error) {
        if ('reason' in err) {
          assert(!value);
          assert.strictEqual(err.code, 'ERR_FALSY_VALUE_REJECTION');
          assert.strictEqual(err.reason, value);
        } else {
          assert.strictEqual(String(value).endsWith(err.message), true);
        }
      } else {
        assert.strictEqual(err, value);
      }
    }));

    // test a Promise factory
    function promiseFn() {
      return Promise.reject(value);
    }

    const cbPromiseFn = callbackify(promiseFn);
    cbPromiseFn(common.mustCall((err, ret) => {
      assert.strictEqual(ret, undefined);
      if (err instanceof Error) {
        if ('reason' in err) {
          assert(!value);
          assert.strictEqual(err.code, 'ERR_FALSY_VALUE_REJECTION');
          assert.strictEqual(err.reason, value);
        } else {
          assert.strictEqual(String(value).endsWith(err.message), true);
        }
      } else {
        assert.strictEqual(err, value);
      }
    }));

    // Test Thenable
    function thenableFn() {
      return {
        then(onRes, onRej) {
          onRej(value);
        }
      };
    }

    const cbThenableFn = callbackify(thenableFn);
    cbThenableFn(common.mustCall((err, ret) => {
      assert.strictEqual(ret, undefined);
      if (err instanceof Error) {
        if ('reason' in err) {
          assert(!value);
          assert.strictEqual(err.code, 'ERR_FALSY_VALUE_REJECTION');
          assert.strictEqual(err.reason, value);
        } else {
          assert.strictEqual(String(value).endsWith(err.message), true);
        }
      } else {
        assert.strictEqual(err, value);
      }
    }));
  }
}

{
  // Test that arguments passed to callbackified function are passed to original
  for (const value of values) {
    async function asyncFn(arg) {
      assert.strictEqual(arg, value);
      return await Promise.resolve(arg);
    }

    const cbAsyncFn = callbackify(asyncFn);
    cbAsyncFn(value, common.mustCall((err, ret) => {
      assert.ifError(err);
      assert.strictEqual(ret, value);
    }));

    function promiseFn(arg) {
      assert.strictEqual(arg, value);
      return Promise.resolve(arg);
    }

    const cbPromiseFn = callbackify(promiseFn);
    cbPromiseFn(value, common.mustCall((err, ret) => {
      assert.ifError(err);
      assert.strictEqual(ret, value);
    }));
  }
}

{
  // Test that `this` binding is the same for callbackified and original
  for (const value of values) {
    const iAmThis = {
      fn(arg) {
        assert.strictEqual(this, iAmThis);
        return Promise.resolve(arg);
      },
    };
    iAmThis.cbFn = callbackify(iAmThis.fn);
    iAmThis.cbFn(value, common.mustCall(function(err, ret) {
      assert.ifError(err);
      assert.strictEqual(ret, value);
      assert.strictEqual(this, iAmThis);
    }));

    const iAmThat = {
      async fn(arg) {
        assert.strictEqual(this, iAmThat);
        return await Promise.resolve(arg);
      },
    };
    iAmThat.cbFn = callbackify(iAmThat.fn);
    iAmThat.cbFn(value, common.mustCall(function(err, ret) {
      assert.ifError(err);
      assert.strictEqual(ret, value);
      assert.strictEqual(this, iAmThat);
    }));
  }
}

{
  // Test that callback that throws emits an `uncaughtException` event
  const fixture = join(fixtureDir, 'callbackify1.js');
  execFile(
    process.execPath,
    [fixture],
    common.mustCall((err, stdout, stderr) => {
      assert.strictEqual(err.code, 1);
      assert.strictEqual(Object.getPrototypeOf(err).name, 'Error');
      assert.strictEqual(stdout, '');
      const errLines = stderr.trim().split(/[\r\n]+/);
      const errLine = errLines.find((l) => /^Error/.exec(l));
      assert.strictEqual(errLine, `Error: ${fixture}`);
    })
  );
}

{
  // Test that handled `uncaughtException` works and passes rejection reason
  const fixture = join(fixtureDir, 'callbackify2.js');
  execFile(
    process.execPath,
    [fixture],
    common.mustCall((err, stdout, stderr) => {
      assert.ifError(err);
      assert.strictEqual(stdout.trim(), fixture);
      assert.strictEqual(stderr, '');
    })
  );
}

{
  // Verify that non-function inputs throw.
  ['foo', null, undefined, false, 0, {}, Symbol(), []].forEach((value) => {
    assert.throws(() => {
      callbackify(value);
    }, common.expectsError({
      code: 'ERR_INVALID_ARG_TYPE',
      type: TypeError,
      message: 'The "original" argument must be of type function'
    }));
  });
}

{
  async function asyncFn() {
    return await Promise.resolve(42);
  }

  const cb = callbackify(asyncFn);
  const args = [];

  // Verify that the last argument to the callbackified function is a function.
  ['foo', null, undefined, false, 0, {}, Symbol(), []].forEach((value) => {
    args.push(value);
    assert.throws(() => {
      cb(...args);
    }, common.expectsError({
      code: 'ERR_INVALID_ARG_TYPE',
      type: TypeError,
      message: 'The "last argument" argument must be of type function'
    }));
  });
}
