/*
 * Copyright 2012-2017 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.boot.autoconfigure.diagnostics.analyzer;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.junit.Test;

import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.util.ClassUtils;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Tests for {@link NoSuchBeanDefinitionFailureAnalyzer}.
 *
 * @author Stephane Nicoll
 */
public class NoSuchBeanDefinitionFailureAnalyzerTests {

	private final NoSuchBeanDefinitionFailureAnalyzer analyzer = new NoSuchBeanDefinitionFailureAnalyzer();

	@Test
	public void failureAnalysisForMultipleBeans() {
		FailureAnalysis analysis = analyzeFailure(
				new NoUniqueBeanDefinitionException(String.class, 2, "Test"));
		assertThat(analysis).isNull();
	}

	@Test
	public void failureAnalysisForNoMatchType() {
		FailureAnalysis analysis = analyzeFailure(createFailure(StringHandler.class));
		assertDescriptionConstructorMissingType(analysis, StringHandler.class, 0,
				String.class);
		assertThat(analysis.getDescription()).doesNotContain(
				"No matching auto-configuration has been found for this type.");
		assertThat(analysis.getAction()).startsWith(String.format(
				"Consider defining a bean of type '%s' in your configuration.",
				String.class.getName()));
	}

	@Test
	public void failureAnalysisForMissingPropertyExactType() {
		FailureAnalysis analysis = analyzeFailure(
				createFailure(StringPropertyTypeConfiguration.class));
		assertDescriptionConstructorMissingType(analysis, StringHandler.class, 0,
				String.class);
		assertBeanMethodDisabled(analysis,
				"did not find property 'spring.string.enabled'",
				TestPropertyAutoConfiguration.class, "string");
		assertActionMissingType(analysis, String.class);
	}

	@Test
	public void failureAnalysisForMissingCollectionType() throws Exception {
		FailureAnalysis analysis = analyzeFailure(
				createFailure(StringCollectionConfiguration.class));
		assertDescriptionConstructorMissingType(analysis, StringCollectionHandler.class,
				0, String.class);
		assertBeanMethodDisabled(analysis,
				"did not find property 'spring.string.enabled'",
				TestPropertyAutoConfiguration.class, "string");
		assertActionMissingType(analysis, String.class);
	}

	@Test
	public void failureAnalysisForMissingMapType() throws Exception {
		FailureAnalysis analysis = analyzeFailure(
				createFailure(StringMapConfiguration.class));
		assertDescriptionConstructorMissingType(analysis, StringMapHandler.class, 0,
				String.class);
		assertBeanMethodDisabled(analysis,
				"did not find property 'spring.string.enabled'",
				TestPropertyAutoConfiguration.class, "string");
		assertActionMissingType(analysis, String.class);
	}

	@Test
	public void failureAnalysisForMissingPropertySubType() {
		FailureAnalysis analysis = analyzeFailure(
				createFailure(IntegerPropertyTypeConfiguration.class));
		assertThat(analysis).isNotNull();
		assertDescriptionConstructorMissingType(analysis, NumberHandler.class, 0,
				Number.class);
		assertBeanMethodDisabled(analysis,
				"did not find property 'spring.integer.enabled'",
				TestPropertyAutoConfiguration.class, "integer");
		assertActionMissingType(analysis, Number.class);
	}

	@Test
	public void failureAnalysisForMissingClassOnAutoConfigurationType() {
		FailureAnalysis analysis = analyzeFailure(
				createFailure(MissingClassOnAutoConfigurationConfiguration.class));
		assertDescriptionConstructorMissingType(analysis, StringHandler.class, 0,
				String.class);
		assertClassDisabled(analysis, "did not find required class 'com.example.FooBar'",
				"string");
		assertActionMissingType(analysis, String.class);
	}

	@Test
	public void failureAnalysisForExcludedAutoConfigurationType() {
		FatalBeanException failure = createFailure(StringHandler.class);
		addExclusions(this.analyzer, TestPropertyAutoConfiguration.class);
		FailureAnalysis analysis = analyzeFailure(failure);
		assertDescriptionConstructorMissingType(analysis, StringHandler.class, 0,
				String.class);
		String configClass = ClassUtils
				.getShortName(TestPropertyAutoConfiguration.class.getName());
		assertClassDisabled(analysis,
				String.format("auto-configuration '%s' was excluded", configClass),
				"string");
		assertActionMissingType(analysis, String.class);
	}

	@Test
	public void failureAnalysisForSeveralConditionsType() {
		FailureAnalysis analysis = analyzeFailure(
				createFailure(SeveralAutoConfigurationTypeConfiguration.class));
		assertDescriptionConstructorMissingType(analysis, StringHandler.class, 0,
				String.class);
		assertBeanMethodDisabled(analysis,
				"did not find property 'spring.string.enabled'",
				TestPropertyAutoConfiguration.class, "string");
		assertClassDisabled(analysis, "did not find required class 'com.example.FooBar'",
				"string");
		assertActionMissingType(analysis, String.class);
	}

	@Test
	public void failureAnalysisForNoMatchName() {
		FailureAnalysis analysis = analyzeFailure(createFailure(StringNameHandler.class));
		assertThat(analysis.getDescription()).startsWith(String.format(
				"Constructor in %s required a bean named '%s' that could not be found",
				StringNameHandler.class.getName(), "test-string"));
		assertThat(analysis.getDescription().contains(
				"No matching auto-configuration has been found for this bean name."));
		assertThat(analysis.getAction()).startsWith(String.format(
				"Consider defining a bean named '%s' in your configuration.",
				"test-string"));
	}

	@Test
	public void failureAnalysisForMissingBeanName() {
		FailureAnalysis analysis = analyzeFailure(
				createFailure(StringMissingBeanNameConfiguration.class));
		assertThat(analysis.getDescription()).startsWith(String.format(
				"Constructor in %s required a bean named '%s' that could not be found",
				StringNameHandler.class.getName(), "test-string"));
		assertBeanMethodDisabled(analysis,
				"@ConditionalOnBean (types: java.lang.Integer; SearchStrategy: all) did not find any beans",
				TestMissingBeanAutoConfiguration.class, "string");
		assertActionMissingName(analysis, "test-string");
	}

	private void assertDescriptionConstructorMissingType(FailureAnalysis analysis,
			Class<?> component, int index, Class<?> type) {
		String expected = String.format(
				"Parameter %s of constructor in %s required a bean of "
						+ "type '%s' that could not be found.",
				index, component.getName(), type.getName());
		assertThat(analysis.getDescription()).startsWith(expected);
	}

	private void assertActionMissingType(FailureAnalysis analysis, Class<?> type) {
		assertThat(analysis.getAction()).startsWith(String
				.format("Consider revisiting the conditions above or defining a bean of type '%s' "
						+ "in your configuration.", type.getName()));
	}

	private void assertActionMissingName(FailureAnalysis analysis, String name) {
		assertThat(analysis.getAction()).startsWith(String
				.format("Consider revisiting the conditions above or defining a bean named '%s' "
						+ "in your configuration.", name));
	}

	private void assertBeanMethodDisabled(FailureAnalysis analysis, String description,
			Class<?> target, String methodName) {
		String expected = String.format("Bean method '%s' in '%s' not loaded because",
				methodName, ClassUtils.getShortName(target));
		assertThat(analysis.getDescription()).contains(expected);
		assertThat(analysis.getDescription()).contains(description);
	}

	private void assertClassDisabled(FailureAnalysis analysis, String description,
			String methodName) {
		String expected = String.format("Bean method '%s' not loaded because",
				methodName);
		assertThat(analysis.getDescription()).contains(expected);
		assertThat(analysis.getDescription()).contains(description);
	}

	private static void addExclusions(NoSuchBeanDefinitionFailureAnalyzer analyzer,
			Class<?>... classes) {
		ConditionEvaluationReport report = (ConditionEvaluationReport) new DirectFieldAccessor(
				analyzer).getPropertyValue("report");
		List<String> exclusions = new ArrayList<>(report.getExclusions());
		for (Class<?> c : classes) {
			exclusions.add(c.getName());
		}
		report.recordExclusions(exclusions);
	}

	private FatalBeanException createFailure(Class<?> config, String... environment) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		this.analyzer.setBeanFactory(context.getBeanFactory());
		EnvironmentTestUtils.addEnvironment(context, environment);
		context.register(config);
		try {
			context.refresh();
			return null;
		}
		catch (FatalBeanException ex) {
			return ex;
		}
		finally {
			context.close();
		}
	}

	private FailureAnalysis analyzeFailure(Exception failure) {
		FailureAnalysis analysis = this.analyzer.analyze(failure);
		if (analysis != null) {
			new LoggingFailureAnalysisReporter().report(analysis);
		}
		return analysis;
	}

	@Configuration
	@ImportAutoConfiguration(TestPropertyAutoConfiguration.class)
	@Import(StringHandler.class)
	protected static class StringPropertyTypeConfiguration {

	}

	@Configuration
	@ImportAutoConfiguration(TestPropertyAutoConfiguration.class)
	@Import(StringCollectionHandler.class)
	protected static class StringCollectionConfiguration {

	}

	@Configuration
	@ImportAutoConfiguration(TestPropertyAutoConfiguration.class)
	@Import(StringMapHandler.class)
	protected static class StringMapConfiguration {

	}

	@Configuration
	@ImportAutoConfiguration(TestPropertyAutoConfiguration.class)
	@Import(NumberHandler.class)
	protected static class IntegerPropertyTypeConfiguration {

	}

	@Configuration
	@ImportAutoConfiguration(TestTypeClassAutoConfiguration.class)
	@Import(StringHandler.class)
	protected static class MissingClassOnAutoConfigurationConfiguration {

	}

	@Configuration
	@ImportAutoConfiguration({ TestPropertyAutoConfiguration.class,
			TestTypeClassAutoConfiguration.class })
	@Import(StringHandler.class)
	protected static class SeveralAutoConfigurationTypeConfiguration {

	}

	@Configuration
	@ImportAutoConfiguration(TestMissingBeanAutoConfiguration.class)
	@Import(StringNameHandler.class)
	protected static class StringMissingBeanNameConfiguration {

	}

	@Configuration
	public static class TestPropertyAutoConfiguration {

		@ConditionalOnProperty("spring.string.enabled")
		@Bean
		public String string() {
			return "Test";
		}

		@ConditionalOnProperty("spring.integer.enabled")
		@Bean
		public Integer integer() {
			return 42;
		}

	}

	@Configuration
	@ConditionalOnClass(name = "com.example.FooBar")
	public static class TestTypeClassAutoConfiguration {

		@Bean
		public String string() {
			return "Test";
		}

	}

	@Configuration
	public static class TestMissingBeanAutoConfiguration {

		@ConditionalOnBean(Integer.class)
		@Bean(name = "test-string")
		public String string() {
			return "Test";
		}

	}

	protected static class StringHandler {

		public StringHandler(String foo) {
		}

	}

	protected static class NumberHandler {

		public NumberHandler(Number foo) {
		}

	}

	protected static class StringNameHandler {

		public StringNameHandler(BeanFactory beanFactory) {
			beanFactory.getBean("test-string");
		}

	}

	protected static class StringCollectionHandler {

		public StringCollectionHandler(Collection<String> collection) {
		}

	}

	protected static class StringMapHandler {

		public StringMapHandler(Map<String, String> map) {
		}

	}

}
