/*
 * Copyright 2021 Google Inc.
 *
 * 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.testing.junit.testparameterinjector;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Verify.verify;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Primitives;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.List;
import java.util.Optional;

/**
 * Annotation to define a test annotation used to have parameterized methods, in either a
 * parameterized or non parameterized test.
 *
 * <p>Parameterized tests enabled by defining a annotation (see {@link TestParameter} as an example)
 * for the type of the parameter, defining a member variable annotated with this annotation, and
 * specifying the parameter with the same annotation for each test, or for the whole class, for
 * example:
 *
 * <pre>{@code
 * @RunWith(TestParameterInjector.class)
 * public class ColorTest {
 *     @Retention(RUNTIME)
 *     @Target({TYPE, METHOD, FIELD})
 *     @TestParameterAnnotation
 *     public @interface ColorParameter {
 *       Color[] value() default {};
 *     }
 *
 *     @ColorParameter({BLUE, WHITE, RED}) private Color color;
 *
 *     @Test
 *     public void test() {
 *       assertThat(paint(color)).isSuccessful();
 *     }
 * }
 * }</pre>
 *
 * <p>An alternative is to use a method parameter for injection:
 *
 * <pre>{@code
 * @RunWith(TestParameterInjector.class)
 * public class ColorTest {
 *     @Retention(RUNTIME)
 *     @Target({TYPE, METHOD, FIELD})
 *     @TestParameterAnnotation
 *     public @interface ColorParameter {
 *       Color[] value() default {};
 *     }
 *
 *     @Test
 *     @ColorParameter({BLUE, WHITE, RED})
 *     public void test(Color color) {
 *       assertThat(paint(color)).isSuccessful();
 *     }
 * }
 * }</pre>
 *
 * <p>Yet another alternative is to use a method parameter for injection, but with the annotation
 * specified on the parameter itself, which helps when multiple arguments share the
 * same @TestParameterAnnotation annotation.
 *
 * <pre>{@code
 * @RunWith(TestParameterInjector.class)
 * public class ColorTest {
 *     @Retention(RUNTIME)
 *     @Target({TYPE, METHOD, FIELD})
 *     @TestParameterAnnotation
 *     public @interface ColorParameter {
 *       Color[] value() default {};
 *     }
 *
 *     @Test
 *     public void test(@ColorParameter({BLUE, WHITE}) Color color1,
 *                      @ColorParameter({WHITE, RED}) Color color2) {
 *       assertThat(paint(color1. color2)).isSuccessful();
 *     }
 * }
 * }</pre>
 *
 * <p>Class constructors can also be annotated with @TestParameterAnnotation annotations, as shown
 * below:
 *
 * <pre>{@code
 * @RunWith(TestParameterInjector.class)
 * public class ColorTest {
 *     @Retention(RUNTIME)
 *     @Target({TYPE, METHOD, FIELD})
 *     public @TestParameterAnnotation
 *     public @interface ColorParameter {
 *       Color[] value() default {};
 *     }
 *
 *     public ColorTest(@ColorParameter({BLUE, WHITE}) Color color) {
 *       ...
 *     }
 *
 *     @Test
 *     public void test() {...}
 * }
 * }</pre>
 *
 * <p>Each field that needs to be injected from a parameter requires its dedicated distinct
 * annotation.
 *
 * <p>If the same annotation is defined both on the class and method, the method parameter values
 * take precedence.
 *
 * <p>If the same annotation is defined both on the class and constructor, the constructor parameter
 * values take precedence.
 *
 * <p>Annotations cannot be duplicated between the constructor or constructor parameters and a
 * method or method parameter.
 *
 * <p>Since the parameter values must be specified in an annotation return value, they are
 * restricted to the annotation method return type set (primitive, Class, Enum, String, etc...). If
 * parameters have to be dynamically generated, the conventional Parameterized mechanism with {@code
 * Parameters} has to be used instead.
 */
@Retention(RUNTIME)
@Target({ANNOTATION_TYPE})
@interface TestParameterAnnotation {
  /**
   * Pattern of the {@link MessageFormat} format to derive the test's name from the parameters.
   *
   * @see {@code Parameters#name()}
   */
  String name() default "{0}";

  /** Specifies a validator for the parameter to determine whether test should be skipped. */
  Class<? extends TestParameterValidator> validator() default DefaultValidator.class;

  /**
   * Specifies a processor for the parameter to invoke arbitrary code before and after the test
   * statement's execution.
   */
  Class<? extends TestParameterProcessor> processor() default DefaultProcessor.class;

  /** Specifies a value provider for the parameter to provide the values to test. */
  Class<? extends TestParameterValueProvider> valueProvider() default DefaultValueProvider.class;

  /** Default {@link TestParameterValidator} implementation which skips no test. */
  class DefaultValidator implements TestParameterValidator {

    @Override
    public boolean shouldSkip(Context context) {
      return false;
    }
  }

  /** Default {@link TestParameterProcessor} implementation which does nothing. */
  class DefaultProcessor implements TestParameterProcessor {
    @Override
    public void before(Object testParameterValue) {}

    @Override
    public void after(Object testParameterValue) {}
  }

  /**
   * Default {@link TestParameterValueProvider} implementation that gets its values from the
   * annotation's `value` method.
   */
  class DefaultValueProvider implements TestParameterValueProvider {

    @Override
    public List<Object> provideValues(Annotation annotation, Optional<Class<?>> parameterClass) {
      Object parameters = getParametersAnnotationValues(annotation, annotation.annotationType());
      checkState(
          parameters.getClass().isArray(),
          "The return value of the value method should be an array");

      int parameterCount = Array.getLength(parameters);
      ImmutableList.Builder<Object> resultBuilder = ImmutableList.builder();
      for (int i = 0; i < parameterCount; i++) {
        Object value = Array.get(parameters, i);
        if (parameterClass.isPresent()) {
          verify(
              Primitives.wrap(parameterClass.get()).isInstance(value),
              "Found %s annotation next to a parameter of type %s which doesn't match"
                  + " (annotation = %s)",
              annotation.annotationType().getSimpleName(),
              parameterClass.get().getSimpleName(),
              annotation);
        }
        resultBuilder.add(value);
      }
      return resultBuilder.build();
    }

    @Override
    public Class<?> getValueType(
        Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass) {
      try {
        Method valueMethod = annotationType.getMethod("value");
        return valueMethod.getReturnType().getComponentType();
      } catch (NoSuchMethodException e) {
        throw new RuntimeException(
            "The @TestParameterAnnotation annotation should have a single value() method.", e);
      }
    }

    /**
     * Returns the parameters of the test parameter, by calling the {@code value} method on the
     * annotation.
     */
    private static Object getParametersAnnotationValues(
        Annotation annotation, Class<? extends Annotation> annotationType) {
      Method valueMethod;
      try {
        valueMethod = annotationType.getMethod("value");
      } catch (NoSuchMethodException e) {
        throw new RuntimeException(
            "The @TestParameterAnnotation annotation should have a single value() method.", e);
      }
      Object parameters;
      try {
        parameters = valueMethod.invoke(annotation);
      } catch (InvocationTargetException e) {
        if (e.getCause() instanceof IllegalAccessError) {
          // There seems to be a bug or at least something weird with the JVM that causes
          // IllegalAccessError to be thrown because the return value is not visible when it is a
          // non-public nested type. See
          // http://mail.openjdk.java.net/pipermail/core-libs-dev/2014-January/024180.html for more
          // info.
          throw new RuntimeException(
              String.format(
                  "Could not access %s.value(). This is probably because %s is not visible to the"
                      + " annotation proxy. To fix this, make %s public.",
                  annotationType.getSimpleName(),
                  valueMethod.getReturnType().getSimpleName(),
                  valueMethod.getReturnType().getSimpleName()));
          // Note: Not chaining the exception to reduce the clutter for the reader
        } else {
          throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e);
        }
      } catch (Exception e) {
        throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e);
      }
      return parameters;
    }
  }
}
