/*
 * Copyright (C) 2008 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.googlecode.guice;

import static com.google.inject.Asserts.getClassPathUrls;
import static com.google.inject.matcher.Matchers.any;

import com.google.common.testing.GcFinalization;
import com.google.inject.AbstractModule;
import com.google.inject.Binder;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.googlecode.guice.PackageVisibilityTestModule.PublicUserOfPackagePrivate;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URLClassLoader;
import javax.inject.Inject;
import junit.framework.TestCase;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

/**
 * This test is in a separate package so we can test package-level visibility with confidence.
 *
 * @author mcculls@gmail.com (Stuart McCulloch)
 */
public class BytecodeGenTest extends TestCase {

  private final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

  private final Module interceptorModule =
      new AbstractModule() {
        @Override
        protected void configure() {
          bindInterceptor(
              any(),
              any(),
              new MethodInterceptor() {
                @Override
                public Object invoke(MethodInvocation chain) throws Throwable {
                  return chain.proceed() + " WORLD";
                }
              });
        }
      };

  private final Module noopInterceptorModule =
      new AbstractModule() {
        @Override
        protected void configure() {
          bindInterceptor(
              any(),
              any(),
              new MethodInterceptor() {
                @Override
                public Object invoke(MethodInvocation chain) throws Throwable {
                  return chain.proceed();
                }
              });
        }
      };

  public void testPackageVisibility() {
    Injector injector = Guice.createInjector(new PackageVisibilityTestModule());
    injector.getInstance(PublicUserOfPackagePrivate.class); // This must pass.
  }

  public void testInterceptedPackageVisibility() {
    Injector injector = Guice.createInjector(interceptorModule, new PackageVisibilityTestModule());
    injector.getInstance(PublicUserOfPackagePrivate.class); // This must pass.
  }

  public void testEnhancerNaming() {
    Injector injector = Guice.createInjector(interceptorModule, new PackageVisibilityTestModule());
    PublicUserOfPackagePrivate pupp = injector.getInstance(PublicUserOfPackagePrivate.class);
    assertTrue(
        pupp.getClass()
            .getName()
            .startsWith(PublicUserOfPackagePrivate.class.getName() + "$$EnhancerByGuice$$"));
  }

  // TODO(sameb): Figure out how to test FastClass naming tests.

  /** Custom URL classloader with basic visibility rules */
  static class TestVisibilityClassLoader extends URLClassLoader {

    final boolean hideInternals;

    TestVisibilityClassLoader(boolean hideInternals) {
      this(TestVisibilityClassLoader.class.getClassLoader(), hideInternals);
    }

    TestVisibilityClassLoader(ClassLoader classloader, boolean hideInternals) {
      super(getClassPathUrls(), classloader);
      this.hideInternals = hideInternals;
    }

    /**
     * Classic parent-delegating classloaders are meant to override findClass. However,
     * non-delegating classloaders (as used in OSGi) instead override loadClass to provide support
     * for "class-space" separation.
     */
    @Override
    protected Class<?> loadClass(final String name, final boolean resolve)
        throws ClassNotFoundException {

      synchronized (this) {
        // check our local cache to avoid duplicates
        final Class<?> clazz = findLoadedClass(name);
        if (clazz != null) {
          return clazz;
        }
      }

      if (name.startsWith("java.")) {

        // standard bootdelegation of java.*
        return super.loadClass(name, resolve);

      } else if (!name.contains(".internal.") && !name.contains(".cglib.")) {

        /*
         * load public and test classes directly from the classpath - we don't
         * delegate to our parent because then the loaded classes would also be
         * able to see private internal Guice classes, as they are also loaded
         * by the parent classloader.
         */
        final Class<?> clazz = findClass(name);
        if (resolve) {
          resolveClass(clazz);
        }
        return clazz;
      }

      // hide internal non-test classes
      if (hideInternals) {
        throw new ClassNotFoundException();
      }
      return super.loadClass(name, resolve);
    }
  }

  /** as loaded by another class loader */
  private Class<ProxyTest> proxyTestClass;

  private Class<ProxyTestImpl> realClass;
  private Module testModule;

  @Override
  @SuppressWarnings("unchecked")
  protected void setUp() throws Exception {
    super.setUp();

    ClassLoader testClassLoader = new TestVisibilityClassLoader(true);
    proxyTestClass = (Class<ProxyTest>) testClassLoader.loadClass(ProxyTest.class.getName());
    realClass = (Class<ProxyTestImpl>) testClassLoader.loadClass(ProxyTestImpl.class.getName());

    testModule =
        new AbstractModule() {
          @Override
          public void configure() {
            bind(proxyTestClass).to(realClass);
          }
        };
  }

  interface ProxyTest {
    String sayHello();
  }

  /**
   * Note: this class must be marked as public or protected so that the Guice custom classloader
   * will intercept it. Private and implementation classes are not intercepted by the custom
   * classloader.
   *
   * @see com.google.inject.internal.BytecodeGen.Visibility
   */
  public static class ProxyTestImpl implements ProxyTest {

    static {
      //System.out.println(ProxyTestImpl.class.getClassLoader());
    }

    @Override
    public String sayHello() {
      return "HELLO";
    }
  }

  public void testProxyClassLoading() throws Exception {
    Object testObject =
        Guice.createInjector(interceptorModule, testModule).getInstance(proxyTestClass);

    // verify method interception still works
    Method m = realClass.getMethod("sayHello");
    assertEquals("HELLO WORLD", m.invoke(testObject));
  }

  public void testSystemClassLoaderIsUsedIfProxiedClassUsesIt() {
    ProxyTest testProxy =
        Guice.createInjector(
                interceptorModule,
                new Module() {
                  @Override
                  public void configure(Binder binder) {
                    binder.bind(ProxyTest.class).to(ProxyTestImpl.class);
                  }
                })
            .getInstance(ProxyTest.class);

    if (ProxyTest.class.getClassLoader() == systemClassLoader) {
      assertSame(testProxy.getClass().getClassLoader(), systemClassLoader);
    } else {
      assertNotSame(testProxy.getClass().getClassLoader(), systemClassLoader);
    }
  }

  public void testProxyClassUnloading() {
    Object testObject =
        Guice.createInjector(interceptorModule, testModule).getInstance(proxyTestClass);
    assertNotNull(testObject.getClass().getClassLoader());
    assertNotSame(testObject.getClass().getClassLoader(), systemClassLoader);

    // take a weak reference to the generated proxy class
    WeakReference<Class<?>> clazzRef = new WeakReference<Class<?>>(testObject.getClass());

    assertNotNull(clazzRef.get());

    // null the proxy
    testObject = null;

    /*
     * this should be enough to queue the weak reference
     * unless something is holding onto it accidentally.
     */
    GcFinalization.awaitClear(clazzRef);

    // This test could be somewhat flaky when the GC isn't working.
    // If it fails, run the test again to make sure it's failing reliably.
    assertNull("Proxy class was not unloaded.", clazzRef.get());
  }

  public void testProxyingPackagePrivateMethods() {
    Injector injector = Guice.createInjector(interceptorModule);
    assertEquals("HI WORLD", injector.getInstance(PackageClassPackageMethod.class).sayHi());
    assertEquals("HI WORLD", injector.getInstance(PublicClassPackageMethod.class).sayHi());
    assertEquals("HI WORLD", injector.getInstance(ProtectedClassProtectedMethod.class).sayHi());
  }

  static class PackageClassPackageMethod {
    String sayHi() {
      return "HI";
    }
  }

  public static class PublicClassPackageMethod {
    String sayHi() {
      return "HI";
    }
  }

  protected static class ProtectedClassProtectedMethod {
    protected String sayHi() {
      return "HI";
    }
  }

  static class Hidden {}

  public static class HiddenMethodReturn {
    public Hidden method() {
      return new Hidden();
    }
  }

  public static class HiddenMethodParameter {
    public void method(Hidden h) {}
  }

  public void testClassLoaderBridging() throws Exception {
    ClassLoader testClassLoader = new TestVisibilityClassLoader(false);

    Class hiddenMethodReturnClass = testClassLoader.loadClass(HiddenMethodReturn.class.getName());
    Class hiddenMethodParameterClass =
        testClassLoader.loadClass(HiddenMethodParameter.class.getName());

    Injector injector = Guice.createInjector(noopInterceptorModule);

    Class hiddenClass = testClassLoader.loadClass(Hidden.class.getName());
    Constructor ctor = hiddenClass.getDeclaredConstructor();

    ctor.setAccessible(true);

    // don't use bridging for proxies with private parameters
    Object o1 = injector.getInstance(hiddenMethodParameterClass);
    o1.getClass().getDeclaredMethod("method", hiddenClass).invoke(o1, ctor.newInstance());

    // don't use bridging for proxies with private return types
    Object o2 = injector.getInstance(hiddenMethodReturnClass);
    o2.getClass().getDeclaredMethod("method").invoke(o2);
  }

  // This tests for a situation where a osgi bundle contains a version of guice.  When guice
  // generates a fast class it will use a bridge classloader
  public void testFastClassUsesBridgeClassloader() throws Throwable {
    Injector injector = Guice.createInjector();
    // These classes are all in the same classloader as guice itself, so other than the private one
    // they can all be fast class invoked
    injector.getInstance(PublicInject.class).assertIsFastClassInvoked();
    injector.getInstance(ProtectedInject.class).assertIsFastClassInvoked();
    injector.getInstance(PackagePrivateInject.class).assertIsFastClassInvoked();
    injector.getInstance(PrivateInject.class).assertIsReflectionInvoked();

    // This classloader will load the types in an loader with a different version of guice/cglib
    // this prevents the use of fastclass for all but the public types (where the bridge
    // classloader can be used).
    MultipleVersionsOfGuiceClassLoader fakeLoader = new MultipleVersionsOfGuiceClassLoader();
    injector
        .getInstance(fakeLoader.loadLogCreatorType(PublicInject.class))
        .assertIsFastClassInvoked();
    injector
        .getInstance(fakeLoader.loadLogCreatorType(ProtectedInject.class))
        .assertIsReflectionInvoked();
    injector
        .getInstance(fakeLoader.loadLogCreatorType(PackagePrivateInject.class))
        .assertIsReflectionInvoked();
    injector
        .getInstance(fakeLoader.loadLogCreatorType(PrivateInject.class))
        .assertIsReflectionInvoked();
  }

  // This classloader simulates an OSGI environment where a bundle has a conflicting definition of
  // cglib (or guice).  This is sort of the opposite of the BridgeClassloader and is meant to test
  // its use.
  static class MultipleVersionsOfGuiceClassLoader extends URLClassLoader {
    MultipleVersionsOfGuiceClassLoader() {
      this(MultipleVersionsOfGuiceClassLoader.class.getClassLoader());
    }

    MultipleVersionsOfGuiceClassLoader(ClassLoader classloader) {
      super(getClassPathUrls(), classloader);
    }

    public Class<? extends LogCreator> loadLogCreatorType(Class<? extends LogCreator> cls)
        throws ClassNotFoundException {
      return loadClass(cls.getName()).asSubclass(LogCreator.class);
    }

    /**
     * Classic parent-delegating classloaders are meant to override findClass. However,
     * non-delegating classloaders (as used in OSGi) instead override loadClass to provide support
     * for "class-space" separation.
     */
    @Override
    protected Class<?> loadClass(final String name, final boolean resolve)
        throws ClassNotFoundException {

      synchronized (this) {
        // check our local cache to avoid duplicates
        final Class<?> clazz = findLoadedClass(name);
        if (clazz != null) {
          return clazz;
        }
      }

      if (name.startsWith("java.")
          || name.startsWith("javax.")
          || name.equals(LogCreator.class.getName())
          || (!name.startsWith("com.google.inject.")
              && !name.contains(".cglib.")
              && !name.startsWith("com.googlecode.guice"))) {

        // standard parent delegation
        return super.loadClass(name, resolve);

      } else {
        // load a new copy of the class
        final Class<?> clazz = findClass(name);
        if (resolve) {
          resolveClass(clazz);
        }
        return clazz;
      }
    }
  }

  public static class LogCreator {
    final Throwable caller;

    public LogCreator() {
      this.caller = new Throwable();
    }

    void assertIsFastClassInvoked() throws Throwable {
      // 2 because the first 2 elements are
      // LogCreator.<init>()
      // Subclass.<init>()
      if (!caller.getStackTrace()[2].getClassName().contains("$$FastClassByGuice$$")) {
        throw new AssertionError("Caller was not FastClass").initCause(caller);
      }
    }

    void assertIsReflectionInvoked() throws Throwable {
      // Scan for a call to Constructor.newInstance, but stop if we see the test itself.
      for (StackTraceElement element : caller.getStackTrace()) {
        if (element.getClassName().equals(BytecodeGenTest.class.getName())) {
          // break when we hit the test method.
          break;
        }
        if (element.getClassName().equals(Constructor.class.getName())
            && element.getMethodName().equals("newInstance")) {
          return;
        }
      }
      throw new AssertionError("Caller was not Constructor.newInstance").initCause(caller);
    }
  }

  public static class PublicInject extends LogCreator {
    @Inject
    public PublicInject() {}
  }

  static class PackagePrivateInject extends LogCreator {
    @Inject
    PackagePrivateInject() {}
  }

  protected static class ProtectedInject extends LogCreator {
    @Inject
    protected ProtectedInject() {}
  }

  private static class PrivateInject extends LogCreator {
    @Inject
    private PrivateInject() {}
  }
}
