// https://github.com/nodejs/node/blob/f8ce9117b19702487eb600493d941f7876e00e01/lib/internal/test_runner/tap_checker.js
'use strict'

const {
  ArrayPrototypeFilter,
  ArrayPrototypeFind,
  NumberParseInt
} = require('#internal/per_context/primordials')
const {
  codes: { ERR_TAP_VALIDATION_ERROR }
} = require('#internal/errors')
const { TokenKind } = require('#internal/test_runner/tap_lexer')

// TODO(@manekinekko): add more validation rules based on the TAP14 spec.
// See https://testanything.org/tap-version-14-specification.html
class TAPValidationStrategy {
  validate (ast) {
    this.#validateVersion(ast)
    this.#validatePlan(ast)
    this.#validateTestPoints(ast)

    return true
  }

  #validateVersion (ast) {
    const entry = ArrayPrototypeFind(
      ast,
      (node) => node.kind === TokenKind.TAP_VERSION
    )

    if (!entry) {
      throw new ERR_TAP_VALIDATION_ERROR('missing TAP version')
    }

    const { version } = entry.node

    // TAP14 specification is compatible with observed behavior of existing TAP13 consumers and producers
    if (version !== '14' && version !== '13') {
      throw new ERR_TAP_VALIDATION_ERROR('TAP version should be 13 or 14')
    }
  }

  #validatePlan (ast) {
    const entry = ArrayPrototypeFind(
      ast,
      (node) => node.kind === TokenKind.TAP_PLAN
    )

    if (!entry) {
      throw new ERR_TAP_VALIDATION_ERROR('missing TAP plan')
    }

    const plan = entry.node

    if (!plan.start) {
      throw new ERR_TAP_VALIDATION_ERROR('missing plan start')
    }

    if (!plan.end) {
      throw new ERR_TAP_VALIDATION_ERROR('missing plan end')
    }

    const planStart = NumberParseInt(plan.start, 10)
    const planEnd = NumberParseInt(plan.end, 10)

    if (planEnd !== 0 && planStart > planEnd) {
      throw new ERR_TAP_VALIDATION_ERROR(
        `plan start ${planStart} is greater than plan end ${planEnd}`
      )
    }
  }

  // TODO(@manekinekko): since we are dealing with a flat AST, we need to
  // validate test points grouped by their "nesting" level. This is because a set of
  // Test points belongs to a TAP document. Each new subtest block creates a new TAP document.
  // https://testanything.org/tap-version-14-specification.html#subtests
  #validateTestPoints (ast) {
    const bailoutEntry = ArrayPrototypeFind(
      ast,
      (node) => node.kind === TokenKind.TAP_BAIL_OUT
    )
    const planEntry = ArrayPrototypeFind(
      ast,
      (node) => node.kind === TokenKind.TAP_PLAN
    )
    const testPointEntries = ArrayPrototypeFilter(
      ast,
      (node) => node.kind === TokenKind.TAP_TEST_POINT
    )

    const plan = planEntry.node

    const planStart = NumberParseInt(plan.start, 10)
    const planEnd = NumberParseInt(plan.end, 10)

    if (planEnd === 0 && testPointEntries.length > 0) {
      throw new ERR_TAP_VALIDATION_ERROR(
        `found ${testPointEntries.length} Test Point${
          testPointEntries.length > 1 ? 's' : ''
        } but plan is ${planStart}..0`
      )
    }

    if (planEnd > 0) {
      if (testPointEntries.length === 0) {
        throw new ERR_TAP_VALIDATION_ERROR('missing Test Points')
      }

      if (!bailoutEntry && testPointEntries.length !== planEnd) {
        throw new ERR_TAP_VALIDATION_ERROR(
          `test Points count ${testPointEntries.length} does not match plan count ${planEnd}`
        )
      }

      for (let i = 0; i < testPointEntries.length; i++) {
        const test = testPointEntries[i].node
        const testId = NumberParseInt(test.id, 10)

        if (testId < planStart || testId > planEnd) {
          throw new ERR_TAP_VALIDATION_ERROR(
            `test ${testId} is out of plan range ${planStart}..${planEnd}`
          )
        }
      }
    }
  }
}

// TAP14 and TAP13 are compatible with each other
class TAP13ValidationStrategy extends TAPValidationStrategy {}
class TAP14ValidationStrategy extends TAPValidationStrategy {}

class TapChecker {
  static TAP13 = '13'
  static TAP14 = '14'

  constructor ({ specs }) {
    switch (specs) {
      case TapChecker.TAP13:
        this.strategy = new TAP13ValidationStrategy()
        break
      default:
        this.strategy = new TAP14ValidationStrategy()
    }
  }

  check (ast) {
    return this.strategy.validate(ast)
  }
}

module.exports = {
  TapChecker,
  TAP14ValidationStrategy,
  TAP13ValidationStrategy
}
