/*
 * Copyright 2009 the original author or authors.
 *
 * Licensed 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.powermock.modules.junit4.internal.impl;

import org.junit.Rule;
import org.junit.internal.runners.InitializationError;
import org.junit.internal.runners.TestMethod;
import org.junit.rules.MethodRule;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;
import org.powermock.core.spi.PowerMockTestListener;
import org.powermock.reflect.Whitebox;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Set;

/**
 * Extends the functionality of {@link PowerMockJUnit44RunnerDelegateImpl} to
 * enable the usage of rules.
 */
@SuppressWarnings("deprecation")
public class PowerMockJUnit47RunnerDelegateImpl extends PowerMockJUnit44RunnerDelegateImpl {
    public boolean hasRules;

    public PowerMockJUnit47RunnerDelegateImpl(Class<?> klass, String[] methodsToRun, PowerMockTestListener[] listeners) throws InitializationError {
        super(klass, methodsToRun, listeners);
    }

    public PowerMockJUnit47RunnerDelegateImpl(Class<?> klass, String[] methodsToRun) throws InitializationError {
        super(klass, methodsToRun);
    }

    public PowerMockJUnit47RunnerDelegateImpl(Class<?> klass) throws InitializationError {
        super(klass);
    }

    @Override
    protected PowerMockJUnit44MethodRunner createPowerMockRunner(final Object testInstance, final TestMethod testMethod, RunNotifier notifier,
                                                                 Description description, final boolean extendsFromTestCase) {
        return new PowerMockJUnit47MethodRunner(testInstance, testMethod, notifier, description, extendsFromTestCase);
    }

    protected class PowerMockJUnit47MethodRunner extends PowerMockJUnit44MethodRunner {

        private Throwable potentialTestFailure;

        protected PowerMockJUnit47MethodRunner(Object testInstance, TestMethod method, RunNotifier notifier, Description description,
                                               boolean extendsFromTestCase) {
            super(testInstance, method, notifier, description, extendsFromTestCase);
        }

        @Override
        public void executeTest(final Method method, final Object testInstance, final Runnable test) {
            // We change the context classloader to the current CL in order for the Mockito
            // framework to load it's plugins (such as MockMaker) correctly.
            final ClassLoader originalCL = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
            final Set<Field> rules;
            try {
                rules = Whitebox.getFieldsAnnotatedWith( testInstance, Rule.class );
            } finally {
                Thread.currentThread().setContextClassLoader(originalCL);
            }
            hasRules = !rules.isEmpty();
            Statement statement = createStatement(method, testInstance, test, rules);
            evaluateStatement(statement);
        }

        private Statement createStatement(Method method, Object testInstance, Runnable test, Set<Field> rules) {
            Statement statement = new TestExecutorStatement(test, testInstance, method);
            for (Field field : rules) {
                try {
                    statement = applyRuleToLastStatement(method,
                            testInstance, field, statement);
                } catch (Throwable e) {
                    super.handleException(testMethod, e);
                }
            }
            return statement;
        }

        protected Statement applyRuleToLastStatement(final Method method, final Object testInstance, Field field,
                                                     final Statement lastStatement) throws IllegalAccessException {
            MethodRule rule = (MethodRule) field.get(testInstance);
            Statement statement = rule.apply(lastStatement, new FrameworkMethod(method), testInstance);
            return statement;
        }

        private void evaluateStatement(Statement statement) {
            try {
                statement.evaluate();
            } catch (Throwable e) {
                //No rule could handle the exception thus we need to add it as a failure.
                super.handleException(testMethod, potentialTestFailure == null ? e : potentialTestFailure);
            }
        }

        /**
         * Since a JUnit 4.7 rule may potentially deal with "unexpected"
         * exceptions we cannot handle the exception before the rule has been
         * completely evaluated. Thus we just store the exception here and
         * rethrow it after the test method has finished executing. In that way
         * the rule may get a chance to handle the exception appropriately.
         */
        @Override
        protected void handleException(TestMethod testMethod, Throwable actualFailure) {
            if (hasRules) {
                potentialTestFailure = actualFailure;
            } else {
                super.handleException(testMethod, actualFailure);
            }
        }

        private void executeTestInSuper(final Method method, final Object testInstance, final Runnable test) {
            super.executeTest(method, testInstance, test);
        }

        private final class TestExecutorStatement extends Statement {
            private final Runnable test;
            private final Object testInstance;
            private final Method method;

            private TestExecutorStatement(Runnable test, Object testInstance, Method method) {
                this.test = test;
                this.testInstance = testInstance;
                this.method = method;
            }

            @Override
            public void evaluate() throws Throwable {
                executeTestInSuper(method, testInstance, test);
                if (potentialTestFailure != null) {
                    // Rethrow the potential failure caught in the test.
                    throw potentialTestFailure;
                }
            }
        }
    }
}
