/*
 * Copyright 2008 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.api.extension.listener;

import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.MockSettings;
import org.mockito.MockitoAnnotations.Mock;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.internal.configuration.InjectingAnnotationEngine;
import org.mockito.internal.util.reflection.GenericMaster;
import org.powermock.api.mockito.internal.configuration.PowerMockitoInjectingAnnotationEngine;
import org.powermock.core.spi.listener.AnnotationEnablerListener;
import org.powermock.core.spi.support.AbstractPowerMockTestListenerBase;
import org.powermock.reflect.Whitebox;

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

import static org.mockito.Mockito.withSettings;
import static org.powermock.api.mockito.PowerMockito.mock;

/**
 * Before each test method all fields annotated with {@link Mock},
 * {@link org.mockito.Mock} or {@link Mock} have mock objects created for them
 * and injected to the fields. It will also delegate to a special implementation
 * of the {@link InjectingAnnotationEngine} in Mockito which inject's spies,
 * captors etc.
 * <p/>
 * It will only inject to fields that haven't been set before (i.e that are
 * <code>null</code>).
 */
@SuppressWarnings("deprecation")
public class AnnotationEnabler extends AbstractPowerMockTestListenerBase implements AnnotationEnablerListener {

    @Override
    public void beforeTestMethod(Object testInstance, Method method, Object[] arguments) throws Exception {

        // Mockito uses context class loader not only to load resources, but to load plugin implementers, but for
        // other purpose default/application class loader is used. So PluginRepository and Plugin could be loaded by
        // different class loaders. Fix will be

        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());

            standardInject(testInstance);
            injectSpiesAndInjectToSetters(testInstance);
            injectCaptor(testInstance);
        } finally {
            Thread.currentThread().setContextClassLoader(contextClassLoader);
        }
    }

    private void injectSpiesAndInjectToSetters(Object testInstance) {
        new PowerMockitoInjectingAnnotationEngine().process(testInstance.getClass(), testInstance);
    }

    private void injectCaptor(Object testInstance) throws Exception {
        Set<Field> fieldsAnnotatedWithCaptor = Whitebox.getFieldsAnnotatedWith(testInstance, Captor.class);
        for (Field field : fieldsAnnotatedWithCaptor) {
            final Object captor = processAnnotationOn(field.getAnnotation(Captor.class), field);
            field.set(testInstance, captor);
        }
    }

    private void standardInject(Object testInstance) throws IllegalAccessException {
        Set<Field> fields = Whitebox.getFieldsAnnotatedWith(testInstance, getMockAnnotations());
        for (Field field : fields) {
            if (field.get(testInstance) != null) {
                continue;
            }
            final Class<?> type = field.getType();
            if (field.isAnnotationPresent(org.powermock.core.classloader.annotations.Mock.class)) {
                org.powermock.core.classloader.annotations.Mock annotation = field
                        .getAnnotation(org.powermock.core.classloader.annotations.Mock.class);
                final String[] value = annotation.value();
                if (value.length != 1 || !"".equals(value[0])) {
                    System.err
                            .println("PowerMockito deprecation: Use PowerMockito.spy(..) for partial mocking instead. A standard mock will be created instead.");
                }
            }

            if (field.isAnnotationPresent(org.mockito.Mock.class)) {
                org.mockito.Mock mockAnnotation = field.getAnnotation(org.mockito.Mock.class);
                MockSettings mockSettings = withSettings();
                Answers answers = mockAnnotation.answer();
                if (answers != null) {
                    mockSettings.defaultAnswer(answers.get());
                }

                Class<?>[] extraInterfaces = mockAnnotation.extraInterfaces();
                if (extraInterfaces != null && extraInterfaces.length > 0) {
                    mockSettings.extraInterfaces(extraInterfaces);
                }

                String name = mockAnnotation.name();
                if (name != null && name.length() > 0) {
                    mockSettings.name(name);
                }

                field.set(testInstance, mock(type, mockSettings));
            } else {
                field.set(testInstance, mock(type));
            }
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public Class<? extends Annotation>[] getMockAnnotations() {
        return new Class[]{org.mockito.Mock.class, Mock.class, org.powermock.core.classloader.annotations.Mock.class};
    }

    private Object processAnnotationOn(Captor annotation, Field field) {
        Class<?> type = field.getType();
        if (!ArgumentCaptor.class.isAssignableFrom(type)) {
            throw new MockitoException("@Captor field must be of the type ArgumentCaptor.\n" + "Field: '"
                    + field.getName() + "' has wrong type\n"
                    + "For info how to use @Captor annotations see examples in javadoc for MockitoAnnotations class.");
        }
        Class cls = new GenericMaster().getGenericType(field);
        return ArgumentCaptor.forClass(cls);
    }
}
