/*
 * Copyright 2017 The Error Prone Authors.
 *
 * 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.google.errorprone.bugpatterns;

import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Unit tests for {@link TypeParameterShadowing} */
@RunWith(JUnit4.class)
public class TypeParameterShadowingTest {

  private final CompilationTestHelper compilationHelper =
      CompilationTestHelper.newInstance(TypeParameterShadowing.class, getClass());
  private final BugCheckerRefactoringTestHelper refactoring =
      BugCheckerRefactoringTestHelper.newInstance(TypeParameterShadowing.class, getClass());

  @Test
  public void singleLevel() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "package foo.bar;",
            "class Test<T> {",
            "  // BUG: Diagnostic contains: T declared in Test",
            "  <T> void something() {}",
            "}")
        .doTest();
  }

  @Test
  public void staticNotFlagged() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "package foo.bar;",
            "class Test<T> {",
            "  static <T> void something() {}",
            "}")
        .doTest();
  }

  @Test
  public void staticMethodInnerDoesntConflictWithOuter() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "package foo.bar;",
            "class Test<D> {",
            "  static <T> void something() {",
            "    class MethodInner<D> {}", // Doesn't clash with Test<D>
            "    class MethodInner2 {",
            "      // BUG: Diagnostic contains: T declared in something",
            "      <T> void clashingMethod() {}",
            "      <D> void nonClashingMethod() {}", // <D> not in scope
            "    }",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void nestedClassDeclarations() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "package foo.bar;",
            "class Test<D> {",
            "  // BUG: Diagnostic contains: D declared in Test",
            "  class Test2<D> {}",
            "}")
        .doTest();
  }

  @Test
  public void twoLevels() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "package foo.bar;",
            "class Test<T> {",
            "  class MyTest<J> {",
            "  // BUG: Diagnostic matches: combo",
            "  public <T,J> void something() {}",
            " }",
            "}")
        .expectErrorMessage(
            "combo", s -> s.contains("J declared in MyTest") && s.contains("T declared in Test"))
        .doTest();
  }

  @Test
  public void renameTypeVar() {
    refactoring
        .addInputLines(
            "in/Test.java",
            "package foo.bar;",
            "class Test<T> {",
            "  /** @param <T> foo */",
            "  <T> void something(T t) { T other = t;}",
            "}")
        .addOutputLines(
            "out/Test.java",
            "package foo.bar;",
            "class Test<T> {",
            "  /** @param <T2> foo */",
            "  <T2> void something(T2 t) { T2 other = t;}",
            "}")
        .doTest(TestMode.TEXT_MATCH);
  }

  @Test
  public void renameRecursiveBound() {
    refactoring
        .addInputLines(
            "in/Test.java",
            "package foo.bar;",
            "class Test<T> {",
            "  <T extends Comparable<T>> void something(T t) { T other = t;}",
            "}")
        .addOutputLines(
            "out/Test.java",
            "package foo.bar;",
            "class Test<T> {",
            "  <T2 extends Comparable<T2>> void something(T2 t) { T2 other = t;}",
            "}")
        .doTest();
  }

  @Test
  public void refactorUnderneathStuff() {
    refactoring
        .addInputLines(
            "in/Test.java",
            "package foo.bar;",
            "class Test<T> {",
            "  <T> void something(T t) { T other = t;}",
            "  <T> T identity(T t) { return t; }",
            "}")
        .addOutputLines(
            "out/Test.java",
            "package foo.bar;",
            "class Test<T> {",
            "  <T2> void something(T2 t) { T2 other = t;}",
            "  <T2> T2 identity(T2 t) { return t; }",
            "}")
        .doTest();
  }

  @Test
  public void refactorMultipleVars() {
    refactoring
        .addInputLines(
            "in/Test.java",
            "package foo.bar;",
            "class Test<T,D> {",
            "  <T,D> void something(T t) { ",
            "    T other = t;",
            "    java.util.List<T> ts = new java.util.ArrayList<T>();",
            "    D d = null; ",
            "  }",
            "}")
        .addOutputLines(
            "out/Test.java",
            "package foo.bar;",
            "class Test<T,D> {",
            "  <T2,D2> void something(T2 t) { ",
            "    T2 other = t;",
            "    java.util.List<T2> ts = new java.util.ArrayList<T2>();",
            "    D2 d = null; ",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void refactorWithNestedTypeParameterDeclaration() {
    // The nested @SuppressWarnings are because there will be multiple findings
    // (T is shadowed multiple times). We're trying to test all of the fixes suggested by the
    // finding generated from the outermost instance method, namely that it doesn't attempt to
    // rewrite symbols T that don't match the T we're rewriting.
    refactoring
        .addInputLines(
            "in/Test.java",
            "package foo.bar;",
            "class Test<T> {",
            "  <T,T2> void something(T t) { ",
            "    T var = t;",
            "    @SuppressWarnings(\"TypeParameterShadowing\")",
            "    class MethodInnerWithGeneric<T> {}",
            "    MethodInnerWithGeneric<T> innerVar = null;",
            "    class MethodInner {",
            "       @SuppressWarnings(\"TypeParameterShadowing\")",
            "       <T> void doSomething() {}",
            "       void doSomethingElse(T t) { this.<T>doSomething(); }",
            "    }",
            "    MethodInner myInner = null;",
            "  }",
            "}")
        .addOutputLines(
            "out/Test.java",
            "package foo.bar;",
            "class Test<T> {",
            "  <T3,T2> void something(T3 t) { ",
            "    T3 var = t;",
            "    @SuppressWarnings(\"TypeParameterShadowing\")",
            "    class MethodInnerWithGeneric<T> {}",
            "    MethodInnerWithGeneric<T3> innerVar = null;",
            "    class MethodInner {",
            "       @SuppressWarnings(\"TypeParameterShadowing\")",
            "       <T> void doSomething() {}",
            "       void doSomethingElse(T3 t) { this.<T3>doSomething(); }",
            "    }",
            "    MethodInner myInner = null;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void refactorCheckForExisting() {
    refactoring
        .addInputLines(
            "in/Test.java",
            "package foo.bar;",
            "class Test<T> {",
            "  class A<T2,T3,T4> {",
            "    <T> void something(T t) { ",
            "      T var = t;",
            "    }",
            "  }",
            "}")
        .addOutputLines(
            "out/Test.java",
            "package foo.bar;",
            "class Test<T> {",
            "  class A<T2,T3,T4> {",
            "    <T5> void something(T5 t) { ",
            "      T5 var = t;",
            "    }",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void refactorMethodInnerInner() {
    refactoring
        .addInputLines(
            "in/Test.java",
            "package foo.bar;",
            "class Test<T> {",
            "  static <D> void something(D t) {",
            "    class B {",
            "      class C<T,D> {}",
            "    }",
            "  }",
            "}")
        .addOutputLines(
            "out/Test.java",
            "package foo.bar;",
            "class Test<T> {",
            "  static <D> void something(D t) {",
            "    class B {",
            // T isn't accessible to the inner since the method is static
            "      class C<T,D2> {}",
            "    }",
            "  }",
            "}")
        .doTest();
  }

  // don't try to read type parameters off the enclosing local variable declaration
  @Test
  public void symbolWithoutTypeParameters() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            "package foo.bar;",
            "import java.util.Map;",
            "import java.util.Comparator;",
            "class Test {",
            "  static Comparator<Map.Entry<Integer, String>> ENTRY_COMPARATOR =",
            "    new Comparator<Map.Entry<Integer, String>>() {",
            "      public int compare(",
            "          Map.Entry<Integer, String> o1, Map.Entry<Integer, String> o2) {",
            "        return 0;",
            "      }",
            "      private <T extends Comparable> int c(T o1, T o2) {",
            "        return 0;",
            "      }",
            "    };",
            "}")
        .doTest();
  }

  @Test
  public void lambdaParameterDesugaring() {
    refactoring
        .addInputLines(
            "in/A.java",
            "import java.util.function.Consumer;",
            "class A<T> {",
            "  abstract class B<T> {",
            "    void f() {",
            "      g(t -> {});",
            "    }",
            "    abstract void g(Consumer<T> c);",
            "  }",
            "}")
        .addOutputLines(
            "out/A.java",
            "import java.util.function.Consumer;",
            "class A<T> {",
            "  abstract class B<T2> {",
            "    void f() {",
            "      g(t -> {});",
            "    }",
            "    abstract void g(Consumer<T2> c);",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void typesWithBounds() {
    refactoring
        .addInputLines(
            "in/Test.java",
            "import java.util.function.Predicate;",
            "class Test<T> {",
            "  <B extends Object & Comparable> void something(B b) {",
            "    class Foo<B extends Object & Comparable> implements Predicate<B> {",
            "        public boolean test(B b) { return false; }",
            "    }",
            "    new Foo<>();",
            "  }",
            "}")
        .addOutputLines(
            "out/Test.java",
            "import java.util.function.Predicate;",
            "class Test<T> {",
            "  <B extends Object & Comparable> void something(B b) {",
            "    class Foo<B2 extends Object & Comparable> implements Predicate<B2> {",
            "      public boolean test(B2 b) { return false; }",
            "    }",
            "    new Foo<>();",
            "  }",
            "}")
        .doTest();
  }
}
