/*
 * Copyright 2011 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.tests.utils.TestClassesExtractor;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * Base class for all test class extractors.
 */
public abstract class AbstractTestClassExtractor implements TestClassesExtractor {

    protected final boolean includeMethods;

    protected AbstractTestClassExtractor(){
        this(false);
    }

    protected AbstractTestClassExtractor(boolean includeMethods) {this.includeMethods = includeMethods;}

    /**
     * If <code>element</code> is a class this method traverses the hierarchy
     * and extracts classes that should be prepared for test in all super
     * classes.
     */
    @Override
    public final String[] getTestClasses(AnnotatedElement element) {
        final Set<String> classesToPrepareForTest = new HashSet<String>();
        if (element instanceof Class<?>) {
            extractClassesFromTestClass((Class<?>) element, classesToPrepareForTest);
        } else {
            extractClassesAndAddThemToList(element, classesToPrepareForTest);
        }
        return classesToPrepareForTest.toArray(new String[classesToPrepareForTest.size()]);
    }

    private void extractClassesFromTestClass(final Class<?> element, Set<String> classesToPrepareForTest) {
        Class<?> classToInvestigate = element;
        while (classToInvestigate != null && !classToInvestigate.equals(Object.class)) {
            extractClassesAndAddThemToList(classToInvestigate, classesToPrepareForTest);
            if (includeMethods) {
                classesToPrepareForTest.addAll(lookOverMethods(classToInvestigate));
            }
            classToInvestigate = classToInvestigate.getSuperclass();
        }
    }

    private Collection<String> lookOverMethods(Class<?> classToInvestigate) {
        Set<String> classesToPrepareForTest = new HashSet<String>();
        for (Method method : classToInvestigate.getMethods()) {
            extractClassesAndAddThemToList(method, classesToPrepareForTest);
        }
        return classesToPrepareForTest;
    }

    private void extractClassesAndAddThemToList(AnnotatedElement elementToExtractClassFrom, final Set<String> classesToPrepareForTest) {
        final String[] classesToModify = getClassesToModify(elementToExtractClassFrom);
        if (classesToModify != null) {
            Collections.addAll(classesToPrepareForTest, classesToModify);
        }
    }

    /**
     * Get the fully qualified names for classes that must should be modified
     * for this <code>element</code>.
     *
     * @param element The element that may contain info regarding which classes that
     *                must be modified by PowerMock.
     * @return An array of fully-qualified names to classes that must be
     * modified by PowerMock for the specific <code>element</code>.
     */
    protected abstract String[] getClassesToModify(AnnotatedElement element);

    @Override
    public boolean isPrepared(AnnotatedElement element, String fullyQualifiedClassName) {
        if (fullyQualifiedClassName == null) {
            throw new IllegalArgumentException("fullyQualifiedClassName cannot be null.");
        }
        final String[] testClasses = getTestClasses(element);
        for (String className : testClasses) {
            if (className.equals(fullyQualifiedClassName)) {
                return true;
            }
        }
        return false;
    }
}
