//
// Copyright (c) 2024, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   17 Jun 24  Brian Frank  Creation
//

**
** TestRunner executes `sys::Test` classes and reports success/failure.
**
@Js
class TestRunner
{

//////////////////////////////////////////////////////////////////////////
// Main
//////////////////////////////////////////////////////////////////////////

  ** Run with given command line arguments
  static Int main(Str[] args)
  {
    targets := Str[,]
    runner  := TestRunner()
    isAll   := false
    isJs    := false
    isEs    := false

    // parse args
    for (i:=0; i<args.size; ++i)
    {
      arg := args[i]
      if (arg == "-help" || arg == "-h" || arg == "-?")
      {
        runner.printUsage
        return -1
      }
      if (arg == "-version")
      {
        runner.printVersion
        return -1
      }
      if (arg == "-v")
      {
        runner.isVerbose = true
        continue
      }
      if (arg == "-all")
      {
        isAll = true
        continue
      }
      if (arg == "-es")
      {
        isEs = true
        continue
      }
      if (arg == "-js")
      {
        isJs = true
        continue
      }
      if (arg.startsWith("-"))
      {
        echo("WARNING: Unknown option: $arg")
        continue
      }
      targets.add(arg)
    }

    // handle js/es re-routing
    if (isEs) return runner.runEs(targets)
    if (isJs) return runner.runJs(targets)

    // run tests
    if (isAll)
    {
      runner.runAll
    }
    else
    {
      if (targets.isEmpty)
      {
        runner.printUsage
        return -1
      }
      runner.runTargets(targets)
    }

    // report sumary and return non-zero if we had failures
    runner.reportSummary
    return runner.numFailures
  }

  ** Print usage
  Void printUsage()
  {
    out.printLine
    out.printLine(
     """Fantom Test Runner
        Usage:
          fant [options] <target>*
        Target:
          <pod>
          <pod>::<Type>
          <pod>::<Type>.<method>
        Options:
          -help, -h, -?  print usage help
          -version       print version
          -v             verbose mode
          -all           test all pods
          -es            test new ECMA JavaScript environment
          -js            test legacy JavaScript environment
        """)
  }

  ** Print version
  Void printVersion()
  {
    out.printLine
    out.printLine(
     """Fantom Test Runner
        Copyright (c) 2006-$Date.today.year, Brian Frank and Andy Frank
        Licensed under the Academic Free License version 3.0

        fan.version:  $typeof.pod.version
        fan.runtime:  $Env.cur.runtime
        fan.platform: $Env.cur.platform
        """)
     out.printLine("Env path:")
     Env.cur.path.each |f| { out.printLine("  $f.osPath") }
     out.printLine
  }

  ** Run new ECMA JS code
  private Int runEs(Str[] targets)
  {
    args := Str[,].add("test").addAll(targets)
    type := Type.find("nodeJs::Main")
    return type.make->main(args)
  }

  ** Run legacy JS code
  private Int runJs(Str[] targets)
  {
    args := Str[,].add("-test").addAll(targets)
    type := Type.find("compilerJs::NodeRunner")
    return type.make->main(args)
  }

//////////////////////////////////////////////////////////////////////////
// Runs
//////////////////////////////////////////////////////////////////////////

  ** Run list of targets from an argument string
  virtual This runTargets(Str[] targets)
  {
    targets.each |target, i|
    {
      if (i > 0) out.printLine
      runTarget(target)
    }
    return this
  }

  ** Run target from an argument string
  virtual This runTarget(Str target)
  {
    // pod
    colons := target.index("::")
    if (colons == null) return runPod(Pod.find(target))

    // pod::Type
    podName := target[0..<colons]
    pod := Pod.find(podName)
    rest := target[colons+2..-1]
    dot := rest.index(".")
    if (dot == null) return runType(pod.type(rest))

    // pod::Type.method
    typeName := rest[0..<dot]
    methodName := rest[dot+1..-1]
    type := pod.type(typeName)
    method := type.method(methodName)
    return runMethod(type, method)
  }

  ** Run on every installed pod
  virtual This runAll()
  {
    Pod.list.each |pod| { doRunPod(pod, true) }
    return this
  }

  ** Run all tests in given pod
  virtual This runPod(Pod pod) { doRunPod(pod, false) }
  private This doRunPod(Pod pod, Bool blankLine)
  {
    types := Type[,]
    pod.types.each |type|
    {
      if (type.fits(Test#) && !type.isAbstract) types.add(type)
    }
    if (types.isEmpty) return this

    if (blankLine) out.printLine
    types.each |type| { runType(type) }

    return this
  }

  ** Run all test methods on a given type
  virtual This runType(Type type)
  {
    numTypes++
    type.methods.each |method|
    {
      if (method.name.startsWith("test") && !method.isAbstract)
        runMethod(type, method)
    }
    return this
  }

  ** Run test method
  virtual This runMethod(Type type, Method method)
  {
    reportStart(type, method)
    Test? test := null
    verifies := 0
    try
    {
      test = type.make
      test->curTestMethod = method
      test->verbose = isVerbose
      onSetup(test)
      method.callOn(test, [,])
      verifies = (Int)test->verifyCount
      reportSuccess(type, method, verifies)
    }
    catch (Err e)
    {
      if (test != null) verifies = test->verifyCount
      failures.add(qname(type, method))
      reportFailure(type, method, e)
    }
    finally
    {
      try
      {
        if (test != null) onTeardown(test)
      }
      catch (Err e)  e.trace
    }
    numMethods += 1
    numVerifies += verifies
    return this
  }

  ** Callback to invoke setup
  virtual Void onSetup(Test test) { test.setup }

  ** Callback to invoke teardown
  virtual Void onTeardown(Test test) { test.teardown }

//////////////////////////////////////////////////////////////////////////
// Reporting
//////////////////////////////////////////////////////////////////////////

  ** Report the start of a test method
  virtual Void reportStart(Type type, Method method)
  {
    out.printLine("-- Run: ${qname(type, method)}")
  }

  ** Report the success and number of verifies
  virtual Void reportSuccess(Type type, Method method, Int verifies)
  {
    out.printLine("   Pass: ${qname(type, method)} [$verifies]")
  }

  ** Report the failure and exception raised
  virtual Void reportFailure(Type type, Method method, Err err)
  {
    out.printLine
    out.printLine("TEST FAILED")
    err.trace(out)
  }

  ** Report summary of tests
  virtual Void reportSummary()
  {
    elapsed := Duration.now - startTicks
    elapsedStr := elapsed > 10sec ? elapsed.toLocale : "${elapsed.toMillis}ms"

    out.printLine
    out.printLine("Time: $elapsedStr")
    out.printLine

    summary := "All tests passed!"
    if (!failures.isEmpty)
    {
      summary = "$failures.size FAILURES"
      out.printLine("Failed:")
      failures.each |qname| { out.printLine("  $qname") }
      out.printLine
    }

    out.printLine("***")
    out.printLine("*** $summary [$numTypes types, $numMethods methods, $numVerifies verifies]")
    out.printLine("***")
  }

  ** Qualified name of a given test type and method
  private Str qname(Type type, Method method)
  {
    type.qname + "." + method.name
  }

/////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////

  ** Output stream for built-in reporting
  OutStream out := Env.cur.out

  ** Should tests be run in verbose mode
  Bool isVerbose

  private Str[] failures := [,]
  private Int numFailures() { failures.size }
  private Duration startTicks := Duration.now
  private Int numTypes
  private Int numMethods
  private Int numVerifies
}