/*
 * Copyright (C) 2020. Uber Technologies
 *
 * 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.uber.lintchecks.recipes.guardrails

import com.android.tools.lint.checks.infrastructure.LintDetectorTest.TestFile
import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
import org.junit.Ignore
import org.junit.Test

class JavaOnlyDetectorTest {

  companion object {
    val JAVA_ONLY: TestFile = java("test/com/uber/lintchecks/recipes/guardrails/JavaOnly.java",
        """
          package com.uber.lintchecks.recipes.guardrails;
          public @interface JavaOnly {
            String value() default "Description";
          }
          """).indented()
    val KOTLIN_ONLY: TestFile = java("test/com/uber/lintchecks/recipes/guardrails/KotlinOnly.java",
        """
          package com.uber.lintchecks.recipes.guardrails;
          public @interface KotlinOnly {
            String value() default "Description";
          }
          """).indented()
  }

  @Test
  fun positive() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            kotlin("test/test/pkg/Test.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  class Test {
                    @JavaOnly fun g() {}
                    @JavaOnly("satisfying explanation") fun f() {}
                    fun m() {
                      g()
                      f()
                      val r = this::g
                    }
                  }
                  """).indented())
        .run()
        .expect("""
          test/test/pkg/Test.kt:7: Error: This method should not be called from Kotlin, see its documentation for details. [JavaOnlyDetector]
              g()
              ~~~
          test/test/pkg/Test.kt:8: Error: This method should not be called from Kotlin: satisfying explanation [JavaOnlyDetector]
              f()
              ~~~
          test/test/pkg/Test.kt:9: Error: This method should not be called from Kotlin, see its documentation for details. [JavaOnlyDetector]
              val r = this::g
                      ~~~~~~~
          3 errors, 0 warnings
        """.trimIndent())
  }

  @Ignore("Un-ignore when https://groups.google.com/forum/#!topic/lint-dev/8Nr0-SDdHbk is fixed")
  @Test
  fun positivePackage() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            java("test/test/pkg/package-info.java",
                """
                  @com.uber.lintchecks.recipes.guardrails.JavaOnly
                  package test.pkg;
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly;""").indented(),
            java("test/test/pkg/A.java",
                """
                  package test.pkg;
                  public class A {
                    public static void f() {}
                  }
                """).indented(),
            kotlin("test/test/pkg2/Test.kt",
                """
                  package test.pkg2
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  class Test {
                    fun m() {
                      test.pkg.A.f()
                    }
                  }
                  """).indented())
        .run()
        .expect("""
          test/test/pkg2/Test.kt:5: Error: This method should not be called from Kotlin: see its documentation for details. [JavaOnlyDetector]
              f()
              ~~~
          1 errors, 0 warnings
        """.trimIndent())
  }

  @Test
  fun annotateBothFunction() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            KOTLIN_ONLY,
            kotlin("test/test/pkg/Test.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  import com.uber.lintchecks.recipes.guardrails.KotlinOnly
                  interface Test {
                    @JavaOnly @KotlinOnly fun f()
                  }
                  """).indented())
        .run()
        .expect("""
          test/test/pkg/Test.kt:5: Error: Cannot annotate functions with both @KotlinOnly and @JavaOnly [JavaOnlyDetector]
            @JavaOnly @KotlinOnly fun f()
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          1 errors, 0 warnings
        """.trimIndent())
  }

  @Test
  fun annotateBothClass() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            KOTLIN_ONLY,
            kotlin("test/test/pkg/Test.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  import com.uber.lintchecks.recipes.guardrails.KotlinOnly
                  @JavaOnly @KotlinOnly interface Test {
                    fun f()
                  }
                  """).indented())
        .run()
        .expect("""
          test/test/pkg/Test.kt:4: Error: Cannot annotate types with both @KotlinOnly and @JavaOnly [JavaOnlyDetector]
          @JavaOnly @KotlinOnly interface Test {
          ^
          1 errors, 0 warnings
        """.trimIndent())
  }

  @Test
  fun requiredOverrideKotlin() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            KOTLIN_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.KotlinOnly
                  interface A {
                    @KotlinOnly fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  class B : A {
                    override fun f()
                  }
                  """).indented())
        .run()
        .expect("""
          test/test/pkg/B.kt:3: Error: Function overrides f in A which is annotated @KotlinOnly, it should also be annotated. [JavaOnlyDetector]
            override fun f()
            ~~~~~~~~~~~~~~~~
          1 errors, 0 warnings
        """.trimIndent())
        .expectFixDiffs("""
          Fix for test/test/pkg/B.kt line 3: Add @KotlinOnly:
          @@ -3 +3
          -   override fun f()
          +   @com.uber.lintchecks.recipes.guardrails.KotlinOnly override fun f()
        """.trimIndent())
  }

  @Test
  fun annotatedOverrideKotlin() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            KOTLIN_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.KotlinOnly
                  interface A {
                    @KotlinOnly fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.KotlinOnly
                  class B : A {
                    @KotlinOnly override fun f()
                  }
                  """).indented())
        .run()
        .expectClean()
  }

  @Test
  fun requiredOverrideJava() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  interface A {
                    @JavaOnly fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  class B : A {
                    override fun f()
                  }
                  """).indented())
        .run()
        .expect("""
          test/test/pkg/B.kt:3: Error: Function overrides f in A which is annotated @JavaOnly, it should also be annotated. [JavaOnlyDetector]
            override fun f()
            ~~~~~~~~~~~~~~~~
          1 errors, 0 warnings
        """.trimIndent())
        .expectFixDiffs("""
          Fix for test/test/pkg/B.kt line 3: Add @JavaOnly:
          @@ -3 +3
          -   override fun f()
          +   @com.uber.lintchecks.recipes.guardrails.JavaOnly override fun f()
        """.trimIndent())
  }

  @Test
  fun annotatedOverrideJava() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  interface A {
                    @JavaOnly fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  class B : A {
                    @JavaOnly override fun f()
                  }
                  """).indented())
        .run()
        .expectClean()
  }

  @Test
  fun requiredOverrideKotlinClass() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            KOTLIN_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.KotlinOnly
                  @KotlinOnly interface A {
                    fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  class B : A {
                    override fun f()
                  }
                  """).indented())
        .run()
        .expect("""
          test/test/pkg/B.kt:2: Error: Type subclasses/implements A in A.kt which is annotated @KotlinOnly, it should also be annotated. [JavaOnlyDetector]
          class B : A {
          ^
          1 errors, 0 warnings
        """.trimIndent())
        .expectFixDiffs("""
          Fix for test/test/pkg/B.kt line 2: Add @KotlinOnly:
          @@ -2 +2
          - class B : A {
          + @com.uber.lintchecks.recipes.guardrails.KotlinOnly class B : A {
        """.trimIndent())
  }

  @Test
  fun annotatedOverrideKotlinClass() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            KOTLIN_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.KotlinOnly
                  @KotlinOnly interface A {
                    fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.KotlinOnly
                  @KotlinOnly class B : A {
                    override fun f()
                  }
                  """).indented())
        .run()
        .expectClean()
  }

  @Test
  fun requiredOverrideJavaClass() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  @JavaOnly interface A {
                    fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  class B : A {
                    override fun f()
                  }
                  """).indented())
        .run()
        .expect("""
          test/test/pkg/B.kt:2: Error: Type subclasses/implements A in A.kt which is annotated @JavaOnly, it should also be annotated. [JavaOnlyDetector]
          class B : A {
          ^
          1 errors, 0 warnings
        """.trimIndent())
        .expectFixDiffs("""
          Fix for test/test/pkg/B.kt line 2: Add @JavaOnly:
          @@ -2 +2
          - class B : A {
          + @com.uber.lintchecks.recipes.guardrails.JavaOnly class B : A {
        """.trimIndent())
  }

  @Test
  fun annotatedOverrideJavaClass() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  @JavaOnly interface A {
                    fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  @JavaOnly class B : A {
                    override fun f()
                  }
                  """).indented())
        .run()
        .expectClean()
  }

  // The interface tries to make Object#toString @JavaOnly, and because
  // the declaration in B is implicit it doesn't get checked.
  // In practice, making default Object methods @JavaOnly isn't super
  // useful - typically users interface with the interface directly
  // (e.g. Hasher) or there's an override that has unwanted behaviour (Localizable).
  // NOTE: This has slightly different behavior than the error prone checker, as in UAST
  // the overridden method actually propagates down through types and ErrorProne's doesn't.
  @Test
  fun interfaceRedeclaresObjectMethod() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            kotlin("test/test/pkg/I.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  interface I {
                    @JavaOnly override fun toString(): String
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  class B : I
                  """).indented(),
            kotlin("test/test/pkg/Test.kt",
                """
                  package test.pkg
                  class Test {
                    fun f(b: B) {
                      b.toString()
                      val i: I = b
                      i.toString()
                    }
                  }
                  """).indented())
        .run()
        .expect("""
          test/test/pkg/Test.kt:4: Error: This method should not be called from Kotlin, see its documentation for details. [JavaOnlyDetector]
              b.toString()
              ~~~~~~~~~~~~
          test/test/pkg/Test.kt:6: Error: This method should not be called from Kotlin, see its documentation for details. [JavaOnlyDetector]
              i.toString()
              ~~~~~~~~~~~~
          2 errors, 0 warnings
        """.trimIndent())
  }

  @Test
  fun lambdaPositiveClass() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  @JavaOnly interface A {
                    fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  class B {
                    fun f(): A = {}
                  }
                  """).indented())
        .run()
        .expect("""
            test/test/pkg/B.kt:3: Error: Cannot create lambda instances of @JavaOnly-annotated type A (in A.kt) in Kotlin. Make a concrete class instead. [JavaOnlyDetector]
              fun f(): A = {}
                           ~~
            1 errors, 0 warnings
        """.trimIndent())
  }

  @Test
  fun lambdaPositiveMethod() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  interface A {
                    @JavaOnly fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  class B {
                    fun f(): A = {}
                  }
                  """).indented())
        .run()
        .expect("""
            test/test/pkg/B.kt:3: Error: This method should not be expressed as a lambda in Kotlin, see its documentation for details. [JavaOnlyDetector]
              fun f(): A = {}
                           ~~
            1 errors, 0 warnings
        """.trimIndent())
  }

  @Test
  fun lambdaNegative() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  interface A {
                    fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  class B {
                    fun f(): A = {}
                  }
                  """).indented())
        .run()
        .expectClean()
  }

  @Test
  fun lambdaNegativeReturnFun() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  interface A {
                    @JavaOnly fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  class B {
                    @JavaOnly fun f(): A = {}
                  }
                  """).indented())
        .run()
        .expectClean()
  }

  @Test
  fun lambdaNegativeReturnClass() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  @JavaOnly interface A {
                    fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  class B {
                    @JavaOnly fun f(): A = {}
                  }
                  """).indented())
        .run()
        .expectClean()
  }

  @Test
  fun anonymousClassPositiveClass() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  @JavaOnly interface A {
                    fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  class B {
                    fun f(): A = object : A() {
                      override fun f() {}
                    }
                  }
                  """).indented())
        .run()
        .expect("""
            test/test/pkg/B.kt:3: Error: Cannot create anonymous instances of @JavaOnly-annotated type A (in A.kt) in Kotlin. Make a concrete class instead. [JavaOnlyDetector]
              fun f(): A = object : A() {
                           ^
            1 errors, 0 warnings
        """.trimIndent())
  }

  @Test
  fun anonymousClassNegative() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  interface A {
                    fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  class B {
                    fun f(): A = object : A() {
                      override fun f() {}
                    }
                  }
                  """).indented())
        .run()
        .expectClean()
  }

  @Test
  fun anonymousClassNegativeReturnMethod() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  interface A {
                    @JavaOnly fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  class B {
                    @JavaOnly fun f(): A = object : A() {
                      @JavaOnly override fun f() {}
                    }
                  }
                  """).indented())
        .run()
        .expectClean()
  }

  @Test
  fun anonymousClassNegativeReturnClass() {
    lint()
        .detector(JavaOnlyDetector())
        .issues(JavaOnlyDetector.ISSUE)
        .files(
            JAVA_ONLY,
            kotlin("test/test/pkg/A.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  @JavaOnly interface A {
                    fun f()
                  }
                  """).indented(),
            kotlin("test/test/pkg/B.kt",
                """
                  package test.pkg
                  import com.uber.lintchecks.recipes.guardrails.JavaOnly
                  class B {
                    @JavaOnly fun f(): A = object : A() {
                      override fun f() {}
                    }
                  }
                  """).indented())
        .run()
        .expectClean()
  }
}
