/*
 * Copyright 2002-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.context.annotation.configuration;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import javax.inject.Provider;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InjectionPoint;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.config.ListFactoryBean;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.context.annotation.Scope;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.tests.sample.beans.ITestBean;
import org.springframework.tests.sample.beans.TestBean;

import static org.junit.Assert.*;

/**
 * Miscellaneous system tests covering {@link Bean} naming, aliases, scoping and
 * error handling within {@link Configuration} class definitions.
 *
 * @author Chris Beams
 * @author Juergen Hoeller
 * @author Sam Brannen
 */
public class ConfigurationClassProcessingTests {

	@Rule
	public final ExpectedException exception = ExpectedException.none();


	@Test
	public void customBeanNameIsRespectedWhenConfiguredViaNameAttribute() {
		customBeanNameIsRespected(ConfigWithBeanWithCustomName.class,
				() -> ConfigWithBeanWithCustomName.testBean, "customName");
	}

	@Test
	public void customBeanNameIsRespectedWhenConfiguredViaValueAttribute() {
		customBeanNameIsRespected(ConfigWithBeanWithCustomNameConfiguredViaValueAttribute.class,
				() -> ConfigWithBeanWithCustomNameConfiguredViaValueAttribute.testBean, "enigma");
	}

	private void customBeanNameIsRespected(Class<?> testClass, Supplier<TestBean> testBeanSupplier, String beanName) {
		GenericApplicationContext ac = new GenericApplicationContext();
		AnnotationConfigUtils.registerAnnotationConfigProcessors(ac);
		ac.registerBeanDefinition("config", new RootBeanDefinition(testClass));
		ac.refresh();

		assertSame(testBeanSupplier.get(), ac.getBean(beanName));

		// method name should not be registered
		exception.expect(NoSuchBeanDefinitionException.class);
		ac.getBean("methodName");
	}

	@Test
	public void aliasesAreRespectedWhenConfiguredViaNameAttribute() {
		aliasesAreRespected(ConfigWithBeanWithAliases.class,
				() -> ConfigWithBeanWithAliases.testBean, "name1");
	}

	@Test
	public void aliasesAreRespectedWhenConfiguredViaValueAttribute() {
		aliasesAreRespected(ConfigWithBeanWithAliasesConfiguredViaValueAttribute.class,
				() -> ConfigWithBeanWithAliasesConfiguredViaValueAttribute.testBean, "enigma");
	}

	private void aliasesAreRespected(Class<?> testClass, Supplier<TestBean> testBeanSupplier, String beanName) {
		TestBean testBean = testBeanSupplier.get();
		BeanFactory factory = initBeanFactory(testClass);

		assertSame(testBean, factory.getBean(beanName));
		Arrays.stream(factory.getAliases(beanName)).map(factory::getBean).forEach(alias -> assertSame(testBean, alias));

		// method name should not be registered
		exception.expect(NoSuchBeanDefinitionException.class);
		factory.getBean("methodName");
	}

	@Test  // SPR-11830
	public void configWithBeanWithProviderImplementation() {
		GenericApplicationContext ac = new GenericApplicationContext();
		AnnotationConfigUtils.registerAnnotationConfigProcessors(ac);
		ac.registerBeanDefinition("config", new RootBeanDefinition(ConfigWithBeanWithProviderImplementation.class));
		ac.refresh();
		assertSame(ac.getBean("customName"), ConfigWithBeanWithProviderImplementation.testBean);
	}

	@Test  // SPR-11830
	public void configWithSetWithProviderImplementation() {
		GenericApplicationContext ac = new GenericApplicationContext();
		AnnotationConfigUtils.registerAnnotationConfigProcessors(ac);
		ac.registerBeanDefinition("config", new RootBeanDefinition(ConfigWithSetWithProviderImplementation.class));
		ac.refresh();
		assertSame(ac.getBean("customName"), ConfigWithSetWithProviderImplementation.set);
	}

	@Test
	public void testFinalBeanMethod() {
		exception.expect(BeanDefinitionParsingException.class);
		initBeanFactory(ConfigWithFinalBean.class);
	}

	@Test
	public void simplestPossibleConfig() {
		BeanFactory factory = initBeanFactory(SimplestPossibleConfig.class);
		String stringBean = factory.getBean("stringBean", String.class);
		assertEquals(stringBean, "foo");
	}

	@Test
	public void configWithObjectReturnType() {
		BeanFactory factory = initBeanFactory(ConfigWithNonSpecificReturnTypes.class);
		assertEquals(Object.class, factory.getType("stringBean"));
		assertFalse(factory.isTypeMatch("stringBean", String.class));
		String stringBean = factory.getBean("stringBean", String.class);
		assertEquals(stringBean, "foo");
	}

	@Test
	public void configWithFactoryBeanReturnType() {
		ListableBeanFactory factory = initBeanFactory(ConfigWithNonSpecificReturnTypes.class);
		assertEquals(List.class, factory.getType("factoryBean"));
		assertTrue(factory.isTypeMatch("factoryBean", List.class));
		assertEquals(FactoryBean.class, factory.getType("&factoryBean"));
		assertTrue(factory.isTypeMatch("&factoryBean", FactoryBean.class));
		assertFalse(factory.isTypeMatch("&factoryBean", BeanClassLoaderAware.class));
		assertFalse(factory.isTypeMatch("&factoryBean", ListFactoryBean.class));
		assertTrue(factory.getBean("factoryBean") instanceof List);

		String[] beanNames = factory.getBeanNamesForType(FactoryBean.class);
		assertEquals(1, beanNames.length);
		assertEquals("&factoryBean", beanNames[0]);

		beanNames = factory.getBeanNamesForType(BeanClassLoaderAware.class);
		assertEquals(1, beanNames.length);
		assertEquals("&factoryBean", beanNames[0]);

		beanNames = factory.getBeanNamesForType(ListFactoryBean.class);
		assertEquals(1, beanNames.length);
		assertEquals("&factoryBean", beanNames[0]);

		beanNames = factory.getBeanNamesForType(List.class);
		assertEquals("factoryBean", beanNames[0]);
	}

	@Test
	public void configurationWithPrototypeScopedBeans() {
		BeanFactory factory = initBeanFactory(ConfigWithPrototypeBean.class);

		TestBean foo = factory.getBean("foo", TestBean.class);
		ITestBean bar = factory.getBean("bar", ITestBean.class);
		ITestBean baz = factory.getBean("baz", ITestBean.class);

		assertSame(foo.getSpouse(), bar);
		assertNotSame(bar.getSpouse(), baz);
	}

	@Test
	public void configurationWithAdaptivePrototypes() {
		AnnotationConfigApplicationContext factory = new AnnotationConfigApplicationContext();
		factory.register(ConfigWithPrototypeBean.class, AdaptiveInjectionPoints.class);
		factory.refresh();

		AdaptiveInjectionPoints adaptive = factory.getBean(AdaptiveInjectionPoints.class);
		assertEquals("adaptiveInjectionPoint1", adaptive.adaptiveInjectionPoint1.getName());
		assertEquals("setAdaptiveInjectionPoint2", adaptive.adaptiveInjectionPoint2.getName());

		adaptive = factory.getBean(AdaptiveInjectionPoints.class);
		assertEquals("adaptiveInjectionPoint1", adaptive.adaptiveInjectionPoint1.getName());
		assertEquals("setAdaptiveInjectionPoint2", adaptive.adaptiveInjectionPoint2.getName());
		factory.close();
	}

	@Test
	public void configurationWithPostProcessor() {
		AnnotationConfigApplicationContext factory = new AnnotationConfigApplicationContext();
		factory.register(ConfigWithPostProcessor.class);
		RootBeanDefinition placeholderConfigurer = new RootBeanDefinition(PropertyPlaceholderConfigurer.class);
		placeholderConfigurer.getPropertyValues().add("properties", "myProp=myValue");
		factory.registerBeanDefinition("placeholderConfigurer", placeholderConfigurer);
		factory.refresh();

		TestBean foo = factory.getBean("foo", TestBean.class);
		ITestBean bar = factory.getBean("bar", ITestBean.class);
		ITestBean baz = factory.getBean("baz", ITestBean.class);

		assertEquals("foo-processed-myValue", foo.getName());
		assertEquals("bar-processed-myValue", bar.getName());
		assertEquals("baz-processed-myValue", baz.getName());

		SpousyTestBean listener = factory.getBean("listenerTestBean", SpousyTestBean.class);
		assertTrue(listener.refreshed);
		factory.close();
	}


	/**
	 * Creates a new {@link BeanFactory}, populates it with a {@link BeanDefinition}
	 * for each of the given {@link Configuration} {@code configClasses}, and then
	 * post-processes the factory using JavaConfig's {@link ConfigurationClassPostProcessor}.
	 * When complete, the factory is ready to service requests for any {@link Bean} methods
	 * declared by {@code configClasses}.
	 */
	private DefaultListableBeanFactory initBeanFactory(Class<?>... configClasses) {
		DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
		for (Class<?> configClass : configClasses) {
			String configBeanName = configClass.getName();
			factory.registerBeanDefinition(configBeanName, new RootBeanDefinition(configClass));
		}
		ConfigurationClassPostProcessor ccpp = new ConfigurationClassPostProcessor();
		ccpp.postProcessBeanDefinitionRegistry(factory);
		ccpp.postProcessBeanFactory(factory);
		RequiredAnnotationBeanPostProcessor rapp = new RequiredAnnotationBeanPostProcessor();
		rapp.setBeanFactory(factory);
		factory.addBeanPostProcessor(rapp);
		factory.freezeConfiguration();
		return factory;
	}


	@Configuration
	static class ConfigWithBeanWithCustomName {

		static TestBean testBean = new TestBean(ConfigWithBeanWithCustomName.class.getSimpleName());

		@Bean(name = "customName")
		public TestBean methodName() {
			return testBean;
		}
	}


	@Configuration
	static class ConfigWithBeanWithCustomNameConfiguredViaValueAttribute {

		static TestBean testBean = new TestBean(ConfigWithBeanWithCustomNameConfiguredViaValueAttribute.class.getSimpleName());

		@Bean("enigma")
		public TestBean methodName() {
			return testBean;
		}
	}


	@Configuration
	static class ConfigWithBeanWithAliases {

		static TestBean testBean = new TestBean(ConfigWithBeanWithAliases.class.getSimpleName());

		@Bean(name = { "name1", "alias1", "alias2", "alias3" })
		public TestBean methodName() {
			return testBean;
		}
	}


	@Configuration
	static class ConfigWithBeanWithAliasesConfiguredViaValueAttribute {

		static TestBean testBean = new TestBean(ConfigWithBeanWithAliasesConfiguredViaValueAttribute.class.getSimpleName());

		@Bean({ "enigma", "alias1", "alias2", "alias3" })
		public TestBean methodName() {
			return testBean;
		}
	}


	@Configuration
	static class ConfigWithBeanWithProviderImplementation implements Provider<TestBean> {

		static TestBean testBean = new TestBean(ConfigWithBeanWithProviderImplementation.class.getSimpleName());

		@Bean(name = "customName")
		public TestBean get() {
			return testBean;
		}
	}


	@Configuration
	static class ConfigWithSetWithProviderImplementation implements Provider<Set<String>> {

		static Set<String> set = Collections.singleton("value");

		@Bean(name = "customName")
		public Set<String> get() {
			return set;
		}
	}


	@Configuration
	static class ConfigWithFinalBean {

		public final @Bean TestBean testBean() {
			return new TestBean();
		}
	}


	@Configuration
	static class SimplestPossibleConfig {

		public @Bean String stringBean() {
			return "foo";
		}
	}


	@Configuration
	static class ConfigWithNonSpecificReturnTypes {

		public @Bean Object stringBean() {
			return "foo";
		}

		public @Bean FactoryBean<?> factoryBean() {
			ListFactoryBean fb = new ListFactoryBean();
			fb.setSourceList(Arrays.asList("element1", "element2"));
			return fb;
		}
	}


	@Configuration
	static class ConfigWithPrototypeBean {

		public @Bean TestBean foo() {
			TestBean foo = new SpousyTestBean("foo");
			foo.setSpouse(bar());
			return foo;
		}

		public @Bean TestBean bar() {
			TestBean bar = new SpousyTestBean("bar");
			bar.setSpouse(baz());
			return bar;
		}

		@Bean @Scope("prototype")
		public TestBean baz() {
			return new TestBean("baz");
		}

		@Bean @Scope("prototype")
		public TestBean adaptive1(InjectionPoint ip) {
			return new TestBean(ip.getMember().getName());
		}

		@Bean @Scope("prototype")
		public TestBean adaptive2(DependencyDescriptor dd) {
			return new TestBean(dd.getMember().getName());
		}
	}


	@Scope("prototype")
	static class AdaptiveInjectionPoints {

		@Autowired @Qualifier("adaptive1")
		public TestBean adaptiveInjectionPoint1;

		public TestBean adaptiveInjectionPoint2;

		@Autowired @Qualifier("adaptive2")
		public void setAdaptiveInjectionPoint2(TestBean adaptiveInjectionPoint2) {
			this.adaptiveInjectionPoint2 = adaptiveInjectionPoint2;
		}
	}


	@SuppressWarnings("unused")
	static class ConfigWithPostProcessor extends ConfigWithPrototypeBean {

		@Value("${myProp}")
		private String myProp;

		@Bean
		public POBPP beanPostProcessor() {
			return new POBPP() {

				String nameSuffix = "-processed-" + myProp;

				public void setNameSuffix(String nameSuffix) {
					this.nameSuffix = nameSuffix;
				}

				@Override
				public Object postProcessBeforeInitialization(Object bean, String beanName) {
					if (bean instanceof ITestBean) {
						((ITestBean) bean).setName(((ITestBean) bean).getName() + nameSuffix);
					}
					return bean;
				}

				@Override
				public Object postProcessAfterInitialization(Object bean, String beanName) {
					return bean;
				}

				public int getOrder() {
					return 0;
				}
			};
		}

		// @Bean
		public BeanFactoryPostProcessor beanFactoryPostProcessor() {
			return new BeanFactoryPostProcessor() {
				@Override
				public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
					BeanDefinition bd = beanFactory.getBeanDefinition("beanPostProcessor");
					bd.getPropertyValues().addPropertyValue("nameSuffix", "-processed-" + myProp);
				}
			};
		}

		@Bean
		public ITestBean listenerTestBean() {
			return new SpousyTestBean("listener");
		}
	}


	public interface POBPP extends BeanPostProcessor {
	}


	private static class SpousyTestBean extends TestBean implements ApplicationListener<ContextRefreshedEvent> {

		public boolean refreshed = false;

		public SpousyTestBean(String name) {
			super(name);
		}

		@Override
		@Required
		public void setSpouse(ITestBean spouse) {
			super.setSpouse(spouse);
		}

		@Override
		public void onApplicationEvent(ContextRefreshedEvent event) {
			this.refreshed = true;
		}
	}

}
