package sbt.compiler.javac

import java.io.File
import java.net.URLClassLoader

import sbt._
import org.specs2.Specification
import org.specs2.matcher.MatchResult
import xsbt.api.{ SameAPI, DefaultShowAPI }
import xsbti.api.SourceAPI
import xsbti.{ Severity, Problem }

object JavaCompilerSpec extends Specification {
  def is = s2"""

  This is a specification for forking + inline-running of the java compiler, and catching Error messages


  Compiling a java file with local javac should
    compile a java file                                             ${works(local)}
    issue errors and warnings                                       ${findsErrors(local)}

  Compiling a file with forked javac should
    compile a java file                                             ${works(forked)}
    issue errors and warnings                                       ${findsErrors(forked)}
    yield the same errors as local javac                            $forkSameAsLocal

  Documenting a file with forked javadoc should
    document a java file                                            ${docWorks(forked)}
    find errors in a java file                                      ${findsDocErrors(forked)}

  Analyzing classes generated by javac should result in
    matching APIs for stable static-final fields                    ${analyzeStaticDifference("String", "\"A\"", "\"A\"")}
    different APIs for static-final fields with changed values      ${analyzeStaticDifference("String", "\"A\"", "\"B\"")}
    different APIs for static-final fields with changed types       ${analyzeStaticDifference("String", "\"1\"", "int", "1")}
    "safe" singleton type names                                     ${analyzeStaticDifference("float", "0.123456789f", "0.123456789f")}
  """

  def docWorks(compiler: JavaTools) = IO.withTemporaryDirectory { out =>
    val (result, problems) = doc(compiler, Seq(knownSampleGoodFile), Seq("-d", out.getAbsolutePath))
    val compiled = result must beTrue
    val indexExists = (new File(out, "index.html")).exists must beTrue setMessage ("index.html does not exist!")
    val classExists = (new File(out, "good.html")).exists must beTrue setMessage ("good.html does not exist!")
    compiled and classExists and indexExists
  }

  def works(compiler: JavaTools) = IO.withTemporaryDirectory { out =>
    val (result, problems) = compile(compiler, Seq(knownSampleGoodFile), Seq("-deprecation", "-d", out.getAbsolutePath))
    val compiled = result must beTrue
    val classExists = (new File(out, "good.class")).exists must beTrue
    val cl = new URLClassLoader(Array(out.toURI.toURL))
    val clazzz = cl.loadClass("good")
    val mthd = clazzz.getDeclaredMethod("test")
    val testResult = mthd.invoke(null)
    val canRun = mthd.invoke(null) must equalTo("Hello")
    compiled and classExists and canRun
  }

  def findsErrors(compiler: JavaTools) = {
    val (result, problems) = compile(compiler, Seq(knownSampleErrorFile), Seq("-deprecation"))
    val errored = result must beFalse
    val foundErrorAndWarning = problems must haveSize(5)
    val importWarn = warnOnLine(lineno = 1, lineContent = Some("import java.rmi.RMISecurityException;"))
    val hasKnownErrors = problems.toSeq must contain(importWarn, errorOnLine(3), warnOnLine(7))
    errored and foundErrorAndWarning and hasKnownErrors
  }

  def findsDocErrors(compiler: JavaTools) = IO.withTemporaryDirectory { out =>
    val (result, problems) = doc(compiler, Seq(knownSampleErrorFile), Seq("-d", out.getAbsolutePath))
    val errored = result must beTrue
    val foundErrorAndWarning = problems must haveSize(2)
    val hasKnownErrors = problems.toSeq must contain(errorOnLine(3), errorOnLine(4))
    errored and foundErrorAndWarning and hasKnownErrors
  }

  /**
   * Compiles with the given constant values, and confirms that if the strings mismatch, then the
   * the APIs mismatch.
   */
  def analyzeStaticDifference(typeName: String, left: String, right: String): MatchResult[Boolean] =
    analyzeStaticDifference(typeName, left, typeName, right)

  def analyzeStaticDifference(leftType: String, left: String, rightType: String, right: String): MatchResult[Boolean] = {
    def compileWithPrimitive(templateType: String, templateValue: String) =
      IO.withTemporaryDirectory { out =>
        // copy the input file to a temporary location and change the templateValue
        val input = new File(out, hasStaticFinalFile.getName())
        IO.writeLines(
          input,
          IO.readLines(hasStaticFinalFile).map { line =>
            line.replace("TYPE", templateType).replace("VALUE", templateValue)
          }
        )

        // then compile it
        val (result, problems) = compile(local, Seq(input), Seq("-d", out.getAbsolutePath))
        val origCompiled = result must beTrue
        val clazzz = new URLClassLoader(Array(out.toURI.toURL)).loadClass("hasstaticfinal")
        (origCompiled, ClassToAPI(Seq(clazzz)))
      }

    // compile with two different primitive values, and confirm that they match if their
    // values match
    val (leftCompiled, leftAPI) = compileWithPrimitive(leftType, left)
    val (rightCompiled, rightAPI) = compileWithPrimitive(rightType, right)
    val apisExpectedMatch = SameAPI(leftAPI, rightAPI) must beEqualTo(left == right)

    leftCompiled and rightCompiled and apisExpectedMatch
  }

  def lineMatches(p: Problem, lineno: Int, lineContent: Option[String] = None): Boolean = {
    def lineContentCheck =
      lineContent match {
        case Some(content) => content.equalsIgnoreCase(p.position.lineContent())
        case _             => true
      }
    def lineNumberCheck = p.position.line.isDefined && (p.position.line.get == lineno)
    lineNumberCheck && lineContentCheck
  }

  def isError(p: Problem): Boolean = p.severity == Severity.Error
  def isWarn(p: Problem): Boolean = p.severity == Severity.Warn

  def errorOnLine(lineno: Int, lineContent: Option[String] = None) =
    beLike[Problem]({
      case p if lineMatches(p, lineno, lineContent) && isError(p) => ok
      case _ => ko
    })
  def warnOnLine(lineno: Int, lineContent: Option[String] = None) =
    beLike[Problem]({
      case p if lineMatches(p, lineno, lineContent) && isWarn(p) => ok
      case _ => ko
    })

  def forkSameAsLocal = {
    val (fresult, fproblems) = compile(forked, Seq(knownSampleErrorFile), Seq("-deprecation"))
    val (lresult, lproblems) = compile(local, Seq(knownSampleErrorFile), Seq("-deprecation"))
    val sameResult = fresult must beEqualTo(lresult)

    val pResults = for ((f, l) <- fproblems zip lproblems) yield {
      val sourceIsSame =
        if (f.position.sourcePath.isDefined) (f.position.sourcePath.get must beEqualTo(l.position.sourcePath.get)).setMessage(s"${f.position} != ${l.position}")
        else l.position.sourcePath.isDefined must beFalse
      val lineIsSame =
        if (f.position.line.isDefined) f.position.line.get must beEqualTo(l.position.line.get)
        else l.position.line.isDefined must beFalse
      val severityIsSame = f.severity must beEqualTo(l.severity)
      // TODO - We should check to see if the levenshtein distance of the messages is close...
      sourceIsSame and lineIsSame and severityIsSame
    }
    val errorsAreTheSame = pResults.reduce(_ and _)
    sameResult and errorsAreTheSame
  }

  def compile(c: JavaTools, sources: Seq[File], args: Seq[String]): (Boolean, Array[Problem]) = {
    val log = Logger.Null
    val reporter = new LoggerReporter(10, log)
    val result = c.compile(sources, args)(log, reporter)
    (result, reporter.problems)
  }

  def doc(c: JavaTools, sources: Seq[File], args: Seq[String]): (Boolean, Array[Problem]) = {
    val log = Logger.Null
    val reporter = new LoggerReporter(10, log)
    val result = c.doc(sources, args)(log, reporter)
    (result, reporter.problems)
  }

  // TODO - Create one with known JAVA HOME.
  def forked = JavaTools(JavaCompiler.fork(), Javadoc.fork())

  def local =
    JavaTools(
      JavaCompiler.local.getOrElse(sys.error("This test cannot be run on a JRE, but only a JDK.")),
      Javadoc.local.getOrElse(Javadoc.fork())
    )

  def cwd =
    (new File(new File(".").getAbsolutePath)).getCanonicalFile

  def knownSampleErrorFile =
    new java.io.File(getClass.getResource("test1.java").toURI)

  def knownSampleGoodFile =
    new java.io.File(getClass.getResource("good.java").toURI)

  def hasStaticFinalFile =
    new java.io.File(getClass.getResource("hasstaticfinal.java").toURI)

}
