/*
 * Copyright (C) 2012 The Guava 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.common.testing;

import static com.google.common.base.Predicates.and;
import static com.google.common.base.Predicates.not;
import static com.google.common.testing.AbstractPackageSanityTests.Chopper.suffix;

import com.google.common.annotations.Beta;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.reflect.ClassPath;
import com.google.common.testing.NullPointerTester.Visibility;
import com.google.j2objc.annotations.J2ObjCIncompatible;
import java.io.IOException;
import java.io.Serializable;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import org.junit.Test;

/**
 * Automatically runs sanity checks against top level classes in the same package of the test that
 * extends {@code AbstractPackageSanityTests}. Currently sanity checks include {@link
 * NullPointerTester}, {@link EqualsTester} and {@link SerializableTester}. For example: <pre>
 * public class PackageSanityTests extends AbstractPackageSanityTests {}
 * </pre>
 *
 * <p>Note that only top-level classes with either a non-private constructor or a non-private static
 * factory method to construct instances can have their instance methods checked. For example: <pre>
 * public class Address {
 *   private final String city;
 *   private final String state;
 *   private final String zipcode;
 *
 *   public Address(String city, String state, String zipcode) {...}
 *
 *   {@literal @Override} public boolean equals(Object obj) {...}
 *   {@literal @Override} public int hashCode() {...}
 *   ...
 * }
 * </pre>
 * <p>No cascading checks are performed against the return values of methods unless the method is a
 * static factory method. Neither are semantics of mutation methods such as {@code
 * someList.add(obj)} checked. For more detailed discussion of supported and unsupported cases, see
 * {@link #testEquals}, {@link #testNulls} and {@link #testSerializable}.
 *
 * <p>For testing against the returned instances from a static factory class, such as <pre>
 * interface Book {...}
 * public class Books {
 *   public static Book hardcover(String title) {...}
 *   public static Book paperback(String title) {...}
 * }
 * </pre>
 *
 * <p>please use {@link ClassSanityTester#forAllPublicStaticMethods}.
 *
 * <p>If not all classes on the classpath should be covered, {@link
 * #ignoreClasses} can be used to exclude certain classes. As a special case, classes with an
 * underscore in the name (like {@code AutoValue_Foo}) can be excluded using
 * <code>ignoreClasses({@link #UNDERSCORE_IN_NAME})</code>.
 *
 * <p>{@link #setDefault} allows subclasses to specify default values for types.
 *
 * <p>This class incurs IO because it scans the classpath and reads classpath resources.
 *
 * @author Ben Yu
 * @since 14.0
 */
@Beta
// TODO: Switch to JUnit 4 and use @Parameterized and @BeforeClass
// Note: @Test annotations are deliberate, as some subclasses specify @RunWith(JUnit4).
@GwtIncompatible
@J2ObjCIncompatible // com.google.common.reflect.ClassPath
public abstract class AbstractPackageSanityTests extends TestCase {

  /**
   * A predicate that matches classes with an underscore in the class name. This can be used with
   * {@link #ignoreClasses} to exclude generated classes, such as the {@code AutoValue_Foo} classes
   * generated by <a href="https://github.com/google/auto/tree/master/value">AutoValue</a>.
   *
   * @since 19.0
   */
  public static final Predicate<Class<?>> UNDERSCORE_IN_NAME = new Predicate<Class<?>>() {
    @Override public boolean apply(Class<?> c) {
      return c.getSimpleName().contains("_");
    }
  };

  /* The names of the expected method that tests null checks. */
  private static final ImmutableList<String> NULL_TEST_METHOD_NAMES = ImmutableList.of(
      "testNulls", "testNull",
      "testNullPointers", "testNullPointer",
      "testNullPointerExceptions", "testNullPointerException");

  /* The names of the expected method that tests serializable. */
  private static final ImmutableList<String> SERIALIZABLE_TEST_METHOD_NAMES = ImmutableList.of(
      "testSerializable", "testSerialization",
      "testEqualsAndSerializable", "testEqualsAndSerialization");

  /* The names of the expected method that tests equals. */
  private static final ImmutableList<String> EQUALS_TEST_METHOD_NAMES = ImmutableList.of(
      "testEquals", "testEqualsAndHashCode",
      "testEqualsAndSerializable", "testEqualsAndSerialization",
      "testEquality");

  private static final Chopper TEST_SUFFIX =
      suffix("Test")
          .or(suffix("Tests"))
          .or(suffix("TestCase"))
          .or(suffix("TestSuite"));

  private final Logger logger = Logger.getLogger(getClass().getName());
  private final ClassSanityTester tester = new ClassSanityTester();
  private Visibility visibility = Visibility.PACKAGE;
  private Predicate<Class<?>> classFilter = new Predicate<Class<?>>() {
    @Override public boolean apply(Class<?> cls) {
      return visibility.isVisible(cls.getModifiers());
    }
  };

  /**
   * Restricts the sanity tests for public API only. By default, package-private API are also
   * covered.
   */
  protected final void publicApiOnly() {
    visibility = Visibility.PUBLIC;
  }

  /**
   * Tests all top-level {@link Serializable} classes in the package. For a serializable Class
   * {@code C}:
   * <ul>
   * <li>If {@code C} explicitly implements {@link Object#equals}, the deserialized instance will be
   *     checked to be equal to the instance before serialization.
   * <li>If {@code C} doesn't explicitly implement {@code equals} but instead inherits it from a
   *     superclass, no equality check is done on the deserialized instance because it's not clear
   *     whether the author intended for the class to be a value type.
   * <li>If a constructor or factory method takes a parameter whose type is interface, a dynamic
   *     proxy will be passed to the method. It's possible that the method body expects an instance
   *     method of the passed-in proxy to be of a certain value yet the proxy isn't aware of the
   *     assumption, in which case the equality check before and after serialization will fail.
   * <li>If the constructor or factory method takes a parameter that {@link
   *     AbstractPackageSanityTests} doesn't know how to construct, the test will fail.
   * <li>If there is no visible constructor or visible static factory method declared by {@code C},
   *     {@code C} is skipped for serialization test, even if it implements {@link Serializable}.
   * <li>Serialization test is not performed on method return values unless the method is a visible
   *     static factory method whose return type is {@code C} or {@code C}'s subtype.
   * </ul>
   *
   * <p>In all cases, if {@code C} needs custom logic for testing serialization, you can add an
   * explicit {@code testSerializable()} test in the corresponding {@code CTest} class, and {@code
   * C} will be excluded from automated serialization test performed by this method.
   */
  @Test
  public void testSerializable() throws Exception {
    // TODO: when we use @BeforeClass, we can pay the cost of class path scanning only once.
    for (Class<?> classToTest
        : findClassesToTest(loadClassesInPackage(), SERIALIZABLE_TEST_METHOD_NAMES)) {
      if (Serializable.class.isAssignableFrom(classToTest)) {
        try {
          Object instance = tester.instantiate(classToTest);
          if (instance != null) {
            if (isEqualsDefined(classToTest)) {
              SerializableTester.reserializeAndAssert(instance);
            } else {
              SerializableTester.reserialize(instance);
            }
          }
        } catch (Throwable e) {
          throw sanityError(classToTest, SERIALIZABLE_TEST_METHOD_NAMES, "serializable test", e);
        }
      }
    }
  }

  /**
   * Performs {@link NullPointerTester} checks for all top-level classes in the package. For a class
   * {@code C}
   * <ul>
   * <li>All visible static methods are checked such that passing null for any parameter that's not
   *     annotated with {@link javax.annotation.Nullable} should throw {@link NullPointerException}.
   * <li>If there is any visible constructor or visible static factory method declared by the class,
   *     all visible instance methods will be checked too using the instance created by invoking the
   *     constructor or static factory method.
   * <li>If the constructor or factory method used to construct instance takes a parameter that
   *     {@link AbstractPackageSanityTests} doesn't know how to construct, the test will fail.
   * <li>If there is no visible constructor or visible static factory method declared by {@code C},
   *     instance methods are skipped for nulls test.
   * <li>Nulls test is not performed on method return values unless the method is a visible static
   *     factory method whose return type is {@code C} or {@code C}'s subtype.
   * </ul>
   *
   * <p>In all cases, if {@code C} needs custom logic for testing nulls, you can add an explicit
   * {@code testNulls()} test in the corresponding {@code CTest} class, and {@code C} will be
   * excluded from the automated null tests performed by this method.
   */
  @Test
  public void testNulls() throws Exception {
    for (Class<?> classToTest
        : findClassesToTest(loadClassesInPackage(), NULL_TEST_METHOD_NAMES)) {
      try {
        tester.doTestNulls(classToTest, visibility);
      } catch (Throwable e) {
        throw sanityError(classToTest, NULL_TEST_METHOD_NAMES, "nulls test", e);
      }
    }
  }

  /**
   * Tests {@code equals()} and {@code hashCode()} implementations for every top-level class in the
   * package, that explicitly implements {@link Object#equals}. For a class {@code C}:
   * <ul>
   * <li>The visible constructor or visible static factory method with the most parameters is used
   *     to construct the sample instances. In case of tie, the candidate constructors or factories
   *     are tried one after another until one can be used to construct sample instances.
   * <li>For the constructor or static factory method used to construct instances, it's checked that
   *     when equal parameters are passed, the result instance should also be equal; and vice versa.
   * <li>Inequality check is not performed against state mutation methods such as {@link List#add},
   *     or functional update methods such as {@link com.google.common.base.Joiner#skipNulls}.
   * <li>If the constructor or factory method used to construct instance takes a parameter that
   *     {@link AbstractPackageSanityTests} doesn't know how to construct, the test will fail.
   * <li>If there is no visible constructor or visible static factory method declared by {@code C},
   *     {@code C} is skipped for equality test.
   * <li>Equality test is not performed on method return values unless the method is a visible
   *     static factory method whose return type is {@code C} or {@code C}'s subtype.
   * </ul>
   *
   * <p>In all cases, if {@code C} needs custom logic for testing {@code equals()}, you can add an
   * explicit {@code testEquals()} test in the corresponding {@code CTest} class, and {@code C} will
   * be excluded from the automated {@code equals} test performed by this method.
   */
  @Test
  public void testEquals() throws Exception {
    for (Class<?> classToTest
        : findClassesToTest(loadClassesInPackage(), EQUALS_TEST_METHOD_NAMES)) {
      if (!classToTest.isEnum() && isEqualsDefined(classToTest)) {
        try {
          tester.doTestEquals(classToTest);
        } catch (Throwable e) {
          throw sanityError(classToTest, EQUALS_TEST_METHOD_NAMES, "equals test", e);
        }
      }
    }
  }

  /**
   * Sets the default value for {@code type}, when dummy value for a parameter of the same type
   * needs to be created in order to invoke a method or constructor. The default value isn't used in
   * testing {@link Object#equals} because more than one sample instances are needed for testing
   * inequality.
   */
  protected final <T> void setDefault(Class<T> type, T value) {
    tester.setDefault(type, value);
  }

  /**
   * Sets two distinct values for {@code type}. These values can be used for both null pointer
   * testing and equals testing.
   *
   * @since 17.0
   */
  protected final <T> void setDistinctValues(Class<T> type, T value1, T value2) {
    tester.setDistinctValues(type, value1, value2);
  }

  /** Specifies that classes that satisfy the given predicate aren't tested for sanity. */
  protected final void ignoreClasses(Predicate<? super Class<?>> condition) {
    this.classFilter = and(this.classFilter, not(condition));
  }

  private static AssertionFailedError sanityError(
      Class<?> cls, List<String> explicitTestNames, String description, Throwable e) {
    String message = String.format(Locale.ROOT,
        "Error in automated %s of %s\n"
            + "If the class is better tested explicitly, you can add %s() to %sTest",
        description, cls, explicitTestNames.get(0), cls.getName());
    AssertionFailedError error = new AssertionFailedError(message);
    error.initCause(e);
    return error;
  }

  /**
   * Finds the classes not ending with a test suffix and not covered by an explicit test
   * whose name is {@code explicitTestName}.
   */
  @VisibleForTesting List<Class<?>> findClassesToTest(
      Iterable<? extends Class<?>> classes, Iterable<String> explicitTestNames) {
    // "a.b.Foo" -> a.b.Foo.class
    TreeMap<String, Class<?>> classMap = Maps.newTreeMap();
    for (Class<?> cls : classes) {
      classMap.put(cls.getName(), cls);
    }
    // Foo.class -> [FooTest.class, FooTests.class, FooTestSuite.class, ...]
    Multimap<Class<?>, Class<?>> testClasses = HashMultimap.create();
    LinkedHashSet<Class<?>> candidateClasses = Sets.newLinkedHashSet();
    for (Class<?> cls : classes) {
      Optional<String> testedClassName = TEST_SUFFIX.chop(cls.getName());
      if (testedClassName.isPresent()) {
        Class<?> testedClass = classMap.get(testedClassName.get());
        if (testedClass != null) {
          testClasses.put(testedClass, cls);
        }
      } else {
        candidateClasses.add(cls);
      }
    }
    List<Class<?>> result = Lists.newArrayList();
    NEXT_CANDIDATE: for (Class<?> candidate : Iterables.filter(candidateClasses, classFilter)) {
      for (Class<?> testClass : testClasses.get(candidate)) {
        if (hasTest(testClass, explicitTestNames)) {
          // covered by explicit test
          continue NEXT_CANDIDATE;
        }
      }
      result.add(candidate);
    }
    return result;
  }

  private List<Class<?>> loadClassesInPackage() throws IOException {
    List<Class<?>> classes = Lists.newArrayList();
    String packageName = getClass().getPackage().getName();
    for (ClassPath.ClassInfo classInfo
        : ClassPath.from(getClass().getClassLoader()).getTopLevelClasses(packageName)) {
      Class<?> cls;
      try {
        cls = classInfo.load();
      } catch (NoClassDefFoundError e) {
        // In case there were linking problems, this is probably not a class we care to test anyway.
        logger.log(Level.SEVERE, "Cannot load class " + classInfo + ", skipping...", e);
        continue;
      }
      if (!cls.isInterface()) {
        classes.add(cls);
      }
    }
    return classes;
  }

  private static boolean hasTest(Class<?> testClass, Iterable<String> testNames) {
    for (String testName : testNames) {
      try {
        testClass.getMethod(testName);
        return true;
      } catch (NoSuchMethodException e) {
        continue;
      }
    }
    return false;
  }

  private static boolean isEqualsDefined(Class<?> cls) {
    try {
      return !cls.getDeclaredMethod("equals", Object.class).isSynthetic();
    } catch (NoSuchMethodException e) {
      return false;
    }
  }

  abstract static class Chopper {

    final Chopper or(final Chopper you) {
      final Chopper i = this;
      return new Chopper() {
        @Override Optional<String> chop(String str) {
          return i.chop(str).or(you.chop(str));
        }
      };
    }

    abstract Optional<String> chop(String str);

    static Chopper suffix(final String suffix) {
      return new Chopper() {
        @Override Optional<String> chop(String str) {
          if (str.endsWith(suffix)) {
            return Optional.of(str.substring(0, str.length() - suffix.length()));
          } else {
            return Optional.absent();
          }
        }
      };
    }
  }
}
