/*
 * 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 static com.google.common.truth.Truth.assertWithMessage;
import static com.google.errorprone.bugpatterns.TypeParameterNaming.TypeParameterNamingClassification.CLASS_NAME_WITH_T;
import static com.google.errorprone.bugpatterns.TypeParameterNaming.TypeParameterNamingClassification.LETTER_WITH_MAYBE_NUMERAL;
import static com.google.errorprone.bugpatterns.TypeParameterNaming.TypeParameterNamingClassification.NON_CLASS_NAME_WITH_T_SUFFIX;
import static com.google.errorprone.bugpatterns.TypeParameterNaming.TypeParameterNamingClassification.UNCLASSIFIED;

import com.google.common.truth.Subject;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import com.google.errorprone.bugpatterns.TypeParameterNaming.TypeParameterNamingClassification;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link TypeParameterNaming} */
@RunWith(JUnit4.class)
public class TypeParameterNamingTest {

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

  @Test
  public void positiveCases() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            """
            // BUG: Diagnostic contains: TypeParameterNaming
            class Test<BadName> {
              // BUG: Diagnostic contains: TypeParameterNaming
              public <T, Foo> void method(Exception e) {}
            }
            """)
        .doTest();
  }

  @Test
  public void refactoring_trailing() {
    refactoring
        .addInputLines(
            "in/Test.java",
            """
            /**
             * @param <BadName> bad name
             */
            class Test<BadName> {
              public <T, Foo> void method(Foo f) {
                BadName bad = null;
                Foo d = f;
              }
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            /**
             * @param <BadNameT> bad name
             */
            class Test<BadNameT> {
              public <T, FooT> void method(FooT f) {
                BadNameT bad = null;
                FooT d = f;
              }
            }
            """)
        .setFixChooser(FixChoosers.FIRST)
        .doTest(TestMode.TEXT_MATCH);
  }

  @Test
  public void refactoring_single() {
    refactoring
        .addInputLines(
            "in/Test.java",
            """
            class Test<BadName> {
              /**
               * @param <Foo> foo
               */
              public <T, Foo> void method(Foo f) {
                BadName bad = null;
                Foo d = f;
              }
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            class Test<B> {
              /**
               * @param <F> foo
               */
              public <T, F> void method(F f) {
                B bad = null;
                F d = f;
              }
            }
            """)
        .setFixChooser(FixChoosers.SECOND)
        .doTest(TestMode.TEXT_MATCH);
  }

  @Test
  public void refactoring_single_number() {
    refactoring
        .addInputLines(
            "in/Test.java",
            """
            class Test<Bar> {
              public <T, Baz> void method(Baz f) {
                Bar bad = null;
                Baz d = f;
              }
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            class Test<B> {
              public <T, B2> void method(B2 f) {
                B bad = null;
                B2 d = f;
              }
            }
            """)
        .setFixChooser(FixChoosers.SECOND)
        .doTest();
  }

  @Test
  public void refactoring_single_number_enclosing() {
    refactoring
        .addInputLines(
            "in/Test.java",
            """
            class Test<Bar> {
              public <T, Baz, Boo> void method(Baz f) {
                Bar bad = null;
                Baz d = f;
                Boo wow = null;
              }
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            class Test<B> {
              public <T, B2, B3> void method(B2 f) {
                B bad = null;
                B2 d = f;
                B3 wow = null;
              }
            }
            """)
        .setFixChooser(FixChoosers.SECOND)
        .doTest();
  }

  @Test
  public void refactoring_single_number_within_scope() {
    refactoring
        .addInputLines(
            "in/Test.java",
            """
            class Test {
              public <T, Baz, Boo> void method(Baz f) {
                Baz d = f;
                Boo wow = null;
              }
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            class Test {
              public <T, B, B2> void method(B f) {
                B d = f;
                B2 wow = null;
              }
            }
            """)
        .setFixChooser(FixChoosers.SECOND)
        .doTest();
  }

  @Test
  public void refactoring_single_number_many_ok() {
    refactoring
        .addInputLines(
            "in/Test.java",
            """
            class Test {
              public <B, B2, B3, B4, Bad> void method(Bad f) {
                Bad d = f;
                B2 wow = null;
              }
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            class Test {
              public <B, B2, B3, B4, B5> void method(B5 f) {
                B5 d = f;
                B2 wow = null;
              }
            }
            """)
        .setFixChooser(FixChoosers.SECOND)
        .doTest();
  }

  @Test
  public void refactoring_single_number_ok_after() {
    refactoring
        .addInputLines(
            "in/Test.java",
            """
            class Test {
              public <B, Bad, B2> void method(Bad f) {
                Bad d = f;
                B2 wow = null;
              }
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            class Test {
              public <B, B3, B2> void method(B3 f) {
                B3 d = f;
                B2 wow = null;
              }
            }
            """)
        .setFixChooser(FixChoosers.SECOND)
        .doTest();
  }

  @Test
  public void refactoring_newNames() {
    refactoring
        .addInputLines(
            "in/Test.java",
            """
            class Test<RESP> {
              public <TBaz, Foo> void method(Foo f) {
                TBaz bad = null;
                Foo d = f;
              }
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            class Test<RespT> {
              public <BazT, FooT> void method(FooT f) {
                BazT bad = null;
                FooT d = f;
              }
            }
            """)
        .setFixChooser(FixChoosers.FIRST)
        .doTest();
  }

  @Test
  public void refactoring_tSuffixes() {
    refactoring
        .addInputLines(
            "in/Test.java",
            """
            class Test<RESP> {
              public <FOOT, BART> void method(FOOT f) {
                BART bad = null;
                FOOT d = f;
              }
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            class Test<RespT> {
              public <F, B> void method(F f) {
                B bad = null;
                F d = f;
              }
            }
            """)
        .setFixChooser(FixChoosers.FIRST)
        .doTest();
  }

  @Test
  public void negativeCases() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            """
            import java.util.ArrayList;

            class Test<MyClassVarT> {
              public <T, T3, SomeOtherT> void method(Exception e) {
                ArrayList<String> dontCheckTypeArguments = new ArrayList<String>();
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_manyNumberedTypes() {
    compilationHelper
        .addSourceLines(
            "Test.java",
            """
            import java.util.ArrayList;

            class Test<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10> {
              public <T1, T2, T3, T4, T5, T6, T7, T8, T9, T10> void method(Exception e) {
                T10 t = null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void refactoring_underscore() {
    refactoring
        .addInputLines(
            "in/Test.java",
            """
            class Test {
              public <_T> void method(_T t) {}
            }
            """)
        .addOutputLines(
            "in/Test.java",
            """
            class Test {
              public <T> void method(T t) {}
            }
            """)
        .setFixChooser(FixChoosers.FIRST)
        .doTest(TestMode.TEXT_MATCH);
  }

  @Test
  public void classifyTypeName_singleLetter() {
    assertKindOfName("T").isEqualTo(LETTER_WITH_MAYBE_NUMERAL);
    assertKindOfName("D").isEqualTo(LETTER_WITH_MAYBE_NUMERAL);
    assertKindOfName("T9").isEqualTo(LETTER_WITH_MAYBE_NUMERAL);
    assertKindOfName("X").isEqualTo(LETTER_WITH_MAYBE_NUMERAL);
  }

  @Test
  public void classifyTypeName_classT() {
    assertKindOfName("FooT").isEqualTo(CLASS_NAME_WITH_T);
    assertKindOfName("FooBarT").isEqualTo(CLASS_NAME_WITH_T);
    assertKindOfName("FoobarT").isEqualTo(CLASS_NAME_WITH_T);
  }

  @Test
  public void classifyTypeName_invalidTypeParameters() {
    assertKindOfName("FooD").isEqualTo(UNCLASSIFIED);
    assertKindOfName("somethingT").isEqualTo(NON_CLASS_NAME_WITH_T_SUFFIX);
    assertKindOfName("Some_TokenT").isEqualTo(NON_CLASS_NAME_WITH_T_SUFFIX);

    // Here, we don't tokenize LOUDT as L_O_U_D_T, but as one token
    assertKindOfName("LOUDT").isEqualTo(NON_CLASS_NAME_WITH_T_SUFFIX);
    // Per Google style guide, acronyms should be lowercase
    assertKindOfName("HTTPHeaderT").isEqualTo(NON_CLASS_NAME_WITH_T_SUFFIX);

    // This is unfortunate, the first 'word' is a single character, but falls into the same bin
    // as above.
    assertKindOfName("ACanalPanamaT").isEqualTo(NON_CLASS_NAME_WITH_T_SUFFIX);
  }

  private static Subject assertKindOfName(String s) {
    return assertWithMessage(s).that(TypeParameterNamingClassification.classify(s));
  }
}
