/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.aries.blueprint.proxy;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;

import org.apache.aries.blueprint.proxy.ProxyTestClassInnerClasses.ProxyTestClassInner;
import org.apache.aries.blueprint.proxy.ProxyTestClassInnerClasses.ProxyTestClassStaticInner;
import org.apache.aries.proxy.InvocationListener;
import org.apache.aries.proxy.impl.SingleInstanceDispatcher;
import org.junit.Test;

public abstract class AbstractProxyTest {

  protected static class TestListener implements InvocationListener {
  
    boolean preInvoke = false;
    boolean postInvoke = false;
    boolean postInvokeExceptionalReturn = false;
    private Method m;
    private Object token;
    private Throwable e;
    
    public Object preInvoke(Object proxy, Method m, Object[] args)
        throws Throwable {
      preInvoke = true;
      token = new Object();
      this.m = m;
      return token;
    }
  
    public void postInvoke(Object token, Object proxy, Method m,
        Object returnValue) throws Throwable {
      postInvoke = this.token == token && this.m == m;
    }
  
    public void postInvokeExceptionalReturn(Object token, Object proxy,
        Method m, Throwable exception) throws Throwable {
      postInvokeExceptionalReturn = this.token == token && this.m == m;
      e = exception;
    }
    
    public void clear() {
      preInvoke = false;
      postInvoke = false;
      postInvokeExceptionalReturn = false;
      token = null;
      m = null;
      e = null;
    }
    
    public Method getLastMethod() {
      return m;
    }
    
    public Throwable getLastThrowable() {
      return e;
    }
  }

  protected abstract Object getProxyInstance(Class<?> proxyClass);
  protected abstract Object getProxyInstance(Class<?> proxyClass, InvocationListener listener);
  protected abstract Class<?> getProxyClass(Class<?> clazz);
  protected abstract Object setDelegate(Object proxy, Callable<Object> dispatcher);
  
  protected Class<?> getTestClass() {
	  return ProxyTestClassGeneral.class;
  }
  
  protected Method getDeclaredMethod(Class<?> testClass, String name,
		Class<?>... classes) throws Exception {
	return getProxyClass(testClass).getDeclaredMethod(name, classes);
  }
  
/**
   * This test uses the ProxySubclassGenerator to generate and load a subclass
   * of the specified getTestClass().
   * 
   * Once the subclass is generated we check that it wasn't null. We check
   * that the InvocationHandler constructor doesn't return a null object
   * either
   * 
   * Test method for
   * {@link org.apache.aries.proxy.impl.ProxySubclassGenerator#generateAndLoadSubclass()}
   * .
   */
  @Test
  public void testGenerateAndLoadProxy() throws Exception
  {
    assertNotNull("Generated proxy subclass was null", getProxyClass(getTestClass()));
    assertNotNull("Generated proxy subclass instance was null", getProxyInstance(getProxyClass(getTestClass())));
  }
  /**
   * Test a basic method invocation on the proxy subclass
   */
  @Test
  public void testMethodInvocation() throws Exception {
    Method m = getDeclaredMethod(getTestClass(), "testMethod", String.class,
        int.class, Object.class);
    String x = "x";
    String returned = (String) m.invoke(getProxyInstance(getProxyClass(getTestClass())), x, 1, new Object());
    assertEquals("Object returned from invocation was not correct.", x, returned);
  }
  
  /**
   * Test different argument types on a method invocation
   */
  @Test
  public void testMethodArgs() throws Exception
  {
    Method m = getDeclaredMethod(getTestClass(), "testArgs", double.class,
        short.class, long.class, char.class, byte.class, boolean.class);
    Character xc = Character.valueOf('x');
    String x = xc.toString();
    String returned = (String) m.invoke(getProxyInstance(getProxyClass(getTestClass())), Double.MAX_VALUE, Short.MIN_VALUE, Long.MAX_VALUE, xc
        .charValue(), Byte.MIN_VALUE, false);
    assertEquals("Object returned from invocation was not correct.", x, returned);
  }
  
  /**
   * Test a method that returns void
   */
  @Test
  public void testReturnVoid() throws Exception
  {
    Method m = getDeclaredMethod(getTestClass(), "testReturnVoid");
    //for these weaving tests we are loading the woven test classes on a different classloader
    //to this class so we need to set the method accessible
    m.setAccessible(true);
    m.invoke(getProxyInstance(getProxyClass(getTestClass())));
  }

  /**
   * Test a method that returns an int
   */
  @Test
  public void testReturnInt() throws Exception
  {
    Method m = getDeclaredMethod(getTestClass(), "testReturnInt");
    //for these weaving tests we are loading the woven test classes on a different classloader
    //to this class so we need to set the method accessible
    m.setAccessible(true);
    Integer returned = (Integer) m.invoke(getProxyInstance(getProxyClass(getTestClass())));
    assertEquals("Expected object was not returned from invocation", Integer.valueOf(17), returned);
  }

  /**
   * Test a method that returns an Integer
   */
  @Test
  public void testReturnInteger() throws Exception
  {
    Method m = getDeclaredMethod(getTestClass(), "testReturnInteger");
    Integer returned = (Integer) m.invoke(getProxyInstance(getProxyClass(getTestClass())));
    assertEquals("Expected object was not returned from invocation", Integer.valueOf(1), returned);
  }

  /**
   * Test a public method declared higher up the superclass hierarchy
   */
  @Test
  public void testPublicHierarchyMethod() throws Exception
  {
    Method m = null;
    try {
      m = getDeclaredMethod(getTestClass(), "bMethod");
    } catch (NoSuchMethodException nsme) {
      m = getProxyClass(getTestClass()).getSuperclass().getDeclaredMethod("bMethod");
    }
    m.invoke(getProxyInstance(getProxyClass(getTestClass())));
  }

  /**
   * Test a protected method declared higher up the superclass hierarchy
   */
  @Test
  public void testProtectedHierarchyMethod() throws Exception
  {
    Method m = null;
    try {
      m = getDeclaredMethod(getTestClass(), "bProMethod");
    } catch (NoSuchMethodException nsme) {
      m = getProxyClass(getTestClass()).getSuperclass().getDeclaredMethod("bProMethod");
    }
    //for these weaving tests we are loading the woven test classes on a different classloader
    //to this class so we need to set the method accessible
    m.setAccessible(true);
    m.invoke(getProxyInstance(getProxyClass(getTestClass())));
  }
  
  /**
   * Test a default method declared higher up the superclass hierarchy
   */
  @Test
  public void testDefaultHierarchyMethod() throws Exception
  {
    Method m = null;
    try {
      m = getDeclaredMethod(getTestClass(), "bDefMethod");
    } catch (NoSuchMethodException nsme) {
      m = getProxyClass(getTestClass()).getSuperclass().getDeclaredMethod("bDefMethod", new Class[] {});
    }
    //for these weaving tests we are loading the woven test classes on a different classloader
    //to this class so we need to set the method accessible
    m.setAccessible(true);
    m.invoke(getProxyInstance(getProxyClass(getTestClass())));
  }

  /**
   * Test a covariant override method
   */
  @Test
  public void testCovariant() throws Exception
  {
    Class<?> proxy = getProxyClass(ProxyTestClassCovariantOverride.class);
    
    Method m = getDeclaredMethod(ProxyTestClassCovariantOverride.class, "getCovariant");
    Object returned = m.invoke(getProxyInstance(proxy));
    assertTrue("Object was of wrong type: " + returned.getClass().getSimpleName(),
        proxy.isInstance(returned));
  }

  /**
   * Test a method with generics
   */
  @Test
  public void testGenerics() throws Exception
  {
    Class<?> proxy = getProxyClass(ProxyTestClassGeneric.class);
    
    Object o = getProxyInstance(proxy);
    Method m = getDeclaredMethod(ProxyTestClassGeneric.class, "setSomething", String.class);
    m.invoke(o, "aString");
    
    if(getClass() == WovenProxyGeneratorTest.class)
    	m = getDeclaredMethod(ProxyTestClassGeneric.class.getSuperclass(), "getSomething");
    else 
        m = getDeclaredMethod(ProxyTestClassGeneric.class, "getSomething");

    Object returned = m.invoke(o);
    assertTrue("Object was of wrong type", String.class.isInstance(returned));
    assertEquals("String had wrong value", "aString", returned);
  }
  
  /**
   * Test that we don't generate classes twice
   */
  @Test
  public void testRetrieveClass() throws Exception
  {
    Class<?> retrieved = getProxyClass(getTestClass());
    assertNotNull("The new class was null", retrieved);
    assertEquals("The same class was not returned", retrieved, getProxyClass(getTestClass()));

  }
  
  @Test
  public void testEquals() throws Exception {
    Object p1 = getProxyInstance(getProxyClass(getTestClass()));
    Object p2 = getProxyInstance(getProxyClass(getTestClass()));
    
    assertFalse("Should not be equal", p1.equals(p2));
    
    Object p3 = getP3();
    
    p1 = setDelegate(p1, new SingleInstanceDispatcher(p3));
    p2 = setDelegate(p2, new SingleInstanceDispatcher(p3));
    
    assertTrue("Should be equal", p1.equals(p2));
    
    Object p4 = getProxyInstance(getProxyClass(getTestClass()));
    Object p5 = getProxyInstance(getProxyClass(getTestClass()));
    
    p4 = setDelegate(p4, new SingleInstanceDispatcher(p1));
    p5 = setDelegate(p5, new SingleInstanceDispatcher(p2));
    
    assertTrue("Should be equal", p4.equals(p5));
  }
  
  protected abstract Object getP3() throws Exception;
  
  @Test
  public void testInterception() throws Throwable {
    
    TestListener tl = new TestListener();
    Object obj = getProxyInstance(getProxyClass(getTestClass()), tl);
    
    assertCalled(tl, false, false, false);
    
    Method m = getDeclaredMethod(getTestClass(), "testReturnInteger", new Class[] {});
    m.invoke(obj);
    
    assertCalled(tl, true, true, false);
    
    tl.clear();
    assertCalled(tl, false, false, false);
    
    m = getDeclaredMethod(getTestClass(), "testException", new Class[] {});
    try {
      m.invoke(obj);
      fail("Should throw an exception");
    } catch (InvocationTargetException re) {
      if(!!!re.getTargetException().getClass().equals(RuntimeException.class))
        throw re.getTargetException();
      assertCalled(tl, true, false, true);
    }
    
    tl.clear();
    assertCalled(tl, false, false, false);
    
    m = getDeclaredMethod(getTestClass(), "testInternallyCaughtException", new Class[] {});
    try {
      m.invoke(obj);
    } finally {
      assertCalled(tl, true, true, false);
    }
  }
  
  protected void assertCalled(TestListener listener, boolean pre, boolean post, boolean ex) {
    assertEquals(pre, listener.preInvoke);
    assertEquals(post, listener.postInvoke);
    assertEquals(ex, listener.postInvokeExceptionalReturn);
  }
  @Test
  public void testStaticInner() throws Exception {
    assertNotNull(getProxyInstance(getProxyClass(ProxyTestClassStaticInner.class)));
  }
  @Test
  public void testInner() throws Exception {
    //An inner class has no no-args (the parent gets added as an arg) so we can't
    //get an instance
    assertNotNull(getProxyClass(ProxyTestClassInner.class));
  }
  
  /**
   * Test an abstract class
   */
  @Test
  public void testAbstractClass() throws Exception
  {
    Object ptca = getProxyInstance(getProxyClass(ProxyTestClassAbstract.class));
    ptca = setDelegate(ptca, new Callable<Object>() {

      public Object call() throws Exception {
        //We have to use a proxy instance here because we need it to be a subclass
        //of the one from the weaving loader in the weaving test...
        return getProxyInstance(ProxyTestClassChildOfAbstract.class);
      }
    });
    
    Method m = ptca.getClass().getDeclaredMethod("getMessage");
    assertEquals("Working", m.invoke(ptca));
  }
}
