/*
 * Copyright 2002-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.springframework.test.context.junit.jupiter.support;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Optional;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.context.ApplicationContext;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;

import static org.springframework.core.annotation.AnnotatedElementUtils.hasAnnotation;

/**
 * Collection of utilities related to autowiring of individual method parameters.
 *
 * @author Sam Brannen
 * @since 5.0
 * @see MethodParameterFactory
 * @see #isAutowirable(Parameter)
 * @see #resolveDependency(Parameter, Class, ApplicationContext)
 */
public abstract class ParameterAutowireUtils {

	private ParameterAutowireUtils() {
		/* no-op */
	}

	/**
	 * Determine if the supplied {@link Parameter} can potentially be
	 * autowired from an {@link ApplicationContext}.
	 * <p>Returns {@code true} if the supplied parameter is of type
	 * {@link ApplicationContext} (or a sub-type thereof) or is annotated or
	 * meta-annotated with {@link Autowired @Autowired},
	 * {@link Qualifier @Qualifier}, or {@link Value @Value}.
	 * @see #resolveDependency(Parameter, Class, ApplicationContext)
	 */
	public static boolean isAutowirable(Parameter parameter) {
		return ApplicationContext.class.isAssignableFrom(parameter.getType())
				|| hasAnnotation(parameter, Autowired.class)
				|| hasAnnotation(parameter, Qualifier.class)
				|| hasAnnotation(parameter, Value.class);
	}

	/**
	 * Resolve the dependency for the supplied {@link Parameter} from the
	 * supplied {@link ApplicationContext}.
	 * <p>Provides comprehensive autowiring support for individual method parameters
	 * on par with Spring's dependency injection facilities for autowired fields and
	 * methods, including support for {@link Autowired @Autowired},
	 * {@link Qualifier @Qualifier}, and {@link Value @Value} with support for property
	 * placeholders and SpEL expressions in {@code @Value} declarations.
	 * <p>The dependency is required unless the parameter is annotated with
	 * {@link Autowired @Autowired} with the {@link Autowired#required required}
	 * flag set to {@code false}.
	 * <p>If an explicit <em>qualifier</em> is not declared, the name of the parameter
	 * will be used as the qualifier for resolving ambiguities.
	 * @param parameter the parameter whose dependency should be resolved
	 * @param containingClass the concrete class that contains the parameter; this may
	 * differ from the class that declares the parameter in that it may be a subclass
	 * thereof, potentially substituting type variables
	 * @param applicationContext the application context from which to resolve the
	 * dependency
	 * @return the resolved object, or {@code null} if none found
	 * @throws BeansException if dependency resolution failed
	 * @see #isAutowirable(Parameter)
	 * @see Autowired#required
	 * @see MethodParameterFactory#createSynthesizingMethodParameter(Parameter)
	 * @see AutowireCapableBeanFactory#resolveDependency(DependencyDescriptor, String)
	 */
	public static Object resolveDependency(Parameter parameter, Class<?> containingClass,
			ApplicationContext applicationContext) {

		boolean required = findMergedAnnotation(parameter, Autowired.class).map(Autowired::required).orElse(true);
		MethodParameter methodParameter = (parameter.getDeclaringExecutable() instanceof Method
				? MethodParameterFactory.createSynthesizingMethodParameter(parameter)
				: MethodParameterFactory.createMethodParameter(parameter));
		DependencyDescriptor descriptor = new DependencyDescriptor(methodParameter, required);
		descriptor.setContainingClass(containingClass);

		return applicationContext.getAutowireCapableBeanFactory().resolveDependency(descriptor, null);
	}

	private static <A extends Annotation> Optional<A> findMergedAnnotation(AnnotatedElement element,
			Class<A> annotationType) {

		return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(element, annotationType));
	}

}
