/*
 * sbt
 * Copyright 2011 - 2018, Lightbend, Inc.
 * Copyright 2008 - 2010, Mark Harrah
 * Licensed under Apache License 2.0 (see LICENSE)
 */

package testpkg

import sbt.internal.bsp.*
import sbt.internal.langserver.ErrorCodes
import sbt.IO
import sbt.internal.protocol.JsonRpcRequestMessage
import sbt.internal.protocol.codec.JsonRPCProtocol.given
import sjsonnew.JsonWriter
import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter }

import java.io.File
import java.net.URI
import java.nio.file.{ Files, Paths }
import java.util.concurrent.atomic.AtomicInteger
import scala.concurrent.duration.*

// starts svr using server-test/buildserver and perform custom server tests
class BuildServerTest extends AbstractServerTest {

  import sbt.internal.bsp.codec.JsonProtocol.given

  override val testDirectory: String = "buildserver"
  private val idGen: AtomicInteger = new AtomicInteger(0)
  private def nextId(): Int = idGen.getAndIncrement()

  test("build/initialize") {
    val id = initializeRequest()
    assert(svr.waitForString(10.seconds) { s =>
      s.contains(s""""id":"$id"""") &&
      s.contains(""""resourcesProvider":true""") &&
      s.contains(""""outputPathsProvider":true""")
    })
  }

  test("workspace/buildTargets") {
    svr.sendJsonRpc(
      """{ "jsonrpc": "2.0", "id": "16", "method": "workspace/buildTargets", "params": {} }"""
    )
    assertProcessing("workspace/buildTargets")
    val result = svr.waitFor[WorkspaceBuildTargetsResult](10.seconds)
    val utilTargetIdentifier = BuildTargetIdentifier(buildTargetUri("util", "Compile"))
    val utilTarget = result.targets.find(_.id == utilTargetIdentifier).get
    assert(utilTarget.id.uri.toString.endsWith("#util/Compile"))
    val runAndTestTarget = result.targets.find(_.displayName.contains("runAndTest")).get
    // runAndTest should declare the dependency to util even if optional
    assert(runAndTestTarget.dependencies.contains(utilTargetIdentifier))
    val buildServerBuildTarget =
      result.targets.find(_.displayName.contains("buildserver-root-build")) match
        case Some(t) => t
        case None    => sys.error(s"buildserver-root-build not in ${result.targets}")
    assert(buildServerBuildTarget.id.uri.toString.endsWith("#buildserver-root-build"))
    assert(!result.targets.exists(_.displayName.contains("badBuildTarget")))
    // Check for JVM based Scala Project, built target should contain Java version information
    val scalaBuildTarget =
      Converter.fromJsonOptionUnsafe[ScalaBuildTarget](utilTarget.data)
    val javaTarget = scalaBuildTarget.jvmBuildTarget
    (javaTarget.flatMap(_.javaVersion), javaTarget.flatMap(_.javaHome)) match {
      case (Some(javaVersion), Some(javaHome)) =>
        assert(javaVersion.equals(sys.props("java.version")))
        assert(javaHome.equals(Paths.get(sys.props("java.home")).toUri))
      case _ => fail("JVM build target should contain javaVersion and javaHome")
    }
  }

  test("buildTarget/sources") {
    val buildTarget = buildTargetUri("util", "Compile")
    val badBuildTarget = buildTargetUri("badBuildTarget", "Compile")
    buildTargetSources(Seq(buildTarget, badBuildTarget))
    val s = svr.waitFor[SourcesResult](10.seconds)
    val sources = s.items.head.sources.map(_.uri)
    assert(sources.contains(new File(svr.baseDirectory, "util/src/main/scala").toURI))
  }

  test("buildTarget/sources: base sources") {
    val buildTarget = buildTargetUri("buildserver-root", "Compile")
    buildTargetSources(Seq(buildTarget))
    val s = svr.waitFor[SourcesResult](10.seconds)
    val sources = s.items.head.sources
    val expectedSource = SourceItem(
      new File(svr.baseDirectory, "BaseSource.scala").toURI,
      SourceItemKind.File,
      generated = false
    )
    assert(sources.contains(expectedSource))
  }

  test("buildTarget/sources: sbt") {
    val x = new URI(s"${svr.baseDirectory.getAbsoluteFile.toURI}#buildserver-root-build")
    buildTargetSources(Seq(x))
    val s = svr.waitFor[SourcesResult](10.seconds)
    val sources = s.items.head.sources.map(_.uri).sorted
    val expectedSources = Vector(
      "build.sbt",
      "project/A.scala",
      "project/src/main/java",
      "project/src/main/scala-3",
      "project/src/main/scala-sbt-2",
      "project/src/main/scala/",
      s"target/out/jvm/scala-${TestProperties.scalaVersion}/buildserver-build/src_managed/main"
    ).map(rel => new File(svr.baseDirectory.getAbsoluteFile, rel).toURI).sorted
    assert(sources == expectedSources)
  }

  test("buildTarget/compile") {
    val buildTarget = buildTargetUri("util", "Compile")
    compile(buildTarget)
    val res = svr.waitFor[BspCompileResult](10.seconds)
    assert(res.statusCode == StatusCode.Success)
  }

  test("buildTarget/compile - reports compilation progress") {
    val buildTarget = buildTargetUri("runAndTest", "Compile")
    compile(buildTarget)
    // This doesn't always come back in 10s on CI.
    assert(svr.waitForString(20.seconds) { s =>
      s.contains("build/taskStart") &&
      s.contains(""""message":"Compiling runAndTest"""")
    })

    assert(svr.waitForString(20.seconds) { s =>
      s.contains("build/taskProgress") &&
      s.contains(""""message":"Compiling runAndTest (15%)"""")
    })

    assert(svr.waitForString(20.seconds) { s =>
      s.contains("build/taskProgress") &&
      s.contains(""""message":"Compiling runAndTest (100%)"""")
    })

    assert(svr.waitForString(20.seconds) { s =>
      s.contains("build/publishDiagnostics") &&
      s.contains(""""diagnostics":[]""")
    })

    assert(svr.waitForString(20.seconds) { s =>
      s.contains("build/taskFinish") &&
      s.contains(""""message":"Compiled runAndTest"""")
    })
  }

  test(
    "buildTarget/compile [diagnostics] don't publish unnecessary for successful compilation case"
  ) {
    val buildTarget = buildTargetUri("diagnostics", "Compile")
    val mainFile = new File(svr.baseDirectory, "diagnostics/src/main/scala/Diagnostics.scala")

    compile(buildTarget)

    assertMessage("build/taskFinish", """"message":"Compiled diagnostics"""")(30.seconds)

    // introduce compile error
    IO.write(
      mainFile,
      """|object Diagnostics {
         |  private val a: Int = ""
         |}""".stripMargin
    )

    compile(buildTarget)

    assertMessage(
      "build/publishDiagnostics",
      "Diagnostics.scala",
      "\"message\":\"type mismatch"
    )(
      duration = 30.seconds,
      message = "should send publishDiagnostics with type error for Main.scala"
    )

    // fix compilation error
    IO.write(
      mainFile,
      """|object Diagnostics {
         |  private val a: Int = 5
         |}""".stripMargin
    )

    reloadWorkspace()
    compile(buildTarget)

    assertMessage("build/publishDiagnostics", "Diagnostics.scala", "\"diagnostics\":[]")(
      duration = 30.seconds,
      message = "should send publishDiagnostics with empty diagnostics"
    )
    assertMessage("build/taskFinish", "\"noOp\":true")()

    // trigger no-op compilation
    compile(buildTarget)

    assert(
      svr.waitForString(20.seconds) { s =>
        if (s.contains("build/publishDiagnostics") && s.contains("Diagnostics.scala"))
          throw new Exception("shouldn't send publishDiagnostics if noOp compilation")
        else s.contains("build/taskFinish") && s.contains("\"noOp\":true")
      },
      "shouldn't send publishDiagnostics if there's no change in diagnostics (were empty, are empty)"
    )
  }

  test("buildTarget/compile [diagnostics] clear stale warnings") {
    val buildTarget = buildTargetUri("diagnostics", "Compile")
    val testFile = new File(svr.baseDirectory, s"diagnostics/src/main/scala/PatternMatch.scala")

    compile(buildTarget)

    assertMessage(
      "build/publishDiagnostics",
      "PatternMatch.scala",
      """"message":"match may not be exhaustive"""
    )(
      duration = 30.seconds,
      message = "should send publishDiagnostics with type error for PatternMatch.scala"
    )

    IO.write(
      testFile,
      """|class PatternMatch {
         |  val opt: Option[Int] = None
         |  opt match {
         |    case Some(value) => ()
         |    case None => ()
         |  }
         |}
         |""".stripMargin
    )

    reloadWorkspace()
    compile(buildTarget)

    assertMessage("build/publishDiagnostics", "PatternMatch.scala", "\"diagnostics\":[]")(
      duration = 30.seconds,
      message = "should send publishDiagnostics with empty diagnostics"
    )
  }

  test("buildTarget/compile - returns StatusCode.Error when compilation fails") {
    // Reproduces #8104: failed BSP compile must return BspCompileResult with statusCode Error,
    // not a JSON-RPC error. Placed after diagnostics tests so shared message queue is not polluted.
    val buildTarget = buildTargetUri("diagnostics", "Compile")
    val mainFile = new File(svr.baseDirectory, "diagnostics/src/main/scala/Diagnostics.scala")
    val original = IO.read(mainFile)
    try {
      IO.write(
        mainFile,
        """|object Diagnostics {
           |  private val a: Int = ""
           |}""".stripMargin
      )
      compile(buildTarget)
      val res = svr.waitFor[BspCompileResult](30.seconds)
      assert(
        res.statusCode == StatusCode.Error,
        s"expected StatusCode.Error, got ${res.statusCode}"
      )
      // Drain notifications from this failing compile so the next test does not consume them
      def ourFailureNotification(s: String): Boolean =
        (s.contains("build/taskStart") && s.contains("diagnostics")) ||
          (s.contains("build/taskFinish") && s.contains("Compiled diagnostics") && s.contains(
            "\"status\":2"
          )) ||
          (s.contains("build/publishDiagnostics") && s.contains("Diagnostics.scala") && s.contains(
            "type mismatch"
          ))
      svr.waitForString(30.seconds)(ourFailureNotification)
      svr.waitForString(30.seconds)(ourFailureNotification)
      svr.waitForString(30.seconds)(ourFailureNotification)
    } finally {
      IO.write(mainFile, original)
    }
  }

  test("buildTarget/compile: Java diagnostics") {
    val buildTarget = buildTargetUri("javaProj", "Compile")

    compile(buildTarget)
    assertMessage(
      "build/publishDiagnostics",
      "Hello.java",
      """"severity":2""",
      """missing type arguments for generic class java.util.List"""
    )(message = "should send publishDiagnostics with severity 2 for Hello.java")

    assertMessage(
      "build/publishDiagnostics",
      "Hello.java",
      """"severity":1""",
      """incompatible types: int cannot be converted to java.lang.String"""
    )(
      message = "should send publishDiagnostics with severity 1 for Hello.java"
    )
  }

  /*
  test("buildTarget/compile [Java diagnostics] clear stale warnings") {
    val buildTarget = buildTargetUri("javaProj", "Compile")
    val testFile = new File(svr.baseDirectory, s"java-proj/src/main/java/example/Hello.java")

    val otherBuildFile = new File(svr.baseDirectory, "force-java-out-of-process-compiler.sbt")
    // Setting `javaHome` will force sbt to shell out to an external Java compiler instead
    // of using the local compilation service offered by the JVM running this SBT instance.
    IO.write(
      otherBuildFile,
      """
        |def jdk: File = sbt.internal.util.Util.javaHome.toFile()
        |lazy val javaProj = project
        |  .in(file("java-proj"))
        |  .settings(
        |    javacOptions += "-Xlint:all",
        |    javaHome := Some(jdk)
        |  )
        |""".stripMargin
    )
    reloadWorkspace()

    compile(buildTarget)

    assertMessage(
      "build/publishDiagnostics",
      "Hello.java",
      """"severity":2""",
      """found raw type"""
    )(message = "should send publishDiagnostics with severity 2 for Hello.java", debug = false)

    assertMessage(
      "build/publishDiagnostics",
      "Hello.java",
      """"severity":1""",
      """incompatible types: int cannot be converted"""
    )(
      message = "should send publishDiagnostics with severity 1 for Hello.java"
    )
    // Note the messages changed slightly in both cases. That's interesting…

    IO.write(
      testFile,
      """|package example;
         |
         |import java.util.List;
         |import java.util.ArrayList;
         |
         |class Hello {
         |    public static void main(String[] args) {
         |        List<String> list = new ArrayList<>();
         |        String msg = "42";
         |        System.out.println(msg);
         |    }
         |}
         |""".stripMargin
    )

    compile(buildTarget)

    /*
    assertMessage(
      "build/publishDiagnostics",
      "Hello.java",
      "\"diagnostics\":[]",
      "\"reset\":true"
    )(
      message = "should send publishDiagnostics with empty diagnostics",
      debug = false
    )
   */

    IO.delete(otherBuildFile)
    reloadWorkspace()
    ()
  }
   */

  test("buildTarget/scalacOptions, buildTarget/javacOptions") {
    val buildTargets = Seq(
      buildTargetUri("util", "Compile"),
      buildTargetUri("badBuildTarget", "Compile"),
    )

    val classDirectoryUri = new File(svr.baseDirectory, "util/classes").toURI
    println(s""""classDirectory":"$classDirectoryUri"""")
    val id1 = scalacOptions(buildTargets)
    assertMessage(
      s""""id":"$id1"""",
      "scala-library-2.13.11.jar",
      s""""classDirectory":"$classDirectoryUri""""
    )()

    val id2 = javacOptions(buildTargets)
    assertMessage(
      s""""id":"$id2"""",
      "scala-library-2.13.11.jar",
      s""""classDirectory":"$classDirectoryUri""""
    )()
  }

  test("buildTarget/cleanCache") {
    def classFile = svr.baseDirectory.toPath.resolve(
      "target/out/jvm/scala-2.13.11/runandtest/classes/main/Main.class"
    )
    val buildTarget = buildTargetUri("runAndTest", "Compile")
    compile(buildTarget)
    svr.waitFor[BspCompileResult](10.seconds)
    assert(Files.exists(classFile))
    svr.sendJsonRpc(
      s"""{ "jsonrpc": "2.0", "id": "${nextId()}", "method": "buildTarget/cleanCache", "params": {
         |  "targets": [{ "uri": "$buildTarget" }]
         |} }""".stripMargin
    )
    assertProcessing("buildTarget/cleanCache")
    val res = svr.waitFor[CleanCacheResult](10.seconds)
    assert(res.cleaned)
    assert(Files.notExists(classFile))
  }

  test("buildTarget/cleanCache: rebuild project") {
    svr.sendJsonRpc(
      s"""{ "jsonrpc": "2.0", "id": "${nextId()}", "method": "workspace/buildTargets", "params": {} }"""
    )
    assertProcessing("workspace/buildTargets")
    val result = svr.waitFor[WorkspaceBuildTargetsResult](10.seconds)
    val allTargets = result.targets.map(_.id.uri)

    svr.sendJsonRpc(
      s"""{ "jsonrpc": "2.0", "id": "${nextId()}", "method": "buildTarget/cleanCache", "params": {
         |  "targets": [
         |    ${allTargets.map(uri => s"""{ "uri": "$uri" }""").mkString(",\n")}
         |  ]
         |} }""".stripMargin
    )
    assertProcessing("buildTarget/cleanCache")
    val res = svr.waitFor[CleanCacheResult](10.seconds)
    assert(res.cleaned)
  }

  test("workspace/reload") {
    val id = nextId()
    svr.sendJsonRpc(
      s"""{ "jsonrpc": "2.0", "id": "$id", "method": "workspace/reload"}"""
    )
    assertProcessing("workspace/reload")
    assertMessage(s""""id":"$id"""", """"result":null""")()
  }

  /*
  test("workspace/reload: send diagnostic and respond with error") {
    // write an other-build.sbt file that does not compile
    val otherBuildFile = svr.baseDirectory.toPath.resolve("other-build.sbt")
    Files.write(
      otherBuildFile,
      """|val someSettings = Seq(
         |  scalacOptions ++= "-deprecation"
         |)
         |""".stripMargin.getBytes
    )
    val id = reloadWorkspace()
    // reload
    assertMessage(
      s""""buildTarget":{"uri":"$metaBuildTarget"}""",
      s""""textDocument":{"uri":"${otherBuildFile.toUri}"}""",
      """"severity":1""",
      """"reset":true"""
    )()

    assertMessage(
      s""""id":"$id"""",
      """"error"""",
      s""""code":${ErrorCodes.InternalError}""",
      "String cannot be appended to Seq[String]"
    )()
    // fix the other-build.sbt file and reload again
    Files.write(
      otherBuildFile,
      """|val someSettings = Seq(
         |  scalacOptions += "-deprecation"
         |)
         |""".stripMargin.getBytes
    )
    reloadWorkspace()
    // assert received an empty diagnostic
    assertMessage(
      s""""buildTarget":{"uri":"$metaBuildTarget"}""",
      s""""textDocument":{"uri":"${otherBuildFile.toUri}"}""",
      """"diagnostics":[]""",
      """"reset":true"""
    )()
    Files.delete(otherBuildFile)
  }
   */

  test("buildTarget/scalaMainClasses") {
    val buildTarget = buildTargetUri("runAndTest", "Compile")
    val badBuildTarget = buildTargetUri("badBuildTarget", "Compile")
    val id = nextId()
    svr.sendJsonRpc(
      s"""{ "jsonrpc": "2.0", "id": "$id", "method": "buildTarget/scalaMainClasses", "params": {
         |  "targets": [{ "uri": "$buildTarget" }, { "uri": "$badBuildTarget" }]
         |} }""".stripMargin
    )
    assertProcessing("buildTarget/scalaMainClasses")
    assertMessage(s""""id":"$id"""", """"class":"main.Main"""")(duration = 30.seconds)
  }

  test("buildTarget/run") {
    val buildTarget = buildTargetUri("runAndTest", "Compile")
    val id = nextId()
    svr.sendJsonRpc(
      s"""{ "jsonrpc": "2.0", "id": "$id", "method": "buildTarget/run", "params": {
         |  "target": { "uri": "$buildTarget" },
         |  "dataKind": "scala-main-class",
         |  "data": { "class": "main.Main" }
         |} }""".stripMargin
    )
    assertProcessing("buildTarget/run")
    assertMessage("build/logMessage", """"message":"Hello World!"""")()
    assertMessage(s""""id":"$id"""", """"statusCode":1""")()
  }

  test("buildTarget/jvmRunEnvironment") {
    val buildTarget = buildTargetUri("runAndTest", "Compile")
    val id = nextId()
    svr.sendJsonRpc(
      s"""|{ "jsonrpc": "2.0",
          |  "id": "$id",
          |  "method": "buildTarget/jvmRunEnvironment",
          |  "params": { "targets": [{ "uri": "$buildTarget" }] }
          |}""".stripMargin
    )
    assertProcessing("buildTarget/jvmRunEnvironment")
    assertMessage(
      s""""id":"$id"""",
      "jsoniter-scala-core_2.13-2.13.11.jar", // compile dependency
      "\"jvmOptions\":[\"Xmx256M\"]",
      "\"environmentVariables\":{\"KEY\":\"VALUE\"}",
      "/buildserver/run-and-test" // working directory
    )()
  }

  test("buildTarget/jvmTestEnvironment") {
    val buildTarget = buildTargetUri("runAndTest", "Test")
    val id = nextId()
    svr.sendJsonRpc(
      s"""|{ "jsonrpc": "2.0",
          |  "id": "$id",
          |  "method": "buildTarget/jvmTestEnvironment",
          |  "params": { "targets": [{ "uri": "$buildTarget" }] }
          |}""".stripMargin
    )
    assertProcessing("buildTarget/jvmTestEnvironment")
    assertMessage(
      s""""id":"$id"""",
      "jsoniter-scala-core_2.13-2.13.11.jar", // compile dependency
      "scalatest_2.13-3.0.8.jar", // test dependency
      "\"jvmOptions\":[\"Xmx512M\"]",
      "\"environmentVariables\":{\"KEY_TEST\":\"VALUE_TEST\"}"
    )()
  }

  test("buildTarget/scalaTestClasses") {
    val buildTarget = buildTargetUri("runAndTest", "Test")
    val badBuildTarget = buildTargetUri("badBuildTarget", "Test")
    val id = nextId()
    svr.sendJsonRpc(
      s"""{ "jsonrpc": "2.0", "id": "$id", "method": "buildTarget/scalaTestClasses", "params": {
         |  "targets": [{ "uri": "$buildTarget" }, { "uri": "$badBuildTarget" }]
         |} }""".stripMargin
    )
    assertProcessing("buildTarget/scalaTestClasses")
    assertMessage(
      s""""id":"$id"""",
      """"tests.FailingTest"""",
      """"tests.PassingTest"""",
      """"framework":"ScalaTest""""
    )()
  }

  test("buildTarget/test: run all tests") {
    val buildTarget = buildTargetUri("runAndTest", "Test")
    val id = nextId()
    svr.sendJsonRpc(
      s"""{ "jsonrpc": "2.0", "id": "$id", "method": "buildTarget/test", "params": {
         |  "targets": [{ "uri": "$buildTarget" }]
         |} }""".stripMargin
    )
    assertProcessing("buildTarget/test")
    assertMessage(s""""id":"$id"""", """"statusCode":2""")()
  }

  test("buildTarget/test: run one test class") {
    val buildTarget = buildTargetUri("runAndTest", "Test")
    val id = nextId()
    svr.sendJsonRpc(
      s"""{ "jsonrpc": "2.0", "id": "$id", "method": "buildTarget/test", "params": {
         |  "targets": [{ "uri": "$buildTarget" }],
         |  "dataKind": "scala-test",
         |  "data": {
         |    "testClasses": [
         |      {
         |        "target": { "uri": "$buildTarget" },
         |        "classes": ["tests.PassingTest"]
         |      }
         |    ]
         |  }
         |} }""".stripMargin
    )
    assertProcessing("buildTarget/test")
    assertMessage(s""""id":"$id"""", """"statusCode":1""")()
  }

  test("buildTarget/compile: report error") {
    val buildTarget = buildTargetUri("reportError", "Compile")
    compile(buildTarget)
    assertMessage(
      s""""buildTarget":{"uri":"$buildTarget"}""",
      """"severity":1""",
      """"reset":true"""
    )()
  }

  test("buildTarget/compile: report warning") {
    val buildTarget = buildTargetUri("reportWarning", "Compile")
    compile(buildTarget)
    assertMessage(
      s""""buildTarget":{"uri":"$buildTarget"}""",
      """"severity":2""",
      """"reset":true"""
    )()
  }

  test("buildTarget/compile: respond error") {
    val buildTarget = buildTargetUri("respondError", "Compile")
    val id = compile(buildTarget)
    assertMessage(
      s""""id":"$id"""",
      """"error"""",
      s""""code":${ErrorCodes.InternalError}""",
      "custom message"
    )()
  }

  test("buildTarget/resources") {
    val buildTarget = buildTargetUri("util", "Compile")
    val badBuildTarget = buildTargetUri("badBuildTarget", "Compile")
    val id = nextId()
    svr.sendJsonRpc(
      s"""{ "jsonrpc": "2.0", "id": "$id", "method": "buildTarget/resources", "params": {
         |  "targets": [{ "uri": "$buildTarget" }, { "uri": "$badBuildTarget" }]
         |} }""".stripMargin
    )
    assertProcessing("buildTarget/resources")
    assertMessage(s""""id":"$id"""", "util/src/main/resources/")()
  }

  test("buildTarget/outputPaths") {
    val buildTarget = buildTargetUri("util", "Compile")
    val badBuildTarget = buildTargetUri("badBuildTarget", "Compile")
    svr.sendJsonRpc(
      s"""{ "jsonrpc": "2.0", "id": "${nextId()}", "method": "buildTarget/outputPaths", "params": {
         |  "targets": [{ "uri": "$buildTarget" }, { "uri": "$badBuildTarget" }]
         |} }""".stripMargin
    )
    assertProcessing("buildTarget/outputPaths")
    val actualResult = svr.waitFor[OutputPathsResult](10.seconds)
    val expectedResult = OutputPathsResult(
      items = Vector(
        OutputPathsItem(
          target = BuildTargetIdentifier(buildTarget),
          outputPaths = Vector(
            OutputPathItem(
              uri = new File(svr.baseDirectory, "target/out/jvm/scala-2.13.11/util/").toURI,
              kind = OutputPathItemKind.Directory
            )
          )
        )
      )
    )
    assert(actualResult == expectedResult)
  }

  ignore("buildTarget/compile: twirl diagnostics (sourcePositionMappers)") {
    val buildTarget = buildTargetUri("twirlProj", "Compile")
    val testFile = new File(svr.baseDirectory, s"twirlProj/src/main/twirl/main.scala.html")

    compile(buildTarget)
    assertMessage(
      "build/publishDiagnostics",
      "main.scala.html",
      """"severity":1""",
      "not found: value title_"
    )(message = "should report diagnostic in Twirl file")
    IO.write(
      testFile,
      """|@(title: String, paragraphs: Seq[String])
         |
         |<!DOCTYPE HTML>
         |<html lang="en">
         |  <head>
         |    <title>@title</title>
         |  </head>
         |  <body>
         |    <h1>@title</h1>
         |    @for(paragraph <- paragraphs) {
         |      <p>@paragraph</p>
         |    }
         |  </body>
         |</html>
         |""".stripMargin
    )
    compile(buildTarget)
    assertMessage(
      "build/publishDiagnostics",
      "main.scala.html",
      """"diagnostics":[]""",
      """"reset":true"""
    )(
      duration = 30.seconds,
      message = "should reset diagnostic in Twirl file"
    )
  }

  private def initializeRequest(): Int = {
    val params = InitializeBuildParams(
      "test client",
      "1.0.0",
      "2.1.0-M1",
      new URI("file://root/"),
      BuildClientCapabilities(Vector("scala")),
      None
    )
    sendRequest("build/initialize", params)
  }

  private def assertProcessing(method: String, debug: Boolean = false): Unit =
    assertMessage("build/logMessage", s""""message":"Processing $method"""")(debug = debug)

  inline def assertMessage(
      parts: String*
  )(duration: FiniteDuration = 10.seconds, debug: Boolean = false, message: String = ""): Unit = {
    def assertion =
      svr.waitForString(duration) { msg =>
        if (debug) println(msg)
        if debug then
          parts.foreach: p =>
            if msg.contains(p) then println(s"> $msg contains $p")
            else ()
        parts.forall(msg.contains)
      }
    if message.nonEmpty then assert(assertion, message)
    else assert(assertion)
  }

  private def reloadWorkspace(): Int =
    sendRequest("workspace/reload")

  private def compile(buildTarget: URI): Int = {
    val params =
      CompileParams(targets = Vector(BuildTargetIdentifier(buildTarget)), None, Vector.empty)
    sendRequest("buildTarget/compile", params)
  }

  private def scalacOptions(buildTargets: Seq[URI]): Int = {
    val targets = buildTargets.map(BuildTargetIdentifier.apply).toVector
    sendRequest("buildTarget/scalacOptions", ScalacOptionsParams(targets))
  }

  private def javacOptions(buildTargets: Seq[URI]): Int = {
    val targets = buildTargets.map(BuildTargetIdentifier.apply).toVector
    sendRequest("buildTarget/scalacOptions", ScalacOptionsParams(targets))
  }

  private def buildTargetSources(buildTargets: Seq[URI]): Int = {
    val targets = buildTargets.map(BuildTargetIdentifier.apply).toVector
    sendRequest("buildTarget/sources", SourcesParams(targets))
  }

  private def sendRequest(method: String): Int = {
    val id = nextId()
    val msg = JsonRpcRequestMessage("2.0", id.toString, method, None)
    val json = Converter.toJson(msg).get
    svr.sendJsonRpc(CompactPrinter(json))
    id
  }

  private def sendRequest[T: JsonWriter](method: String, params: T): Int = {
    val id = nextId()
    val msg = JsonRpcRequestMessage("2.0", id.toString, method, Converter.toJson(params).get)
    val json = Converter.toJson(msg).get
    svr.sendJsonRpc(CompactPrinter(json))
    if (method != "build/initialize") assertProcessing(method)
    id
  }

  private def buildTargetUri(project: String, config: String): URI =
    new URI(s"${svr.baseDirectory.getAbsoluteFile.toURI}#$project/$config")

  private def metaBuildTarget: String =
    s"${svr.baseDirectory.getAbsoluteFile.toURI}project/#buildserver-build/Compile"
}
