import TestCase from "./test-case"
import { Writable } from "stream"

const symbols = {
  pass: ".",
  fail: "F",
  todo: "-",
  skip: "",
  error: "E",
}

/**
 * Tabulates test result data and provides functionality for pretty printing it.
 */
export default class Tabulator {
  private total = 0
  private counts = { pass: 0, fail: 0, todo: 0, skip: 0, error: 0 }
  private output: Writable
  private failures: TestCase[] = []
  private todos: TestCase[] = []
  private verbose: boolean

  /**
   * @param output the output stream to print the results to
   * @param verbose whether the output should be printed verbosely
   */
  constructor(output: Writable, verbose = false) {
    this.output = output
    this.verbose = verbose
  }

  /**
   * Returns 1 if there are any failures or 0 otherwise.
   */
  exitCode(): number {
    return this.failures.length == 0 ? 0 : 1
  }

  /**
   * Append the results of the test case to the total results and print.
   */
  tabulate(test: TestCase): void {
    const result = test.result()
    this.total++
    this.counts[result.type]++
    if (result.type === "fail" || result.type === "error") {
      this.failures.push(test)
    } else if (result.type === "todo") {
      this.todos.push(test)
    }

    if (result.type !== "skip") {
      const symbol = symbols[result.type]
      if (this.verbose) {
        this.writeLine(`${symbol} ${test.dir.relPath()}`)
      } else {
        this.output.write(symbols[result.type])
      }
    }
  }

  /**
   * Print the accumulated results of all tabulated tests.
   */
  printResults(): void {
    this.writeLine()
    this.printFailuresAndErrors()
    if (this.verbose) {
      this.printTodos()
    }
    this.writeLine(
      `${this.total} runs, ${this.counts.pass} passing, ${this.counts.fail} failures, ${this.counts.todo} todo, ${this.counts.skip} ignored, ${this.counts.error} errors`
    )
  }

  private writeLine(text: string = ""): void {
    this.output.write(text + "\n")
  }

  private printFailuresAndErrors(): void {
    for (const failure of this.failures) {
      const result = failure.result()
      if (result.type === "fail") {
        // If it's a test failure (a mismatch between expected and actual output),
        // log the message and diff (if available)
        this.writeLine(`Failure: ${failure.dir.relPath()}`)
        this.writeLine(result.message)
        if (result.diff) {
          this.writeLine(result.diff)
        }
      } else {
        // Otherwise it's an error (thrown due to invalid specs or other reasons),
        // so print the stacktrace
        this.writeLine(`Error: ${failure.dir.relPath()}`)
        this.writeLine(result.error?.stack)
      }
      this.writeLine()
    }
  }

  private printTodos(): void {
    for (const todo of this.todos) {
      this.writeLine(`TODO: ${todo.dir.relPath()}`)
      this.writeLine()
    }
  }
}