'use strict'
// We need TWO queues (work and subtest) and one jobs pool
//
// The pool stores buffered subtests being run in parallel.
//
// When new subtests are created, they get put in the work queue and also
// in the subtests queue if they are buffered and jobs>0.  When we put a
// test in the subtest queue, we also process it.
//
// Processing the subtest queue means moving tests into the jobs pool until
// the jobs pool length is at this.jobs
//
// Any output functions get put in the work queue if its length > 0 (ie,
// no cutting the line)
//
// Processing the work queue means walking until we run out of things, or
// encounter an unfinished test.  When we encounter ANY kind of test, we
// block until its output is completed, dumping it all into the parser.

const Base = require('./base.js')
const Spawn = require('./spawn.js')
const Stdin = require('./stdin.js')
const Deferred = require('trivial-deferred')
const Pool = require('yapool')
const TestPoint = require('./point.js')
const parseTestArgs = require('./parse-test-args.js')
const loop = require('function-loop')
const path = require('path')

const extraFromError = require('./extra-from-error.js')
const tsame = require('tsame') // same thing, strict or not
const tmatch = require('tmatch') // ok with partial estimates
const stack = require('./stack.js')
const synonyms = require('./synonyms.js')
const assert = require('assert')
const util = require('util')
const ownOr = require('own-or')
const ownOrEnv = require('own-or-env')
const Promise = require('bluebird')
const bindObj = require('bind-obj-methods')
const cwd = process.cwd()

// A sigil object for implicit end() calls that should not
// trigger an error if the user then calls t.end()
const IMPLICIT = Symbol('implicit t.end()')

// Sigil to put in the queue to signal the end of all things
const EOF = Symbol('EOF')

const _currentAssert = Symbol('_currentAssert')
const _end = Symbol('_end')
const _snapshot = Symbol('_snapshot')
const Snapshot = require('./snapshot.js')

const hasOwn = (obj, key) =>
  Object.prototype.hasOwnProperty.call(obj, key)

const isRegExp = re =>
  Object.prototype.toString.call(re) === '[object RegExp]'

class Test extends Base {
  constructor (options) {
    options = options || {}
    super(options)
    this.pushedEnd = false
    this.jobs = ownOr(options, 'jobs', 1)

    this.subtests = []
    this.pool = new Pool()
    this.queue = ['TAP version 13\n']

    // snapshots are keyed off of the main file that loads the
    // root test object. Typically, this is the TAP object.
    // To do this, we climb the ladder and only save in the teardown
    // of that root (parentless) test object.  This allows handling
    // cases where the same test name can be used multiple times
    // in a single test file, which would otherwise clobber snapshots.
    this.writeSnapshot = ownOrEnv(
      options, 'snapshot', 'TAP_SNAPSHOT', true)
    if (this.parent && this.parent[_snapshot])
      this[_snapshot] = this.parent[_snapshot]
    else
      this[_snapshot] = new Snapshot(this)

    this.noparallel = false
    if (options.cb)
      this.cb = this.domain.bind(options.cb)

    this.occupied = false
    this[_currentAssert] = null
    this.count = 0
    this.n = 0
    this.ended = false
    this.explicitEnded = false
    this.multiEndThrew = false
    this.assertAt = null
    this.assertStack = null
    this.planEnd = -1
    this.onBeforeEach = []
    this.onAfterEach = []
    this.ranAfterEach = false

    // bind all methods to this object, so we can pass t.end as a callback
    // and do `const test = require('tap').test` like people do.
    const bound = Object.create(null)
    bindObj(this, this, bound)
    bindObj(this, Object.getPrototypeOf(this), bound)
    bindObj(this, Test.prototype, bound)
  }

  spawn (cmd, args, options, name) {
    if (typeof args === 'string')
      args = [ args ]

    args = args || []

    if (typeof options === 'string') {
      name = options
      options = {}
    }

    options = options || {}
    options.name = ownOr(options, 'name', name)
    options.command = cmd
    options.args = args

    return this.sub(Spawn, options, Test.prototype.spawn)
  }

  sub (Class, extra, caller) {
    if (this.results || this.ended) {
      const er = new Error('cannot create subtest after parent test end')
      this.threw(er)
      return Promise.resolve(this)
    }

    if (!extra.skip && this.grep.length) {
      const m = this.grep[0].test(extra.name)
      const match = this.grepInvert ? !m : m
      if (!match) {
        const p = 'filter' + (this.grepInvert ? ' out' : '') + ': '
        extra.skip = p + this.grep[0]
      }
    }

    if (extra.only && !this.runOnly)
      this.comment('%j has `only` set but all tests run', extra.name)

    if (this.runOnly && !extra.only)
      extra.skip = 'filter: only'

    if (extra.todo || extra.skip) {
      this.pass(extra.name, extra)
      return Promise.resolve(this)
    }

    if (!extra.grep) {
      extra.grep = this.grep.slice(1)
      extra.grepInvert = this.grepInvert
    }

    extra.indent = '    '
    if (this.jobs > 1 && process.env.TAP_BUFFER === undefined)
      extra.buffered = ownOr(extra, 'buffered', true)
    else
      extra.buffered = ownOrEnv(extra, 'buffered', 'TAP_BUFFER', true)

    extra.bail = ownOr(extra, 'bail', this.bail)
    extra.parent = this
    extra.stack = stack.captureString(80, caller)
    const t = new Class(extra)

    this.queue.push(t)
    this.subtests.push(t)

    const d = new Deferred()
    t.deferred = d
    this.process()
    return d.promise
  }

  todo (name, extra, cb) {
    extra = parseTestArgs(name, extra, cb)
    extra.todo = true
    return this.sub(Test, extra, Test.prototype.todo)
  }

  skip (name, extra, cb) {
    extra = parseTestArgs(name, extra, cb)
    extra.skip = true
    return this.sub(Test, extra, Test.prototype.skip)
  }

  only (name, extra, cb) {
    extra = parseTestArgs(name, extra, cb)
    extra.only = true
    return this.sub(Test, extra, Test.prototype.only)
  }

  test (name, extra, cb) {
    extra = parseTestArgs(name, extra, cb)
    return this.sub(Test, extra, Test.prototype.test)
  }

  stdin (name, extra) {
    /* istanbul ignore next */
    extra = parseTestArgs(name, extra, false, '/dev/stdin')
    return this.sub(Stdin, extra, Test.prototype.stdin)
  }

  bailout (message) {
    if (this.parent && (this.results || this.ended))
      this.parent.bailout(message)
    else {
      this.process()
      message = message ? ' ' + ('' + message).trim() : ''
      message = message.replace(/[\r\n]/g, ' ')
      this.parser.write('Bail out!' + message + '\n')
    }
    this.end(IMPLICIT)
    this.process()
  }

  comment () {
    const body = util.format.apply(util, arguments)
    const message = '# ' + body.split(/\r?\n/).join('\n# ') + '\n'

    if (this.results)
      this.write(message)
    else
      this.queue.push(message)
    this.process()
  }

  timeout (options) {
    options = options || {}
    options.expired = options.expired || this.name
    if (this.occupied)
      this.occupied.timeout(options)
    else
      Base.prototype.timeout.call(this, options)
    this.end(IMPLICIT)
  }

  main (cb) {
    this.setTimeout(this.options.timeout)
    this.debug('MAIN pre', this)

    const end = () => {
      this.debug(' > implicit end for promise')
      this.end(IMPLICIT)
      done()
    }

    const done = (er) => {
      if (er)
        this.threw(er)

      if (this.results || this.bailedOut)
        cb()
      else
        this.ondone = cb
    }

    // This bit of overly clever line-noise wraps the call to user-code
    // in a try-catch. We can't rely on the domain for this yet, because
    // the 'end' event can trigger a throw after the domain is unhooked,
    // but before this is no longer the official "active test"
    const ret = (() => {
      try {
        return this.cb(this)
      } catch (er) {
        this.threw(er)
      }
    })()

    if (ret && ret.then) {
      this.promise = ret
      ret.tapAbortPromise = done
      ret.then(end, done)
    } else
      done()

    this.debug('MAIN post', this)
  }

  process () {
    if (this.processing)
      return this.debug(' < already processing')

    this.debug('\nPROCESSING(%s)', this.name, this.queue.length)
    this.processing = true

    while (!this.occupied) {
      const p = this.queue.shift()
      if (!p)
        break
      if (p instanceof Base) {
        this.processSubtest(p)
      } else if (p === EOF) {
        this.debug(' > EOF', this.name)
        // I AM BECOME EOF, DESTROYER OF STREAMS
        if (this.writeSnapshot)
          this[_snapshot].save()
        this.parser.end()
      } else if (p instanceof TestPoint) {
        this.debug(' > TESTPOINT')
        this.parser.write(p.ok + (++this.n) + p.message)
      } else if (typeof p === 'string') {
        this.debug(' > STRING')
        this.parser.write(p)
      } else {
        /* istanbul ignore else */
        if (Array.isArray(p)) {
          this.debug(' > METHOD')
          const m = p.shift()
          this[m].apply(this, p)
        } else {
          throw new Error('weird thing got in the queue')
        }
      }
    }

    while (!this.noparallel && this.pool.length < this.jobs) {
      const p = this.subtests.shift()
      if (!p)
        break

      if (!p.buffered) {
        this.noparallel = true
        break
      }

      this.debug('start subtest', p)
      this.pool.add(p)
      if (this.bailedOut)
        this.onbufferedend(p)
      else
        this.runBeforeEach(p, () =>
          p.main(() => this.onbufferedend(p)))
    }

    this.debug('done processing', this.queue, this.occupied)
    this.processing = false

    // just in case any tests ended, and we have sync stuff still
    // waiting around in the queue to be processed
    if (!this.occupied && this.queue.length)
      this.process()

    this.maybeAutoend()
  }

  processSubtest (p) {
    this.debug(' > subtest')
    this.occupied = p
    if (!p.buffered) {
      if (this.bailedOut)
        return this.onindentedend(p)
      this.debug(' > subtest indented')
      p.pipe(this.parser, { end: false })
      this.runBeforeEach(p, () =>
        this.writeSubComment(p, () =>
          p.main(() => this.onindentedend(p))))
    } else if (p.readyToProcess) {
      this.debug(' > subtest buffered, finished')
      // finished!  do the thing!
      this.occupied = null
      if (!p.passing() || !p.silent) {
        this.queue.unshift(['emitSubTeardown', p])
        this.printResult(p.passing(), p.name, p.options, true)
      }
    } else {
      this.occupied = p
      this.debug(' > subtest buffered, unfinished', p)
      // unfinished buffered test.
      // nothing to do yet, just leave it there.
      this.queue.unshift(p)
    }
  }

  emitSubTeardown (p) {
    try {
      p.emit('teardown')
    } catch (er) {
      delete p.options.time
      p.threw(er)
    }
  }

  writeSubComment (p, cb) {
    const comment = '# Subtest' +
      (p.name ? ': ' + p.name : '') +
      '\n'
    this.parser.write(comment)
    cb()
  }

  onbufferedend (p) {
    delete p.ondone
    p.results = p.results || {}
    p.readyToProcess = true
    const to = p.options.timeout
    const dur = (to && p.passing()) ? Date.now() - p.start : null
    if (dur && dur > to)
      p.timeout()
    else
      p.setTimeout(false)
    this.debug('%s.onbufferedend', this.name, p.name, p.results.bailout)
    this.pool.remove(p)
    p.options.tapChildBuffer = p.output || ''
    p.options.stack = ''
    if (p.time)
      p.options.time = p.time
    if (this.occupied === p)
      this.occupied = null
    p.deferred.resolve(this)
    this.process()
  }

  onindentedend (p) {
    delete p.ondone
    this.debug('onindentedend', p)
    this.noparallel = false
    const sti = this.subtests.indexOf(p)
    if (sti !== -1)
      this.subtests.splice(sti, 1)
    p.readyToProcess = true
    p.results = p.results || {}
    if (p.time)
      p.options.time = p.time
    const to = p.options.timeout
    const dur = (to && p.passing()) ? Date.now() - p.start : null
    if (dur && dur > to)
      p.timeout()
    else
      p.setTimeout(false)
    this.debug('onindentedend %s(%s)', this.name, p.name)
    if (!p.bailedOut)
      assert.equal(this.occupied, p)
    this.occupied = null
    this.debug('OIE(%s) b>shift into queue', this.name, this.queue)
    p.options.stack = ''

    this.queue.unshift(['emitSubTeardown', p])
    this.printResult(p.passing(), p.name, p.options, true)

    this.debug('OIE(%s) shifted into queue', this.name, this.queue)
    p.deferred.resolve(this)
    this.process()
  }

  addAssert (name, length, fn) {
    if (!name)
      throw new TypeError('name is required for addAssert')

    if (!(typeof length === 'number' && length >= 0))
      throw new TypeError('number of args required')

    if (typeof fn !== 'function')
      throw new TypeError('function required for addAssert')

    if (Test.prototype[name] || this[name])
      throw new TypeError('attempt to re-define `' + name + '` assert')

    function ASSERT () {
      this.currentAssert = ASSERT
      const args = new Array(length + 2)
      for (let i = 0; i < length; i++) {
        args[i] = arguments[i]
      }
      if (typeof arguments[length] === 'object') {
        args[length] = ''
        args[length + 1] = arguments[length]
      } else {
        args[length] = arguments[length] || ''
        args[length + 1] = arguments[length + 1] || {}
      }

      return fn.apply(this, args)
    }
    this[name] = ASSERT
  }

  printResult (ok, message, extra, front) {
    const n = this.count + 1
    this.currentAssert = Test.prototype.printResult
    const fn = this[_currentAssert]
    this[_currentAssert] = null

    if (this.planEnd !== -1 && n > this.planEnd) {
      if (!this.passing())
        return

      const failMessage = this.explicitEnded
          ? 'test after end() was called'
          : 'test count exceeds plan'

      const er = new Error(failMessage)
      Error.captureStackTrace(er, fn)
      er.test = this.name
      er.plan = this.planEnd
      this.threw(er)
      return
    }

    extra = extra || {}

    if (extra.expectFail)
      ok = !ok

    if (this.assertAt) {
      extra.at = this.assertAt
      this.assertAt = null
    }

    if (this.assertStack) {
      extra.stack = this.assertStack
      this.assertStack = null
    }

    if (hasOwn(extra, 'stack') && !hasOwn(extra, 'at'))
      extra.at = stack.parseLine(extra.stack.split('\n')[0])

    if (!ok && !extra.skip && !hasOwn(extra, 'at')) {
      assert.equal(typeof fn, 'function')
      extra.at = stack.at(fn)
      if (!extra.todo)
        extra.stack = stack.captureString(80, fn)
    }

    const diagnostic =
      typeof extra.diagnostic === 'boolean' ? extra.diagnostic
      : process.env.TAP_DIAG === '0' ? false
      : process.env.TAP_DIAG === '1' ? true
      : extra.skip ? false
      : !ok

    if (diagnostic)
      extra.diagnostic = true

    this.count = n
    message = message + ''
    const res = { ok: ok, message: message, extra: extra }
    const output = new TestPoint(ok, message, extra)
    // when we jump the queue, skip an extra line
    if (front)
      output.message = output.message.trimRight() + '\n\n'

    if (front) {
      this.emit('result', res)
      this.parser.write(output.ok + (++this.n) + output.message)
    } else
      this.queue.push(['emit', 'result', res], output)

    if (this.planEnd === this.count)
      this.end(IMPLICIT)

    this.process()
  }

  pragma (set) {
    const p = Object.keys(set).reduce((acc, i) =>
      acc + 'pragma ' + (set[i] ? '+' : '-') + i + '\n', '')
    this.queue.push(p)
    this.process()
  }

  plan (n, comment) {
    if (this.bailedOut)
      return

    if (this.planEnd !== -1) {
      throw new Error('Cannot set plan more than once')
    }

    if (typeof n !== 'number' || n < 0) {
      throw new TypeError('plan must be a number')
    }

    // Cannot get any tests after a trailing plan, or a plan of 0
    const ending = this.count !== 0 || n === 0

    if (n === 0 && comment && !this.options.skip)
      this.options.skip = comment

    this.planEnd = n
    comment = comment ? ' # ' + comment.trim() : ''
    this.queue.push('1..' + n + comment + '\n')

    if (ending)
      this.end(IMPLICIT)
    else
      this.process()
  }

  end (implicit) {
    this.debug('END implicit=%j', implicit === IMPLICIT)
    if (this.ended && implicit === IMPLICIT)
      return

    // beyond here we have to be actually done with things, or else
    // the semantic checks on counts and such will be off.
    if (!queueEmpty(this) || this.occupied) {
      if (!this.pushedEnd)
        this.queue.push(['end', implicit])
      this.pushedEnd = true
      return this.process()
    }

    if (!this.ranAfterEach && this.parent) {
      this.ranAfterEach = true
      this.parent.runAfterEach(this, () => this[_end](implicit))
      return
    } else
      this[_end](implicit)
  }

  [_end] (implicit) {
    this.ended = true

    if (implicit !== IMPLICIT && !this.multiEndThrew) {
      if (this.explicitEnded) {
        this.multiEndThrew = true
        const er = new Error('test end() method called more than once')
        Error.captureStackTrace(er, this[_currentAssert] ||
          Test.prototype[_end])
        er.test = this.name
        this.threw(er)
        return
      }
      this.explicitEnded = true
    }

    if (this.planEnd === -1) {
      this.debug('END(%s) implicit plan', this.name, this.count)
      this.plan(this.count)
    }

    this.queue.push(EOF)
    this.process()
  }

  threw (er, extra, proxy) {
    this.debug('THREW', er.message, extra, proxy)

    // event emitters 'error' events need to re-throw so that they
    // can jump out of the flow like a normal throw.  They'll just
    // end up back here once that happens, though, unless there's a
    // try/catch somewhere in the call stack.
    if (er.domainEmitter) {
      delete er.domainEmitter
      throw er
    }

    if (this.name && !proxy)
      er.test = this.name
    if (!proxy)
      extra = extraFromError(er, extra, this.options)
    Base.prototype.threw.call(this, er, extra, proxy)

    if (!this.results) {
      this.fail(extra.message || er.message, extra)
      if (!proxy)
        this.end(IMPLICIT)
    }
    this.process()
  }

  runBeforeEach (who, cb) {
    if (this.parent)
      this.parent.runBeforeEach(who, () => {
        loop(who, this.onBeforeEach, cb, who.threw)
      })
    else
      loop(who, this.onBeforeEach, cb, who.threw)
  }

  runAfterEach (who, cb) {
    loop(who, this.onAfterEach, () => {
      if (this.parent)
        this.parent.runAfterEach(who, cb)
      else
        cb()
    }, who.threw)
  }

  beforeEach (fn) {
    this.onBeforeEach.push(fn)
  }

  afterEach (fn) {
    this.onAfterEach.push(fn)
  }

  teardown (fn) {
    this.on('teardown', fn)
  }

  shouldAutoend () {
    const should = (
      this.options.autoend &&
      !this.ended &&
      !this.occupied &&
      queueEmpty(this) &&
      !this.pool.length &&
      !this.subtests.length &&
      this.planEnd === -1
    )
    return should
  }

  autoend (value) {
    // set to false to NOT trigger autoend
    if (value === false) {
      this.options.autoend = false
      clearTimeout(this.autoendTimer)
    } else {
      this.options.autoend = true
      this.maybeAutoend()
    }
  }

  maybeAutoend () {
    if (this.shouldAutoend()) {
      clearTimeout(this.autoendTimer)
      this.autoendTimer = setTimeout(() => {
        if (this.shouldAutoend()) {
          clearTimeout(this.autoendTimer)
          this.autoendTimer = setTimeout(() => {
            if (this.shouldAutoend())
              this.end(IMPLICIT)
          })
        }
      })
    }
  }

  endAll (sub) {
    // in the case of the root TAP test object, we might sometimes
    // call endAll on a bailing-out test, as the process is ending
    // In that case, we WILL have a this.occupied and a full queue
    // These cases are very rare to encounter in other Test objs tho
    this.processing = true
    if (this.occupied) {
      const p = this.occupied
      if (p.endAll)
        p.endAll(true)
      else {
        p.parser.abort('test unfinished')
      }
    } else if (sub) {
      this.process()
      if (queueEmpty(this)) {
        const options = Object.assign({}, this.options)
        this.options.at = null
        this.options.stack = ''
        options.test = this.name
        this.fail('test unfinished', options)
      }
    }

    if (this.promise && this.promise.tapAbortPromise)
      this.promise.tapAbortPromise()

    if (this.occupied) {
      this.queue.unshift(this.occupied)
      this.occupied = null
    }

    endAllQueue(this.queue)
    this.processing = false
    this.process()
    this.parser.end()
  }

  get currentAssert () {
    return this[_currentAssert]
  }

  set currentAssert (fn) {
    if (!this[_currentAssert])
      this[_currentAssert] = fn
  }

  pass (message, extra) {
    this.currentAssert = Test.prototype.pass

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}

    this.printResult(true, message || '(unnamed test)', extra)
    return true
  }

  fail (message, extra) {
    this.currentAssert = Test.prototype.fail

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}

    this.printResult(false, message || '(unnamed test)', extra)
    return !!(extra.todo || extra.skip)
  }

  ok (obj, message, extra) {
    this.currentAssert = Test.prototype.ok

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}
    message = message || 'expect truthy value'
    return obj ? this.pass(message, extra) : this.fail(message, extra)
  }

  notOk (obj, message, extra) {
    this.currentAssert = Test.prototype.notOk

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}
    message = message || 'expect falsey value'
    return this.ok(!obj, message, extra)
  }

  error (er, message, extra) {
    this.currentAssert = Test.prototype.error

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}

    if (!er) {
      return this.pass(message || 'should not error', extra)
    }

    if (!(er instanceof Error)) {
      extra.found = er
      return this.fail(message || 'non-Error error encountered', extra)
    }

    message = message || er.message
    extra.found = er
    return this.fail(message, extra)
  }

  equal (found, wanted, message, extra) {
    this.currentAssert = Test.prototype.equal

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}

    message = message || 'should be equal'
    if (found === wanted) {
      return this.pass(message, extra)
    }

    extra.found = found
    extra.wanted = wanted
    extra.compare = '==='

    if (typeof found === 'object' &&
        typeof wanted === 'object' &&
        found &&
        wanted &&
        tsame(found, wanted)) {
      extra.note = 'Objects never === one another'
    }

    return this.fail(message, extra)
  }

  not (found, wanted, message, extra) {
    this.currentAssert = Test.prototype.not

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}

    message = message || 'should not be equal'
    if (found !== wanted) {
      return this.pass(message, extra)
    }

    extra.found = found
    extra.doNotWant = wanted
    extra.compare = '!=='

    return this.fail(message, extra)
  }

  same (found, wanted, message, extra) {
    this.currentAssert = Test.prototype.same

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}

    message = message || 'should be equivalent'
    extra.found = found
    extra.wanted = wanted
    return this.ok(tsame(found, wanted), message, extra)
  }

  notSame (found, wanted, message, extra) {
    this.currentAssert = Test.prototype.notSame

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}

    message = message || 'should not be equivalent'
    extra.found = found
    extra.doNotWant = wanted
    return this.notOk(tsame(found, wanted), message, extra)
  }

  strictSame (found, wanted, message, extra) {
    this.currentAssert = Test.prototype.strictSame

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}

    message = message || 'should be equivalent strictly'
    extra.found = found
    extra.wanted = wanted
    return this.ok(tsame.strict(found, wanted), message, extra)
  }

  strictNotSame (found, wanted, message, extra) {
    this.currentAssert = Test.prototype.strictNotSame

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}

    message = message || 'should not be equivalent strictly'
    extra.found = found
    extra.doNotWant = wanted
    return this.notOk(tsame.strict(found, wanted), message, extra)
  }

  get fullname () {
    const main = process.argv[1] || ''
    return (this.parent ? this.parent.fullname
      : main.indexOf(cwd) === 0 ? main.substr(cwd.length + 1)
      : path.basename(main)) +
      ' ' + (this.name || '').trim()
  }

  matchSnapshot (found, message, extra) {
    this.currentAssert = Test.prototype.matchSnapshot

    if (message && typeof message === 'object')
      extra = message, message = 'must match snapshot'

    if (!extra)
      extra = {}

    // use notOk because snap doesn't return a truthy value
    const m = this.fullname + ' > ' + message
    if (typeof found !== 'string')
      found = util.inspect(found, {
        showHidden: false,
        depth: Infinity
      })

    return this.writeSnapshot
      ? this.notOk(this[_snapshot].snap(found, m),
        message, extra)
      : this.equal(found, this[_snapshot].read(m),
        message, extra)
  }

  match (found, wanted, message, extra) {
    this.currentAssert = Test.prototype.match

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}

    message = message || 'should match pattern provided'
    extra.found = found
    extra.pattern = wanted
    return this.ok(tmatch(found, wanted), message, extra)
  }

  notMatch (found, wanted, message, extra) {
    this.currentAssert = Test.prototype.notMatch

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}

    message = message || 'should not match pattern provided'
    extra.found = found
    extra.pattern = wanted
    return this.ok(!tmatch(found, wanted), message, extra)
  }

  type (obj, klass, message, extra) {
    this.currentAssert = Test.prototype.type

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}

    const name = typeof klass === 'function' ?
      klass.name || '(anonymous constructor)'
      : klass

    message = message || 'type is ' + name

    // simplest case, it literally is the same thing
    if (obj === klass) {
      return this.pass(message, extra)
    }

    const tof = typeof obj
    const type = (!obj && tof === 'object') ? 'null'
      // treat as object, but not Object
      // t.type(() => {}, Function)
      : (tof === 'function' &&
        typeof klass === 'function' &&
        klass !== Object) ? 'object'
      : tof

    if (type === 'object' && klass !== 'object') {
      if (typeof klass === 'function') {
        extra.found = Object.getPrototypeOf(obj).constructor.name
        extra.wanted = name
        return this.ok(obj instanceof klass, message, extra)
      }

      // check prototype chain for name
      // at this point, we already know klass is not a function
      // if the klass specified is an obj in the proto chain, pass
      // if the name specified is the name of a ctor in the chain, pass
      for (let p = obj; p; p = Object.getPrototypeOf(p)) {
        const ctor = p.constructor && p.constructor.name
        if (p === klass || ctor === name) {
          return this.pass(message, extra)
        }
      }
    }

    return this.equal(type, name, message, extra)
  }

  throws (_fn, _wanted, _message, _extra) {
    this.currentAssert = Test.prototype.throws

    let fn, wanted, message, extra
    for (let i = 0; i < arguments.length; i++) {
      const arg = arguments[i]
      if (typeof arg === 'function') {
        if (arg === Error || arg.prototype instanceof Error) {
          wanted = arg
        } else if (!fn) {
          fn = arg
        }
      } else if (typeof arg === 'string' && arg) {
        message = arg
      } else if (typeof arg === 'object') {
        if (!wanted) {
          wanted = arg
        } else {
          extra = arg
        }
      }
    }

    if (!extra)
      extra = {}

    if (!message)
      message = fn && fn.name || 'expected to throw'

    if (wanted) {
      if (wanted instanceof Error) {
        const w = {
          message: wanted.message
        }
        if (wanted.name) {
          w.name = wanted.name
        }

        // intentionally copying non-local properties, since this
        // is an Error object, and those are funky.
        for (let i in wanted) {
          w[i] = wanted[i]
        }
        wanted = w

        message += ': ' + (wanted.name || 'Error') + ' ' + wanted.message
        extra.wanted = wanted
      }
    }

    if (typeof fn !== 'function') {
      extra.todo = true
      return this.pass(message, extra)
    }

    try {
      fn()
      return this.fail(message, extra)
    } catch (er) {
      // 'name' is a getter.
      if (er.name) {
        Object.defineProperty(er, 'name', {
          value: er.name + '',
          enumerable: true,
          configurable: true,
          writable: true
        })
      }

      const actual = isRegExp(wanted) ? er.message : er
      return wanted ? this.match(actual, wanted, message, extra)
        : this.pass(message, extra)
    }
  }

  doesNotThrow (fn, message, extra) {
    this.currentAssert = Test.prototype.doesNotThrow

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}

    if (typeof fn === 'string') {
      const x = fn
      fn = message
      message = x
    }

    if (!message) {
      message = fn && fn.name || 'expected to not throw'
    }

    if (typeof fn !== 'function') {
      extra.todo = true
      return this.pass(message, extra)
    }

    try {
      fn()
      return this.pass(message, extra)
    } catch (er) {
      const e = extraFromError(er, extra)
      e.message = er.message
      return this.fail(message, e)
    }
  }

  // like throws, but rejects a returned promise instead
  // also, can pass in a promise instead of a function
  rejects (_fn, _wanted, _message, _extra) {
    this.currentAssert = Test.prototype.rejects

    let fn, wanted, extra, promise, message
    for (let i = 0; i < arguments.length; i++) {
      const arg = arguments[i]
      if (typeof arg === 'function') {
        if (arg === Error || arg.prototype instanceof Error) {
          wanted = arg
        } else if (!fn) {
          fn = arg
        }
      } else if (typeof arg === 'string' && arg) {
        message = arg
      } else if (arg && typeof arg.then === 'function' && !promise) {
        promise = arg
      } else if (typeof arg === 'object') {
        if (!wanted) {
          wanted = arg
        } else {
          extra = arg
        }
      }
    }

    if (!extra)
      extra = {}

    if (!message)
      message = fn && fn.name || 'expect rejected Promise'

    if (wanted) {
      if (wanted instanceof Error) {
        const w = {
          message: wanted.message
        }
        if (wanted.name)
          w.name = wanted.name

        // intentionally copying non-local properties, since this
        // is an Error object, and those are funky.
        for (let i in wanted) {
          w[i] = wanted[i]
        }
        wanted = w

        message += ': ' + (wanted.name || 'Error') + ' ' + wanted.message
        extra.wanted = wanted
      }
    }

    if (!promise && typeof fn !== 'function') {
      extra.todo = true
      this.pass(message, extra)
      return Promise.resolve(this)
    }

    if (!promise)
      promise = fn()

    const textra = {
      buffered: true,
      todo: extra.todo,
      skip: extra.skip
    }

    if (!promise || typeof promise.then !== 'function')
      return this.test(message, textra, t => {
        t.fail(message, extra)
        t.end()
      })

    // have to do as a subtest, because promises are async
    extra.at = stack.at(this.currentAssert)
    return this.test(message, textra, t => promise.then(value => {
      extra.found = value
      t.fail(message, extra)
    }, er => {
      // 'name' is a getter.
      if (er.name) {
        Object.defineProperty(er, 'name', {
          value: er.name + '',
          enumerable: true,
          configurable: true,
          writable: true
        })
      }

      const actual = isRegExp(wanted) ? er.message : er
      return wanted ? t.match(actual, wanted, message, extra)
        : t.pass(message, extra)
    }))
  }

  resolves (promise, message, extra) {
    this.currentAssert = Test.prototype.resolves

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}

    if (!message)
      message = 'expect resolving Promise'

    if (typeof promise === 'function')
      promise = promise()

    // have to do as a subtest, because promises are async
    extra.at = stack.at(this.currentAssert)

    const textra = {
      buffered: true,
      todo: extra.todo,
      skip: extra.skip
    }

    if (!promise || typeof promise.then !== 'function')
      return this.test(message, textra, t => {
        t.fail(message, extra)
        t.end()
      })

    return this.test(message, textra, t =>
      promise.then(value => {
        extra.found = value
        t.pass(message, extra)
      }))
  }

  resolveMatch (promise, wanted, message, extra) {
    this.currentAssert = Test.prototype.resolveMatch

    if (message && typeof message === 'object')
      extra = message, message = ''

    if (!extra)
      extra = {}

    if (!message)
      message = 'expect resolving Promise'

    // have to do as a subtest, because promises are async
    extra.at = stack.at(this.currentAssert)

    const textra = {
      buffered: true,
      todo: extra.todo,
      skip: extra.skip
    }

    if (!promise || typeof promise.then !== 'function')
      return this.test(message, textra, t => {
        t.fail(message, extra)
        t.end()
      })

    return this.test(message, textra, t =>
      promise.then(value => {
        extra.found = value
        t.match(value, wanted, extra)
      }))
  }
}

const endAllQueue = queue => {
  queue.forEach((p, i) => {
    if ((p instanceof Base) && !p.readyToProcess)
      queue[i] = new TestPoint(false,
        'child test left in queue:' +
        ' t.' + p.constructor.name.toLowerCase() + ' ' + p.name,
        p.options)
  })
  queue.push(['end', IMPLICIT])
}

const queueEmpty = t =>
  t.queue.length === 0 ||
    t.queue.length === 1 && t.queue[0] === 'TAP version 13\n'

Test.prototype.done = Test.prototype.end
Test.prototype.tearDown = Test.prototype.teardown

Object.keys(synonyms)
  .filter(c => Test.prototype[c])
  .forEach(c => synonyms[c].forEach(s =>
    Object.defineProperty(Test.prototype, s, {
      value: Test.prototype[c],
      enumerable: false,
      configurable: true,
      writable: true
    })))

module.exports = Test
