namespace rooibos
  ' @ignore
  class TestRunner
    public testScene = invalid
    public testReporters = []
    public nodeContext = invalid
    public config = invalid
    public testSuites = []
    private runtimeConfig = invalid
    private stats = invalid
    private top = invalid

    public function new(testScene, nodeContext)
      m.testScene = testScene
      if nodeContext.top = invalid
        nodeContext.top = testScene
      end if
      nodeContext.scene = testScene

      m.top = nodeContext.top
      m.nodeContext = nodeContext

      m.stats = new rooibos.Stats()
      m.runtimeConfig = new rooibos.RuntimeConfig()
      m.config = m.runtimeConfig.getRuntimeConfig()

      m.testReporters = m.getTestReporters()
    end function

    ' Executes all tests for a project, as per the config
    public function run()

      for each reporter in m.testReporters
        if rooibos.common.isFunction(reporter.onBegin)
          reporter.onBegin({ runner: m })
        end if
      end for

      rooibosTimer = createObject("roTimespan")
      rooibosTimer.mark()
      suiteNames = m.runtimeConfig.getAllTestSuitesNames()
      isFailed = false
      failedText = ""
      i = 0
      numSuites = suiteNames.count()
      testSuite = invalid
      for each name in suiteNames
        i++
        'bs:disable-next-line
        suiteClass = m.runtimeConfig.getTestSuiteClassWithName(name)
        testSuite = invalid
        if suiteClass <> invalid
          testSuite = suiteClass()
          testSuite.testRunner = m
          testSuite.testReporters = m.testReporters
          testSuite.global = m.nodeContext.global
          testSuite.context = m.nodeContext
          testSuite.top = m.nodeContext.top
          testSuite.scene = m.nodeContext.global.testsScene
          testSuite.catchCrashes = m.config.catchCrashes
          testSuite.throwOnFailedAssertion = m.config.throwOnFailedAssertion
          testSuite.scene.testText = `Running Suite ${i} of ${numSuites}: ${name}`
          m.runTestSuite(testSuite)
          if m.stats.hasFailures = true
            if not isFailed
              isFailed = true
              testSuite.scene.statusColor = "#DA3633"
            end if
            if m.config.failFast = true
              exit for
            end if
          end if

          if testSuite.stats.hasFailures
            failedText = name + chr(10) + failedText
            testSuite.scene.failedText = "Failed Suites: " + chr(10) + failedText
          end if
        else
          rooibos.common.logError(`Could not create test for suite : ${name}`)
          failedText = "COULD NOT CREATE suite " + name + chr(10) + failedText
          testSuite.scene.failedText = "Failed Suites: " + chr(10) + failedText
        end if
      end for

      if not isFailed and testSuite <> invalid
        testSuite.scene.statusColor = "#238636"
      end if

      if testSuite = invalid
        m.nodeContext.global.testsScene.failedText = "No tests were found"
      end if

      m.stats.time = rooibosTimer.totalMilliseconds()

      for each reporter in m.testReporters
        if rooibos.common.isFunction(reporter.onEnd)
          reporter.onEnd({ stats: m.stats })
        end if
      end for

      rooibosResult = {
        stats: m.stats
        testSuites: m.testSuites
      }
      m.nodeContext.global.testsScene.rooibosTestResult = rooibosResult

      if m.config.isRecordingCodeCoverage
        rooibos.Coverage.reportCodeCoverage()

        if m.config.printLcov = true
          rooibos.Coverage.printLCovInfo()
        end if
      else
        rooibos.common.logDebug("rooibos.Coverage.reportCodeCoverage is not a function")
      end if

      ' Final results to be logged after all test reporters have finished
      resultStatus = "PASS"
      if isFailed then resultStatus = "FAIL"
      print "[Rooibos Result]: " + resultStatus
      print "[Rooibos Shutdown]"

      if m.config.sendHomeOnFinish <> false
        m.sendHomeKeypress()
      end if
    end function

    public function runInNodeMode(nodeTestName)
      'bs:disable-next-line
      suiteClass = m.runtimeConfig.getTestSuiteClassWithName(nodeTestName)
      testSuite = invalid

      if suiteClass <> invalid
        testSuite = suiteClass()
        testSuite.testReporters = m.testReporters
        testSuite.global = m.nodeContext.global
        testSuite.node = m.nodeContext
        testSuite.top = m.nodeContext.top
        testSuite.scene = m.nodeContext.global.testsScene
        testSuite.catchCrashes = m.config.catchCrashes
        testSuite.throwOnFailedAssertion = m.config.throwOnFailedAssertion
        m.nodeContext.testSuite = testSuite
        m.nodeTestName = nodeTestName
        m.nodeContext.testRunner = m
      end if

      if testSuite <> invalid
        m.currentTestSuite = testSuite
        testSuite.testRunner = m
        rooibos.common.logDebug("Running suite as a Node test!")
        testSuite.run()
        return invalid
      else
        rooibos.common.logError(`Could not create test suite ${nodeTestName}`)
      end if

      rooibos.common.logError(`(runInNodeMode) executing node test ${nodeTestName} was unsuccessful.`)

      return invalid
    end function

    private function onTestSuiteComplete()
      testSuite = m.currentTestSuite
      if testSuite <> invalid
        return {
          stats: testSuite.stats
          tests: testSuite.tests
        }

      else
        rooibos.common.logError(`could not create test suite ${m.testRunner.nodeTestName}`)
      end if

      rooibos.common.logError(`(onTestSuiteComplete) executing node test ${m.testRunner.nodeTestName} was unsuccessful.`)
      return invalid
    end function

    private function runTestSuite(testSuite) as void
      if testSuite.groupsData <> invalid and testSuite.groupsData.count() > 0
        m.testSuites.push(testSuite)

        if testSuite.isNodeTest
          m.runNodeTest(testSuite)
        else
          testSuite.run()
        end if
        m.stats.merge(testSuite.stats)
      end if
    end function

    private function waitForField(target, fieldName, delay = 500, maxAttempts = 10)
      attempts = 0
      if target = invalid
        return false
      end if

      initialValue = target[fieldName]
      while target[fieldName] = initialValue
        port = CreateObject("roMessagePort")
        wait(delay, port)
        attempts++
        if attempts = maxAttempts
          return false
        end if
        rooibos.common.logTrace(`waiting for signal field '${fieldName}' - ${attempts} VALUE ${target[fieldName]}`)
      end while

      return true
    end function

    function wait(delay = 1)
      port = CreateObject("roMessagePort")
      wait(delay, port)
    end function

    private function runNodeTest(testSuite) as void
      if testSuite.generatedNodeName <> ""
        rooibos.common.logDebug(`+++++RUNNING NODE TEST${chr(10)}node type is ${testSuite.generatedNodeName}`)

        node = m.testScene.createChild(testSuite.generatedNodeName)
        port = CreateObject("roMessagePort")
        node.observeField("rooibosTestResult", port)
        ' Trigger test via observer so that thread ownership
        ' can be transferred to the render thread
        node.rooibosRunSuite = true

        'wait on the field
        if type(node) = "roSGNode"
          event = invalid
          while true
            event = wait(0, port)
            if type(event) = "roSGNodeEvent"
              exit while
            end if
          end while
          nodeResults = event.getData()

          m.groups = []
          for each groupData in testSuite.groupsData
            'bs:disable-next-line
            group = new TestGroup(testSuite, groupData)
            testSuite.groups.push(group)
          end for

          if nodeResults <> invalid
            testSuite.stats.merge(nodeResults.stats)
            m.mergeGroups(testSuite, nodeResults.groups)
          else
            rooibos.common.logError(`The node test ${testSuite.name} did not indicate test completion. Did you call m.done() in your test? Did you correctly configure your node test? Please refer to : https://github.com/rokucommunity/rooibos/blob/master/docs/index.md#testing-scenegraph-nodes`)
          end if
          m.testScene.removeChild(node)
          return

        else
          rooibos.common.logError(`Could not create node required to execute tests for ${testSuite.name}${chr(10)}Node of type ${testSuite.generatedNodeName} was not found/could not be instantiated`)
        end if

      else
        rooibos.common.logError(`Could not create node required to execute tests for ${testSuite.name}${chr(10)}No node type was provided`)
      end if

      testSuite.stats.hasFailures = true
      testSuite.failedCount += testSuite.testsData.count()
    end function

    private function mergeGroups(testSuite, groups)
      for i = 0 to testSuite.groups.count() - 1
        group = groups[i]
        realGroup = testSuite.groups[i]
        realGroup.stats.merge(group.stats)
        realGroup.hasFailures = group.hasFailures
        realGroup.tests = []
        for testDataIndex = 0 to group.testsData.count() - 1
          testData = group.testsData[testDataIndex]
          realTest = new rooibos.Test(m, testData, testSuite)
          realGroup.tests.push(realTest)
          test = group.tests[testDataIndex]
          realTest.result.merge(test.result)
        end for
      end for
    end function

    private function sendHomeKeypress()
      ut = createObject("roUrlTransfer")
      ut.SetUrl("http://localhost:8060/keypress/Home")
      ut.PostFromString("")
    end function

    private function getTestReporters()
      testReporters = []
      for each factory in m.config.reporters
        if rooibos.common.isFunction(factory)
          testReporters.push(factory(m))
        end if
      end for
      if testReporters.isEmpty()
        testReporters.push(new rooibos.ConsoleTestReporter(m))
      end if
      return testReporters
    end function

  end class

  '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  '++ This code is called inside of the node
  '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  ' @ignore
  function createTestNode(nodeType) as object
    node = createObject("roSGNode", nodeType)

    if type(node) = "roSGNode" and node.subType() = nodeType
      m.top.AppendChild(node)
      return node
    else
      rooibos.common.logError(`Error creating test node of type ${nodeType}`)
      return invalid
    end if
  end function

  ' @ignore
  function runNodeTestSuite(name)
    nodeRunner = new TestRunner(m.top.getScene(), m)
    return nodeRunner.runInNodeMode(name)
  end function

  ' @ignore
  function onTestSuiteComplete()
    m.testRunner.onTestSuiteComplete()
  end function
end namespace
