package play.modules.springtester;

import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.support.GenericApplicationContext;
import play.modules.spring.Spring;
import play.modules.spring.SpringPlugin;

import javax.annotation.Resource;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Set;

public final class SpringMockitoMaster {

    public void before(Object object, Map<String, BeanDefinition> definitions) throws IllegalAccessException {
        // Inject resources into test before we put in the mocks to make sure resources are all "real".
        injectAnnotatedField(Resource.class, object);
        // Here we are inject a resource that will be wrapped using Mockito's spy (partial mock) functionality.
        injectAnnotatedField(PartialMock.class, object);
        injectMocks(object);
        registerMocksAndPartialMocksInSpringContext(object, definitions);
        resolveBeanDefinitions(definitions);
        // Only inject the Subject into the test once the mocks have been registered.
        injectAnnotatedField(Subject.class, object);
    }

    public void after(Map<String, BeanDefinition> definitions) {
        Set<String> beanNames = definitions.keySet();
        // Here we are restoring all the original bean definitions as they were before we pummelled them with our mocks and such.
        for (String name : beanNames) {
            BeanDefinition definition = definitions.get(name);
            SpringPlugin.applicationContext.removeBeanDefinition(name);
            SpringPlugin.applicationContext.registerBeanDefinition(name, definition);
        }
        definitions.clear();
    }

    public void registerBeanDefinition(String name, Object object, Map<String, BeanDefinition> definitions) {
        GenericApplicationContext context = SpringPlugin.applicationContext;
        if (context.containsBeanDefinition(name)) {
            BeanDefinition beanDefinition = context.getBeanDefinition(name);
            // Here we are storing the original bean definition so we can restore it after the test is run.
            definitions.put(name, beanDefinition);
            context.removeBeanDefinition(name);
        }
        // Register the new bean definition which will dispense mocks instead of the real beans.
        GenericBeanDefinition definition = createMockitoFactoryBeanDefinition(object);
        context.registerBeanDefinition(name, definition);
    }

    private void resolveBeanDefinitions(Map<String, BeanDefinition> definitions) {
        Set<String> keys = definitions.keySet();
        // Used to ensure that the definition is resolved and is available via Spring.getBeanOfType(Class).
        // We only try and resolve them all once we have injected all mocks/stubs so we don't run into
        // circular dependency issues.
        for (String key : keys) Spring.getBean(key);
    }

    private void injectAnnotatedField(Class<? extends Annotation> annotation, Object object) throws IllegalAccessException {
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(annotation)) injectDependency(annotation, object, field);
        }
    }

    private void injectDependency(Class<? extends Annotation> annotation, Object object, Field field) throws IllegalAccessException {
        Class<?> fieldTypeClass = field.getType();
        field.setAccessible(true);
        Object value = getSpringBeanOfType(annotation, fieldTypeClass);
        field.set(object, value);
    }

    private Object getSpringBeanOfType(Class<? extends Annotation> annotation, Class<?> fieldTypeClass) {
        Object bean = Spring.getBeanOfType(fieldTypeClass);
        if (annotation == PartialMock.class) return Mockito.spy(bean);
        return bean;
    }

    private void injectMocks(Object object) {
        MockitoAnnotations.initMocks(object);
    }

    private void registerMocksAndPartialMocksInSpringContext(Object object, Map<String, BeanDefinition> definitions) throws IllegalAccessException {
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(Mock.class)) registerMockInSpringContext(object, field, definitions);
            if (field.isAnnotationPresent(PartialMock.class)) registerPartialMockInSpringContext(object, field, definitions);
        }
    }

    private void registerMockInSpringContext(Object object, Field field, Map<String, BeanDefinition> definitions) throws IllegalAccessException {
        Mock mock = field.getAnnotation(Mock.class);
        String name = mock.name();
        registerObjectInSpringContext(name, object, field, definitions);
    }

    private void registerPartialMockInSpringContext(Object object, Field field, Map<String, BeanDefinition> definitions) throws IllegalAccessException {
        PartialMock partialMock = field.getAnnotation(PartialMock.class);
        String name = partialMock.name();
        registerObjectInSpringContext(name, object, field, definitions);
    }

    private void registerObjectInSpringContext(String name, Object object, Field field, Map<String, BeanDefinition> definitions) throws IllegalAccessException {
        Object fieldObject = getValue(object, field);
        if ("".equals(name)) registerBeanDefinitionWithFieldName(field, fieldObject, definitions);
        else registerBeanDefinition(name, fieldObject, definitions);
    }

    private Object getValue(Object object, Field field) throws IllegalAccessException {
        field.setAccessible(true);
        return field.get(object);
    }

    private void registerBeanDefinitionWithFieldName(Field field, Object object, Map<String, BeanDefinition> definitions) {
        String fieldName = field.getName();
        registerBeanDefinition(fieldName, object, definitions);
    }

    private GenericBeanDefinition createMockitoFactoryBeanDefinition(Object object) {
        GenericBeanDefinition definition = new GenericBeanDefinition();
        definition.setBeanClass(MockitoFactory.class);
        definition.setFactoryMethodName("create");
        ConstructorArgumentValues values = new ConstructorArgumentValues();
        // Make sure all beans are created with no state to be verified.
        Mockito.reset(object);
        values.addGenericArgumentValue(object);
        definition.setConstructorArgumentValues(values);
        return definition;
    }
}
