package org.powermock.reflect.internal.proxy;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.Factory;
import net.sf.cglib.proxy.InvocationHandler;
import org.junit.Before;
import org.junit.Test;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

import static org.assertj.core.api.Java6Assertions.assertThat;

public class ProxyFrameworksTest {
    
    private ProxyFrameworks proxyFrameworks;
    
    @Before
    public void setUp() throws Exception {
        proxyFrameworks = new ProxyFrameworks();
    }
    
    @Test
    public void should_throw_illegal_argument_exception_if_class_is_null() throws Exception {
        assertThat(proxyFrameworks.getUnproxiedType(null)).isNull();
    }
    
    @Test
    public void should_return_null_if_object_is_null() throws Exception {
        assertThat(proxyFrameworks.getUnproxiedType((Object) null)).isNull();
    }
    
    @Test
    public void should_return_original_class_if_object_not_proxy() throws Exception {
        SomeClass someClass = new SomeClass();
        
        UnproxiedType unproxiedType = proxyFrameworks.getUnproxiedType(someClass);
        
        assertThatOriginalTypeInstanceOf(unproxiedType, SomeClass.class);
    }
    
    @Test
    public void should_return_original_class_if_proxy_created_with_java() {
        SomeInterface someInterface = createJavaProxy(new Class[]{ SomeInterface.class });
        
        UnproxiedType unproxiedType = proxyFrameworks.getUnproxiedType(someInterface);
        
        assertThatOriginalTypeInstanceOf(unproxiedType, SomeInterface.class);
    }
    
    @Test
    public void should_return_original_class_if_proxy_created_with_cglib() {
        SomeClass someClass = (SomeClass) createCglibProxy(SomeClass.class);
        
        UnproxiedType unproxiedType = proxyFrameworks.getUnproxiedType(someClass);
        
        assertThatOriginalTypeInstanceOf(unproxiedType, SomeClass.class);
    }
    
    @Test
    public void should_not_detect_synthetic_classes_as_cglib_proxy() throws Exception {
        final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        final Enumeration<URL> resources = classLoader.getResources("org/powermock/reflect/internal/proxy");
    
        final List<URL> urls = new ArrayList<URL>();
    
        while (resources.hasMoreElements()) {
            urls.add(resources.nextElement());
        }
        
        String className = "Some$$SyntheticClass$$Lambda";
        byte[] bytes = ClassFactory.create(className);
        CustomClassLoader customClassLoader = new CustomClassLoader(urls.toArray(new URL[urls.size()]), classLoader);
        
        Class<?> defineClass = customClassLoader.defineClass(className, bytes);
        
        UnproxiedType unproxiedType = proxyFrameworks.getUnproxiedType(defineClass.newInstance());
        
        assertThatOriginalTypeInstanceOf(unproxiedType, defineClass);
    }
    
    @Test
    public void should_return_object_as_original_class_if_no_non_no_mocking_interfaces() {
        Factory someClass = (Factory) createCglibProxy(Factory.class);
        
        UnproxiedType unproxiedType = proxyFrameworks.getUnproxiedType(someClass);
        
        assertThatOriginalTypeInstanceOf(unproxiedType, Object.class);
    }
    
    @Test
    public void should_return_interface_as_original_type_if_only_one_non_mocking_interface() {
        Factory someClass = (Factory) createCglibProxy(Factory.class, SomeInterface.class);
        
        UnproxiedType unproxiedType = proxyFrameworks.getUnproxiedType(someClass);
        
        assertThatOriginalTypeInstanceOf(unproxiedType, SomeInterface.class);
    }
    
    @Test
    public void should_return_interface_and_original_type_if_proxy_has_interface_and_superclass() {
        SomeClass someClass = (SomeClass) createCglibProxy(SomeClass.class, SomeInterface.class, AnotherInterface.class);
        
        UnproxiedType unproxiedType = proxyFrameworks.getUnproxiedType(someClass);
    
        assertThatOriginalTypeInstanceOfAndInterfaces(unproxiedType, SomeClass.class, new Class[]{
                SomeInterface.class,
                AnotherInterface.class
        });
    }
    
    @Test
    public void should_return_interfaces_if_proxy_create_from_several_interfaces() {
        
        Class[] interfaces = {SomeInterface.class, AnotherInterface.class};
        
        SomeInterface someInterface = createJavaProxy(interfaces);
        
        UnproxiedType unproxiedType = proxyFrameworks.getUnproxiedType(someInterface);
        
        assertThatOriginalIsNullAndInterfaces(
                unproxiedType, interfaces
        );
    }
    
    private Object createCglibProxy(Class<?> superclass, Class... interfaces) {
        if (interfaces.length == 0){
            return Enhancer.create(
                    superclass,
                    new InvocationHandler() {
                        @Override
                        public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                            return method.invoke(o, objects);
                        }
                    });
        }else {
            return Enhancer.create(
                    superclass,
                    interfaces,
                    new InvocationHandler() {
                        @Override
                        public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                            return method.invoke(o, objects);
                        }
                    });
        }
    }
    
    private SomeInterface createJavaProxy(Class[] interfaces) {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        return (SomeInterface) Proxy.newProxyInstance(
                classLoader,
                interfaces,
                new java.lang.reflect.InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        return method.invoke(proxy, args);
                    }
                });
    }
    
    private void assertThatOriginalIsNullAndInterfaces(UnproxiedType unproxiedType, Class[] expectedInterfaces) {
        assertThat(unproxiedType.getOriginalType()).isNull();
        assertThat(unproxiedType.getInterfaces())
                .containsExactlyInAnyOrder(expectedInterfaces);
    }
    
    private void assertThatOriginalTypeInstanceOf(UnproxiedType unproxiedType, Class<?> expectedClass) {
        assertThat(unproxiedType.getOriginalType()).isEqualTo(expectedClass);
        assertThat(unproxiedType.getInterfaces()).isEmpty();
    }
    
    private void assertThatOriginalTypeInstanceOfAndInterfaces(UnproxiedType unproxiedType, Class<?> expectedClass,Class[] expectedInterfaces ) {
        assertThat(unproxiedType.getOriginalType()).isEqualTo(expectedClass);
        assertThat(unproxiedType.getInterfaces())
                .containsExactlyInAnyOrder(expectedInterfaces);
    }
    
    private static class CustomClassLoader extends URLClassLoader {
    
        private CustomClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent);
        }
        
        Class<?> defineClass(String name, byte[] b) {
            return defineClass(name, b, 0, b.length);
        }
    }
    
}