'use strict'

var defined = require('./defined')
var EventEmitter = require('events').EventEmitter
var inherits = require('util').inherits
var through = require('./through')
var resumer = require('./resumer')
var inspect = require('./object-inspect')
var bind = require('./function-bind')
var has = bind.call(Function.call, Object.prototype.hasOwnProperty)
var regexpTest = bind.call(Function.call, RegExp.prototype.test)
var yamlIndicators = /:|-|\?/
var nextTick = typeof setImmediate !== 'undefined'
  ? setImmediate
  : process.nextTick

module.exports = Results
inherits(Results, EventEmitter)

function Results () {
  if (!(this instanceof Results)) return new Results()
  this.count = 0
  this.fail = 0
  this.pass = 0
  this._stream = through()
  this.tests = []
  this._only = null
}

Results.prototype.createStream = function (opts) {
  if (!opts) opts = {}
  var self = this
  var output
  var testId = 0

  if (opts.objectMode) {
    output = through()
    self.on('_push', function ontest (t, extra) {
      if (!extra) extra = {}
      var id = testId++
      t.once('prerun', function () {
        var row = {
          type: 'test',
          name: t.name,
          id: id
        }
        if (has(extra, 'parent')) {
          row.parent = extra.parent
        }
        output.queue(row)
      })
      t.on('test', function (st) {
        ontest(st, { parent: id })
      })
      t.on('result', function (res) {
        res.test = id
        res.type = 'assert'
        output.queue(res)
      })
      t.on('end', function () {
        output.queue({ type: 'end', test: id })
      })
    })
    self.on('done', function () { output.queue(null) })
  } else {
    output = resumer()
    output.queue('TAP version 13\n')
    self._stream.pipe(output)
  }

  nextTick(function next () {
    var t
    // eslint-disable-next-line no-cond-assign
    while (t = getNextTest(self)) {
      t.run()
      if (!t.ended) return t.once('end', function () { nextTick(next) })
    }
    self.emit('done')
  })

  return output
}

Results.prototype.push = function (t) {
  var self = this
  self.tests.push(t)
  self._watch(t)
  self.emit('_push', t)
}

Results.prototype.only = function (t) {
  this._only = t
}

Results.prototype._watch = function (t) {
  var self = this
  var write = function (s) { self._stream.queue(s) }
  t.once('prerun', function () {
    write('# ' + t.name + '\n')
  })

  t.on('result', function (res) {
    if (typeof res === 'string') {
      write('# ' + res + '\n')
      return
    }
    write(encodeResult(res, self.count + 1))
    self.count++

    if (res.ok) self.pass++
    else {
      self.fail++
      self.emit('fail')
    }
  })

  t.on('test', function (st) { self._watch(st) })
}

Results.prototype.close = function () {
  var self = this
  if (self.closed) self._stream.emit('error', new Error('ALREADY CLOSED'))
  self.closed = true
  var write = function (s) { self._stream.queue(s) }

  write('\n1..' + self.count + '\n')
  write('# tests ' + self.count + '\n')
  write('# pass  ' + self.pass + '\n')
  if (self.fail) write('# fail  ' + self.fail + '\n')
  else write('\n# ok\n')

  self._stream.queue(null)
}

function encodeResult (res, count) {
  var output = ''
  output += (res.ok ? 'ok ' : 'not ok ') + count
  output += res.name ? ' ' + res.name.toString().replace(/\s+/g, ' ') : ''

  if (res.skip) output += ' # SKIP'
  else if (res.todo) output += ' # TODO'

  output += '\n'
  if (res.ok) return output

  var outer = '  '
  var inner = outer + '  '
  output += outer + '---\n'
  output += inner + 'operator: ' + res.operator + '\n'

  if (has(res, 'expected') || has(res, 'actual')) {
    var ex = inspect(res.expected, { depth: res.objectPrintDepth })
    var ac = inspect(res.actual, { depth: res.objectPrintDepth })

    if (Math.max(ex.length, ac.length) > 65 || invalidYaml(ex) || invalidYaml(ac)) {
      output += inner + 'expected: |-\n' + inner + '  ' + ex + '\n'
      output += inner + 'actual: |-\n' + inner + '  ' + ac + '\n'
    } else {
      output += inner + 'expected: ' + ex + '\n'
      output += inner + 'actual:   ' + ac + '\n'
    }
  }
  if (res.at) {
    output += inner + 'at: ' + res.at + '\n'
  }

  var actualStack = res.actual && (typeof res.actual === 'object' || typeof res.actual === 'function') ? res.actual.stack : undefined
  var errorStack = res.error && res.error.stack
  var stack = defined(actualStack, errorStack)
  if (stack) {
    var lines = String(stack).split('\n')
    output += inner + 'stack: |-\n'
    for (var i = 0; i < lines.length; i++) {
      output += inner + '  ' + lines[i] + '\n'
    }
  }

  output += outer + '...\n'
  return output
}

function getNextTest (results) {
  if (!results._only) {
    return results.tests.shift()
  }

  do {
    var t = results.tests.shift()
    if (!t) continue
    if (results._only === t) {
      return t
    }
  } while (results.tests.length !== 0)
}

function invalidYaml (str) {
  return regexpTest(yamlIndicators, str)
}
