/*
 * 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;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.MockitoAnnotations;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.type.StandardAnnotationMetadata;
import org.springframework.mock.env.MockEnvironment;

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

/**
 * Tests for {@link AutoConfigurationImportSelector}
 *
 * @author Andy Wilkinson
 * @author Stephane Nicoll
 * @author Madhura Bhave
 */
public class AutoConfigurationImportSelectorTests {

	private final TestAutoConfigurationImportSelector importSelector = new TestAutoConfigurationImportSelector();

	private final ConfigurableListableBeanFactory beanFactory = new DefaultListableBeanFactory();

	private final MockEnvironment environment = new MockEnvironment();

	private List<AutoConfigurationImportFilter> filters = new ArrayList<>();

	@Rule
	public ExpectedException expected = ExpectedException.none();

	@Before
	public void setup() {
		MockitoAnnotations.initMocks(this);
		this.importSelector.setBeanFactory(this.beanFactory);
		this.importSelector.setEnvironment(this.environment);
		this.importSelector.setResourceLoader(new DefaultResourceLoader());
	}

	@Test
	public void importsAreSelectedWhenUsingEnableAutoConfiguration() {
		String[] imports = selectImports(BasicEnableAutoConfiguration.class);
		assertThat(imports).hasSameSizeAs(SpringFactoriesLoader.loadFactoryNames(
				EnableAutoConfiguration.class, getClass().getClassLoader()));
		assertThat(this.importSelector.getLastEvent().getExclusions()).isEmpty();
	}

	@Test
	public void classExclusionsAreApplied() {
		String[] imports = selectImports(
				EnableAutoConfigurationWithClassExclusions.class);
		assertThat(imports).hasSize(getAutoConfigurationClassNames().size() - 1);
		assertThat(this.importSelector.getLastEvent().getExclusions())
				.contains(FreeMarkerAutoConfiguration.class.getName());
	}

	@Test
	public void classExclusionsAreAppliedWhenUsingSpringBootApplication() {
		String[] imports = selectImports(SpringBootApplicationWithClassExclusions.class);
		assertThat(imports).hasSize(getAutoConfigurationClassNames().size() - 1);
		assertThat(this.importSelector.getLastEvent().getExclusions())
				.contains(FreeMarkerAutoConfiguration.class.getName());
	}

	@Test
	public void classNamesExclusionsAreApplied() {
		String[] imports = selectImports(
				EnableAutoConfigurationWithClassNameExclusions.class);
		assertThat(imports).hasSize(getAutoConfigurationClassNames().size() - 1);
		assertThat(this.importSelector.getLastEvent().getExclusions())
				.contains(MustacheAutoConfiguration.class.getName());
	}

	@Test
	public void classNamesExclusionsAreAppliedWhenUsingSpringBootApplication() {
		String[] imports = selectImports(
				SpringBootApplicationWithClassNameExclusions.class);
		assertThat(imports).hasSize(getAutoConfigurationClassNames().size() - 1);
		assertThat(this.importSelector.getLastEvent().getExclusions())
				.contains(MustacheAutoConfiguration.class.getName());
	}

	@Test
	public void propertyExclusionsAreApplied() {
		this.environment.setProperty("spring.autoconfigure.exclude",
				FreeMarkerAutoConfiguration.class.getName());
		String[] imports = selectImports(BasicEnableAutoConfiguration.class);
		assertThat(imports).hasSize(getAutoConfigurationClassNames().size() - 1);
		assertThat(this.importSelector.getLastEvent().getExclusions())
				.contains(FreeMarkerAutoConfiguration.class.getName());
	}

	@Test
	public void severalPropertyExclusionsAreApplied() {
		this.environment.setProperty("spring.autoconfigure.exclude",
				FreeMarkerAutoConfiguration.class.getName() + ","
						+ MustacheAutoConfiguration.class.getName());
		testSeveralPropertyExclusionsAreApplied();
	}

	@Test
	public void severalPropertyExclusionsAreAppliedWithExtraSpaces() {
		this.environment.setProperty("spring.autoconfigure.exclude",
				FreeMarkerAutoConfiguration.class.getName() + " , "
						+ MustacheAutoConfiguration.class.getName() + " ");
		testSeveralPropertyExclusionsAreApplied();
	}

	@Test
	public void severalPropertyYamlExclusionsAreApplied() {
		this.environment.setProperty("spring.autoconfigure.exclude[0]",
				FreeMarkerAutoConfiguration.class.getName());
		this.environment.setProperty("spring.autoconfigure.exclude[1]",
				MustacheAutoConfiguration.class.getName());
		testSeveralPropertyExclusionsAreApplied();
	}

	private void testSeveralPropertyExclusionsAreApplied() {
		String[] imports = selectImports(BasicEnableAutoConfiguration.class);
		assertThat(imports).hasSize(getAutoConfigurationClassNames().size() - 2);
		assertThat(this.importSelector.getLastEvent().getExclusions()).contains(
				FreeMarkerAutoConfiguration.class.getName(),
				MustacheAutoConfiguration.class.getName());
	}

	@Test
	public void combinedExclusionsAreApplied() {
		this.environment.setProperty("spring.autoconfigure.exclude",
				ThymeleafAutoConfiguration.class.getName());
		String[] imports = selectImports(
				EnableAutoConfigurationWithClassAndClassNameExclusions.class);
		assertThat(imports).hasSize(getAutoConfigurationClassNames().size() - 3);
		assertThat(this.importSelector.getLastEvent().getExclusions()).contains(
				FreeMarkerAutoConfiguration.class.getName(),
				MustacheAutoConfiguration.class.getName(),
				ThymeleafAutoConfiguration.class.getName());
	}

	@Test
	public void nonAutoConfigurationClassExclusionsShouldThrowException()
			throws Exception {
		this.expected.expect(IllegalStateException.class);
		selectImports(EnableAutoConfigurationWithFaultyClassExclude.class);
	}

	@Test
	public void nonAutoConfigurationClassNameExclusionsWhenPresentOnClassPathShouldThrowException()
			throws Exception {
		this.expected.expect(IllegalStateException.class);
		selectImports(EnableAutoConfigurationWithFaultyClassNameExclude.class);
	}

	@Test
	public void nonAutoConfigurationPropertyExclusionsWhenPresentOnClassPathShouldThrowException()
			throws Exception {
		this.environment.setProperty("spring.autoconfigure.exclude",
				"org.springframework.boot.autoconfigure."
						+ "AutoConfigurationImportSelectorTests.TestConfiguration");
		this.expected.expect(IllegalStateException.class);
		selectImports(BasicEnableAutoConfiguration.class);
	}

	@Test
	public void nameAndPropertyExclusionsWhenNotPresentOnClasspathShouldNotThrowException()
			throws Exception {
		this.environment.setProperty("spring.autoconfigure.exclude",
				"org.springframework.boot.autoconfigure.DoesNotExist2");
		selectImports(EnableAutoConfigurationWithAbsentClassNameExclude.class);
		assertThat(this.importSelector.getLastEvent().getExclusions())
				.containsExactlyInAnyOrder(
						"org.springframework.boot.autoconfigure.DoesNotExist1",
						"org.springframework.boot.autoconfigure.DoesNotExist2");
	}

	@Test
	public void filterShouldFilterImports() throws Exception {
		String[] defaultImports = selectImports(BasicEnableAutoConfiguration.class);
		this.filters.add(new TestAutoConfigurationImportFilter(defaultImports, 1));
		this.filters.add(new TestAutoConfigurationImportFilter(defaultImports, 3, 4));
		String[] filtered = selectImports(BasicEnableAutoConfiguration.class);
		assertThat(filtered).hasSize(defaultImports.length - 3);
		assertThat(filtered).doesNotContain(defaultImports[1], defaultImports[3],
				defaultImports[4]);
	}

	@Test
	public void filterShouldSupportAware() throws Exception {
		TestAutoConfigurationImportFilter filter = new TestAutoConfigurationImportFilter(
				new String[] {});
		this.filters.add(filter);
		selectImports(BasicEnableAutoConfiguration.class);
		assertThat(filter.getBeanFactory()).isEqualTo(this.beanFactory);
	}

	private String[] selectImports(Class<?> source) {
		return this.importSelector.selectImports(new StandardAnnotationMetadata(source));
	}

	private List<String> getAutoConfigurationClassNames() {
		return SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,
				getClass().getClassLoader());
	}

	private class TestAutoConfigurationImportSelector
			extends AutoConfigurationImportSelector {

		private AutoConfigurationImportEvent lastEvent;

		@Override
		protected List<AutoConfigurationImportFilter> getAutoConfigurationImportFilters() {
			return AutoConfigurationImportSelectorTests.this.filters;
		}

		@Override
		protected List<AutoConfigurationImportListener> getAutoConfigurationImportListeners() {
			return Collections.<AutoConfigurationImportListener>singletonList(
					new AutoConfigurationImportListener() {

						@Override
						public void onAutoConfigurationImportEvent(
								AutoConfigurationImportEvent event) {
							TestAutoConfigurationImportSelector.this.lastEvent = event;
						}

					});
		}

		public AutoConfigurationImportEvent getLastEvent() {
			return this.lastEvent;
		}

	}

	private static class TestAutoConfigurationImportFilter
			implements AutoConfigurationImportFilter, BeanFactoryAware {

		private final Set<String> nonMatching = new HashSet<>();

		private BeanFactory beanFactory;

		TestAutoConfigurationImportFilter(String[] configurations, int... nonMatching) {
			for (int i : nonMatching) {
				this.nonMatching.add(configurations[i]);
			}
		}

		@Override
		public boolean[] match(String[] autoConfigurationClasses,
				AutoConfigurationMetadata autoConfigurationMetadata) {
			boolean[] result = new boolean[autoConfigurationClasses.length];
			for (int i = 0; i < result.length; i++) {
				result[i] = !this.nonMatching.contains(autoConfigurationClasses[i]);
			}
			return result;
		}

		@Override
		public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
			this.beanFactory = beanFactory;
		}

		public BeanFactory getBeanFactory() {
			return this.beanFactory;
		}

	}

	@Configuration
	private class TestConfiguration {

	}

	@EnableAutoConfiguration
	private class BasicEnableAutoConfiguration {

	}

	@EnableAutoConfiguration(exclude = FreeMarkerAutoConfiguration.class)
	private class EnableAutoConfigurationWithClassExclusions {

	}

	@SpringBootApplication(exclude = FreeMarkerAutoConfiguration.class)
	private class SpringBootApplicationWithClassExclusions {

	}

	@EnableAutoConfiguration(excludeName = "org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration")
	private class EnableAutoConfigurationWithClassNameExclusions {

	}

	@EnableAutoConfiguration(exclude = MustacheAutoConfiguration.class, excludeName = "org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration")
	private class EnableAutoConfigurationWithClassAndClassNameExclusions {

	}

	@EnableAutoConfiguration(exclude = TestConfiguration.class)
	private class EnableAutoConfigurationWithFaultyClassExclude {

	}

	@EnableAutoConfiguration(excludeName = "org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests.TestConfiguration")
	private class EnableAutoConfigurationWithFaultyClassNameExclude {

	}

	@EnableAutoConfiguration(excludeName = "org.springframework.boot.autoconfigure.DoesNotExist1")
	private class EnableAutoConfigurationWithAbsentClassNameExclude {

	}

	@SpringBootApplication(excludeName = "org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration")
	private class SpringBootApplicationWithClassNameExclusions {

	}

}
