// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/lib/internal/test_runner/harness.js
'use strict'
const {
  ArrayPrototypeForEach,
  SafeMap,
  SafeWeakSet
} = require('#internal/per_context/primordials')
const {
  createHook,
  executionAsyncId
} = require('async_hooks')
const {
  codes: {
    ERR_TEST_FAILURE
  }
} = require('#internal/errors')
const { kEmptyObject } = require('#internal/util')
const { getOptionValue } = require('#internal/options')
const { kCancelledByParent, Test, ItTest, Suite } = require('#internal/test_runner/test')
const { setupTestReporters } = require('#internal/test_runner/utils')
const { bigint: hrtime } = process.hrtime

const isTestRunnerCli = getOptionValue('--test')
const testResources = new SafeMap()
const wasRootSetup = new SafeWeakSet()

function createTestTree (options = kEmptyObject) {
  return setup(new Test({ __proto__: null, ...options, name: '<root>' }))
}

function createProcessEventHandler (eventName, rootTest) {
  return (err) => {
    // Check if this error is coming from a test. If it is, fail the test.
    const test = testResources.get(executionAsyncId())

    if (!test) {
      // Node.js 14.x crashes if the error is throw here.
      if (process.version.startsWith('v14.')) return
      throw err
    }

    if (test.finished) {
      // If the test is already finished, report this as a top level
      // diagnostic since this is a malformed test.
      const msg = `Warning: Test "${test.name}" generated asynchronous ` +
        'activity after the test ended. This activity created the error ' +
        `"${err}" and would have caused the test to fail, but instead ` +
        `triggered an ${eventName} event.`

      rootTest.diagnostic(msg)
      process.exitCode = 1
      return
    }

    test.fail(new ERR_TEST_FAILURE(err, eventName))
    test.postRun()
  }
}

function setup (root) {
  if (wasRootSetup.has(root)) {
    return root
  }
  const hook = createHook({
    init (asyncId, type, triggerAsyncId, resource) {
      if (resource instanceof Test) {
        testResources.set(asyncId, resource)
        return
      }

      const parent = testResources.get(triggerAsyncId)

      if (parent !== undefined) {
        testResources.set(asyncId, parent)
      }
    },
    destroy (asyncId) {
      testResources.delete(asyncId)
    }
  })

  hook.enable()

  const exceptionHandler =
    createProcessEventHandler('uncaughtException', root)
  const rejectionHandler =
    createProcessEventHandler('unhandledRejection', root)

  const exitHandler = () => {
    root.postRun(new ERR_TEST_FAILURE(
      'Promise resolution is still pending but the event loop has already resolved',
      kCancelledByParent))

    hook.disable()
    process.removeListener('unhandledRejection', rejectionHandler)
    process.removeListener('uncaughtException', exceptionHandler)
  }

  const terminationHandler = () => {
    exitHandler()
    process.exit()
  }

  process.on('uncaughtException', exceptionHandler)
  process.on('unhandledRejection', rejectionHandler)
  process.on('beforeExit', exitHandler)
  // TODO(MoLow): Make it configurable to hook when isTestRunnerCli === false.
  if (isTestRunnerCli) {
    process.on('SIGINT', terminationHandler)
    process.on('SIGTERM', terminationHandler)
  }

  root.startTime = hrtime()

  wasRootSetup.add(root)
  return root
}

let globalRoot
function getGlobalRoot () {
  if (!globalRoot) {
    globalRoot = createTestTree()
    globalRoot.reporter.once('test:fail', () => {
      process.exitCode = 1
    })
    setupTestReporters(globalRoot.reporter)
  }
  return globalRoot
}

function test (name, options, fn) {
  const subtest = getGlobalRoot().createSubtest(Test, name, options, fn)
  return subtest.start()
}

function runInParentContext (Factory) {
  function run (name, options, fn, overrides) {
    const parent = testResources.get(executionAsyncId()) || getGlobalRoot()
    const subtest = parent.createSubtest(Factory, name, options, fn, overrides)
    if (parent === getGlobalRoot()) {
      subtest.start()
    }
  }

  const cb = (name, options, fn) => {
    run(name, options, fn)
  }

  ArrayPrototypeForEach(['skip', 'todo'], (keyword) => {
    cb[keyword] = (name, options, fn) => {
      run(name, options, fn, { [keyword]: true })
    }
  })
  return cb
}

function hook (hook) {
  return (fn, options) => {
    const parent = testResources.get(executionAsyncId()) || getGlobalRoot()
    parent.createHook(hook, fn, options)
  }
}

module.exports = {
  createTestTree,
  test,
  describe: runInParentContext(Suite),
  it: runInParentContext(ItTest),
  before: hook('before'),
  after: hook('after'),
  beforeEach: hook('beforeEach'),
  afterEach: hook('afterEach')
}
