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

import org.easymock.Mock;
import org.easymock.TestSubject;
import org.powermock.reflect.Whitebox;

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

/**
 * The class injects mocks created with {@link Mock}, {@link org.powermock.api.easymock.annotation.Mock}
 * and {@link org.powermock.core.classloader.annotations.Mock} to fields of objects which is annotated with {@link TestSubject}
 * @see TestSubject
 * @since 1.6.5
 */
@SuppressWarnings({"deprecation", "WeakerAccess"})
class TestSubjectInjector {

    private final Object testInstance;
    private final AnnotationGlobalMetadata globalMetadata;

    public TestSubjectInjector(Object testInstance, AnnotationGlobalMetadata globalMetadata) {

        this.testInstance = testInstance;
        this.globalMetadata = globalMetadata;
    }

    @SuppressWarnings("unchecked")
    protected void injectTestSubjectMocks() throws IllegalAccessException {

        Set<Field> testSubjectFields = Whitebox.getFieldsAnnotatedWith(testInstance, TestSubject.class);
        for (Field testSubjectField : testSubjectFields) {
            Object testSubject = testSubjectField.get(testInstance);
            if (testSubject == null) {
                throw new NullPointerException("Have you forgotten to instantiate " + testSubjectField.getName() + "?");
            }
            injectTestSubjectFields(testSubject);

        }
    }

    protected void injectTestSubjectFields(Object testSubject) throws IllegalAccessException {
        Set<Field> targetFields = new HashSet<Field>(Whitebox.getAllInstanceFields(testSubject));
        targetFields = injectByName(targetFields, testSubject);
        injectByType(targetFields, testSubject);
    }

    void injectByType(Set<Field> targetFields, Object testSubject) throws IllegalAccessException {
        for (Field targetField : targetFields) {

            InjectionTarget target = new InjectionTarget(targetField);

            AnnotationMockMetadata toAssign = findUniqueAssignable(target);

            if (toAssign == null) {
                continue;
            }

            target.inject(testSubject, toAssign);
        }
    }

    AnnotationMockMetadata findUniqueAssignable(InjectionTarget target) {
        AnnotationMockMetadata toAssign = null;
        for (AnnotationMockMetadata mockMetadata : globalMetadata.getUnqualifiedInjections()) {
            if (target.accepts(mockMetadata)) {
                if (toAssign != null) {
                    throw new RuntimeException(String.format("At least two mocks can be assigned to '%s': %s and %s", target.getField(), toAssign.getMock(), mockMetadata.getMock()));
                }
                toAssign = mockMetadata;
            }
        }
        return toAssign;
    }

    Set<Field> injectByName(Set<Field> targetFields, Object targetObject) throws IllegalAccessException {
        Class<?> targetClass = targetObject.getClass();

        for (AnnotationMockMetadata mockMetadata : globalMetadata.getQualifiedInjections()) {
            Field targetField = getFieldByName(targetClass, mockMetadata.getQualifier());

            if (targetField == null) {
                continue;
            }

            InjectionTarget target = new InjectionTarget(targetField);

            if (target.accepts(mockMetadata)) {
                target.inject(targetObject, mockMetadata);
                targetFields.remove(targetField);
            }
        }

        return targetFields;
    }

    private Field getFieldByName(Class<?> clazz, String fieldName) {
        try {
            return clazz.getDeclaredField(fieldName);
        } catch (NoSuchFieldException e) {
            return null;
        } catch (SecurityException e) {
            return null;
        }
    }

    private static class InjectionTarget {

        private final Field field;

        public InjectionTarget(Field field) {

            this.field = field;
        }

        public Field getField() {
            return field;
        }

        public boolean accepts(AnnotationMockMetadata mockMetadata) {
            return field.getType().isAssignableFrom(mockMetadata.getType());
        }

        public void inject(Object targetObject, AnnotationMockMetadata mockMetadata) throws IllegalAccessException {
            field.setAccessible(true);

            Object value = field.get(targetObject);

            if (value == null) {
                field.set(targetObject, mockMetadata.getMock());
            }
        }

    }
}