import EventEmitter from 'events'
import chalk from 'chalk'
import { Stats } from 'webpack'
import createStatsFormatter from '../webpack/util/createStatsFormatter'
import {
  ENTRY_ADDED_EVENT,
  ENTRY_REMOVED_EVENT,
  EXCEPTION_EVENT,
  MOCHA_ABORTED_EVENT,
  MOCHA_BEGIN_EVENT,
  MOCHA_FINISHED_EVENT,
  WEBPACK_READY_EVENT,
  WEBPACK_START_EVENT,
  UNCAUGHT_EXCEPTION_EVENT
} from '../util/constants'

type ReporterOptions = {
  eventEmitter: EventEmitter
  interactive: boolean
  clearTerminal: boolean
  quiet: boolean
  cwd: string
}

const log = (...args: Array<any>) => {
  console.log(...args) // eslint-disable-line no-console
  console.log() // eslint-disable-line no-console
}

const formatTitleInfo = title => chalk.inverse('', title, '')
const formatTitleWarn = title => chalk.black.bgYellow('', title, '')
const formatTitleError = title => chalk.white.bold.bgRed('', title, '')

class Reporter {
  added: Array<string>
  removed: Array<string>
  options: ReporterOptions
  formatStats: (
    stats: Stats
  ) => { warnings: Array<string>; errors: Array<string> }

  constructor(options: ReporterOptions) {
    const { cwd, eventEmitter } = options

    this.options = options
    this.added = []
    this.removed = []
    this.formatStats = createStatsFormatter(cwd)

    eventEmitter.on(UNCAUGHT_EXCEPTION_EVENT, this.onUncaughtException)
    eventEmitter.on(EXCEPTION_EVENT, this.onLoadingException)
    eventEmitter.on(WEBPACK_START_EVENT, this.onWebpackStart)
    eventEmitter.on(WEBPACK_READY_EVENT, this.onWebpackReady)
    eventEmitter.on(MOCHA_BEGIN_EVENT, this.onMochaStart)
    eventEmitter.on(MOCHA_ABORTED_EVENT, this.onMochaAbort)
    eventEmitter.on(MOCHA_FINISHED_EVENT, this.onMochaReady)
    eventEmitter.on(ENTRY_ADDED_EVENT, this.onEntryAdded)
    eventEmitter.on(ENTRY_REMOVED_EVENT, this.onEntryRemoved)
  }

  logInfo(...args: Array<any>) {
    if (!this.options.quiet) {
      log(...args)
    }
  }

  clearTerminal() {
    if (this.options.clearTerminal && this.options.interactive) {
      process.stdout.write(
        process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H'
      )
    }
  }

  static displayErrors(severity: string, errors: Array<any>) {
    const errorCount = errors.length

    const message =
      severity === 'error'
        ? `Failed to compile with ${chalk.red(`${errorCount} ${severity}(s)`)}`
        : `Compiled with ${chalk.yellow(`${errorCount} ${severity}(s)`)}`

    const titleColor = severity === 'error' ? formatTitleError : formatTitleWarn
    log(titleColor('WEBPACK'), message)
    errors.forEach(err => log(err))
  }

  onUncaughtException = (err: Error) => {
    log(
      formatTitleError('UNCAUGHT EXCEPTION'),
      'Exception occurred after running tests'
    )
    log(err.stack)
  }

  onLoadingException = (err: Error) => {
    log(
      formatTitleError('RUNTIME EXCEPTION'),
      'Exception occurred while loading your tests'
    )
    log(err.stack)
  }

  onWebpackStart = () => {
    this.clearTerminal()
    if (this.added.length > 0) {
      this.logInfo(
        formatTitleInfo('MOCHA'),
        'The following test entry files were added:'
      )
      this.logInfo(this.added.map(f => `+ ${f}`).join('\n'))
    }

    if (this.removed.length > 0) {
      this.logInfo(
        formatTitleInfo('MOCHA'),
        'The following test entry files were removed:'
      )
      this.logInfo(this.removed.map(f => `- ${f}`).join('\n'))
    }

    this.logInfo(formatTitleInfo('WEBPACK'), 'Compiling...')

    this.added.length = 0
    this.removed.length = 0
  }

  onWebpackReady = (err?: Error, stats?: Stats) => {
    this.clearTerminal()
    if (stats != null) {
      const { errors, warnings } = this.formatStats(stats)

      if (errors.length === 0 && warnings.length === 0) {
        const { startTime, endTime } = stats
        const compileTime = endTime - startTime
        this.logInfo(
          formatTitleInfo('WEBPACK'),
          `Compiled successfully in ${chalk.green(`${compileTime}ms`)}`
        )
        return
      }

      if (errors.length > 0) {
        Reporter.displayErrors('error', errors)
        return
      }

      if (warnings.length > 0) {
        Reporter.displayErrors('warning', warnings)
      }
    } else {
      Reporter.displayErrors('error', [err])
    }
  }

  onMochaStart = () => {
    this.logInfo(formatTitleInfo('MOCHA'), 'Testing...')
  }

  onMochaAbort = () => {
    this.logInfo(formatTitleInfo('MOCHA'), 'Tests aborted')
  }

  onMochaReady = (failures: number) => {
    if (failures === 0) {
      this.logInfo(
        formatTitleInfo('MOCHA'),
        `Tests completed ${chalk.green('successfully')}`
      )
    } else {
      this.logInfo(
        formatTitleInfo('MOCHA'),
        `Tests completed with ${chalk.red(`${failures} failure(s)`)}`
      )
    }
  }

  onEntryAdded = (file: string) => {
    this.added.push(file)
  }

  onEntryRemoved = (file: string) => {
    this.removed.push(file)
  }
}

export default function testRunnerReporter(options: ReporterOptions) {
  new Reporter(options) // eslint-disable-line no-new
}
