// https://github.com/nodejs/node/blob/4788c0d2a02a2e7c7088c6f39d9f13a4209ba863/lib/internal/test_runner/utils.js
'use strict'
const {
  ArrayPrototypePush,
  ObjectGetOwnPropertyDescriptor,
  SafePromiseAllReturnArrayLike,
  RegExp,
  RegExpPrototypeExec,
  SafeMap
} = require('#internal/per_context/primordials')
const { basename } = require('path')
const { createWriteStream } = require('fs')
const { createDeferredPromise } = require('#internal/util')
const { getOptionValue } = require('#internal/options')

const {
  codes: {
    ERR_INVALID_ARG_VALUE,
    ERR_TEST_FAILURE
  },
  kIsNodeError
} = require('#internal/errors')
const { compose } = require('readable-stream')

const kMultipleCallbackInvocations = 'multipleCallbackInvocations'
const kRegExpPattern = /^\/(.*)\/([a-z]*)$/
const kSupportedFileExtensions = /\.[cm]?js$/
const kTestFilePattern = /((^test(-.+)?)|(.+[.\-_]test))\.[cm]?js$/

function doesPathMatchFilter (p) {
  return RegExpPrototypeExec(kTestFilePattern, basename(p)) !== null
}

function isSupportedFileType (p) {
  return RegExpPrototypeExec(kSupportedFileExtensions, p) !== null
}

function createDeferredCallback () {
  let calledCount = 0
  const { promise, resolve, reject } = createDeferredPromise()
  const cb = (err) => {
    calledCount++

    // If the callback is called a second time, let the user know, but
    // don't let them know more than once.
    if (calledCount > 1) {
      if (calledCount === 2) {
        throw new ERR_TEST_FAILURE(
          'callback invoked multiple times',
          kMultipleCallbackInvocations
        )
      }

      return
    }

    if (err) {
      return reject(err)
    }

    resolve()
  }

  return { promise, cb }
}

function isTestFailureError (err) {
  return err?.code === 'ERR_TEST_FAILURE' && kIsNodeError in err
}

function convertStringToRegExp (str, name) {
  const match = RegExpPrototypeExec(kRegExpPattern, str)
  const pattern = match?.[1] ?? str
  const flags = match?.[2] || ''

  try {
    return new RegExp(pattern, flags)
  } catch (err) {
    const msg = err?.message

    throw new ERR_INVALID_ARG_VALUE(
      name,
      str,
      `is an invalid regular expression.${msg ? ` ${msg}` : ''}`
    )
  }
}

const kBuiltinDestinations = new SafeMap([
  ['stdout', process.stdout],
  ['stderr', process.stderr]
])

const kBuiltinReporters = new SafeMap([
  ['spec', '#internal/test_runner/reporter/spec'],
  ['dot', '#internal/test_runner/reporter/dot'],
  ['tap', '#internal/test_runner/reporter/tap']
])

const kDefaultReporter = 'tap'
const kDefaultDestination = 'stdout'

function tryBuiltinReporter (name) {
  const builtinPath = kBuiltinReporters.get(name)

  if (builtinPath === undefined) {
    return
  }

  return require(builtinPath)
}

async function getReportersMap (reporters, destinations) {
  return SafePromiseAllReturnArrayLike(reporters, async (name, i) => {
    const destination = kBuiltinDestinations.get(destinations[i]) ?? createWriteStream(destinations[i])

    // Load the test reporter passed to --test-reporter
    let reporter = tryBuiltinReporter(name)

    if (reporter === undefined) {
      reporter = await import(name)
    }

    if (reporter?.default) {
      reporter = reporter.default
    }

    if (reporter?.prototype && ObjectGetOwnPropertyDescriptor(reporter.prototype, 'constructor')) {
      reporter = new reporter() // eslint-disable-line new-cap
    }

    if (!reporter) {
      throw new ERR_INVALID_ARG_VALUE('Reporter', name, 'is not a valid reporter')
    }

    return { __proto__: null, reporter, destination }
  })
}

async function setupTestReporters (testsStream) {
  const destinations = getOptionValue('--test-reporter-destination') ?? []
  const reporters = getOptionValue('--test-reporter') ?? []

  if (reporters.length === 0 && destinations.length === 0) {
    ArrayPrototypePush(reporters, kDefaultReporter)
  }

  if (reporters.length === 1 && destinations.length === 0) {
    ArrayPrototypePush(destinations, kDefaultDestination)
  }

  if (destinations.length !== reporters.length) {
    throw new ERR_INVALID_ARG_VALUE('--test-reporter', reporters,
      'must match the number of specified \'--test-reporter-destination\'')
  }

  const reportersMap = await getReportersMap(reporters, destinations)
  for (let i = 0; i < reportersMap.length; i++) {
    const { reporter, destination } = reportersMap[i]
    compose(testsStream, reporter).pipe(destination)
  }
}

module.exports = {
  convertStringToRegExp,
  createDeferredCallback,
  doesPathMatchFilter,
  isSupportedFileType,
  isTestFailureError,
  setupTestReporters
}
