/*
 * $Id$
 *
 * License Agreement.
 *
 * Rich Faces - Natural Ajax for Java Server Faces (JSF)
 *
 * Copyright (C) 2007 Exadel, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */
package org.richfaces.cdk;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.easymock.EasyMock;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;

import com.google.inject.AbstractModule;
import com.google.inject.BindingAnnotation;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.ScopeAnnotation;
import com.google.inject.TypeLiteral;
import com.google.inject.binder.AnnotatedBindingBuilder;

/**
 * <p class="changed_added_4_0">
 * </p>
 *
 * @author asmirnov@exadel.com
 *
 */
public class CdkTestRunner extends BlockJUnit4ClassRunner {
    /**
     * <p class="changed_added_4_0">
     * </p>
     *
     * @param klass
     * @throws InitializationError
     * @throws InitializationError
     */
    public CdkTestRunner(Class<?> klass) throws InitializationError {
        super(klass);
    }

    /**
     * Gets all declared fields and all inherited fields.
     */
    protected Set<Field> getFields(Class<?> c) {
        Set<Field> fields = new HashSet<Field>(Arrays.asList(c.getDeclaredFields()));
        while ((c = c.getSuperclass()) != null) {
            for (Field f : c.getDeclaredFields()) {
                if (!Modifier.isStatic(f.getModifiers()) && !Modifier.isPrivate(f.getModifiers())) {
                    fields.add(f);
                }
            }
        }
        return fields;
    }

    @Override
    protected void runChild(FrameworkMethod method, RunNotifier notifier) {
        super.runChild(method, notifier);
    }

    @Override
    protected Object createTest() throws Exception {
        Class<?> c = getTestClass().getJavaClass();
        Set<Field> testFields = getFields(c);

        // make sure we have one (and only one) @Unit field
        // Field unitField = getUnitField(testFields);
        // if ( unitField.getAnnotation(Mock.class) != null ) {
        // throw new IncompatibleAnnotationException(Unit.class, Mock.class);
        // }
        //
        final Map<Field, Binding> fieldValues = getMockValues(testFields);
        // if ( fieldValues.containsKey(unitField)) {
        // throw new IncompatibleAnnotationException(Unit.class, unitField.getType());
        // }

        Object test = createTest(c, fieldValues);

        // any field values created by AtUnit but not injected by the container are injected here.
        for (Field field : fieldValues.keySet()) {
            Binding binding = fieldValues.get(field);
            field.setAccessible(true);
            if (null != binding.getValue() && field.get(test) == null) {
                field.set(test, binding.getValue());
            }
        }

        return test;
    }

    private Object createTest(Class<?> testClass, Map<Field, Binding> fieldValues) throws Exception {
        FieldModule fields = new FieldModule(fieldValues);

        Injector injector;
        Object test = super.createTest();
        if (Module.class.isAssignableFrom(testClass)) {
            injector = Guice.createInjector(fields, (Module) testClass.newInstance());
        } else {
            injector = Guice.createInjector(fields);
        }
        injector.injectMembers(test);
        return test;
    }

    protected static final class FieldModule extends AbstractModule implements MockController {
        final Map<Field, Binding> fields;

        public FieldModule(Map<Field, Binding> fields) {
            this.fields = fields;
        }

        @Override
        @SuppressWarnings("unchecked")
        protected void configure() {
            // Bind mock controllet to this instance, to automatically replay/verify all mocks created by runner.
            bind(MockController.class).toInstance(this);
            // map field values by type
            for (Field field : fields.keySet()) {
                TypeLiteral literal = TypeLiteral.get(field.getGenericType());
                AnnotatedBindingBuilder builder = bind(literal);
                // Check field annotations.
                Annotation[] fieldAnnotations = field.getAnnotations();
                for (Annotation annotation : fieldAnnotations) {
                    Class<? extends Annotation> annotationType = annotation.annotationType();
                    if (/* annotationType.isAnnotationPresent(Qualifier.class)|| */annotationType
                            .isAnnotationPresent(BindingAnnotation.class)) {
                        builder.annotatedWith(annotation);
                    }
                    if (annotationType.isAnnotationPresent(ScopeAnnotation.class)) {
                        builder.in(annotationType);
                    }
                }
                Binding binding = fields.get(field);
                if (null != binding.getValue()) {
                    builder.toInstance(binding.getValue());
                } else if (null != binding.getImplementation()) {
                    builder.to(binding.getImplementation());
                } else if (null != binding.getProvider()) {
                    builder.toProvider(binding.getProvider());
                }
            }
        }

        @Override
        public void replay() {
            for (Binding field : fields.values()) {
                if (null != field.getValue()) {
                    EasyMock.replay(field.getValue());
                }
            }
        }

        @Override
        public void verify() {
            for (Binding field : fields.values()) {
                if (null != field.getValue()) {
                    EasyMock.verify(field.getValue());
                }
            }
        }
    }

    /**
     * <p class="changed_added_4_0">
     * Binding definition storage
     * </p>
     *
     * @author asmirnov@exadel.com
     *
     */
    protected static final class Binding {
        private Object value;
        private Class<?> implementation;
        private Class<? extends Provider<?>> provider;

        protected Binding() {
        }

        /**
         * <p class="changed_added_4_0">
         * </p>
         *
         * @param value the value to set
         */
        void setValue(Object value) {
            this.value = value;
        }

        /**
         * <p class="changed_added_4_0">
         * </p>
         *
         * @return the value
         */
        Object getValue() {
            return value;
        }

        /**
         * <p class="changed_added_4_0">
         * </p>
         *
         * @param implementation the implementation to set
         */
        void setImplementation(Class<?> implementation) {
            this.implementation = implementation;
        }

        /**
         * <p class="changed_added_4_0">
         * </p>
         *
         * @return the implementation
         */
        Class<?> getImplementation() {
            return implementation;
        }

        /**
         * <p class="changed_added_4_0">
         * </p>
         *
         * @param provider the provider to set
         */
        void setProvider(Class<? extends Provider<?>> provider) {
            this.provider = provider;
        }

        /**
         * <p class="changed_added_4_0">
         * </p>
         *
         * @return the provider
         */
        Class<? extends Provider<?>> getProvider() {
            return provider;
        }
    }

    private Map<Field, Binding> getMockValues(Set<Field> testFields) {
        Map<Field, Binding> mocksAndStubs = new HashMap<Field, Binding>();
        // TODO - create annotation attribute that tells runner to use the scme Mock Controller to create related mocks.
        for (Field field : testFields) {
            if (field.getAnnotation(Mock.class) != null) {
                Binding bind = new Binding();
                bind.setValue(EasyMock.createStrictMock(field.getType()));
                mocksAndStubs.put(field, bind);
            } else if (field.getAnnotation(Stub.class) != null) {
                Binding bind = new Binding();
                bind.setValue(EasyMock.createNiceMock(field.getType()));
                mocksAndStubs.put(field, bind);
            } else if (null != field.getAnnotation(As.class)) {
                Binding bind = new Binding();
                bind.setImplementation(field.getAnnotation(As.class).value());
                mocksAndStubs.put(field, bind);
            } else if (null != field.getAnnotation(AsProvider.class)) {
                Binding bind = new Binding();
                bind.setProvider(field.getAnnotation(AsProvider.class).value());
                mocksAndStubs.put(field, bind);
            }
        }

        return mocksAndStubs;
    }
}
