//See LICENSE for license details.
package firesim.firesim

import java.io.File

import scala.concurrent.{Future, Await, ExecutionContext}
import scala.sys.process.{stringSeqToProcess, ProcessLogger}
import scala.io.Source
import org.scalatest.Suites

import freechips.rocketchip.diplomacy._
import freechips.rocketchip.system.{RocketTestSuite, BenchmarkTestSuite}
import freechips.rocketchip.system.TestGeneration._
import freechips.rocketchip.system.DefaultTestSuites._

abstract class FireSimTestSuite(
    topModuleClass: String,
    targetConfigs: String,
    platformConfigs: String,
    N: Int = 8
  ) extends firesim.TestSuiteCommon {
  import scala.concurrent.duration._
  import ExecutionContext.Implicits.global

  val topModuleProject = "firesim.firesim"

  val chipyardLongName = topModuleProject + "." + topModuleClass + "." + targetConfigs

  // From TestSuiteCommon
  val targetTuple = s"$topModuleClass-$targetConfigs-$platformConfigs"
  val commonMakeArgs = Seq(s"DESIGN=${topModuleClass}",
                           s"TARGET_CONFIG=${targetConfigs}",
                           s"PLATFORM_CONFIG=${platformConfigs}")

  override lazy val genDir  = new File(firesimDir, s"generated-src/${chipyardLongName}")


  def invokeMlSimulator(backend: String, name: String, debug: Boolean, additionalArgs: Seq[String] = Nil) = {
    make((Seq(s"${outDir.getAbsolutePath}/${name}.%s".format(if (debug) "vpd" else "out"),
              s"EMUL=${backend}")
         ++ additionalArgs):_*)
  }

  def runTest(backend: String, name: String, debug: Boolean, additionalArgs: Seq[String] = Nil) = {
    compileMlSimulator(backend, debug)
    if (isCmdAvailable(backend)) {
      it should s"pass in ML simulation on ${backend}" in {
        assert(invokeMlSimulator(backend, name, debug, additionalArgs) == 0)
      }
    }
  }

  def runSuite(backend: String, debug: Boolean = false)(suite: RocketTestSuite) {
    // compile emulators
    behavior of s"${suite.makeTargetName} running on $backend"
    if (isCmdAvailable(backend)) {
      val postfix = suite match {
        case _: BenchmarkTestSuite | _: BlockdevTestSuite | _: NICTestSuite => ".riscv"
        case _ => ""
      }
      it should s"pass all tests in ${suite.makeTargetName}" in {
        val results = suite.names.toSeq sliding (N, N) map { t =>
          val subresults = t map (name =>
            Future(name -> invokeMlSimulator(backend, s"$name$postfix", debug)))
          Await result (Future sequence subresults, Duration.Inf)
        }
        results.flatten foreach { case (name, exitcode) =>
          assert(exitcode == 0, "Failed $name")
        }
      }
    } else {
      ignore should s"pass $backend"
    }
  }

  // Checks the collected trace log matches the behavior of a chisel printf
  def diffTracelog(verilatedLog: String) {
    behavior of "captured instruction trace"
    it should s"match the chisel printf in ${verilatedLog}" in {
      def getLines(file: File): Seq[String] = Source.fromFile(file).getLines.toList

      val printfPrefix =  "TRACEPORT 0: "
      val verilatedOutput  = getLines(new File(outDir,  s"/${verilatedLog}")).collect({
        case line if line.startsWith(printfPrefix) => line.stripPrefix(printfPrefix) })

      // Last bit indicates the core was under reset; reject those tokens
      // Tail to drop the first token which is initialized in the channel
      val synthPrintOutput = getLines(new File(genDir, s"/TRACEFILE")).tail.filter(line =>
        (line.last.toInt & 1) == 0)

      assert(math.abs(verilatedOutput.size - synthPrintOutput.size) <= 1,
        s"\nPrintf Length: ${verilatedOutput.size}, Trace Length: ${synthPrintOutput.size}")
      assert(verilatedOutput.nonEmpty)
      for ( (vPrint, sPrint) <- verilatedOutput.zip(synthPrintOutput) ) {
        assert(vPrint == sPrint)
      }
    }
  }

  mkdirs
  behavior of s"Tuple: ${targetTuple}"
  elaborateAndCompile()
  runTest("verilator", "rv64ui-p-simple", false, Seq(s"""EXTRA_SIM_ARGS=+trace-humanreadable0"""))
  runSuite("verilator")(benchmarks)
}

class RocketF1Tests extends FireSimTestSuite("FireSim", "DDR3FRFCFSLLC4MB_FireSimQuadRocketConfig", "WithSynthAsserts_BaseF1Config")
class BoomF1Tests extends FireSimTestSuite("FireSim", "DDR3FRFCFSLLC4MB_FireSimLargeBoomConfig", "BaseF1Config")
class RocketNICF1Tests extends FireSimTestSuite("FireSim", "WithNIC_DDR3FRFCFSLLC4MB_FireSimRocketConfig", "BaseF1Config")
// Multiclock tests
class RocketMulticlockF1Tests extends FireSimTestSuite(
  "FireSim",
  "FireSimMulticlockRocketConfig",
  "WithSynthAsserts_BaseF1Config")

class CVA6F1Tests extends FireSimTestSuite("FireSim", "WithNIC_DDR3FRFCFSLLC4MB_FireSimCVA6Config", "BaseF1Config")

// This test suite only mirrors what is run in CI. CI invokes each test individually, using a testOnly call.
class CITests extends Suites(
  new RocketF1Tests,
  new BoomF1Tests,
  new RocketNICF1Tests,
  new RocketMulticlockF1Tests)
