/*
 * Copyright 2013 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.gwtmockito;

import static org.mockito.Mockito.mock;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.GWTBridge;
import com.google.gwt.i18n.client.Messages;
import com.google.gwt.i18n.client.constants.NumberConstantsImpl;
import com.google.gwt.i18n.client.impl.LocaleInfoImpl;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwtmockito.fakes.FakeClientBundleProvider;
import com.google.gwtmockito.fakes.FakeLocaleInfoImplProvider;
import com.google.gwtmockito.fakes.FakeMessagesProvider;
import com.google.gwtmockito.fakes.FakeNumberConstantsImplProvider;
import com.google.gwtmockito.fakes.FakeProvider;
import com.google.gwtmockito.fakes.FakeUiBinderProvider;
import com.google.gwtmockito.impl.ReturnsCustomMocks;

import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * A library to make Mockito-based testing of GWT applications easier. Most
 * users won't have to reference this class directly and should instead use
 * {@link GwtMockitoTestRunner}. Users who cannot use that class (e.g. tests
 * using JUnit3) can invoke {@link #initMocks} directly in their setUp and
 * {@link #tearDown} in their tearDown methods.
 * <p>
 * Note that calling {@link #initMocks} and {@link #tearDown} directly does
 * <i>not</i> implement {@link GwtMockitoTestRunner}'s behavior of implementing
 * native methods and making final methods mockable. The only way to get this
 * behavior is by using {@link GwtMockitoTestRunner}.
 * <p>
 * Once {@link #initMocks} has been invoked, test code can safely call
 * GWT.create without exceptions. Doing so will return either a mock object
 * registered with {@link GwtMock}, a fake object specified by a call to
 * {@link #useProviderForType}, or a new mock instance if no other binding
 * exists. Fakes for types extending the following are provided by default:
 * <ul>
 *   <li> UiBinder: uses a fake that populates all UiFields with GWT.create'd
 *        widgets, allowing them to be mocked like other calls to GWT.create.
 *        See {@link FakeUiBinderProvider} for details.
 *   <li> ClientBundle: Uses a fake that will return fake CssResources as
 *        defined below, and will return fake versions of other resources that
 *        return unique strings for getText and getSafeUri. See
 *        {@link FakeClientBundleProvider} for details.
 *   <li> Messages, CssResource, and SafeHtmlTemplates: uses a fake that
 *        implements each method by returning a String of SafeHtml based on the
 *        name of the method and any arguments passed to it. The exact format is
 *        undefined. See {@link FakeMessagesProvider} for details.
 * </ul>
 * <p>
 * The type returned from GWT.create will generally be the same as the type
 * passed in. The exception is when GWT.create'ing a subclass of
 * {@link RemoteService} - in this case, the result of GWT.create will be the
 * Async version of that interface as defined by gwt-rpc.
 * <p>
 * If {@link #initMocks} is called manually, it is important to invoke
 * {@link #tearDown} once the test has been completed. Failure to do so can
 * cause state to leak between tests.
 *
 * @see GwtMockitoTestRunner
 * @see GwtMock
 * @author [email protected] (Erik Kuefler)
 */
public class GwtMockito {

  private static final Map<Class<?>, FakeProvider<?>> DEFAULT_FAKE_PROVIDERS =
      new HashMap<Class<?>, FakeProvider<?>>();
  static {
    DEFAULT_FAKE_PROVIDERS.put(ClientBundle.class, new FakeClientBundleProvider());
    DEFAULT_FAKE_PROVIDERS.put(CssResource.class, new FakeMessagesProvider<CssResource>());
    DEFAULT_FAKE_PROVIDERS.put(LocaleInfoImpl.class, new FakeLocaleInfoImplProvider());
    DEFAULT_FAKE_PROVIDERS.put(Messages.class, new FakeMessagesProvider<Messages>());
    DEFAULT_FAKE_PROVIDERS.put(NumberConstantsImpl.class, new FakeNumberConstantsImplProvider());
    DEFAULT_FAKE_PROVIDERS.put(
        SafeHtmlTemplates.class, new FakeMessagesProvider<SafeHtmlTemplates>());
    DEFAULT_FAKE_PROVIDERS.put(UiBinder.class, new FakeUiBinderProvider());
  }

  private static Bridge bridge;

  /**
   * Causes all calls to GWT.create to be intercepted to return a mock or fake
   * object, and populates any {@link GwtMock}-annotated fields with mockito
   * mocks. This method should be usually be called during the setUp method of a
   * test case. Note that it explicitly calls
   * {@link MockitoAnnotations#initMocks}, so there is no need to call that
   * method separately. See the class description for more details.
   *
   * @param owner class to scan for {@link GwtMock}-annotated fields - almost
   *              always "this" in unit tests
   */
  public static void initMocks(Object owner) {
    // Create a new bridge and register built-in type providers
    bridge = new Bridge();
    for (Entry<Class<?>, FakeProvider<?>> entry : DEFAULT_FAKE_PROVIDERS.entrySet()) {
      useProviderForType(entry.getKey(), entry.getValue());
    }

    // Install the bridge and populate mock fields
    boolean success = false;
    try {
      setGwtBridge(bridge);
      registerGwtMocks(owner);
      MockitoAnnotations.initMocks(owner);
      success = true;
    } finally {
      if (!success) {
        tearDown();
      }
    }
  }

  /**
   * Resets GWT.create to its default behavior. This method should be called
   * after any test that called initMocks completes, usually in your test's
   * tearDown method. Failure to do so can introduce unexpected ordering
   * dependencies in tests.
   */
  public static void tearDown() {
    setGwtBridge(null);
  }

  /**
   * Specifies that the given provider should be used to GWT.create instances of
   * the given type and its subclasses. If multiple providers could produce a
   * given class (for example, if a provide is registered for a type and its
   * supertype), the provider for the more specific type is chosen. An exception
   * is thrown if this type is ambiguous. Note that if you just want to return a
   * Mockito mock from GWT.create, it's probably easier to use {@link GwtMock}
   * instead.
   */
  public static void useProviderForType(Class<?> type, FakeProvider<?> provider) {
    if (bridge == null) {
      throw new IllegalStateException("Must call initMocks() before calling useProviderForType()");
    }
    if (bridge.registeredMocks.containsKey(type)) {
      throw new IllegalArgumentException(
          "Can't use a provider for a type that already has a @GwtMock declared");
    }
    bridge.registeredProviders.put(type, provider);
  }

  /**
   * Returns a new fake object of the given type assuming a fake provider is
   * available for that type. Additional fake providers can be registered via
   * {@link #useProviderForType}.
   *
   * @param type type to get a fake object for
   * @return a fake of the given type, as returned by an applicable provider
   * @throws IllegalArgumentException if no provider for the given type (or one
   *                                  of its superclasses) has been registered
   */
  public static <T> T getFake(Class<T> type) {
    // If initMocks hasn't been called, read from the default fake provider map. This allows static
    // fields to be initialized with fakes in tests that don't use the GwtMockito test runner.
    T fake = getFakeFromProviderMap(
        type,
        bridge != null ? bridge.registeredProviders : DEFAULT_FAKE_PROVIDERS);
    if (fake == null) {
      throw new IllegalArgumentException("No fake provider has been registered "
          + "for " + type.getSimpleName() + ". Call useProviderForType to "
          + "register a provider before calling getFake.");
    }
    return fake;
  }

  private static void registerGwtMocks(Object owner) {
    Class<? extends Object> clazz = owner.getClass();

    while (!"java.lang.Object".equals(clazz.getName())) {
      for (Field field : clazz.getDeclaredFields()) {
        if (field.isAnnotationPresent(GwtMock.class)) {
          Object mock = Mockito.mock(field.getType());
          if (bridge.registeredMocks.containsKey(field.getType())) {
            throw new IllegalArgumentException("Owner declares multiple @GwtMocks for type "
                + field.getType().getSimpleName() + "; only one is allowed. Did you mean to "
                + "use a standard @Mock?");
          }
          bridge.registeredMocks.put(field.getType(), mock);
          field.setAccessible(true);
          try {
            field.set(owner, mock);
          } catch (IllegalAccessException e) {
            throw new IllegalStateException("Failed to make field accessible: " + field);
          }
        }
      }

      clazz = clazz.getSuperclass();
    }
  }

  private static void setGwtBridge(GWTBridge bridge) {
    try {
      Method setBridge = GWT.class.getDeclaredMethod("setBridge", GWTBridge.class);
      setBridge.setAccessible(true);
      setBridge.invoke(null, bridge);
    } catch (SecurityException e) {
      throw new RuntimeException(e);
    } catch (InvocationTargetException e) {
      throw new RuntimeException(e.getCause());
    } catch (IllegalAccessException e) {
      throw new AssertionError("Impossible since setBridge was made accessible");
    } catch (NoSuchMethodException e) {
      throw new AssertionError("Impossible since setBridge is known to exist");
    }
  }

  private static <T> T getFakeFromProviderMap(Class<T> type, Map<Class<?>, FakeProvider<?>> map) {
      // See if we have any providers for this type or its supertypes.
      Map<Class<?>, FakeProvider<?>> legalProviders = new HashMap<Class<?>, FakeProvider<?>>();
      for (Entry<Class<?>, FakeProvider<?>> entry : map.entrySet()) {
        if (entry.getKey().isAssignableFrom(type)) {
          legalProviders.put(entry.getKey(), entry.getValue());
        }
      }

      // Filter the set of legal providers to the most specific type.
      Map<Class<?>, FakeProvider<?>> filteredProviders = new HashMap<Class<?>, FakeProvider<?>>();
      for (Entry<Class<?>, FakeProvider<?>> candidate : legalProviders.entrySet()) {
        boolean isSpecific = true;
        for (Entry<Class<?>, FakeProvider<?>> other : legalProviders.entrySet()) {
          if (candidate != other && candidate.getKey().isAssignableFrom(other.getKey())) {
            isSpecific = false;
            break;
          }
        }
        if (isSpecific) {
          filteredProviders.put(candidate.getKey(), candidate.getValue());
        }
      }

      // If exactly one provider remains, use it.
      if (filteredProviders.size() == 1) {
        // We know this is safe since we checked that the types are assignable
        @SuppressWarnings({"rawtypes", "cast"})
        Class rawType = (Class) type;
        return (T) filteredProviders.values().iterator().next().getFake(rawType);
      } else if (filteredProviders.isEmpty()) {
        return null;
      } else {
        throw new IllegalArgumentException("Can't decide which provider to use for " +
            type.getSimpleName() +
            ", it could be provided as any of the following: " +
            mapToSimpleNames(filteredProviders.keySet()) +
            ". Add a provider for " +
            type.getSimpleName() +
            " to resolve this ambiguity.");
      }
  }

    private static Set<String> mapToSimpleNames(Set<Class<?>> classes) {
      Set<String> simpleNames = new HashSet<String>();
      for (Class<?> clazz : classes) {
        simpleNames.add(clazz.getSimpleName());
      }
      return simpleNames;
    }

  private static class Bridge extends GWTBridge {
    private final Map<Class<?>, FakeProvider<?>> registeredProviders =
        new HashMap<Class<?>, FakeProvider<?>>();
    private final Map<Class<?>, Object> registeredMocks = new HashMap<Class<?>, Object>();

    @Override
    @SuppressWarnings("unchecked") // safe since we check whether the type is assignable
    public <T> T create(Class<?> createdType) {
      // If we're creating a RemoteService, assume that the result of GWT.create is being assigned
      // to the async version of that service. Otherwise, assume it's being assigned to the same
      // type we're creating.
      Class<?> assignedType = RemoteService.class.isAssignableFrom(createdType)
          ? getAsyncType((Class<? extends RemoteService>) createdType)
          : createdType;

      // First check if we have a GwtMock for this exact being assigned to and use it if so.
      if (registeredMocks.containsKey(assignedType)) {
        return (T) registeredMocks.get(assignedType);
      }

      // Next check if we have a fake provider that can provide a fake for the type being created.
      T fake = (T) getFakeFromProviderMap(createdType, registeredProviders);
      if (fake != null) {
        return fake;
      }

      // If nothing has been registered, just return a new mock for the type being assigned.
      return (T) mock(assignedType, new ReturnsCustomMocks());
    }

    @Override
    public String getVersion() {
      return getClass().getName();
    }

    @Override
    public boolean isClient() {
      return false;
    }

    @Override
    public void log(String message, Throwable e) {
      System.err.println(message + "\n");
      if (e != null) {
        e.printStackTrace();
      }
    }

    /** Returns the corresponding async service type for the given remote service type. */
    private Class<?> getAsyncType(Class<? extends RemoteService> type) {
      Class<?> asyncType;
      try {
        asyncType = Class.forName(type.getCanonicalName() + "Async");
      } catch (ClassNotFoundException e) {
        throw new IllegalArgumentException(
            type.getCanonicalName() + " does not have a corresponding async interface", e);
      }
      return asyncType;
    }

  }
}