/**
 * 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.cxf.common.injection;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import javax.annotation.Resources;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.apache.cxf.resource.ResourceManager;
import org.apache.cxf.resource.ResourceResolver;

import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class ResourceInjectorTest {
    private static final String RESOURCE_ONE = "resource one";
    private static final String RESOURCE_TWO = "resource two";
    private static final String RESOURCE_THREE = "resource three";

    private ResourceInjector injector;

    public void setUpResourceManager(String pfx) {

        ResourceManager resMgr = mock(ResourceManager.class);
        List<ResourceResolver> resolvers = new ArrayList<>();

        when(resMgr.getResourceResolvers()).thenReturn(resolvers);
        when(resMgr.resolveResource(pfx + "resource1", String.class, resolvers)).thenReturn(RESOURCE_ONE);
        when(resMgr.resolveResource("resource2", String.class, resolvers)).thenReturn(RESOURCE_TWO);
        when(resMgr.resolveResource("resource3", CharSequence.class, resolvers)).thenReturn(RESOURCE_THREE);

        injector = new ResourceInjector(resMgr);
    }

    @Test
    public void testFieldInjection() {
        setUpResourceManager(FieldTarget.class.getCanonicalName() + "/");
        doInjectTest(new FieldTarget());
    }


    @Test
    public void testFieldInSuperClassInjection() {
        setUpResourceManager("org.apache.cxf.common.injection.FieldTarget/");
        doInjectTest(new SubFieldTarget());
    }

    @Test
    public void testSetterInSuperClassInjection() {
        setUpResourceManager("org.apache.cxf.common.injection.SetterTarget/");
        doInjectTest(new SubSetterTarget());
    }

    @Test
    public void testSetterInjection() {
        setUpResourceManager(SetterTarget.class.getCanonicalName() + "/");
        doInjectTest(new SetterTarget());
    }

    @Test
    public void testProxyInjection() {
        setUpResourceManager(SetterTarget.class.getCanonicalName() + "/");
        doInjectTest(getProxyObject(), SetterTarget.class);
    }

    @Test
    public void testEnhancedInjection() {
        setUpResourceManager(FieldTarget.class.getCanonicalName() + "/");
        doInjectTest(getEnhancedObject());
    }

    @Test
    public void testClassLevelInjection() {
        setUpResourceManager("");
        doInjectTest(new ClassTarget());
    }

    @Test
    public void testResourcesContainer() {
        setUpResourceManager("");
        doInjectTest(new ResourcesContainerTarget());
    }

    @Test
    public void testPostConstruct() {
        setUpResourceManager(SetterTarget.class.getCanonicalName() + "/");

        SetterTarget target = new SetterTarget();
        doInjectTest(target);
        assertTrue(target.injectionCompleteCalled());
    }

    @Test
    public void testPreDestroy() {
        injector = new ResourceInjector(null, null);
        SetterTarget target = new SetterTarget();
        injector.destroy(target);
        assertTrue(target.preDestroyCalled());
    }

    private void doInjectTest(Target target) {
        doInjectTest(target, target.getClass());
    }

    private void doInjectTest(Target target, Class<?> clazz) {

        injector.inject(target, clazz);
        injector.construct(target);
        assertNotNull(target.getResource1());
        assertEquals(RESOURCE_ONE, target.getResource1());

        assertNotNull(target.getResource2());
        assertEquals(RESOURCE_TWO, target.getResource2());

        assertNotNull(target.getResource3());
        assertEquals(RESOURCE_THREE, target.getResource3());
    }

    private Target getProxyObject() {
        return (Target)Proxy.newProxyInstance(ISetterTarget.class.getClassLoader(),
                                              new Class[] {ISetterTarget.class},
                                              new ProxyClass(new SetterTarget()));
    }

    private FieldTarget getEnhancedObject() {
        Enhancer e = new Enhancer();
        e.setSuperclass(FieldTarget.class);
        e.setCallback(new CallInterceptor());
        return (FieldTarget)e.create();
    }

}


interface Target {
    String getResource1();
    String getResource2();
    CharSequence getResource3();
}

class CallInterceptor implements MethodInterceptor {

    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        Object retValFromSuper = null;
        if (!Modifier.isAbstract(method.getModifiers())) {
            retValFromSuper = proxy.invokeSuper(obj, args);
        }
        return retValFromSuper;
    }
}


class FieldTarget implements Target {

    @Resource
    private String resource1;

    @Resource(name = "resource2")
    private String resource2foo;

    @Resource(name = "resource3")
    private CharSequence resource3foo;

    public String getResource1() {
        return resource1;
    }

    public String getResource2() {
        return resource2foo;
    }

    public CharSequence getResource3() {
        return resource3foo;
    }

    public String toString() {
        return "[" + resource1 + ":" + resource2foo + ":" + resource3foo + "]";
    }

}

class SubFieldTarget extends FieldTarget {
}

class SubSetterTarget extends SetterTarget {

}

interface ISetterTarget extends Target {
    void setResource1(String argResource1);
    void setResource2(String argResource2);
    void setResource3(CharSequence argResource3);
}

class ProxyClass implements InvocationHandler {
    Object obj;

    ProxyClass(Object o) {
        obj = o;
    }

    public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
        Object result = null;
        try {
            Class<?>[] types = new Class[0];
            if (args != null) {
                types = new Class[args.length];
                for (int i = 0; i < args.length; i++) {
                    types[i] = args[i].getClass();
                    if ("setResource3".equals(m.getName()) && types[i].equals(String.class)) {
                        types[i] = CharSequence.class;
                    }
                }
            }
            Method target = obj.getClass().getMethod(m.getName(), types);
            result = target.invoke(obj, args);
        } catch (InvocationTargetException e) {
            // Do nothing here
        } catch (Exception eBj) {
            eBj.printStackTrace();
        }
        return result;
    }
}
class SetterTarget implements Target {

    private String resource1;
    private String resource2;
    private CharSequence resource3;
    private boolean injectionCompletePublic;
    private boolean injectionCompletePrivate;
    private boolean preDestroy;
    private boolean preDestroyPrivate;

    public final String getResource1() {
        return this.resource1;
    }

    @Resource
    public final void setResource1(final String argResource1) {
        this.resource1 = argResource1;
    }

    public final String getResource2() {
        return this.resource2;
    }

    @Resource(name = "resource2")
    public void setResource2(final String argResource2) {
        this.resource2 = argResource2;
    }

    public final CharSequence getResource3() {
        return this.resource3;
    }

    @Resource(name = "resource3")
    public void setResource3(final CharSequence argResource3) {
        this.resource3 = argResource3;
    }

    @PostConstruct
    public void injectionIsAllFinishedNowThankYouVeryMuch() {
        injectionCompletePublic = true;

        // stick this here to keep PMD happy...
        injectionIsAllFinishedNowThankYouVeryMuchPrivate();
    }

    @PostConstruct
    private void injectionIsAllFinishedNowThankYouVeryMuchPrivate() {
        injectionCompletePrivate = true;
    }

    @PreDestroy
    public void preDestroyMethod() {
        preDestroy = true;
    }

    @PreDestroy
    private void preDestroyMethodPrivate() {
        preDestroyPrivate = true;
    }

    public boolean injectionCompleteCalled() {
        return injectionCompletePrivate && injectionCompletePublic;
    }

    public boolean preDestroyCalled() {
        return preDestroy && preDestroyPrivate;
    }

    // dummy method to access the private methods to avoid compile warnings
    public void dummyMethod() {
        preDestroyMethodPrivate();
        setResource2("");
    }
}

//CHECKSTYLE:OFF
@Resource(name = "resource1")
class ClassTarget implements Target {

    @Resource(name = "resource3")
    public CharSequence resource3foo;
    @Resource(name = "resource2")
    public String resource2foo;
    private String res1;

    public final void setResource1(String res) {
        res1 = res;
    }

    public final String getResource1() {
        return res1;
    }

    public final String getResource2() {
        return resource2foo;
    }

    public final CharSequence getResource3() {
        return resource3foo;
    }
}



@Resources({@Resource(name = "resource1"),
            @Resource(name = "resource2"),
            @Resource(name = "resource3") })
class ResourcesContainerTarget implements Target {

    private String res1;
    private String resource2;
    private CharSequence resource3;

    public final void setResource1(String res) {
        res1 = res;
    }

    public final String getResource1() {
        return res1;
    }

    public final String getResource2() {
        return resource2;
    }

    public final CharSequence getResource3() {
        return resource3;
    }
}