/*
 *   Copyright 2016 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.tests.utils.impl;

import org.powermock.core.classloader.annotations.PowerMockListener;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.core.reporter.MockingFrameworkReporterFactory;
import org.powermock.core.spi.PowerMockTestListener;
import org.powermock.reflect.Whitebox;
import org.powermock.tests.utils.RunnerTestSuiteChunker;
import org.powermock.tests.utils.TestChunk;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

/**
 * Abstract base class for test suite chunking, i.e. a suite is chunked into
 * several smaller pieces which are ran with different classloaders. A chunk is
 * defined by the {@link PrepareForTest} annotation and whichever test-method
 * annotation the actual implementation-class specifies by overriding the
 * method {@link #testMethodAnnotation()}. This to make sure that you
 * can byte-code manipulate classes in tests without impacting on other tests.
 * 
 */
public abstract class AbstractTestSuiteChunkerImpl<T> extends AbstractCommonTestSuiteChunkerImpl implements RunnerTestSuiteChunker {

    /*
     * The classes listed in this set has been chunked and its delegates has
     * been created.
     */
    protected final Set<Class<?>> delegatesCreatedForTheseClasses = new LinkedHashSet<Class<?>>();

    // A list of junit delegates.
    protected final List<T> delegates = new ArrayList<T>();

    protected volatile int testCount = NOT_INITIALIZED;

    protected AbstractTestSuiteChunkerImpl(Class<?> testClass) throws Exception {
        super(testClass);
    }

    protected AbstractTestSuiteChunkerImpl(Class<?>... testClasses) throws Exception {
        super(testClasses);
    }

    protected Object getPowerMockTestListenersLoadedByASpecificClassLoader(Class<?> clazz, ClassLoader classLoader) {
        try {
            int defaultListenerSize = DEFAULT_TEST_LISTENERS_SIZE;
            Class<?> annotationEnablerClass = null;
            try {
                annotationEnablerClass = Class.forName("org.powermock.api.extension.listener.AnnotationEnabler", false, classLoader);
            } catch (ClassNotFoundException e) {
                // Annotation enabler wasn't found in class path
                defaultListenerSize = 0;
            }

            registerProxyframework(classLoader);

            final Class<?> powerMockTestListenerType = Class.forName(PowerMockTestListener.class.getName(), false, classLoader);
            Object testListeners = null;
            if (clazz.isAnnotationPresent(PowerMockListener.class)) {
                PowerMockListener annotation = clazz.getAnnotation(PowerMockListener.class);
                final Class<? extends PowerMockTestListener>[] powerMockTestListeners = annotation.value();
                if (powerMockTestListeners.length > 0) {
                    testListeners = Array.newInstance(powerMockTestListenerType, powerMockTestListeners.length + defaultListenerSize);
                    for (int i = 0; i < powerMockTestListeners.length; i++) {
                        String testListenerClassName = powerMockTestListeners[i].getName();
                        final Class<?> listenerTypeLoadedByClassLoader = Class.forName(testListenerClassName, false, classLoader);
                        Array.set(testListeners, i, Whitebox.newInstance(listenerTypeLoadedByClassLoader));
                    }
                }
            } else {
                testListeners = Array.newInstance(powerMockTestListenerType, defaultListenerSize);
            }

            // Add default annotation enabler listener
            if (annotationEnablerClass != null) {
                Array.set(testListeners, Array.getLength(testListeners) - 1, Whitebox.newInstance(annotationEnablerClass));
            }

            return testListeners;
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException("PowerMock internal error: Failed to load class.", e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public final void createTestDelegators(Class<?> testClass, List<TestChunk> chunks) throws Exception {
        for (TestChunk chunk : chunks) {
            ClassLoader classLoader = chunk.getClassLoader();
            List<Method> methodsToTest = chunk.getTestMethodsToBeExecutedByThisClassloader();
            T runnerDelegator = createDelegatorFromClassloader(classLoader, testClass, methodsToTest);
            delegates.add(runnerDelegator);
        }
        delegatesCreatedForTheseClasses.add(testClass);
    }

    protected abstract T createDelegatorFromClassloader(ClassLoader classLoader, Class<?> testClass, final List<Method> methodsToTest)
            throws Exception;

    /**
     * Get the internal test index for a junit runner delegate based on the
     * "real" original test index. For example, the test may need to run a
     * single test, for example the test with index 3. However since PowerMock
     * may have chunked the test suite to use many classloaders and junit
     * delegators the index (3) must be mapped to an internal representation for
     * the specific junit runner delegate. This is what this method does. I.e.
     * it will iterate through all junit runner delegates and see if they
     * contain the test with index 3, in the internal index of this test
     * delegator is returned.
     * 
     * @param originalTestIndex
     *            The original test index as seen by the test runner.
     * @return The internal test index as seen by PowerMock or <code>-1</code>
     *         if no index was found.
     * 
     */
    public int getInternalTestIndex(int originalTestIndex) {
        Set<Entry<Integer, List<Integer>>> delegatorEntrySet = testAtDelegateMapper.entrySet();
        for (Entry<Integer, List<Integer>> entry : delegatorEntrySet) {
            final List<Integer> testIndexesForThisDelegate = entry.getValue();
            final int internalIndex = testIndexesForThisDelegate.indexOf(originalTestIndex);
            if (internalIndex != INTERNAL_INDEX_NOT_FOUND) {
                return internalIndex;
            }
        }
        return INTERNAL_INDEX_NOT_FOUND;
    }

    /**
     * Get the junit runner delegate that handles the test at index
     * <code>testIndex</code>. Throws a {@link RuntimeException} if a delegator
     * is not found for the specific test index.
     * 
     * @param testIndex
     *            The test index that a delegator should hold.
     * @return The index for of the junit runner delegate as seen by JTestRack.
     */
    public int getDelegatorIndex(int testIndex) {
        int delegatorIndex = -1;
        Set<Entry<Integer, List<Integer>>> entrySet = testAtDelegateMapper.entrySet();
        for (Entry<Integer, List<Integer>> entry : entrySet) {
            // If the delegator contains the test case, return the index of the
            // delegator.
            if (entry.getValue().contains(testIndex)) {
                delegatorIndex = entry.getKey();
                break;
            }
        }

        if (delegatorIndex == -1) {
            throw new RuntimeException("Internal error: Failed to find the delgator index.");
        }
        return delegatorIndex;
    }

    public Class<?>[] getTestClasses() {
        return testClasses;
    }


    @SuppressWarnings("unchecked")
    protected MockingFrameworkReporterFactory getFrameworkReporterFactory() {
        Class<MockingFrameworkReporterFactory> mockingFrameworkReporterFactoryClass;
        try {
            ClassLoader classLoader = this.getClass().getClassLoader();
            mockingFrameworkReporterFactoryClass = (Class<MockingFrameworkReporterFactory>) classLoader.loadClass("org.powermock.api.extension.reporter.MockingFrameworkReporterFactoryImpl");
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException(
                                                   "Extension API internal error: org.powermock.api.extension.reporter.MockingFrameworkReporterFactoryImpl could not be located in classpath.");
        }

        return Whitebox.newInstance(mockingFrameworkReporterFactoryClass);
    }

}
