/*
 *   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.powermock.api.easymock.EasyMockConfiguration;
import org.powermock.api.easymock.annotation.Mock;
import org.powermock.api.easymock.annotation.MockNice;
import org.powermock.api.easymock.annotation.MockStrict;
import org.powermock.reflect.Whitebox;

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

/**
 * This class works like as {@link org.easymock.EasyMockSupport} and is used to create and inject mocks to
 * annotated fields of an instance of test class.
 * @see Mock
 * @see org.easymock.Mock
 * @see org.easymock.TestSubject
 */
@SuppressWarnings("WeakerAccess")
public class EasyMockAnnotationSupport {

    private final Object testInstance;
    private final AnnotationMockCreatorFactory annotationMockCreatorFactory;
    private final AnnotationGlobalMetadata globalMetadata;
    private final EasyMockConfiguration easyMockConfiguration;

    public EasyMockAnnotationSupport(Object testInstance) {
        this.testInstance = testInstance;
        this.annotationMockCreatorFactory = new AnnotationMockCreatorFactory();
        this.globalMetadata = new AnnotationGlobalMetadata();
        this.easyMockConfiguration = EasyMockConfiguration.getConfiguration();
    }

    public void injectMocks() throws Exception {
        injectStrictMocks();
        injectNiceMocks();
        injectDefaultMocks();
        injectTestSubjectMocks();
    }

    protected void injectStrictMocks() throws Exception {
        inject(testInstance, MockStrict.class, annotationMockCreatorFactory.createStrictMockCreator());
    }

    protected void injectNiceMocks() throws Exception {
        inject(testInstance, MockNice.class, annotationMockCreatorFactory.createNiceMockCreator());
    }

    @SuppressWarnings("deprecation")
    protected void injectDefaultMocks() throws Exception {
        inject(testInstance, Mock.class, annotationMockCreatorFactory.createDefaultMockCreator());
        inject(testInstance, org.powermock.core.classloader.annotations.Mock.class, annotationMockCreatorFactory.createDefaultMockCreator());
    }

    @SuppressWarnings("unchecked")
    protected void injectTestSubjectMocks() throws IllegalAccessException {
        if (easyMockConfiguration.isTestSubjectSupported()) {
            TestSubjectInjector testSubjectInjector = new TestSubjectInjector(testInstance, globalMetadata);
            testSubjectInjector.injectTestSubjectMocks();
        }
    }


    protected void inject(Object injectCandidateInstance, Class<? extends Annotation> annotation, AnnotationMockCreator mockCreator) throws Exception {

        AnnotationMockScanner scanner = new AnnotationMockScanner(annotation);

        List<AnnotationMockMetadata> mocksMetadata = scanner.scan(injectCandidateInstance);
        globalMetadata.add(mocksMetadata);

        for (AnnotationMockMetadata mockMetadata : mocksMetadata) {
            injectMock(injectCandidateInstance, mockMetadata, mockCreator, new AnnotationInjectFieldSearcher());
        }


    }

    protected void injectMock(Object injectCandidateInstance, AnnotationMockMetadata mockMetadata,
                              AnnotationMockCreator mockCreator, InjectFieldSearcher fieldSearch) throws IllegalAccessException {
        Object mock = createMock(mockCreator, mockMetadata);
        Field field = fieldSearch.findField(injectCandidateInstance, mockMetadata);
        if (field != null && mock != null) {
            field.setAccessible(true);
            field.set(injectCandidateInstance, mock);
        }
    }


    protected Object createMock(AnnotationMockCreator mockCreator, AnnotationMockMetadata mockMetadata) {
        if (mockMetadata.getMock() == null) {
            Object mock = mockCreator.createMockInstance(mockMetadata.getType(), mockMetadata.getMethods());
            mockMetadata.setMock(mock);
        }
        return mockMetadata.getMock();
    }


    protected interface InjectFieldSearcher {
        Field findField(Object instance, AnnotationMockMetadata mockMetadata);
    }


    @SuppressWarnings("unchecked")
    protected static class AnnotationInjectFieldSearcher implements InjectFieldSearcher {

        @Override
        public Field findField(Object instance, AnnotationMockMetadata mockMetadata) {
            Set<Field> candidates = Whitebox.getFieldsAnnotatedWith(instance, mockMetadata.getAnnotation());
            if (candidates.size() == 1) {
                return candidates.iterator().next();
            }
            return null;
        }
    }
    
}