package wacc

import org.scalatest.BeforeAndAfterAll
import org.scalatest.Inspectors.forEvery
import org.scalatest.matchers.should.Matchers._
import org.scalatest.freespec.AsyncFreeSpec
import cats.effect.testing.scalatest.AsyncIOSpec
import java.io.File
import java.nio.file.Path
import sys.process._
import scala.io.Source
import cats.effect.IO
import wacc.{compile as compileWacc}

class ParallelExamplesSpec extends AsyncFreeSpec with AsyncIOSpec with BeforeAndAfterAll {

  val files =
    allWaccFiles("wacc-examples/valid").map { p =>
      (p.toString, List(0))
    } ++
      allWaccFiles("wacc-examples/invalid/syntaxErr").map { p =>
        (p.toString, List(100))
      } ++
      allWaccFiles("wacc-examples/invalid/semanticErr").map { p =>
        (p.toString, List(200))
      } ++
      allWaccFiles("wacc-examples/invalid/whack").map { p =>
        (p.toString, List(100, 200))
      } ++
      allWaccFiles("extension/examples/valid").map { p =>
        (p.toString, List(0))
      } ++
      allWaccFiles("extension/examples/invalid/syntax").map { p =>
        (p.toString, List(100))
      } ++
      allWaccFiles("extension/examples/invalid/semantics").map { p =>
        (p.toString, List(200))
      }

  forEvery(files) { (filename, expectedResult) =>
    val baseFilename = filename.stripSuffix(".wacc")

    s"$filename" - {
      "should be compiled with correct result" in {
        if (fileIsPendingFrontend(filename))
          IO.pure(pending)
        else
          compileWacc(Path.of(filename), outputDir = None, log = false).map { result =>
            expectedResult should contain(result)
          }
      }

      if (expectedResult == List(0)) {
        "should run with correct result" in {
          if (fileIsDisallowedBackend(filename))
            IO.pure(succeed)
          else if (fileIsPendingBackend(filename))
            IO.pure(pending)
          else
            for {
              contents <- IO(Source.fromFile(File(filename)).getLines.toList)
              inputLine = extractInput(contents)
              expectedOutput = extractOutput(contents)
              expectedExit = extractExit(contents)

              asmFilename = baseFilename + ".s"
              execFilename = baseFilename
              gccResult <- IO(s"gcc -o $execFilename -z noexecstack $asmFilename".!)

              _ = assert(gccResult == 0)

              stdout <- IO.pure(new StringBuilder)
              process <- IO {
                s"timeout 5s $execFilename" run ProcessIO(
                  in = w => {
                    w.write(inputLine.getBytes)
                    w.close()
                  },
                  out = Source.fromInputStream(_).addString(stdout),
                  err = _ => ()
                )
              }

              exitCode <- IO.pure(process.exitValue)

            } yield {
              exitCode shouldBe expectedExit
              normalizeOutput(stdout.toString) shouldBe expectedOutput
            }
        }
      }
    }
  }

  def allWaccFiles(dir: String): IndexedSeq[os.Path] =
    val d = java.io.File(dir)
    os.walk(os.Path(d.getAbsolutePath)).filter(_.ext == "wacc")

  private def fileIsDisallowedBackend(filename: String): Boolean =
    filename.matches("^.*wacc-examples/valid/advanced.*$")

  private def fileIsPendingFrontend(filename: String): Boolean =
    List(
      // "^.*extension/examples/invalid/syntax/imports/importBadSyntax.*$",
      // "^.*extension/examples/invalid/semantics/imports.*$",
      // "^.*extension/examples/valid/imports.*$"
    ).exists(filename.matches)

  private def fileIsPendingBackend(filename: String): Boolean =
    List(
      // "^.*extension/examples/invalid/syntax/imports.*$",
      // "^.*extension/examples/invalid/semantics/imports.*$",
      // "^.*extension/examples/valid/imports.*$"
    ).exists(filename.matches)

  private def extractInput(contents: List[String]): String =
    contents
      .find(_.matches("^# ?[Ii]nput:.*$"))
      .map(_.split(":").last.strip + "\n")
      .getOrElse("")

  private def extractOutput(contents: List[String]): String = {
    val outputLineIdx = contents.indexWhere(_.matches("^# ?[Oo]utput:.*$"))
    if (outputLineIdx == -1) ""
    else
      contents
        .drop(outputLineIdx + 1)
        .takeWhile(_.startsWith("#"))
        .map(_.stripPrefix("#").stripLeading)
        .mkString("\n")
  }

  private def extractExit(contents: List[String]): Int = {
    val exitLineIdx = contents.indexWhere(_.matches("^# ?[Ee]xit:.*$"))
    if (exitLineIdx == -1) 0
    else contents(exitLineIdx + 1).stripPrefix("#").strip.toInt
  }

  private def normalizeOutput(output: String): String =
    output
      .replaceAll("0x[0-9a-f]+", "#addrs#")
      .replaceAll("fatal error:.*", "#runtime_error#\u0000")
      .takeWhile(_ != '\u0000')
}
