/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.ktfmt.format

import com.facebook.ktfmt.testutil.assertFormatted
import com.facebook.ktfmt.testutil.assertThatFormatting
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@Suppress("FunctionNaming")
@RunWith(JUnit4::class)
class GoogleStyleFormatterKtTest {

  @Test
  fun `kitchen sink of tests`() {
    // Don't add more tests here
    val code =
        """
        |fun
        |f  (
        |a : Int
        | , b: Double , c:String) {           var result = 0
        | val aVeryLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongVar = 43
        |          foo.bar.zed.accept(
        |
        |          )
        |
        |          foo(
        |
        |          )
        |
        |          foo.bar.zed.accept(
        |                DoSomething.bar()
        |          )
        |
        |          bar(
        |          ImmutableList.newBuilder().add(1).add(1).add(1).add(1).add(1).add(1).add(1).add(1).add(1).add(1).build())
        |
        |
        |          ImmutableList.newBuilder().add(1).add(1).add(1).add(1).add(1).add(1).add(1).add(1).add(1).add(1).build()
        |     }
        |"""
            .trimMargin()

    val expected =
        """
        |fun f(a: Int, b: Double, c: String) {
        |  var result = 0
        |  val aVeryLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongVar =
        |    43
        |  foo.bar.zed.accept()
        |
        |  foo()
        |
        |  foo.bar.zed.accept(DoSomething.bar())
        |
        |  bar(
        |    ImmutableList.newBuilder()
        |      .add(1)
        |      .add(1)
        |      .add(1)
        |      .add(1)
        |      .add(1)
        |      .add(1)
        |      .add(1)
        |      .add(1)
        |      .add(1)
        |      .add(1)
        |      .build()
        |  )
        |
        |  ImmutableList.newBuilder()
        |    .add(1)
        |    .add(1)
        |    .add(1)
        |    .add(1)
        |    .add(1)
        |    .add(1)
        |    .add(1)
        |    .add(1)
        |    .add(1)
        |    .add(1)
        |    .build()
        |}
        |"""
            .trimMargin()

    assertThatFormatting(code).withOptions(Formatter.GOOGLE_FORMAT).isEqualTo(expected)
    // Don't add more tests here
  }

  @Test
  fun `class params are placed each in their own line`() =
      assertFormatted(
          """
      |-----------------------------------------
      |class Foo(
      |  a: Int,
      |  var b: Double,
      |  val c: String
      |) {
      |  //
      |}
      |
      |class Foo(
      |  a: Int,
      |  var b: Double,
      |  val c: String
      |)
      |
      |class Foo(
      |  a: Int,
      |  var b: Int,
      |  val c: Int
      |) {
      |  //
      |}
      |
      |class Bi(
      |  a: Int,
      |  var b: Int,
      |  val c: Int
      |) {
      |  //
      |}
      |
      |class C(a: Int, var b: Int, val c: Int) {
      |  //
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `function params are placed each in their own line`() =
      assertFormatted(
          """
      |-----------------------------------------
      |fun foo12(
      |  a: Int,
      |  var b: Double,
      |  val c: String
      |) {
      |  //
      |}
      |
      |fun foo12(
      |  a: Int,
      |  var b: Double,
      |  val c: String
      |)
      |
      |fun foo12(
      |  a: Int,
      |  var b: Double,
      |  val c: String
      |) = 5
      |
      |fun foo12(
      |  a: Int,
      |  var b: Int,
      |  val c: Int
      |) {
      |  //
      |}
      |
      |fun bi12(
      |  a: Int,
      |  var b: Int,
      |  val c: Int
      |) {
      |  //
      |}
      |
      |fun c12(a: Int, var b: Int, val c: Int) {
      |  //
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `return type doesn't fit in one line`() =
      assertFormatted(
          """
      |--------------------------------------------------
      |interface X {
      |  fun f(
      |    arg1: Arg1Type,
      |    arg2: Arg2Type
      |  ): Map<String, Map<String, Double>>? {
      |    //
      |  }
      |
      |  fun functionWithGenericReturnType(
      |    arg1: Arg1Type,
      |    arg2: Arg2Type
      |  ): Map<String, Map<String, Double>>? {
      |    //
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `indent parameters after a break when there's a lambda afterwards`() =
      assertFormatted(
          """
      |---------------------------
      |class C {
      |  fun method() {
      |    Foo.FooBar(
      |        param1,
      |        param2
      |      )
      |      .apply {
      |        //
      |        foo
      |      }
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `no forward propagation of breaks in call expressions (at trailing lambda)`() =
      assertFormatted(
          """
      |--------------------------
      |fun test() {
      |  foo_bar_baz__zip<A>(b) {
      |    c
      |  }
      |  foo.bar(baz).zip<A>(b) {
      |    c
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `forward propagation of breaks in call expressions (at value args)`() =
      assertFormatted(
          """
      |----------------------
      |fun test() {
      |  foo_bar_baz__zip<A>(
      |    b
      |  ) {
      |    c
      |  }
      |}
      |
      |fun test() {
      |  foo.bar(baz).zip<A>(
      |    b
      |  ) {
      |    c
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `forward propagation of breaks in call expressions (at type args)`() =
      assertFormatted(
          """
      |-------------------
      |fun test() {
      |  foo_bar_baz__zip<
      |    A
      |  >(
      |    b
      |  ) {
      |    c
      |  }
      |  foo.bar(baz).zip<
      |    A
      |  >(
      |    b
      |  ) {
      |    c
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `expected indent in methods following single-line strings`() =
      assertFormatted(
          """
      |-------------------------
      |"Hello %s"
      |  .format(expression)
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `forced break between multi-line strings and their selectors`() =
      assertFormatted(
          """
      |-------------------------
      |val STRING =
      |  $TQ
      |  |foo
      |  |$TQ
      |    .wouldFit()
      |
      |val STRING =
      |  $TQ
      |  |foo
      |  |----------------------------------$TQ
      |    .wouldntFit()
      |
      |val STRING =
      |  $TQ
      |  |foo
      |  |$TQ
      |    .firstLink()
      |    .secondLink()
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `properly break fully qualified nested user types`() =
      assertFormatted(
          """
      |-------------------------------------------------------
      |val complicated:
      |  com.example.interesting.SomeType<
      |    com.example.interesting.SomeType<Int, Nothing>,
      |    com.example.interesting.SomeType<
      |      com.example.interesting.SomeType<Int, Nothing>,
      |      Nothing
      |    >
      |  > =
      |  DUMMY
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `don't one-line lambdas following argument breaks`() =
      assertFormatted(
          """
      |------------------------------------------------------------------------
      |class Foo : Bar() {
      |  fun doIt() {
      |    // don't break in lambda, no argument breaks found
      |    fruit.forEach { eat(it) }
      |
      |    // break in lambda, without comma
      |    fruit.forEach(
      |      someVeryLongParameterNameThatWillCauseABreak,
      |      evenWithoutATrailingCommaOnTheParameterListSoLetsSeeIt
      |    ) {
      |      eat(it)
      |    }
      |
      |    // break in the lambda, with comma
      |    fruit.forEach(
      |      fromTheVine = true,
      |    ) {
      |      eat(it)
      |    }
      |
      |    // don't break in the inner lambda, as nesting doesn't respect outer levels
      |    fruit.forEach(
      |      fromTheVine = true,
      |    ) {
      |      fruit.forEach { eat(it) }
      |    }
      |
      |    // don't break in the lambda, as breaks don't propagate
      |    fruit
      |      .onlyBananas(
      |        fromTheVine = true,
      |      )
      |      .forEach { eat(it) }
      |
      |    // don't break in the inner lambda, as breaks don't propagate to parameters
      |    fruit.onlyBananas(
      |      fromTheVine = true,
      |      processThem = { eat(it) },
      |    ) {
      |      eat(it)
      |    }
      |
      |    // don't break in the inner lambda, as breaks don't propagate to the body
      |    fruit.onlyBananas(
      |      fromTheVine = true,
      |    ) {
      |      val anon = { eat(it) }
      |    }
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `function calls with multiple arguments`() =
      assertFormatted(
          """
      |fun f() {
      |  foo(1, 2, 3)
      |
      |  foo(
      |    123456789012345678901234567890,
      |    123456789012345678901234567890,
      |    123456789012345678901234567890
      |  )
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT)

  @Test
  fun `line breaks inside when expressions and conditions`() =
      assertFormatted(
          """
      |fun f() {
      |  return Text.create(c)
      |    .onTouch {
      |      when (it.motionEvent.action) {
      |        ACTION_DOWN ->
      |          Toast.makeText(it.view.context, "Down!", Toast.LENGTH_SHORT, blablabla).show()
      |        ACTION_UP -> Toast.makeText(it.view.context, "Up!", Toast.LENGTH_SHORT).show()
      |        ACTION_DOWN ->
      |          Toast.makeText(
      |              it.view.context,
      |              "Down!",
      |              Toast.LENGTH_SHORT,
      |              blablabla,
      |              blablabl,
      |              blablabl,
      |              blablabl,
      |              blabla
      |            )
      |            .show()
      |      }
      |    }
      |    .build()
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
      )

  @Test
  fun `anonymous function`() =
      assertFormatted(
          """
      |fun f() {
      |  setListener(
      |    fun(number: Int) {
      |      println(number)
      |    }
      |  )
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
      )

  @Test
  fun `avoid newline before lambda argument if it is named`() =
      assertFormatted(
          """
      |private fun f(items: List<Int>) {
      |  doSomethingCool(
      |    items,
      |    lambdaArgument = {
      |      step1()
      |      step2()
      |    }
      |  ) {
      |    it.doIt()
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
      )

  @Test
  fun `anonymous function with receiver`() =
      assertFormatted(
          """
      |fun f() {
      |  setListener(
      |    fun View.() {
      |      println(this)
      |    }
      |  )
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
      )

  @Test
  fun `function calls with multiple named arguments`() =
      assertFormatted(
          """
      |fun f() {
      |  foo(1, b = 2, c = 3)
      |
      |  foo(
      |    123456789012345678901234567890,
      |    b = 23456789012345678901234567890,
      |    c = 3456789012345678901234567890
      |  )
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT)

  @Test
  fun `Arguments are blocks`() =
      assertFormatted(
          """
      |--------------------------------------------------
      |override fun visitProperty(property: KtProperty) {
      |  builder.sync(property)
      |  builder.block(ZERO) {
      |    declareOne(
      |      kind = DeclarationKind.FIELD,
      |      modifiers = property.modifierList,
      |      valOrVarKeyword =
      |        property.valOrVarKeyword.text,
      |      typeParametersBlaBla =
      |        property.typeParameterList,
      |      receiver = property.receiverTypeReference,
      |      name = property.nameIdentifier?.text,
      |      type = property.typeReference,
      |      typeConstraintList =
      |        property.typeConstraintList,
      |      delegate = property.delegate,
      |      initializer = property.initializer
      |    )
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `keep last expression in qualified indented`() =
      assertFormatted(
          """
      |-----------------------
      |fun f() {
      |  Stuff()
      |    .doIt(
      |      Foo.doIt()
      |        .doThat()
      |    )
      |    .doIt(
      |      Foo.doIt()
      |        .doThat()
      |    )
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `if expression with else`() =
      assertFormatted(
          """
      |fun maybePrint(b: Boolean) {
      |  println(if (b) 1 else 2)
      |  println(
      |    if (b) {
      |      val a = 1 + 1
      |      2 * a
      |    } else 2
      |  )
      |  return if (b) 1 else 2
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT)

  @Test
  fun `named arguments indent their value expression`() =
      assertFormatted(
          """
      |fun f() =
      |  Bar(
      |    tokens =
      |      mutableListOf<Token>().apply {
      |        // Printing
      |        print()
      |      },
      |    duration = duration
      |  )
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT)

  @Test
  fun `breaking long binary operations`() =
      assertFormatted(
          """
      |--------------------
      |fun foo() {
      |  val finalWidth =
      |    value1 +
      |      value2 +
      |      (value3 +
      |        value4 +
      |        value5) +
      |      foo(v) +
      |      (1 + 2) +
      |      function(
      |        value7,
      |        value8
      |      ) +
      |      value9
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `handle casting with breaks`() =
      assertFormatted(
          """
      |-------------------
      |fun castIt(
      |  something: Any
      |) {
      |  doIt(
      |    something
      |      as List<*>
      |  )
      |  doIt(
      |    something
      |      is List<*>
      |  )
      |  println(
      |    something
      |      is
      |      List<String>
      |  )
      |  doIt(
      |    something
      |      as
      |      List<String>
      |  )
      |  println(
      |    something
      |      is
      |      PairList<
      |        String, Int
      |      >
      |  )
      |  doIt(
      |    something
      |      as
      |      PairList<
      |        String, Int
      |      >
      |  )
      |  println(
      |    a is Int &&
      |      b is String
      |  )
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `line breaks in function arguments`() =
      assertFormatted(
          """
      |--------------------------------------------------
      |fun f() {
      |  computeBreaks(
      |    javaOutput.commentsHelper,
      |    maxWidth,
      |    Doc.State(+0, 0)
      |  )
      |  computeBreaks(
      |    output.commentsHelper,
      |    maxWidth,
      |    State(0)
      |  )
      |  doc.computeBreaks(
      |    javaOutput.commentsHelper,
      |    maxWidth,
      |    Doc.State(+0, 0)
      |  )
      |  doc.computeBreaks(
      |    output.commentsHelper,
      |    maxWidth,
      |    State(0)
      |  )
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  // TODO: there's a bug here - the last case shouldn't break after 'foo'.
  @Test
  fun `different indentation in chained calls`() =
      assertFormatted(
          """
      |----------------------
      |fun f() {
      |  fooDdoIt(
      |    foo1,
      |    foo2,
      |    foo3
      |  )
      |  foo.doIt(
      |    foo1,
      |    foo2,
      |    foo3
      |  )
      |  foo
      |    .doIt(
      |      foo1,
      |      foo2,
      |      foo3
      |    )
      |    .doThat()
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `a secondary constructor with many arguments passed to delegate`() =
      assertFormatted(
          """
      |--------------------------------------------------
      |data class Foo {
      |  constructor(
      |    val number: Int,
      |    val name: String,
      |    val age: Int,
      |    val title: String,
      |    val offspring: List<Foo>
      |  ) : this(
      |    number,
      |    name,
      |    age,
      |    title,
      |    offspring,
      |    offspring
      |  )
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `a secondary constructor with no arguments passed to delegate`() =
      assertFormatted(
          """
      |--------------------------------------------------
      |data class Foo {
      |  constructor() :
      |    this(
      |      Foo.createSpeciallyDesignedParameter(),
      |      Foo.createSpeciallyDesignedParameter(),
      |    )
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `handle trailing commas (function calls)`() =
      assertFormatted(
          """
      |------------------------
      |fun main() {
      |  foo(
      |    3,
      |  )
      |
      |  foo<Int>(
      |    3,
      |  )
      |
      |  foo<
      |    Int,
      |  >(
      |    3,
      |  )
      |
      |  foo<Int>(
      |    "asdf",
      |    "asdf"
      |  )
      |
      |  foo<
      |    Int,
      |  >(
      |    "asd",
      |    "asd",
      |  )
      |
      |  foo<
      |    Int,
      |    Boolean,
      |  >(
      |    3,
      |  )
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `an assortment of tests for emitQualifiedExpression`() =
      assertFormatted(
          """
      |--------------------------------------
      |fun f() {
      |  // Regression test: https://github.com/facebookincubator/ktfmt/issues/56
      |  kjsdfglkjdfgkjdfkgjhkerjghkdfj
      |    ?.methodName1()
      |
      |  // a series of field accesses followed by a single call expression
      |  // is kept together.
      |  abcdefghijkl.abcdefghijkl
      |    ?.methodName2()
      |
      |  // Similar to above.
      |  abcdefghijkl.abcdefghijkl
      |    ?.methodName3
      |    ?.abcdefghijkl()
      |
      |  // Multiple call expressions cause each part of the expression
      |  // to be placed on its own line.
      |  abcdefghijkl
      |    ?.abcdefghijkl
      |    ?.methodName4()
      |    ?.abcdefghijkl()
      |
      |  // Don't break first call expression if it fits.
      |  foIt(something.something.happens())
      |    .thenReturn(result)
      |
      |  // Break after `longerThanFour(` because it's longer than 4 chars
      |  longerThanFour(
      |      something.somethingBlaBla
      |        .happens()
      |    )
      |    .thenReturn(result)
      |
      |  // Similarly to above, when part of qualified expression.
      |  foo
      |    .longerThanFour(
      |      something.somethingBlaBla
      |        .happens()
      |    )
      |    .thenReturn(result)
      |
      |  // Keep 'super' attached to the method name
      |  super.abcdefghijkl
      |    .methodName4()
      |    .abcdefghijkl()
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `keep parenthesis and braces together when there's only one lambda argument`() =
      assertFormatted(
          """
      |fun f() {
      |  doIt({})
      |  doIt({ it + it })
      |  doIt({
      |    val a = it
      |    a + a
      |  })
      |  doIt(functor = { it + it })
      |  doIt(
      |    functor = {
      |      val a = it
      |      a + a
      |    }
      |  )
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT)

  @Test
  fun `chained calls that don't fit in one line`() =
      assertFormatted(
          """
      |---------------------------
      |fun f() {
      |  foo(
      |      println("a"),
      |      println("b")
      |    )
      |    .bar(
      |      println("b"),
      |      println("b")
      |    )
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `lambda assigned to variable does not break before brace`() =
      assertFormatted(
          """
      |fun doIt() {
      |  val lambda = {
      |    doItOnce()
      |    doItTwice()
      |  }
      |}
      |
      |fun foo() = {
      |  doItOnce()
      |  doItTwice()
      |}
      |"""
              .trimMargin())

  @Test
  fun `comma separated lists, no automatic trailing break after lambda params`() =
      assertFormatted(
          """
      |----------------------------
      |fun foo() {
      |  someExpr.let { x -> x }
      |  someExpr.let { x, y -> x }
      |
      |  someExpr.let { paramFits
      |    ->
      |    butNotArrow
      |  }
      |  someExpr.let { params, fit
      |    ->
      |    butNotArrow
      |  }
      |
      |  someExpr.let {
      |    parameterToLong ->
      |    fits
      |  }
      |  someExpr.let {
      |    tooLong,
      |    together ->
      |    fits
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `comma separated lists, no automatic trailing break after supertype list`() =
      assertFormatted(
          """
      |----------------------------
      |class Foo() :
      |  ThisList,
      |  WillBe,
      |  TooLong(thats = ok) {
      |  fun someMethod() {
      |    val forceBodyBreak = 0
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `if expression with multiline condition`() =
      assertFormatted(
          """
      |----------------------------
      |fun foo() {
      |  if (
      |    expressions1 &&
      |      expression2 &&
      |      expression3
      |  ) {
      |    bar()
      |  }
      |
      |  if (
      |    foo(
      |      expressions1 &&
      |        expression2 &&
      |        expression3
      |    )
      |  ) {
      |    bar()
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `if expression with condition that exactly fits to line`() =
      assertFormatted(
          """
      |-------------------------
      |fun foo() {
      |  if (
      |    e1 && e2 && e3 = e4
      |  ) {
      |    bar()
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `when() expression with multiline condition`() =
      assertFormatted(
          """
      |-----------------------
      |fun foo() {
      |  when (
      |    expressions1 +
      |      expression2 +
      |      expression3
      |  ) {
      |    1 -> print(1)
      |    2 -> print(2)
      |  }
      |
      |  when (
      |    foo(
      |      expressions1 &&
      |        expression2 &&
      |        expression3
      |    )
      |  ) {
      |    1 -> print(1)
      |    2 -> print(2)
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `when expression with condition that exactly fits to line`() =
      assertFormatted(
          """
      |---------------------------
      |fun foo() {
      |  when (
      |    e1 && e2 && e3 = e4
      |  ) {
      |    1 -> print(1)
      |    2 -> print(2)
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `while expression with multiline condition`() =
      assertFormatted(
          """
      |----------------------------
      |fun foo() {
      |  while (
      |    expressions1 &&
      |      expression2 &&
      |      expression3
      |  ) {
      |    bar()
      |  }
      |
      |  while (
      |    foo(
      |      expressions1 &&
      |        expression2 &&
      |        expression3
      |    )
      |  ) {
      |    bar()
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `while expression with condition that exactly fits to line`() =
      assertFormatted(
          """
      |----------------------------
      |fun foo() {
      |  while (
      |    e1 && e2 && e3 = e4
      |  ) {
      |    bar()
      |  }
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `handle destructuring declaration`() =
      assertFormatted(
          """
      |-------------------------------------------
      |fun f() {
      |  val (a, b: Int) = listOf(1, 2)
      |  val (asd, asd, asd, asd, asd, asd, asd) =
      |    foo.bar(asdasd, asdasd)
      |
      |  val (accountType, accountId) =
      |    oneTwoThreeFourFiveSixSeven(
      |      foo,
      |      bar,
      |      zed,
      |      boo
      |    )
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `trailing break argument list`() =
      assertFormatted(
          """
      |-------------------
      |fun method() {
      |  Foo.FooBar(
      |    longParameter
      |  )
      |  Foo.FooBar(
      |    param1,
      |    param2
      |  )
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `trailing break chains`() =
      assertFormatted(
          """
      |-------------
      |bar(
      |  FooOpClass
      |    .doOp(1)
      |    .doOp(2)
      |)
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `wrapping for long function types`() =
      assertFormatted(
          """
      |------------------------
      |var listener:
      |  (
      |    a: String,
      |    b: String,
      |    c: String,
      |    d: String
      |  ) -> Unit
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `function call following long multiline string`() =
      assertFormatted(
          """
      |--------------------------------
      |fun f() {
      |  val str1 =
      |    $TQ
      |    Some very long string that might mess things up
      |    $TQ
      |      .trimIndent()
      |
      |  val str2 =
      |    $TQ
      |    Some very long string that might mess things up
      |    $TQ
      |      .trimIndent(someArg)
      |}
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  @Test
  fun `array-literal in annotation`() =
      assertFormatted(
          """
      |--------------------------------
      |@Anno(
      |  array =
      |    [
      |      someItem,
      |      andAnother,
      |      noTrailingComma
      |    ]
      |)
      |class Host
      |
      |@Anno(
      |  array =
      |    [
      |      someItem,
      |      andAnother,
      |      withTrailingComma,
      |    ]
      |)
      |class Host
      |
      |@Anno(
      |  array =
      |    [
      |      // Comment
      |      someItem,
      |      // Comment
      |      andAnother,
      |      // Comment
      |      withTrailingComment
      |      // Comment
      |      // Comment
      |    ]
      |)
      |class Host
      |"""
              .trimMargin(),
          formattingOptions = Formatter.GOOGLE_FORMAT,
          deduceMaxWidth = true)

  companion object {
    /** Triple quotes, useful to use within triple-quoted strings. */
    private const val TQ = "\"\"\""
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.ktfmt.format

import com.google.common.truth.Truth.assertThat
import org.junit.Test

class KotlinInputTest {
  @Test
  fun `Comments are toks not tokens`() {
    val code = "/** foo */ class F {}"
    val input = KotlinInput(code, Parser.parse(code))
    assertThat(input.getTokens().map { it.tok.text })
        .containsExactly("class", "F", "{", "}", "")
        .inOrder()
    assertThat(input.getTokens()[0].toksBefore.map { it.text })
        .containsExactly("/** foo */", " ")
        .inOrder()
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.ktfmt.format

import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class TokenizerTest {
  @Test
  fun `PsiWhiteSpace are split to newlines and maximal-length whitespaces`() {
    val code =
        listOf(
                "val  a = ", //
                "", //
                "     ", //
                "     15")
            .joinToString("\n")

    val file = Parser.parse(code)
    val tokenizer = Tokenizer(code, file)
    file.accept(tokenizer)

    assertThat(tokenizer.toks.map { it.originalText })
        .containsExactly("val", "  ", "a", " ", "=", " ", "\n", "\n", "     ", "\n", "     ", "15")
        .inOrder()
  }

  @Test
  fun `Strings are returns as a single token`() {
    val code =
        listOf(
                "val a=\"\"\"",
                "  ",
                "   ",
                "    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do ",
                "    Lorem",
                "    ",
                "     ",
                "      \"\"\"",
                "val b=\"lorem ipsum\"",
                "      ",
                "    ")
            .joinToString("\n")

    val file = Parser.parse(code)
    val tokenizer = Tokenizer(code, file)
    file.accept(tokenizer)

    assertThat(tokenizer.toks.map { it.originalText })
        .containsExactly(
            "val",
            " ",
            "a",
            "=",
            listOf(
                    "\"\"\"",
                    " ${WhitespaceTombstones.SPACE_TOMBSTONE}",
                    "  ${WhitespaceTombstones.SPACE_TOMBSTONE}",
                    "    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do${WhitespaceTombstones.SPACE_TOMBSTONE}",
                    "    Lorem",
                    "   ${WhitespaceTombstones.SPACE_TOMBSTONE}",
                    "    ${WhitespaceTombstones.SPACE_TOMBSTONE}",
                    "      \"\"\"")
                .joinToString("\n"),
            "\n",
            "val",
            " ",
            "b",
            "=",
            "\"lorem ipsum\"",
            "\n",
            "      ",
            "\n",
            "    ")
        .inOrder()
  }

  @Test
  fun `Token index is advanced after a string token`() {
    val code = """
      |val b="a"
      |val a=5
      |""".trimMargin().trimMargin()

    val file = Parser.parse(code)
    val tokenizer = Tokenizer(code, file)
    file.accept(tokenizer)

    assertThat(tokenizer.toks.map { it.originalText })
        .containsExactly("val", " ", "b", "=", "\"a\"", "\n", "val", " ", "a", "=", "5")
        .inOrder()
    assertThat(tokenizer.toks.map { it.index })
        .containsExactly(0, -1, 1, 2, 3, -1, 4, -1, 5, 6, 7)
        .inOrder()
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.ktfmt.format

import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class WhitespaceTombstonesTest {

  @Test
  fun testReplaceTrailingWhitespaceWithTombstone() {
    assertThat(WhitespaceTombstones.replaceTrailingWhitespaceWithTombstone("")).isEqualTo("")

    assertThat(WhitespaceTombstones.replaceTrailingWhitespaceWithTombstone("  sdfl"))
        .isEqualTo("  sdfl")
    assertThat(WhitespaceTombstones.replaceTrailingWhitespaceWithTombstone("  sdfl "))
        .isEqualTo("  sdfl${WhitespaceTombstones.SPACE_TOMBSTONE}")
    assertThat(WhitespaceTombstones.replaceTrailingWhitespaceWithTombstone("  sdfl  "))
        .isEqualTo("  sdfl ${WhitespaceTombstones.SPACE_TOMBSTONE}")

    assertThat(WhitespaceTombstones.replaceTrailingWhitespaceWithTombstone("  sdfl  \n skdjfh"))
        .isEqualTo("  sdfl ${WhitespaceTombstones.SPACE_TOMBSTONE}\n skdjfh")
    assertThat(WhitespaceTombstones.replaceTrailingWhitespaceWithTombstone("  sdfl  \n skdjfh "))
        .isEqualTo(
            "  sdfl ${WhitespaceTombstones.SPACE_TOMBSTONE}\n skdjfh${WhitespaceTombstones.SPACE_TOMBSTONE}")
    assertThat(WhitespaceTombstones.replaceTrailingWhitespaceWithTombstone("  sdfl  \n\n skdjfh "))
        .isEqualTo(
            "  sdfl ${WhitespaceTombstones.SPACE_TOMBSTONE}\n\n skdjfh${WhitespaceTombstones.SPACE_TOMBSTONE}")
  }
}


/*
 * Copyright (c) Tor Norbye.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

@file:Suppress("PropertyName", "PrivatePropertyName")

package com.facebook.ktfmt.kdoc

import com.google.common.truth.Truth.assertThat
import java.io.BufferedReader
import java.io.File

/**
 * Verifies that two KDoc comment strings render to the same HTML documentation using Dokka. This is
 * used by the test infrastructure to make sure that the transformations we're allowing are not
 * changing the appearance of the documentation.
 *
 * Unfortunately, just diffing HTML strings isn't always enough, because dokka will preserve some
 * text formatting which is immaterial to the HTML appearance. Therefore, if you've also installed
 * Pandoc, it will use that to generate a text rendering of the HTML which is then used for diffing
 * instead. (Even this isn't fullproof because pandoc also preserves some details that should not
 * matter). Text rendering does drop a lot of markup (such as bold and italics) so it would be
 * better to compare in some other format, such as PDF, but unfortunately, the PDF rendering doesn't
 * appear to be stable; rendering the same document twice yields a binary diff.
 *
 * Dokka no longer provides a fat/shadow jar; instead you have to download a bunch of different
 * dependencies. Therefore, for convenience this is set up to point to an AndroidX checkout, which
 * has all the prebuilts. Point the below to AndroidX and the rest should work.
 */
class DokkaVerifier(private val tempFolder: File) {
  // Configuration parameters
  // Checkout of https://github.com/androidx/androidx
  private val ANDROIDX_HOME: String? = null

  // Optional install of pandoc, e.g. "/opt/homebrew/bin/pandoc"
  private val PANDOC: String? = null

  // JDK install
  private val JAVA_HOME: String? = System.getenv("JAVA_HOME") ?: System.getProperty("java.home")

  fun verify(before: String, after: String) {
    JAVA_HOME ?: return
    ANDROIDX_HOME ?: return

    val androidx = File(ANDROIDX_HOME)
    if (!androidx.isDirectory) {
      return
    }

    val prebuilts = File(androidx, "prebuilts")
    if (!prebuilts.isDirectory) {
      println("AndroidX prebuilts not found; not verifying with Dokka")
    }
    val cli = find(prebuilts, "org.jetbrains.dokka", "dokka-cli")
    val analysis = find(prebuilts, "org.jetbrains.dokka", "dokka-analysis")
    val base = find(prebuilts, "org.jetbrains.dokka", "dokka-base")
    val compiler = find(prebuilts, "org.jetbrains.dokka", "kotlin-analysis-compiler")
    val intellij = find(prebuilts, "org.jetbrains.dokka", "kotlin-analysis-intellij")
    val coroutines = find(prebuilts, "org.jetbrains.kotlinx", "kotlinx-coroutines-core")
    val html = find(prebuilts, "org.jetbrains.kotlinx", "kotlinx-html-jvm")
    val freemarker = find(prebuilts, "org.freemarker", "freemarker")

    val src = File(tempFolder, "src")
    val out = File(tempFolder, "dokka")
    src.mkdirs()
    out.mkdirs()

    val beforeFile = File(src, "before.kt")
    beforeFile.writeText("${before.split("\n").joinToString("\n") { it.trim() }}\nclass Before\n")

    val afterFile = File(src, "after.kt")
    afterFile.writeText("${after.split("\n").joinToString("\n") { it.trim() }}\nclass After\n")

    val args = mutableListOf<String>()
    args.add(File(JAVA_HOME, "bin/java").path)
    args.add("-jar")
    args.add(cli.path)
    args.add("-pluginsClasspath")
    val pathSeparator =
        ";" // instead of File.pathSeparator as would have been reasonable (e.g. : on Unix)
    val path =
        listOf(analysis, base, compiler, intellij, coroutines, html, freemarker).joinToString(
            pathSeparator) {
              it.path
            }
    args.add(path)
    args.add("-sourceSet")
    args.add("-src $src") // (nested parameter within -sourceSet)
    args.add("-outputDir")
    args.add(out.path)
    executeProcess(args)

    fun getHtml(file: File): String {
      val rendered = file.readText()
      val begin = rendered.indexOf("
")
      val end = rendered.indexOf("
", begin)
      return rendered.substring(begin, end).replace(Regex(" +"), " ").replace(">", ">\n")
    }

    fun getText(file: File): String? {
      return if (PANDOC != null) {
        val pandocFile = File(PANDOC)
        if (!pandocFile.isFile) {
          error("Cannot execute $pandocFile")
        }
        val outFile = File(out, "text.text")
        executeProcess(listOf(PANDOC, file.path, "-o", outFile.path))
        val rendered = outFile.readText()

        val begin = rendered.indexOf("[]{.copy-popup-icon}Content copied to clipboard")
        val end = rendered.indexOf("::: tabbedcontent", begin)
        rendered.substring(begin, end).replace(Regex(" +"), " ").replace(">", ">\n")
      } else {
        null
      }
    }

    val indexBefore = File("$out/root/[root]/-before/index.html")
    val beforeContents = getHtml(indexBefore)
    val indexAfter = File("$out/root/[root]/-after/index.html")
    val afterContents = getHtml(indexAfter)
    if (beforeContents != afterContents) {
      val beforeText = getText(indexBefore)
      val afterText = getText(indexAfter)
      if (beforeText != null && afterText != null) {
        assertThat(beforeText).isEqualTo(afterText)
        return
      }

      assertThat(beforeContents).isEqualTo(afterContents)
    }
  }

  private fun find(prebuilts: File, group: String, artifact: String): File {
    val versionDir = File(prebuilts, "androidx/external/${group.replace('.','/')}/$artifact")
    val versions =
        versionDir.listFiles().filter { it.name.first().isDigit() }.sortedByDescending { it.name }
    for (version in versions.map { it.name }) {
      val jar = File(versionDir, "$version/$artifact-$version.jar")
      if (jar.isFile) {
        return jar
      }
    }
    error("Could not find a valid jar file for $group:$artifact")
  }

  private fun executeProcess(args: List<String>) {
    var input: BufferedReader? = null
    var error: BufferedReader? = null
    try {
      val process = Runtime.getRuntime().exec(args.toTypedArray())
      input = process.inputStream.bufferedReader()
      error = process.errorStream.bufferedReader()
      val exitVal = process.waitFor()
      if (exitVal != 0) {
        val sb = StringBuilder()
        sb.append("Failed to execute process\n")
        sb.append("Command args:\n")
        for (arg in args) {
          sb.append("  ").append(arg).append("\n")
        }
        sb.append("Standard output:\n")
        var line: String?
        while (input.readLine().also { line = it } != null) {
          sb.append(line).append("\n")
        }
        sb.append("Error output:\n")
        while (error.readLine().also { line = it } != null) {
          sb.append(line).append("\n")
        }
        error(sb.toString())
      }
    } catch (t: Throwable) {
      val sb = StringBuilder()
      for (arg in args) {
        sb.append("  ").append(arg).append("\n")
      }
      t.printStackTrace()
      error("Could not run process:\n$sb")
    } finally {
      input?.close()
      error?.close()
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.ktfmt.kdoc

import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class EscapingTest {

  @Test
  fun `escaping kdoc`() {
    assertThat(Escaping.escapeKDoc("/** foo */")).isEqualTo("/** foo */")
    assertThat(Escaping.escapeKDoc("/*** foo */")).isEqualTo("/*** foo */")
    assertThat(Escaping.escapeKDoc("/** * foo */")).isEqualTo("/** * foo */")
    assertThat(Escaping.escapeKDoc("/** /* foo */")).isEqualTo("/** \u0004\u0005 foo */")
    assertThat(Escaping.escapeKDoc("/** /* foo */ */"))
        .isEqualTo("/** \u0004\u0005 foo \u0005\u0004 */")

    assertThat(Escaping.escapeKDoc("/* /* foo */ */"))
        .isEqualTo("/* \u0004\u0005 foo \u0005\u0004 */")
  }
}