import osproc, streams, os, strutils, re
{.experimental.}

## Compiler as a service tester.
##
## Please read docs/idetools.txt for information about this.


type
  TRunMode = enum
    ProcRun, CaasRun, SymbolProcRun

  NimSession* = object
    nim: Process # Holds the open process for CaasRun sessions, nil otherwise.
    mode: TRunMode # Stores the type of run mode the session was started with.
    lastOutput: string # Preserves the last output, needed for ProcRun mode.
    filename: string # Appended to each command starting with '>'. Also a var.
    modname: string # Like filename but without extension.
    nimcache: string # Input script based name for the nimcache dir.

const
  modes = [CaasRun, ProcRun, SymbolProcRun]
  filenameReplaceVar = "$TESTNIM"
  moduleReplaceVar = "$MODULE"
  silentReplaceVar = "$SILENT"
  silentReplaceText = "--verbosity:0 --hints:off"

var
  TesterDir = getAppDir() / ".."
  NimBin = TesterDir / "../bin/nim"

proc replaceVars(session: var NimSession, text: string): string =
  result = text.replace(filenameReplaceVar, session.filename)
  result = result.replace(moduleReplaceVar, session.modname)
  result = result.replace(silentReplaceVar, silentReplaceText)

proc startNimSession(project, script: string, mode: TRunMode):
                        NimSession =
  let (dir, name, ext) = project.splitFile
  result.mode = mode
  result.lastOutput = ""
  result.filename = name & ext
  result.modname = name

  let (nimcacheDir, nimcacheName, nimcacheExt) = script.splitFile
  result.nimcache = "SymbolProcRun." & nimcacheName

  if mode == SymbolProcRun:
    removeDir(nimcacheDir / result.nimcache)
  else:
    removeDir(nimcacheDir / "nimcache")

  if mode == CaasRun:
    result.nim = startProcess(NimBin, workingDir = dir,
      args = ["serve", "--server.type:stdin", name])

proc doCaasCommand(session: var NimSession, command: string): string =
  assert session.mode == CaasRun
  session.nim.inputStream.write(session.replaceVars(command) & "\n")
  session.nim.inputStream.flush

  result = ""

  while true:
    var line = TaintedString("")
    if session.nim.outputStream.readLine(line):
      if line.string == "": break
      result.add(line.string & "\n")
    else:
      result = "FAILED TO EXECUTE: " & command & "\n" & result
      break

proc doProcCommand(session: var NimSession, command: string): string =
  try:
    assert session.mode == ProcRun or session.mode == SymbolProcRun
  except:
    result = "FAILED TO EXECUTE: " & command & "\n" & result
  var
    process = startProcess(NimBin, args = session.replaceVars(command).split)
    stream = outputStream(process)
    line = TaintedString("")

  result = ""
  while stream.readLine(line):
    if result.len > 0: result &= "\n"
    result &= line.string

  process.close()

proc doCommand(session: var NimSession, command: string) =
  if session.mode == CaasRun:
    if not session.nim.running:
      session.lastOutput = "FAILED TO EXECUTE: " & command & "\n" &
          "Exit code " & $session.nim.peekExitCode
      return
    session.lastOutput = doCaasCommand(session,
                                       command & " " & session.filename)
  else:
    var command = command
    # For symbol runs we prepend the necessary parameters to avoid clobbering
    # the normal nimcache.
    if session.mode == SymbolProcRun:
      command = "--symbolFiles:on --nimcache:" & session.nimcache &
                " " & command
    session.lastOutput = doProcCommand(session,
                                       command & " " & session.filename)

proc destroy(session: var NimSession) {.destructor.} =
  if session.mode == CaasRun:
    session.nim.close

proc doScenario(script: string, output: Stream, mode: TRunMode, verbose: bool): bool =
  result = true

  var f = open(script)
  var project = TaintedString("")

  if f.readLine(project):
    var
      s = startNimSession(script.parentDir / project.string, script, mode)
      tline = TaintedString("")
      ln = 1

    while f.readLine(tline):
      var line = tline.string
      inc ln

      # Filter lines by run mode, removing the prefix if the mode is current.
      for testMode in modes:
        if line.startsWith($testMode):
          if testMode != mode:
            line = ""
          else:
            line = line[len($testMode)..len(line) - 1].strip
          break

      if line.strip.len == 0: continue

      if line.startsWith("#"):
        output.writeLine line
        continue
      elif line.startsWith(">"):
        s.doCommand(line.substr(1).strip)
        output.writeLine line, "\n", if verbose: s.lastOutput else: ""
      else:
        var expectMatch = true
        var pattern = s.replaceVars(line)
        if line.startsWith("!"):
          pattern = pattern.substr(1).strip
          expectMatch = false

        let actualMatch =
          s.lastOutput.find(re(pattern, flags = {reStudy})) != -1

        if expectMatch == actualMatch:
          output.writeLine "SUCCESS ", line
        else:
          output.writeLine "FAILURE ", line
          result = false

iterator caasTestsRunner*(filter = "", verbose = false): tuple[test,
                                              output: string, status: bool,
                                              mode: TRunMode] =
  for scenario in os.walkFiles(TesterDir / "caas/*.txt"):
    if filter.len > 0 and find(scenario, filter) == -1: continue
    for mode in modes:
      var outStream = newStringStream()
      let r = doScenario(scenario, outStream, mode, verbose)
      yield (scenario, outStream.data, r, mode)

when isMainModule:
  var
    filter = ""
    failures = 0
    verbose = false

  for i in 0..paramCount() - 1:
    let param = string(paramStr(i + 1))
    case param
    of "verbose": verbose = true
    else: filter = param

  if verbose and len(filter) > 0:
    echo "Running only test cases matching filter '$1'" % [filter]

  for test, output, result, mode in caasTestsRunner(filter, verbose):
    if not result or verbose:
      echo "Mode ", $mode, " (", if result: "succeeded)" else: "failed)"
      echo test
      echo output
      echo "---------\n"
    if not result:
      failures += 1

  quit(failures)
