/*
 * Copyright 2018 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;

/** Tests for {@link UnusedMethod}. */
@RunWith(JUnit4.class)
public final class UnusedMethodTest {

  private final CompilationTestHelper helper =
      CompilationTestHelper.newInstance(UnusedMethod.class, getClass());
  private final BugCheckerRefactoringTestHelper refactoringHelper =
      BugCheckerRefactoringTestHelper.newInstance(UnusedMethod.class, getClass());

  @Test
  public void unusedNative() {
    helper
        .addSourceLines(
            "UnusedNative.java",
            """
            package unusedvars;

            public class UnusedNative {
              private native void aNativeMethod();
            }
            """)
        .doTest();
  }

  @Test
  public void unusedPrivateMethod() {
    helper
        .addSourceLines(
            "UnusedPrivateMethod.java",
            """
            package unusedvars;

            import com.google.errorprone.annotations.Keep;
            import java.lang.annotation.ElementType;
            import java.lang.annotation.Retention;
            import java.lang.annotation.RetentionPolicy;
            import java.lang.annotation.Target;
            import javax.inject.Inject;

            public class UnusedPrivateMethod {
              public void test() {
                used();
              }

              private void used() {}

              // BUG: Diagnostic contains: Method 'notUsed' is never used.
              private void notUsed() {}

              @Inject
              private void notUsedExempted() {}

              @Keep
              @Target(ElementType.METHOD)
              @Retention(RetentionPolicy.SOURCE)
              private @interface ProvidesCustom {}
            }
            """)
        .doTest();
  }

  @Test
  public void unuseds() {
    helper
        .addSourceLines(
            "Unuseds.java",
            """
            package unusedvars;

            import java.io.IOException;
            import java.io.ObjectStreamException;
            import java.util.List;
            import javax.inject.Inject;

            public class Unuseds {
              // BUG: Diagnostic contains:
              private void notUsedMethod() {}

              // BUG: Diagnostic contains:
              private static void staticNotUsedMethod() {}

              @SuppressWarnings({"deprecation", "unused"})
              class UsesSuppressWarning {
                private int f1;

                private void test1() {
                  int local;
                }

                @SuppressWarnings(value = "unused")
                private void test2() {
                  int local;
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void exemptedMethods() {
    helper
        .addSourceLines(
            "Unuseds.java",
            """
            package unusedvars;

            import java.io.IOException;
            import java.io.ObjectStreamException;

            public class Unuseds implements java.io.Serializable {
              private void readObject(java.io.ObjectInputStream in) throws IOException {
                in.hashCode();
              }

              private void writeObject(java.io.ObjectOutputStream out) throws IOException {
                out.writeInt(123);
              }

              private Object readResolve() {
                return null;
              }

              private void readObjectNoData() throws ObjectStreamException {}
            }
            """)
        .doTest();
  }

  @Test
  public void exemptedByName() {
    helper
        .addSourceLines(
            "Unuseds.java",
            """
            package unusedvars;

            class ExemptedByName {
              private void unused1(
                  int a,
                  int unusedParam,
                  int customUnused1,
                  int customUnused2,
                  int prefixUnused1Param,
                  int prefixUnused2Param) {
                int unusedLocal = a;
              }
            }
            """)
        .setArgs(
            "-XepOpt:Unused:exemptNames=customUnused1,customUnused2",
            "-XepOpt:Unused:exemptPrefixes=prefixunused1,prefixunused2")
        .doTest();
  }

  @Test
  public void exemptedByCustomAnnotation() {
    helper
        .addSourceLines(
            "Foo.java",
            """
            package example;

            @interface Foo {}
            """)
        .addSourceLines(
            "ExemptedByCustomAnnotation.java",
            """
            package example;

            class ExemptedByCustomAnnotation {
              @Foo
              private void bar() {}
            }
            """)
        .setArgs("-XepOpt:UnusedMethod:ExemptingMethodAnnotations=example.Foo")
        .doTest();
  }

  @Test
  public void suppressions() {
    helper
        .addSourceLines(
            "Unuseds.java",
            """
            package unusedvars;

            class Suppressed {
              @SuppressWarnings({"deprecation", "unused"})
              class UsesSuppressWarning {
                private void test1() {
                  int local;
                }

                @SuppressWarnings(value = "unused")
                private void test2() {
                  int local;
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void removal_javadocsAndNonJavadocs() {
    refactoringHelper
        .addInputLines(
            "UnusedWithComment.java",
            """
            package unusedvars;

            public class UnusedWithComment {
              /** Method comment */
              private void test1() {}

              /** Method comment */
              private void test2() {}

              // Non javadoc comment
              private void test3() {}
            }
            """)
        .addOutputLines(
            "UnusedWithComment.java",
            """
            package unusedvars;

            public class UnusedWithComment {}
            """)
        .doTest(TestMode.TEXT_MATCH);
  }

  @Test
  public void usedInLambda() {
    helper
        .addSourceLines(
            "UsedInLambda.java",
            """
            package unusedvars;

            import java.util.Arrays;
            import java.util.List;
            import java.util.function.Function;
            import java.util.stream.Collectors;

            /** Method parameters used in lambdas and anonymous classes */
            public class UsedInLambda {
              private Function<Integer, Integer> usedInLambda() {
                return x -> 1;
              }

              private String print(Object o) {
                return o.toString();
              }

              public List<String> print(List<Object> os) {
                return os.stream().map(this::print).collect(Collectors.toList());
              }

              public static void main(String[] args) {
                System.err.println(new UsedInLambda().usedInLambda());
                System.err.println(new UsedInLambda().print(Arrays.asList(1, 2, 3)));
              }
            }
            """)
        .doTest();
  }

  @Test
  public void onlyForMethodReference() {
    helper
        .addSourceLines(
            "Test.java",
            """
            import java.util.function.Predicate;

            class Test {
              private static boolean foo(int a) {
                return true;
              }

              Predicate<Integer> pred = Test::foo;
            }
            """)
        .doTest();
  }

  @Test
  public void methodSource() {
    helper
        .addSourceLines(
            "MethodSource.java",
            """
            package org.junit.jupiter.params.provider;

            public @interface MethodSource {
              String[] value();
            }
            """)
        .addSourceLines(
            "Test.java",
            """
            import java.util.stream.Stream;
            import org.junit.jupiter.params.provider.MethodSource;

            class Test {
              @MethodSource("parameters")
              void test() {}

              private static Stream<String> parameters() {
                return Stream.of();
              }
            }
            """)
        .doTest();
  }

  @Test
  public void qualifiedMethodSource() {
    helper
        .addSourceLines(
            "MethodSource.java",
            """
            package org.junit.jupiter.params.provider;

            public @interface MethodSource {
              String[] value();
            }
            """)
        .addSourceLines(
            "Test.java",
            """
            import java.util.stream.Stream;
            import org.junit.jupiter.params.provider.MethodSource;

            class Test {
              @MethodSource("Test#parameters")
              void test() {}

              private static Stream<String> parameters() {
                return Stream.of();
              }
            }
            """)
        .doTest();
  }

  @Test
  public void nestedQualifiedMethodSource() {
    helper
        .addSourceLines(
            "MethodSource.java",
            """
            package org.junit.jupiter.params.provider;

            public @interface MethodSource {
              String[] value();
            }
            """)
        .addSourceLines(
            "Test.java",
            """
            import java.util.stream.Stream;
            import org.junit.jupiter.params.provider.MethodSource;

            class Test {
              // @Nested
              public class NestedTest {
                @MethodSource("Test#parameters")
                void test() {}
              }

              private static Stream<String> parameters() {
                return Stream.of();
              }
            }
            """)
        .doTest();
  }

  @Test
  public void overriddenMethodNotCalledWithinClass() {
    helper
        .addSourceLines(
            "Test.java",
            """
            class Test {
              private class Inner {
                @Override
                public String toString() {
                  return null;
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void methodWithinPrivateInnerClass_isEligible() {
    helper
        .addSourceLines(
            "Test.java",
            """
            class Test {
              private class Inner {
                // BUG: Diagnostic contains:
                public void foo() {}
              }
            }
            """)
        .doTest();
  }

  @Test
  public void unusedConstructor() {
    helper
        .addSourceLines(
            "Test.java",
            """
            class Test {
              // BUG: Diagnostic contains: Constructor 'Test'
              private Test(int a) {}
            }
            """)
        .doTest();
  }

  @Test
  public void unusedConstructor_refactoredToPrivateNoArgVersion() {
    refactoringHelper
        .addInputLines(
            "Test.java",
            """
            class Test {
              private Test(int a) {}
            }
            """)
        .addOutputLines(
            "Test.java",
            """
            class Test {
              private Test() {}
            }
            """)
        .doTest();
  }

  @Test
  public void unusedConstructor_finalFieldsLeftDangling_noFix() {
    refactoringHelper
        .addInputLines(
            "Test.java",
            """
            class Test {
              private final int a;

              private Test(int a) {
                this.a = a;
              }
            }
            """)
        .expectUnchanged()
        .doTest();
  }

  @Test
  public void unusedConstructor_nonFinalFields_stillRefactored() {
    refactoringHelper
        .addInputLines(
            "Test.java",
            """
            class Test {
              private int a;

              private Test(int a) {}
            }
            """)
        .addOutputLines(
            "Test.java",
            """
            class Test {
              private int a;

              private Test() {}
            }
            """)
        .doTest();
  }

  @Test
  public void unusedConstructor_removed() {
    refactoringHelper
        .addInputLines(
            "Test.java",
            """
            class Test {
              private Test(int a) {}

              private Test(String a) {}

              public Test of() {
                return new Test(1);
              }
            }
            """)
        .addOutputLines(
            "Test.java",
            """
            class Test {
              private Test(int a) {}

              public Test of() {
                return new Test(1);
              }
            }
            """)
        .doTest();
  }

  @Test
  public void privateConstructor_calledWithinClass() {
    helper
        .addSourceLines(
            "Test.java",
            """
            class Test {
              private Test(int a) {}

              public Test of(int a) {
                return new Test(a);
              }
            }
            """)
        .doTest();
  }

  @Test
  public void zeroArgConstructor_notFlagged() {
    helper
        .addSourceLines(
            "Test.java",
            """
            class Test {
              private Test() {}
            }
            """)
        .doTest();
  }

  @Test
  public void annotationProperty_assignedByname() {
    helper
        .addSourceLines(
            "Test.java",
            """
            class Test {
              private @interface Anno {
                int value() default 1;
              }

              @Anno(value = 1)
              int b;
            }
            """)
        .doTest();
  }

  @Test
  public void annotationProperty_assignedAsDefault() {
    helper
        .addSourceLines(
            "Test.java",
            """
            class Test {
              private @interface Anno {
                int value();
              }

              @Anno(1)
              int a;
            }
            """)
        .doTest();
  }

  @Test
  public void effectivelyPrivateMethodMadeVisible() {
    helper
        .addSourceLines(
            "Test.java",
            """
            class Test {
              private class A {
                public void foo() {}
              }

              public class B extends A {}
            }
            """)
        .doTest();
  }

  @Test
  public void effectivelyPrivateMethodMadeVisible_bySubclassImplementingPublicInterface() {
    helper
        .addSourceLines(
            "Test.java",
            """
            class Test {
              private static class A {
                public void a() {}
              }

              public interface B {
                public void a();
              }

              public static final class C extends A implements B {}
            }
            """)
        .doTest();
  }

  @Test
  public void anonymousClassMethod() {
    refactoringHelper
        .addInputLines(
            "Test.java",
            """
            class T {
              void f() {
                var r =
                    new Runnable() {
                      public void foo() {}

                      @Override
                      public void run() {}
                    };
              }

              enum E implements Runnable {
                ONE {
                  public void foo() {}

                  @Override
                  public void run() {}
                };
              }
            }
            """)
        .addOutputLines(
            "Test.java",
            """
            class T {
              void f() {
                var r =
                    new Runnable() {
                      @Override
                      public void run() {}
                    };
              }

              enum E implements Runnable {
                ONE {
                  @Override
                  public void run() {}
                };
              }
            }
            """)
        .doTest();
  }
}
