// SPDX-License-Identifier: Apache-2.0

package firrtl.jqf

import collection.JavaConverters._

import java.io.{File, FileNotFoundException, IOException, PrintWriter}
import java.net.MalformedURLException

import edu.berkeley.cs.jqf.fuzz.junit.GuidedFuzzing
import edu.berkeley.cs.jqf.fuzz.repro.ReproGuidance
import edu.berkeley.cs.jqf.instrument.InstrumentingClassLoader

case class JQFReproOptions(
  classpath: Seq[String] = null,
  testClassName: String = null,
  testMethod: String = null,
  input: File = null,

  logCoverage: Option[File] = None,
  excludes: Seq[String] = Seq.empty,
  includes: Seq[String] = Seq.empty,
  printArgs: Boolean = false
)

object JQFRepro {
  final def main(args: Array[String]): Unit = {
    val parser = new scopt.OptionParser[JQFReproOptions]("JQF-Repro") {
        opt[String]("classpath")
          .required()
          .unbounded()
          .action((x, c) => c.copy(classpath = x.split(":")))
          .text("the classpath to instrument and load the test class from")
        opt[String]("testClassName")
          .required()
          .unbounded()
          .action((x, c) => c.copy(testClassName = x))
          .text("the full class path of the test class")
        opt[String]("testMethod")
          .required()
          .unbounded()
          .action((x, c) => c.copy(testMethod = x))
          .text("the method of the test class to run")
        opt[File]("input")
          .required()
          .unbounded()
          .action((x, c) => c.copy(input = x))
          .text("input file or directory to reproduce test case(s)")

        opt[File]("logCoverage")
          .unbounded()
          .action((x, c) => c.copy(logCoverage = Some(x)))
          .text("output file to dump coverage info")
        opt[Seq[String]]("excludes")
          .unbounded()
          .action((x, c) => c.copy(excludes = x))
          .text("comma-separated list of FQN prefixes to exclude from coverage instrumentation")
        opt[Seq[String]]("includes")
          .unbounded()
          .action((x, c) => c.copy(includes = x))
          .text("comma-separated list of FQN prefixes to forcibly include, even if they match an exclude")
        opt[Unit]("printArgs")
          .unbounded()
          .action((_, c) => c.copy(printArgs = true))
          .text("whether to print the args to each test case")
    }

    parser.parse(args, JQFReproOptions()) match {
      case Some(opts) => execute(opts)
      case _ => System.exit(1)
    }
  }

  def execute(opts: JQFReproOptions): Unit = {
    // Configure classes to instrument
    if (opts.excludes.nonEmpty) {
      System.setProperty("janala.excludes", opts.excludes.mkString(","))
    }
    if (opts.includes.nonEmpty) {
      System.setProperty("janala.includes", opts.includes.mkString(","))
    }

    val loader = try {
      new InstrumentingClassLoader(
        opts.classpath.toArray,
        getClass().getClassLoader())
    } catch  {
      case e: MalformedURLException =>
        throw new JQFException("Could not get project classpath", e)
    }

    // If a coverage dump file was provided, enable logging via system property
    if (opts.logCoverage.isDefined) {
      System.setProperty("jqf.repro.logUniqueBranches", "true")
    }

    // If args should be printed, set system property
    if (opts.printArgs) {
      System.setProperty("jqf.repro.printArgs", "true")
    }

    if (!opts.input.exists() || !opts.input.canRead()) {
      throw new JQFException("Cannot find or open file " + opts.input)
    }


    val (guidance, result) = try {
      val guidance = new ReproGuidance(opts.input, null)
      val result = GuidedFuzzing.run(opts.testClassName, opts.testMethod, loader, guidance, System.out)
      guidance -> result
    } catch {
      case e: ClassNotFoundException => throw new JQFException("Could not load test class", e);
      case e: IllegalArgumentException => throw new JQFException("Bad request", e);
      case e: FileNotFoundException => throw new JQFException("File not found", e);
      case e: RuntimeException => throw new JQFException("Internal error", e);
    }

    // If a coverage dump file was provided, then dump coverage
    if (opts.logCoverage.isDefined) {
      val coverageSet = guidance.getBranchesCovered()
      assert(coverageSet != null) // Should not happen if we set the system property above
      val sortedCoverage = coverageSet.asScala.toSeq.sorted
      try {
        val covOut = new PrintWriter(opts.logCoverage.get)
        sortedCoverage.foreach(covOut.println(_))
      } catch {
        case e: IOException =>
          throw new JQFException("Could not dump coverage info.", e)
      }
    }

    if (!result.wasSuccessful()) {
      throw new JQFException("Test case produces a failure.")
    }
  }
}
