/*
 * JBoss, Home of Professional Open Source
 * Copyright 2011, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.test.faces.mockito.runner;

import static org.jboss.test.faces.annotation.Environment.Feature.APPLICATION;
import static org.jboss.test.faces.annotation.Environment.Feature.EL_CONTEXT;
import static org.jboss.test.faces.annotation.Environment.Feature.EXTERNAL_CONTEXT;
import static org.jboss.test.faces.annotation.Environment.Feature.FACES_CONTEXT;
import static org.jboss.test.faces.annotation.Environment.Feature.FACTORIES;
import static org.jboss.test.faces.annotation.Environment.Feature.RENDER_KIT;
import static org.jboss.test.faces.annotation.Environment.Feature.RESPONSE_WRITER;
import static org.jboss.test.faces.annotation.Environment.Feature.SERVLET_REQUEST;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import javax.el.ELContext;
import javax.faces.application.Application;
import javax.faces.application.ApplicationFactory;
import javax.faces.application.ViewHandler;
import javax.faces.context.ExceptionHandlerFactory;
import javax.faces.context.ExternalContext;
import javax.faces.context.ExternalContextFactory;
import javax.faces.context.FacesContext;
import javax.faces.context.FacesContextFactory;
import javax.faces.context.PartialViewContextFactory;
import javax.faces.lifecycle.LifecycleFactory;
import javax.faces.render.RenderKit;
import javax.faces.render.RenderKitFactory;
import javax.faces.render.ResponseStateManager;
import javax.faces.view.facelets.TagHandlerDelegateFactory;
import javax.inject.Inject;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.jboss.test.faces.annotation.Environment;
import org.jboss.test.faces.mockito.MockFacesEnvironment;
import org.jboss.test.faces.writer.RecordingResponseWriter;
import org.junit.internal.runners.model.MultipleFailureException;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.mockito.MockitoAnnotations;
import org.mockito.internal.runners.util.FrameworkUsageValidator;
import org.mockito.runners.MockitoJUnitRunner;

/**
 * <p>
 * JUnit runner for running mocked JSF environment.
 * </p>
 * 
 * <p>
 * It initialized {@link MockFacesEnvironment} and is able to inject mocked JSF classes held by {@link MockFacesEnvironment} to
 * current test instance.
 * </p>
 * 
 * <p>
 * Similarly to {@link MockitoJUnitRunner}, it uses {@link MockitoAnnotations#initMocks(Object)} on test instance to mock all
 * the test dependencies annotated by Mockito annotations.
 * </p>
 * 
 * 
 * TODO cleanup of injections
 * 
 * TODO create tests for failures
 * 
 * @author <a href="mailto:lfryc@redhat.com">Lukas Fryc</a>
 */
public class FacesMockitoRunner extends BlockJUnit4ClassRunner {

    /** The mocked JSF environment. */
    private MockFacesEnvironment environment;

    /**
     * Instantiates a new faces mockito runner.
     * 
     * @param klass the class
     * @throws InitializationError the initialization error
     */
    public FacesMockitoRunner(Class<?> klass) throws InitializationError {
        super(klass);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.junit.runners.ParentRunner#run(org.junit.runner.notification.RunNotifier)
     */
    @Override
    public void run(RunNotifier notifier) {
        notifier.addListener(new FrameworkUsageValidator(notifier));
        super.run(notifier);
    }

    @Override
    protected Statement withBefores(final FrameworkMethod method, final Object target, final Statement originalStatement) {
        final Statement onlyBefores = super.withBefores(method, target, new EmptyStatement());
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                runBefore(target);
                onlyBefores.evaluate();
                originalStatement.evaluate();
            }
        };
    }

    @Override
    protected Statement withAfters(final FrameworkMethod method, final Object target, final Statement originalStatement) {
        final Statement onlyAfters = super.withAfters(method, target, new EmptyStatement());

        final Statement runnerAfters = new Statement() {
            @Override
            public void evaluate() throws Throwable {
                runAfter(target);
            }
        };

        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                multiExecute(originalStatement, onlyAfters, runnerAfters);
            }
        };
    }

    /**
     * Run before each test
     * 
     * @param target the target test instance
     */
    protected void runBefore(Object target) {
        MockitoAnnotations.initMocks(target);

        environment = new MockFacesEnvironment();
        processInjections(target);
        processFeatures(target);
    }

    /**
     * Run after each test
     * 
     * @param target the target test instance
     */
    protected void runAfter(Object target) {
        environment.release();
        environment = null;
    }

    /**
     * Process features.
     * 
     * @param target the target test instance
     */
    private void processFeatures(Object target) {
        Environment annotation = target.getClass().getAnnotation(Environment.class);
        Environment.Feature[] features = new Environment.Feature[0];
        if (annotation != null) {
            features = annotation.value();
        }

        for (Environment.Feature feature : features) {
            initializeFeature(feature);
        }
    }

    /**
     * Process injections.
     * 
     * @param target the target test instance
     */
    private void processInjections(Object target) {
        for (Class<?> clazz = target.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
            for (Field field : clazz.getDeclaredFields()) {
                if (field.getAnnotation(Inject.class) != null) {
                    Object injection = getInjection(field.getType());
                    if (injection != null) {
                        inject(field, target, injection);
                    }
                }
            }
        }
    }

    /**
     * Injects the given injection to field of target test instance.
     * 
     * @param field the field
     * @param target the target
     * @param injection the injection
     */
    private void inject(Field field, Object target, Object injection) {
        boolean accessible = field.isAccessible();
        if (!accessible) {
            field.setAccessible(true);
        }
        try {
            field.set(target, injection);
        } catch (Exception e) {
            throw new IllegalStateException("Cannot inject value to " + field, e);
        }
        if (!accessible) {
            field.setAccessible(false);
        }
    }

    /**
     * Initialize feature with currenct mocked environment
     * 
     * @param feature the feature
     */
    private void initializeFeature(Environment.Feature feature) {
        switch (feature) {
            case FACTORIES:
                environment.withFactories();
                break;
            case APPLICATION:
                environment.withApplication();
                break;
            case EL_CONTEXT:
                environment.withELContext();
                break;
            case EXTERNAL_CONTEXT:
                environment.withExternalContext();
                break;
            case RENDER_KIT:
                environment.withRenderKit();
                break;
            case RESPONSE_WRITER:
                environment.withResponseWriter();
                break;
            case SERVLET_REQUEST:
                environment.withServletRequest();
                break;
            default:
                break;
        }
    }

    /**
     * <p>
     * Tries to get mock for given type.
     * </p>
     * 
     * <p>
     * Before injecting, it initializes all dependencies needed by given mock type by initializing given feature.
     * </p>
     * 
     * @param type the JSF class type to initialized and inject
     * @return the injection or null if given type ais unknown
     */
    private Object getInjection(Class<?> type) {
        if (type == MockFacesEnvironment.class) {
            return environment;
        }
        if (type == FacesContext.class) {
            initializeFeature(FACES_CONTEXT);
            return environment.getFacesContext();
        }
        if (type == Application.class) {
            initializeFeature(APPLICATION);
            return environment.getApplication();
        }
        if (type == ELContext.class) {
            initializeFeature(EL_CONTEXT);
            return environment.getElContext();
        }
        if (type == ApplicationFactory.class) {
            initializeFeature(FACTORIES);
            return environment.getApplicationFactory();
        }
        if (type == ServletContext.class) {
            initializeFeature(SERVLET_REQUEST);
            return environment.getServletContext();
        }
        if (type == ExceptionHandlerFactory.class) {
            initializeFeature(FACTORIES);
            return environment.getExceptionHandlerFactory();
        }
        if (type == ExternalContext.class) {
            initializeFeature(EXTERNAL_CONTEXT);
            return environment.getExternalContext();
        }
        if (type == ExternalContextFactory.class) {
            initializeFeature(FACTORIES);
            return environment.getExternalContextFactory();
        }
        if (type == FacesContextFactory.class) {
            initializeFeature(FACTORIES);
            return environment.getFacesContextFactory();
        }
        if (type == LifecycleFactory.class) {
            initializeFeature(FACTORIES);
            return environment.getLifecycleFactory();
        }
        if (type == PartialViewContextFactory.class) {
            initializeFeature(FACTORIES);
            return environment.getPartialViewContextFactory();
        }
        if (type == RenderKit.class) {
            initializeFeature(RENDER_KIT);
            return environment.getRenderKit();
        }
        if (type == RenderKitFactory.class) {
            initializeFeature(FACTORIES);
            return environment.getRenderKitFactory();
        }
        if (type == HttpServletRequest.class) {
            initializeFeature(SERVLET_REQUEST);
            return environment.getRequest();
        }
        if (type == HttpServletResponse.class) {
            initializeFeature(SERVLET_REQUEST);
            return environment.getResponse();
        }
        if (type == ResponseStateManager.class) {
            initializeFeature(RENDER_KIT);
            return environment.getResponseStateManager();
        }
        if (RecordingResponseWriter.class.isAssignableFrom(type)) {
            initializeFeature(RESPONSE_WRITER);
            return environment.getResponseWriter();
        }
        if (type == TagHandlerDelegateFactory.class) {
            initializeFeature(FACTORIES);
            return environment.getTagHandlerDelegateFactory();
        }
        if (type == ViewHandler.class) {
            initializeFeature(APPLICATION);
            return environment.getViewHandler();
        }
        return null;
    }

    /**
     * A helper to safely execute multiple statements in one.<br/>
     * 
     * Will execute all statements even if they fail, all exceptions will be kept. If multiple {@link Statement}s fail, a
     * {@link MultipleFailureException} will be thrown.
     * 
     * @author <a href="mailto:aslak@redhat.com">Aslak Knutsen</a>
     * @version $Revision: $
     */
    private void multiExecute(Statement... statements) throws Throwable {
        List<Throwable> exceptions = new ArrayList<Throwable>();
        for (Statement command : statements) {
            try {
                command.evaluate();
            } catch (Exception e) {
                exceptions.add(e);
            }
        }
        if (exceptions.isEmpty()) {
            return;
        }
        if (exceptions.size() == 1) {
            throw exceptions.get(0);
        }
        throw new MultipleFailureException(exceptions);
    }

    private static class EmptyStatement extends Statement {
        @Override
        public void evaluate() throws Throwable {
        }
    }
}
