/*
 * Copyright (C) 2007 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.collect;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import junit.framework.TestCase;

/**
 * Base test case for testing the variety of forwarding classes.
 *
 * @author Robert Konigsberg
 * @author Louis Wasserman
 */
public abstract class ForwardingTestCase extends TestCase {

  private final List<String> calls = new ArrayList<String>();

  private void called(String id) {
    calls.add(id);
  }

  protected String getCalls() {
    return calls.toString();
  }

  protected boolean isCalled() {
    return !calls.isEmpty();
  }

  @SuppressWarnings("unchecked")
  protected <T> T createProxyInstance(Class<T> c) {
    /*
     * This invocation handler only registers that a method was called,
     * and then returns a bogus, but acceptable, value.
     */
    InvocationHandler handler = new InvocationHandler() {
      @Override
      public Object invoke(Object proxy, Method method, Object[] args)
          throws Throwable {
        called(asString(method));

        return getDefaultValue(method.getReturnType());
      }
    };

    return (T) Proxy.newProxyInstance(c.getClassLoader(),
        new Class[] { c }, handler);
  }

  private static final Joiner COMMA_JOINER = Joiner.on(",");

  /*
   * Returns string representation of a method.
   *
   * If the method takes no parameters, it returns the name (e.g.
   * "isEmpty". If the method takes parameters, it returns the simple names
   * of the parameters (e.g. "put(Object,Object)".)
   */
  private String asString(Method method) {
    String methodName = method.getName();
    Class<?>[] parameterTypes = method.getParameterTypes();

    if (parameterTypes.length == 0) {
      return methodName;
    }

    Iterable<String> parameterNames = Iterables.transform(
        Arrays.asList(parameterTypes),
        new Function<Class<?>, String>() {
          @Override
          public String apply(Class<?> from) {
            return from.getSimpleName();
          }
    });
    return methodName + "(" + COMMA_JOINER.join(parameterNames) + ")";
  }

  private static Object getDefaultValue(Class<?> returnType) {
    if (returnType == boolean.class || returnType == Boolean.class) {
      return Boolean.FALSE;
    } else if (returnType == int.class || returnType == Integer.class) {
      return 0;
    } else if ((returnType == Set.class) || (returnType == Collection.class)) {
      return Collections.emptySet();
    } else if (returnType == Iterator.class) {
      return Iterators.emptyModifiableIterator();
    } else if (returnType.isArray()) {
      return Array.newInstance(returnType.getComponentType(), 0);
    } else if ("java.util.function.Predicate".equals(returnType.getCanonicalName())
        || ("java.util.function.Consumer".equals(returnType.getCanonicalName()))) {
      // Generally, methods that accept java.util.function.* instances
      // don't like to get null values.  We generate them dynamically
      // using Proxy so that we can have Java 7 compliant code.
      InvocationHandler handler = new InvocationHandler() {
          @Override public Object invoke(Object proxy, Method method,
              Object[] args) {
            // Crude, but acceptable until we can use Java 8.  Other
            // methods have default implementations, and it is hard to
            // distinguish.
            if ("test".equals(method.getName())
                || "accept".equals(method.getName())) {
              return getDefaultValue(method.getReturnType());
            }
            throw new IllegalStateException(
                "Unexpected " + method + " invoked on " + proxy);
          }
        };
      return Proxy.newProxyInstance(returnType.getClassLoader(),
          new Class[] { returnType },
          handler);
    } else {
      return null;
    }
  }

  protected static <T> void callAllPublicMethods(Class<T> theClass, T object)
      throws InvocationTargetException {
    for (Method method : theClass.getMethods()) {
      Class<?>[] parameterTypes = method.getParameterTypes();
      Object[] parameters = new Object[parameterTypes.length];
      for (int i = 0; i < parameterTypes.length; i++) {
        parameters[i] = getDefaultValue(parameterTypes[i]);
      }
      try {
        try {
          method.invoke(object, parameters);
        } catch (InvocationTargetException ex) {
          try {
            throw ex.getCause();
          } catch (UnsupportedOperationException unsupported) {
            // this is a legit exception
          }
        }
      } catch (Throwable cause) {
        throw new InvocationTargetException(cause,
            method + " with args: " + Arrays.toString(parameters));
      }
    }
  }
}
