// A facade from the tap-parser to the Mocha "Runner" object.
// Note that pass/fail/suite events need to also mock the "Runnable"
// objects (either "Suite" or "Test") since these have functions
// which are called by the formatters.

module.exports = Runner

// relevant events:
//
// start()
//   Start of the top-level test set
//
// end()
//   End of the top-level test set.
//
// fail(test, err)
//   any "not ok" test that is not the trailing test for a suite
//   of >0 test points.
//
// pass(test)
//   any "ok" test point that is not the trailing test for a suite
//   of >0 tests
//
// pending(test)
//   Any "todo" test
//
// suite(suite)
//   A suite is a child test with >0 test points.  This is a little bit
//   tricky, because TAP will provide a "child" event before we know
//   that it's a "suite".  We see the "# Subtest: name" comment as the
//   first thing in the subtest.  Then, when we get our first test point,
//   we know that it's a suite, and can emit the event with the mock suite.
//
// suite end(suite)
//   Emitted when we end the subtest
//
// test(test)
//   Any test point which is not the trailing test for a suite.
//
// test end(test)
//   Emitted immediately after the "test" event because test points are
//   not async in TAP.

var util = require('util')
var Test = require('./test.js')
var Suite = require('./suite.js')
var Writable = require('stream').Writable
if (!Writable) {
  try {
    Writable = require('readable-stream').Writable
  } catch (er) {
    throw new Error('Please install "readable-stream" to use this module ' +
                    'with Node.js v0.8 and before')
  }
}

var Parser = require('tap-parser')

// $1 = number, $2 = units
var timere = /^#\s*time=((?:0|[1-9][0-9]*?)(?:\.[0-9]+)?)(ms|s)?$/

util.inherits(Runner, Writable)

function Runner (options) {
  if (!(this instanceof Runner))
    return new Runner(options)

  var parser = this.parser = new Parser(options)
  this.startTime = new Date()

  attachEvents(this, parser, 0)
  Writable.call(this, options)
}

Runner.prototype.write = function () {
  if (!this.emittedStart) {
    this.emittedStart = true
    this.emit('start')
  }

  return this.parser.write.apply(this.parser, arguments)
}

Runner.prototype.end = function () {
  return this.parser.end.apply(this.parser, arguments)
}

Parser.prototype.fullTitle = function () {
  if (!this.parent)
    return this.name || ''
  else
    return (this.parent.fullTitle() + ' ' + (this.name || '')).trim()
}

function attachEvents (runner, parser, level) {
  parser.runner = runner

  if (level === 0) {
    parser.on('line', function (c) {
      runner.emit('line', c)
    })
    parser.on('version', function (v) {
      runner.emit('version', v)
    })
    parser.on('complete', function (res) {
      runner.emit('end')
    })
    parser.on('comment', function (c) {
      var tmatch = c.trim().match(timere)
      if (tmatch) {
        var t = +tmatch[1]
        if (tmatch[2] === 's')
          t *= 1000
        parser.time = t
        if (runner.stats)
          runner.stats.duration = t
      }
    })
  }

  parser.emittedSuite = false
  parser.didAssert = false
  parser.printed = false
  parser.name = ''
  parser.doingChild = null

  parser.on('complete', function (res) {
    if (!res.ok) {
      var fail = { ok: false, diag: {} }
      var count = res.count
      if (res.plan) {
        var plan = res.plan.end - res.plan.start + 1
        if (count !== plan) {
          fail.name = 'test count !== plan'
          fail.diag = {
            found: count,
            wanted: plan
          }
        } else {
          // probably handled on child parser
          return
        }
      } else {
        fail.name = 'missing plan'
      }
      fail.diag.results = res
      emitTest(parser, fail)
    }
  })

  parser.on('child', function (child) {
    //console.log('>>> child')
    child.parent = parser
    attachEvents(runner, child, level + 1)

    // if we're in a suite, but we haven't emitted it yet, then we
    // know that an assert will follow this child, even if there are
    // no others. That means that we will definitely have a 'suite'
    // event to emit.
    emitSuite(this)

    this.didAssert = true
    this.doingChild = child
  })

  parser.on('comment', function (c) {
    if (!this.printed && c.match(/^# Subtest: /)) {
      c = c.trim().replace(/^# Subtest: /, '')
      this.name = c
    }
  })

  // Just dump all non-parsing stuff to stderr
  parser.on('extra', function (c) {
    process.stderr.write(c)
  })

  parser.on('assert', function (result) {
    emitSuite(this)

    // no need to print the trailing assert for subtests
    // we've already emitted a 'suite end' event for this.
    // UNLESS, there were no other asserts, AND it's root level
    if (this.doingChild) {
      var suite = this.doingChild.suite
      if (this.doingChild.name === result.name) {
        if (suite) {
          if (result.time)
            suite.duration = result.time

          // If it's ok so far, but the ending result is not-ok, then
          // that means that it exited non-zero.  Emit the test so
          // that we can print it as a failure.
          if (suite.ok && !result.ok)
            emitTest(this, result)
        }
      }

      var emitOn = this
      var dc = this.doingChild
      this.doingChild = null

      if (!dc.didAssert && dc.level === 1) {
        emitOn = dc
      } else if (dc.didAssert) {
        if (dc.suite)
          runner.emit('suite end', dc.suite)
        return
      } else {
        emitOn = this
      }

      emitSuite(emitOn)
      emitTest(emitOn, result)
      if (emitOn !== this && emitOn.suite) {
        runner.emit('suite end', emitOn.suite)
        delete emitOn.suite
      }
      if (dc.suite) {
        runner.emit('suite end', dc.suite)
      }
      return
    }

    this.didAssert = true
    this.doingChild = null

    emitTest(this, result)
  })

  parser.on('complete', function (results) {
    this.results = results
  })

  parser.on('bailout', function (reason) {
    var suite = this.suite
    runner.emit('bailout', reason, suite)
    if (suite)
      this.suite = suite.parent
  })

  // proxy all stream events directly
  var streamEvents = [
    'pipe', 'prefinish', 'finish', 'unpipe', 'close'
  ]

  streamEvents.forEach(function (ev) {
    parser.on(ev, function () {
      var args = [ev]
      args.push.apply(args, arguments)
      runner.emit.apply(runner, args)
    })
  })
}

function emitSuite (parser) {
  //console.log('emitSuite', parser.emittedSuite, parser.level, parser.name)
  if (!parser.emittedSuite && parser.name) {
    parser.emittedSuite = true
    var suite = parser.suite = new Suite(parser)
    if (parser.parent && parser.parent.suite)
      parser.parent.suite.suites.push(suite)
    if (parser.runner.stats)
      parser.runner.stats.suites ++

    parser.runner.emit('suite', suite)
  }
}

function emitTest (parser, result) {
  var runner = parser.runner
  var test = new Test(result, parser)

  if (parser.suite) {
    parser.suite.tests.push(test)
    if (!result.ok) {
      for (var p = parser; p && p.suite; p = p.parent) {
        p.suite.ok = false
      }
    }
    parser.suite.ok = parser.suite.ok && result.ok
  }

  runner.emit('test', test)
  if (result.skip || result.todo) {
    runner.emit('pending', test)
  } else if (result.ok) {
    runner.emit('pass', test)
  } else {
    var error = getError(result)
    runner.emit('fail', test, error)
  }
  runner.emit('test end', test)
}

function getError (result) {
  var err

  function reviveStack (stack) {
    if (!stack)
      return null

    return stack.trim().split('\n').map(function (line) {
      return '    at ' + line
    }).join('\n')
  }

  if (result.diag && result.diag.error) {
    err = {
      name: result.diag.error.name || 'Error',
      message: result.diag.error.message,
      toString: function () {
        return this.name + ': ' + this.message
      },
      stack: result.diag.error.stack
    }
  } else {
    err = {
      message: (result.name || '(unnamed error)').replace(/^Error: /, ''),
      toString: function () {
        return 'Error: ' + this.message
      },
      stack: result.diag && result.diag.stack
    }
  }

  var diag = result.diag

  if (err.stack)
    err.stack = err.toString() + '\n' + reviveStack(err.stack)

  if (diag) {
    var hasFound = diag.hasOwnProperty('found')
    var hasWanted = diag.hasOwnProperty('wanted')

    if (hasFound)
      err.actual = diag.found

    if (hasWanted)
      err.expected = diag.wanted

    if (hasFound && hasWanted)
      err.showDiff = true
  }

  return err
}

