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

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;

import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;

import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.ItemDeprecation;
import org.springframework.boot.configurationprocessor.metadata.ItemHint;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
import org.springframework.boot.configurationprocessor.metadata.Metadata;
import org.springframework.boot.configurationprocessor.metadata.TestJsonConverter;
import org.springframework.boot.configurationsample.incremental.BarProperties;
import org.springframework.boot.configurationsample.incremental.FooProperties;
import org.springframework.boot.configurationsample.incremental.RenamedBarProperties;
import org.springframework.boot.configurationsample.lombok.LombokExplicitProperties;
import org.springframework.boot.configurationsample.lombok.LombokInnerClassProperties;
import org.springframework.boot.configurationsample.lombok.LombokInnerClassWithGetterProperties;
import org.springframework.boot.configurationsample.lombok.LombokSimpleDataProperties;
import org.springframework.boot.configurationsample.lombok.LombokSimpleProperties;
import org.springframework.boot.configurationsample.lombok.SimpleLombokPojo;
import org.springframework.boot.configurationsample.method.DeprecatedMethodConfig;
import org.springframework.boot.configurationsample.method.EmptyTypeMethodConfig;
import org.springframework.boot.configurationsample.method.InvalidMethodConfig;
import org.springframework.boot.configurationsample.method.MethodAndClassConfig;
import org.springframework.boot.configurationsample.method.SimpleMethodConfig;
import org.springframework.boot.configurationsample.simple.ClassWithNestedProperties;
import org.springframework.boot.configurationsample.simple.DeprecatedSingleProperty;
import org.springframework.boot.configurationsample.simple.HierarchicalProperties;
import org.springframework.boot.configurationsample.simple.NotAnnotated;
import org.springframework.boot.configurationsample.simple.SimpleCollectionProperties;
import org.springframework.boot.configurationsample.simple.SimplePrefixValueProperties;
import org.springframework.boot.configurationsample.simple.SimpleProperties;
import org.springframework.boot.configurationsample.simple.SimpleTypeProperties;
import org.springframework.boot.configurationsample.specific.BoxingPojo;
import org.springframework.boot.configurationsample.specific.BuilderPojo;
import org.springframework.boot.configurationsample.specific.DeprecatedUnrelatedMethodPojo;
import org.springframework.boot.configurationsample.specific.DoubleRegistrationProperties;
import org.springframework.boot.configurationsample.specific.ExcludedTypesPojo;
import org.springframework.boot.configurationsample.specific.GenericConfig;
import org.springframework.boot.configurationsample.specific.InnerClassAnnotatedGetterConfig;
import org.springframework.boot.configurationsample.specific.InnerClassHierachicalProperties;
import org.springframework.boot.configurationsample.specific.InnerClassProperties;
import org.springframework.boot.configurationsample.specific.InnerClassRootConfig;
import org.springframework.boot.configurationsample.specific.InvalidAccessorProperties;
import org.springframework.boot.configurationsample.specific.InvalidDoubleRegistrationProperties;
import org.springframework.boot.configurationsample.specific.SimplePojo;
import org.springframework.boot.junit.compiler.TestCompiler;
import org.springframework.util.FileCopyUtils;

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

/**
 * Tests for {@link ConfigurationMetadataAnnotationProcessor}.
 *
 * @author Stephane Nicoll
 * @author Phillip Webb
 * @author Andy Wilkinson
 * @author Kris De Volder
 */
public class ConfigurationMetadataAnnotationProcessorTests {

	@Rule
	public TemporaryFolder temporaryFolder = new TemporaryFolder();

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

	private TestCompiler compiler;

	@Before
	public void createCompiler() throws IOException {
		this.compiler = new TestCompiler(this.temporaryFolder);
	}

	@Test
	public void notAnnotated() throws Exception {
		ConfigurationMetadata metadata = compile(NotAnnotated.class);
		assertThat(metadata.getItems()).isEmpty();
	}

	@Test
	public void simpleProperties() throws Exception {
		ConfigurationMetadata metadata = compile(SimpleProperties.class);
		assertThat(metadata)
				.has(Metadata.withGroup("simple").fromSource(SimpleProperties.class));
		assertThat(metadata).has(Metadata.withProperty("simple.the-name", String.class)
				.fromSource(SimpleProperties.class)
				.withDescription("The name of this simple properties.")
				.withDefaultValue("boot").withDeprecation(null, null));
		assertThat(metadata).has(Metadata.withProperty("simple.flag", Boolean.class)
				.fromSource(SimpleProperties.class).withDescription("A simple flag.")
				.withDeprecation(null, null));
		assertThat(metadata).has(Metadata.withProperty("simple.comparator"));
		assertThat(metadata).doesNotHave(Metadata.withProperty("simple.counter"));
		assertThat(metadata).doesNotHave(Metadata.withProperty("simple.size"));
	}

	@Test
	public void simplePrefixValueProperties() throws Exception {
		ConfigurationMetadata metadata = compile(SimplePrefixValueProperties.class);
		assertThat(metadata).has(Metadata.withGroup("simple")
				.fromSource(SimplePrefixValueProperties.class));
		assertThat(metadata).has(Metadata.withProperty("simple.name", String.class)
				.fromSource(SimplePrefixValueProperties.class));
	}

	@Test
	public void simpleTypeProperties() throws Exception {
		ConfigurationMetadata metadata = compile(SimpleTypeProperties.class);
		assertThat(metadata).has(
				Metadata.withGroup("simple.type").fromSource(SimpleTypeProperties.class));
		assertThat(metadata)
				.has(Metadata.withProperty("simple.type.my-string", String.class));
		assertThat(metadata)
				.has(Metadata.withProperty("simple.type.my-byte", Byte.class));
		assertThat(metadata)
				.has(Metadata.withProperty("simple.type.my-primitive-byte", Byte.class));
		assertThat(metadata)
				.has(Metadata.withProperty("simple.type.my-char", Character.class));
		assertThat(metadata).has(
				Metadata.withProperty("simple.type.my-primitive-char", Character.class));
		assertThat(metadata)
				.has(Metadata.withProperty("simple.type.my-boolean", Boolean.class));
		assertThat(metadata).has(
				Metadata.withProperty("simple.type.my-primitive-boolean", Boolean.class));
		assertThat(metadata)
				.has(Metadata.withProperty("simple.type.my-short", Short.class));
		assertThat(metadata).has(
				Metadata.withProperty("simple.type.my-primitive-short", Short.class));
		assertThat(metadata)
				.has(Metadata.withProperty("simple.type.my-integer", Integer.class));
		assertThat(metadata).has(
				Metadata.withProperty("simple.type.my-primitive-integer", Integer.class));
		assertThat(metadata)
				.has(Metadata.withProperty("simple.type.my-long", Long.class));
		assertThat(metadata)
				.has(Metadata.withProperty("simple.type.my-primitive-long", Long.class));
		assertThat(metadata)
				.has(Metadata.withProperty("simple.type.my-double", Double.class));
		assertThat(metadata).has(
				Metadata.withProperty("simple.type.my-primitive-double", Double.class));
		assertThat(metadata)
				.has(Metadata.withProperty("simple.type.my-float", Float.class));
		assertThat(metadata).has(
				Metadata.withProperty("simple.type.my-primitive-float", Float.class));
		assertThat(metadata.getItems().size()).isEqualTo(18);
	}

	@Test
	public void hierarchicalProperties() throws Exception {
		ConfigurationMetadata metadata = compile(HierarchicalProperties.class);
		assertThat(metadata).has(Metadata.withGroup("hierarchical")
				.fromSource(HierarchicalProperties.class));
		assertThat(metadata).has(Metadata.withProperty("hierarchical.first", String.class)
				.fromSource(HierarchicalProperties.class));
		assertThat(metadata)
				.has(Metadata.withProperty("hierarchical.second", String.class)
						.fromSource(HierarchicalProperties.class));
		assertThat(metadata).has(Metadata.withProperty("hierarchical.third", String.class)
				.fromSource(HierarchicalProperties.class));
	}

	@Test
	@SuppressWarnings("deprecation")
	public void deprecatedProperties() throws Exception {
		Class<?> type = org.springframework.boot.configurationsample.simple.DeprecatedProperties.class;
		ConfigurationMetadata metadata = compile(type);
		assertThat(metadata).has(Metadata.withGroup("deprecated").fromSource(type));
		assertThat(metadata).has(Metadata.withProperty("deprecated.name", String.class)
				.fromSource(type).withDeprecation(null, null));
		assertThat(metadata)
				.has(Metadata.withProperty("deprecated.description", String.class)
						.fromSource(type).withDeprecation(null, null));
	}

	@Test
	public void singleDeprecatedProperty() throws Exception {
		Class<?> type = DeprecatedSingleProperty.class;
		ConfigurationMetadata metadata = compile(type);
		assertThat(metadata).has(Metadata.withGroup("singledeprecated").fromSource(type));
		assertThat(metadata)
				.has(Metadata.withProperty("singledeprecated.new-name", String.class)
						.fromSource(type));
		assertThat(metadata).has(Metadata
				.withProperty("singledeprecated.name", String.class).fromSource(type)
				.withDeprecation("renamed", "singledeprecated.new-name"));
	}

	@Test
	public void deprecatedOnUnrelatedSetter() throws Exception {
		Class<?> type = DeprecatedUnrelatedMethodPojo.class;
		ConfigurationMetadata metadata = compile(type);
		assertThat(metadata).has(Metadata.withGroup("not.deprecated").fromSource(type));
		assertThat(metadata)
				.has(Metadata.withProperty("not.deprecated.counter", Integer.class)
						.withNoDeprecation().fromSource(type));
		assertThat(metadata)
				.has(Metadata.withProperty("not.deprecated.flag", Boolean.class)
						.withNoDeprecation().fromSource(type));
	}

	@Test
	public void boxingOnSetter() throws IOException {
		Class<?> type = BoxingPojo.class;
		ConfigurationMetadata metadata = compile(type);
		assertThat(metadata).has(Metadata.withGroup("boxing").fromSource(type));
		assertThat(metadata).has(
				Metadata.withProperty("boxing.flag", Boolean.class).fromSource(type));
		assertThat(metadata).has(
				Metadata.withProperty("boxing.counter", Integer.class).fromSource(type));
	}

	@Test
	public void parseCollectionConfig() throws Exception {
		ConfigurationMetadata metadata = compile(SimpleCollectionProperties.class);
		// getter and setter
		assertThat(metadata).has(Metadata.withProperty("collection.integers-to-names",
				"java.util.Map<java.lang.Integer,java.lang.String>"));
		assertThat(metadata).has(Metadata.withProperty("collection.longs",
				"java.util.Collection<java.lang.Long>"));
		assertThat(metadata).has(Metadata.withProperty("collection.floats",
				"java.util.List<java.lang.Float>"));
		// getter only
		assertThat(metadata).has(Metadata.withProperty("collection.names-to-integers",
				"java.util.Map<java.lang.String,java.lang.Integer>"));
		assertThat(metadata).has(Metadata.withProperty("collection.bytes",
				"java.util.Collection<java.lang.Byte>"));
		assertThat(metadata).has(Metadata.withProperty("collection.doubles",
				"java.util.List<java.lang.Double>"));
	}

	@Test
	public void simpleMethodConfig() throws Exception {
		ConfigurationMetadata metadata = compile(SimpleMethodConfig.class);
		assertThat(metadata)
				.has(Metadata.withGroup("foo").fromSource(SimpleMethodConfig.class));
		assertThat(metadata).has(Metadata.withProperty("foo.name", String.class)
				.fromSource(SimpleMethodConfig.Foo.class));
		assertThat(metadata).has(Metadata.withProperty("foo.flag", Boolean.class)
				.fromSource(SimpleMethodConfig.Foo.class));
	}

	@Test
	public void invalidMethodConfig() throws Exception {
		ConfigurationMetadata metadata = compile(InvalidMethodConfig.class);
		assertThat(metadata).has(Metadata.withProperty("something.name", String.class)
				.fromSource(InvalidMethodConfig.class));
		assertThat(metadata).isNotEqualTo(Metadata.withProperty("invalid.name"));
	}

	@Test
	public void methodAndClassConfig() throws Exception {
		ConfigurationMetadata metadata = compile(MethodAndClassConfig.class);
		assertThat(metadata).has(Metadata.withProperty("conflict.name", String.class)
				.fromSource(MethodAndClassConfig.Foo.class));
		assertThat(metadata).has(Metadata.withProperty("conflict.flag", Boolean.class)
				.fromSource(MethodAndClassConfig.Foo.class));
		assertThat(metadata).has(Metadata.withProperty("conflict.value", String.class)
				.fromSource(MethodAndClassConfig.class));
	}

	@Test
	public void emptyTypeMethodConfig() throws Exception {
		ConfigurationMetadata metadata = compile(EmptyTypeMethodConfig.class);
		assertThat(metadata).isNotEqualTo(Metadata.withProperty("something.foo"));
	}

	@Test
	public void deprecatedMethodConfig() throws Exception {
		Class<DeprecatedMethodConfig> type = DeprecatedMethodConfig.class;
		ConfigurationMetadata metadata = compile(type);
		assertThat(metadata).has(Metadata.withGroup("foo").fromSource(type));
		assertThat(metadata).has(Metadata.withProperty("foo.name", String.class)
				.fromSource(DeprecatedMethodConfig.Foo.class)
				.withDeprecation(null, null));
		assertThat(metadata).has(Metadata.withProperty("foo.flag", Boolean.class)
				.fromSource(DeprecatedMethodConfig.Foo.class)
				.withDeprecation(null, null));
	}

	@Test
	@SuppressWarnings("deprecation")
	public void deprecatedMethodConfigOnClass() throws Exception {
		Class<?> type = org.springframework.boot.configurationsample.method.DeprecatedClassMethodConfig.class;
		ConfigurationMetadata metadata = compile(type);
		assertThat(metadata).has(Metadata.withGroup("foo").fromSource(type));
		assertThat(metadata).has(Metadata.withProperty("foo.name", String.class)
				.fromSource(
						org.springframework.boot.configurationsample.method.DeprecatedClassMethodConfig.Foo.class)
				.withDeprecation(null, null));
		assertThat(metadata).has(Metadata.withProperty("foo.flag", Boolean.class)
				.fromSource(
						org.springframework.boot.configurationsample.method.DeprecatedClassMethodConfig.Foo.class)
				.withDeprecation(null, null));
	}

	@Test
	public void innerClassRootConfig() throws Exception {
		ConfigurationMetadata metadata = compile(InnerClassRootConfig.class);
		assertThat(metadata).has(Metadata.withProperty("config.name"));
	}

	@Test
	public void innerClassProperties() throws Exception {
		ConfigurationMetadata metadata = compile(InnerClassProperties.class);
		assertThat(metadata)
				.has(Metadata.withGroup("config").fromSource(InnerClassProperties.class));
		assertThat(metadata).has(
				Metadata.withGroup("config.first").ofType(InnerClassProperties.Foo.class)
						.fromSource(InnerClassProperties.class));
		assertThat(metadata).has(Metadata.withProperty("config.first.name"));
		assertThat(metadata).has(Metadata.withProperty("config.first.bar.name"));
		assertThat(metadata).has(
				Metadata.withGroup("config.the-second", InnerClassProperties.Foo.class)
						.fromSource(InnerClassProperties.class));
		assertThat(metadata).has(Metadata.withProperty("config.the-second.name"));
		assertThat(metadata).has(Metadata.withProperty("config.the-second.bar.name"));
		assertThat(metadata).has(Metadata.withGroup("config.third")
				.ofType(SimplePojo.class).fromSource(InnerClassProperties.class));
		assertThat(metadata).has(Metadata.withProperty("config.third.value"));
		assertThat(metadata).has(Metadata.withProperty("config.fourth"));
		assertThat(metadata).isNotEqualTo(Metadata.withGroup("config.fourth"));
	}

	@Test
	public void innerClassPropertiesHierachical() throws Exception {
		ConfigurationMetadata metadata = compile(InnerClassHierachicalProperties.class);
		assertThat(metadata).has(Metadata.withGroup("config.foo")
				.ofType(InnerClassHierachicalProperties.Foo.class));
		assertThat(metadata).has(Metadata.withGroup("config.foo.bar")
				.ofType(InnerClassHierachicalProperties.Bar.class));
		assertThat(metadata).has(Metadata.withGroup("config.foo.bar.baz")
				.ofType(InnerClassHierachicalProperties.Foo.Baz.class));
		assertThat(metadata).has(Metadata.withProperty("config.foo.bar.baz.blah"));
		assertThat(metadata).has(Metadata.withProperty("config.foo.bar.bling"));
	}

	@Test
	public void innerClassAnnotatedGetterConfig() throws Exception {
		ConfigurationMetadata metadata = compile(InnerClassAnnotatedGetterConfig.class);
		assertThat(metadata).has(Metadata.withProperty("specific.value"));
		assertThat(metadata).has(Metadata.withProperty("foo.name"));
		assertThat(metadata).isNotEqualTo(Metadata.withProperty("specific.foo"));
	}

	@Test
	public void nestedClassChildProperties() throws Exception {
		ConfigurationMetadata metadata = compile(ClassWithNestedProperties.class);
		assertThat(metadata).has(Metadata.withGroup("nestedChildProps")
				.fromSource(ClassWithNestedProperties.NestedChildClass.class));
		assertThat(metadata)
				.has(Metadata
						.withProperty("nestedChildProps.child-class-property",
								Integer.class)
						.fromSource(ClassWithNestedProperties.NestedChildClass.class)
						.withDefaultValue(20));
		assertThat(metadata)
				.has(Metadata
						.withProperty("nestedChildProps.parent-class-property",
								Integer.class)
						.fromSource(ClassWithNestedProperties.NestedChildClass.class)
						.withDefaultValue(10));
	}

	@Test
	public void builderPojo() throws IOException {
		ConfigurationMetadata metadata = compile(BuilderPojo.class);
		assertThat(metadata).has(Metadata.withProperty("builder.name"));
	}

	@Test
	public void excludedTypesPojo() throws IOException {
		ConfigurationMetadata metadata = compile(ExcludedTypesPojo.class);
		assertThat(metadata).has(Metadata.withProperty("excluded.name"));
		assertThat(metadata).isNotEqualTo(Metadata.withProperty("excluded.class-loader"));
		assertThat(metadata).isNotEqualTo(Metadata.withProperty("excluded.data-source"));
		assertThat(metadata).isNotEqualTo(Metadata.withProperty("excluded.print-writer"));
		assertThat(metadata).isNotEqualTo(Metadata.withProperty("excluded.writer"));
		assertThat(metadata).isNotEqualTo(Metadata.withProperty("excluded.writer-array"));
	}

	@Test
	public void invalidAccessor() throws IOException {
		ConfigurationMetadata metadata = compile(InvalidAccessorProperties.class);
		assertThat(metadata).has(Metadata.withGroup("config"));
		assertThat(metadata.getItems()).hasSize(1);
	}

	@Test
	public void doubleRegistration() throws IOException {
		ConfigurationMetadata metadata = compile(DoubleRegistrationProperties.class);
		assertThat(metadata).has(Metadata.withGroup("one"));
		assertThat(metadata).has(Metadata.withGroup("two"));
		assertThat(metadata).has(Metadata.withProperty("one.value"));
		assertThat(metadata).has(Metadata.withProperty("two.value"));
		assertThat(metadata.getItems()).hasSize(4);
	}

	@Test
	public void invalidDoubleRegistration() throws IOException {
		this.thrown.expect(IllegalStateException.class);
		this.thrown.expectMessage("Compilation failed");
		compile(InvalidDoubleRegistrationProperties.class);
	}

	@Test
	public void genericTypes() throws IOException {
		ConfigurationMetadata metadata = compile(GenericConfig.class);
		assertThat(metadata).has(Metadata.withGroup("generic").ofType(
				"org.springframework.boot.configurationsample.specific.GenericConfig"));
		assertThat(metadata).has(Metadata.withGroup("generic.foo").ofType(
				"org.springframework.boot.configurationsample.specific.GenericConfig$Foo"));
		assertThat(metadata).has(Metadata.withGroup("generic.foo.bar").ofType(
				"org.springframework.boot.configurationsample.specific.GenericConfig$Bar"));
		assertThat(metadata).has(Metadata.withGroup("generic.foo.bar.biz").ofType(
				"org.springframework.boot.configurationsample.specific.GenericConfig$Bar$Biz"));
		assertThat(metadata).has(Metadata.withProperty("generic.foo.name")
				.ofType(String.class).fromSource(GenericConfig.Foo.class));
		assertThat(metadata).has(Metadata.withProperty("generic.foo.string-to-bar")
				.ofType("java.util.Map<java.lang.String,org.springframework.boot.configurationsample.specific.GenericConfig.Bar<java.lang.Integer>>")
				.fromSource(GenericConfig.Foo.class));
		assertThat(metadata).has(Metadata.withProperty("generic.foo.string-to-integer")
				.ofType("java.util.Map<java.lang.String,java.lang.Integer>")
				.fromSource(GenericConfig.Foo.class));
		assertThat(metadata).has(Metadata.withProperty("generic.foo.bar.name")
				.ofType("java.lang.String").fromSource(GenericConfig.Bar.class));
		assertThat(metadata).has(Metadata.withProperty("generic.foo.bar.biz.name")
				.ofType("java.lang.String").fromSource(GenericConfig.Bar.Biz.class));
		assertThat(metadata.getItems()).hasSize(9);
	}

	@Test
	public void lombokDataProperties() throws Exception {
		ConfigurationMetadata metadata = compile(LombokSimpleDataProperties.class);
		assertSimpleLombokProperties(metadata, LombokSimpleDataProperties.class, "data");
	}

	@Test
	public void lombokSimpleProperties() throws Exception {
		ConfigurationMetadata metadata = compile(LombokSimpleProperties.class);
		assertSimpleLombokProperties(metadata, LombokSimpleProperties.class, "simple");
	}

	@Test
	public void lombokExplicitProperties() throws Exception {
		ConfigurationMetadata metadata = compile(LombokExplicitProperties.class);
		assertSimpleLombokProperties(metadata, LombokExplicitProperties.class,
				"explicit");
	}

	@Test
	public void lombokInnerClassProperties() throws Exception {
		ConfigurationMetadata metadata = compile(LombokInnerClassProperties.class);
		assertThat(metadata).has(Metadata.withGroup("config")
				.fromSource(LombokInnerClassProperties.class));
		assertThat(metadata).has(Metadata.withGroup("config.first")
				.ofType(LombokInnerClassProperties.Foo.class)
				.fromSource(LombokInnerClassProperties.class));
		assertThat(metadata).has(Metadata.withProperty("config.first.name"));
		assertThat(metadata).has(Metadata.withProperty("config.first.bar.name"));
		assertThat(metadata).has(
				Metadata.withGroup("config.second", LombokInnerClassProperties.Foo.class)
						.fromSource(LombokInnerClassProperties.class));
		assertThat(metadata).has(Metadata.withProperty("config.second.name"));
		assertThat(metadata).has(Metadata.withProperty("config.second.bar.name"));
		assertThat(metadata)
				.has(Metadata.withGroup("config.third").ofType(SimpleLombokPojo.class)
						.fromSource(LombokInnerClassProperties.class));
		// For some reason the annotation processor resolves a type for SimpleLombokPojo
		// that is resolved (compiled) and the source annotations are gone. Because we
		// don't see the @Data annotation anymore, no field is harvested. What is crazy is
		// that a sample project works fine so this seem to be related to the unit test
		// environment for some reason. assertThat(metadata,
		// containsProperty("config.third.value"));
		assertThat(metadata).has(Metadata.withProperty("config.fourth"));
		assertThat(metadata).isNotEqualTo(Metadata.withGroup("config.fourth"));
	}

	@Test
	public void lombokInnerClassWithGetterProperties() throws IOException {
		ConfigurationMetadata metadata = compile(
				LombokInnerClassWithGetterProperties.class);
		assertThat(metadata).has(Metadata.withGroup("config")
				.fromSource(LombokInnerClassWithGetterProperties.class));
		assertThat(metadata).has(Metadata.withGroup("config.first")
				.ofType(LombokInnerClassWithGetterProperties.Foo.class)
				.fromSourceMethod("getFirst()")
				.fromSource(LombokInnerClassWithGetterProperties.class));
		assertThat(metadata).has(Metadata.withProperty("config.first.name"));
		assertThat(metadata.getItems()).hasSize(3);
	}

	@Test
	public void mergingOfAdditionalProperty() throws Exception {
		ItemMetadata property = ItemMetadata.newProperty(null, "foo", "java.lang.String",
				AdditionalMetadata.class.getName(), null, null, null, null);
		writeAdditionalMetadata(property);
		ConfigurationMetadata metadata = compile(SimpleProperties.class);
		assertThat(metadata).has(Metadata.withProperty("simple.comparator"));
		assertThat(metadata).has(Metadata.withProperty("foo", String.class)
				.fromSource(AdditionalMetadata.class));
	}

	@Test
	public void mergeExistingPropertyDefaultValue() throws Exception {
		ItemMetadata property = ItemMetadata.newProperty("simple", "flag", null, null,
				null, null, true, null);
		writeAdditionalMetadata(property);
		ConfigurationMetadata metadata = compile(SimpleProperties.class);
		assertThat(metadata).has(Metadata.withProperty("simple.flag", Boolean.class)
				.fromSource(SimpleProperties.class).withDescription("A simple flag.")
				.withDeprecation(null, null).withDefaultValue(true));
		assertThat(metadata.getItems()).hasSize(4);
	}

	@Test
	public void mergeExistingPropertyDescription() throws Exception {
		ItemMetadata property = ItemMetadata.newProperty("simple", "comparator", null,
				null, null, "A nice comparator.", null, null);
		writeAdditionalMetadata(property);
		ConfigurationMetadata metadata = compile(SimpleProperties.class);
		assertThat(metadata)
				.has(Metadata.withProperty("simple.comparator", "java.util.Comparator<?>")
						.fromSource(SimpleProperties.class)
						.withDescription("A nice comparator."));
		assertThat(metadata.getItems()).hasSize(4);
	}

	@Test
	public void mergeExistingPropertyDeprecation() throws Exception {
		ItemMetadata property = ItemMetadata.newProperty("simple", "comparator", null,
				null, null, null, null,
				new ItemDeprecation("Don't use this.", "simple.complex-comparator"));
		writeAdditionalMetadata(property);
		ConfigurationMetadata metadata = compile(SimpleProperties.class);
		assertThat(metadata)
				.has(Metadata.withProperty("simple.comparator", "java.util.Comparator<?>")
						.fromSource(SimpleProperties.class)
						.withDeprecation("Don't use this.", "simple.complex-comparator"));
		assertThat(metadata.getItems()).hasSize(4);
	}

	@Test
	public void mergeExistingPropertyDeprecationOverride() throws Exception {
		ItemMetadata property = ItemMetadata.newProperty("singledeprecated", "name", null,
				null, null, null, null,
				new ItemDeprecation("Don't use this.", "single.name"));
		writeAdditionalMetadata(property);
		ConfigurationMetadata metadata = compile(DeprecatedSingleProperty.class);
		assertThat(metadata).has(
				Metadata.withProperty("singledeprecated.name", String.class.getName())
						.fromSource(DeprecatedSingleProperty.class)
						.withDeprecation("Don't use this.", "single.name"));
		assertThat(metadata.getItems()).hasSize(3);
	}

	@Test
	public void mergeOfInvalidAdditionalMetadata() throws IOException {
		File additionalMetadataFile = createAdditionalMetadataFile();
		FileCopyUtils.copy("Hello World", new FileWriter(additionalMetadataFile));

		this.thrown.expect(IllegalStateException.class);
		this.thrown.expectMessage("Compilation failed");
		compile(SimpleProperties.class);
	}

	@Test
	public void mergingOfSimpleHint() throws Exception {
		writeAdditionalHints(ItemHint.newHint("simple.the-name",
				new ItemHint.ValueHint("boot", "Bla bla"),
				new ItemHint.ValueHint("spring", null)));
		ConfigurationMetadata metadata = compile(SimpleProperties.class);
		assertThat(metadata).has(Metadata.withProperty("simple.the-name", String.class)
				.fromSource(SimpleProperties.class)
				.withDescription("The name of this simple properties.")
				.withDefaultValue("boot").withDeprecation(null, null));
		assertThat(metadata).has(Metadata.withHint("simple.the-name")
				.withValue(0, "boot", "Bla bla").withValue(1, "spring", null));
	}

	@Test
	public void mergingOfHintWithNonCanonicalName() throws Exception {
		writeAdditionalHints(ItemHint.newHint("simple.theName",
				new ItemHint.ValueHint("boot", "Bla bla")));
		ConfigurationMetadata metadata = compile(SimpleProperties.class);
		assertThat(metadata).has(Metadata.withProperty("simple.the-name", String.class)
				.fromSource(SimpleProperties.class)
				.withDescription("The name of this simple properties.")
				.withDefaultValue("boot").withDeprecation(null, null));
		assertThat(metadata).has(
				Metadata.withHint("simple.the-name").withValue(0, "boot", "Bla bla"));
	}

	@Test
	public void mergingOfHintWithProvider() throws Exception {
		writeAdditionalHints(new ItemHint("simple.theName",
				Collections.<ItemHint.ValueHint>emptyList(),
				Arrays.asList(
						new ItemHint.ValueProvider("first",
								Collections.<String, Object>singletonMap("target",
										"org.foo")),
						new ItemHint.ValueProvider("second", null))));
		ConfigurationMetadata metadata = compile(SimpleProperties.class);
		assertThat(metadata).has(Metadata.withProperty("simple.the-name", String.class)
				.fromSource(SimpleProperties.class)
				.withDescription("The name of this simple properties.")
				.withDefaultValue("boot").withDeprecation(null, null));
		assertThat(metadata).has(Metadata.withHint("simple.the-name")
				.withProvider("first", "target", "org.foo").withProvider("second"));
	}

	@Test
	public void mergingOfAdditionalDeprecation() throws Exception {
		writePropertyDeprecation(ItemMetadata.newProperty("simple", "wrongName",
				"java.lang.String", null, null, null, null,
				new ItemDeprecation("Lame name.", "simple.the-name")));
		ConfigurationMetadata metadata = compile(SimpleProperties.class);
		assertThat(metadata).has(Metadata.withProperty("simple.wrong-name", String.class)
				.withDeprecation("Lame name.", "simple.the-name"));
	}

	@Test
	public void mergingOfAdditionalMetadata() throws Exception {
		File metaInfFolder = new File(this.compiler.getOutputLocation(), "META-INF");
		metaInfFolder.mkdirs();
		File additionalMetadataFile = new File(metaInfFolder,
				"additional-spring-configuration-metadata.json");
		additionalMetadataFile.createNewFile();
		JSONObject property = new JSONObject();
		property.put("name", "foo");
		property.put("type", "java.lang.String");
		property.put("sourceType", AdditionalMetadata.class.getName());
		JSONArray properties = new JSONArray();
		properties.put(property);
		JSONObject additionalMetadata = new JSONObject();
		additionalMetadata.put("properties", properties);
		FileWriter writer = new FileWriter(additionalMetadataFile);
		writer.append(additionalMetadata.toString(2));
		writer.flush();
		writer.close();
		ConfigurationMetadata metadata = compile(SimpleProperties.class);
		assertThat(metadata).has(Metadata.withProperty("simple.comparator"));
		assertThat(metadata).has(Metadata.withProperty("foo", String.class)
				.fromSource(AdditionalMetadata.class));
	}

	@Test
	public void incrementalBuild() throws Exception {
		TestProject project = new TestProject(this.temporaryFolder, FooProperties.class,
				BarProperties.class);
		assertThat(project.getOutputFile(MetadataStore.METADATA_PATH).exists()).isFalse();
		ConfigurationMetadata metadata = project.fullBuild();
		assertThat(project.getOutputFile(MetadataStore.METADATA_PATH).exists()).isTrue();
		assertThat(metadata).has(
				Metadata.withProperty("foo.counter").fromSource(FooProperties.class));
		assertThat(metadata).has(
				Metadata.withProperty("bar.counter").fromSource(BarProperties.class));
		metadata = project.incrementalBuild(BarProperties.class);
		assertThat(metadata).has(
				Metadata.withProperty("foo.counter").fromSource(FooProperties.class));
		assertThat(metadata).has(
				Metadata.withProperty("bar.counter").fromSource(BarProperties.class));
		project.addSourceCode(BarProperties.class,
				BarProperties.class.getResourceAsStream("BarProperties.snippet"));
		metadata = project.incrementalBuild(BarProperties.class);
		assertThat(metadata).has(Metadata.withProperty("bar.extra"));
		assertThat(metadata).has(Metadata.withProperty("foo.counter"));
		assertThat(metadata).has(Metadata.withProperty("bar.counter"));
		project.revert(BarProperties.class);
		metadata = project.incrementalBuild(BarProperties.class);
		assertThat(metadata).isNotEqualTo(Metadata.withProperty("bar.extra"));
		assertThat(metadata).has(Metadata.withProperty("foo.counter"));
		assertThat(metadata).has(Metadata.withProperty("bar.counter"));
	}

	@Test
	public void incrementalBuildAnnotationRemoved() throws Exception {
		TestProject project = new TestProject(this.temporaryFolder, FooProperties.class,
				BarProperties.class);
		ConfigurationMetadata metadata = project.fullBuild();
		assertThat(metadata).has(Metadata.withProperty("foo.counter"));
		assertThat(metadata).has(Metadata.withProperty("bar.counter"));
		project.replaceText(BarProperties.class, "@ConfigurationProperties",
				"//@ConfigurationProperties");
		metadata = project.incrementalBuild(BarProperties.class);
		assertThat(metadata).has(Metadata.withProperty("foo.counter"));
		assertThat(metadata).isNotEqualTo(Metadata.withProperty("bar.counter"));
	}

	@Test
	public void incrementalBuildTypeRenamed() throws Exception {
		TestProject project = new TestProject(this.temporaryFolder, FooProperties.class,
				BarProperties.class);
		ConfigurationMetadata metadata = project.fullBuild();
		assertThat(metadata).has(
				Metadata.withProperty("foo.counter").fromSource(FooProperties.class));
		assertThat(metadata).has(
				Metadata.withProperty("bar.counter").fromSource(BarProperties.class));
		assertThat(metadata).doesNotHave(Metadata.withProperty("bar.counter")
				.fromSource(RenamedBarProperties.class));
		project.delete(BarProperties.class);
		project.add(RenamedBarProperties.class);
		metadata = project.incrementalBuild(RenamedBarProperties.class);
		assertThat(metadata).has(
				Metadata.withProperty("foo.counter").fromSource(FooProperties.class));
		assertThat(metadata).doesNotHave(
				Metadata.withProperty("bar.counter").fromSource(BarProperties.class));
		assertThat(metadata).has(Metadata.withProperty("bar.counter")
				.fromSource(RenamedBarProperties.class));
	}

	private void assertSimpleLombokProperties(ConfigurationMetadata metadata,
			Class<?> source, String prefix) {
		assertThat(metadata).has(Metadata.withGroup(prefix).fromSource(source));
		assertThat(metadata).doesNotHave(Metadata.withProperty(prefix + ".id"));
		assertThat(metadata).has(Metadata.withProperty(prefix + ".name", String.class)
				.fromSource(source).withDescription("Name description."));
		assertThat(metadata).has(Metadata.withProperty(prefix + ".description"));
		assertThat(metadata).has(Metadata.withProperty(prefix + ".counter"));
		assertThat(metadata).has(Metadata.withProperty(prefix + ".number")
				.fromSource(source).withDefaultValue(0).withDeprecation(null, null));
		assertThat(metadata).has(Metadata.withProperty(prefix + ".items"));
		assertThat(metadata).doesNotHave(Metadata.withProperty(prefix + ".ignored"));
	}

	private ConfigurationMetadata compile(Class<?>... types) throws IOException {
		TestConfigurationMetadataAnnotationProcessor processor = new TestConfigurationMetadataAnnotationProcessor(
				this.compiler.getOutputLocation());
		this.compiler.getTask(types).call(processor);
		return processor.getMetadata();
	}

	private void writeAdditionalMetadata(ItemMetadata... metadata) throws Exception {
		TestJsonConverter converter = new TestJsonConverter();
		File additionalMetadataFile = createAdditionalMetadataFile();
		JSONObject additionalMetadata = new JSONObject();
		JSONArray properties = new JSONArray();
		for (ItemMetadata itemMetadata : metadata) {
			properties.put(converter.toJsonObject(itemMetadata));
		}
		additionalMetadata.put("properties", properties);
		writeMetadata(additionalMetadataFile, additionalMetadata);
	}

	private void writeAdditionalHints(ItemHint... hints) throws Exception {
		TestJsonConverter converter = new TestJsonConverter();
		File additionalMetadataFile = createAdditionalMetadataFile();
		JSONObject additionalMetadata = new JSONObject();
		additionalMetadata.put("hints", converter.toJsonArray(Arrays.asList(hints)));
		writeMetadata(additionalMetadataFile, additionalMetadata);
	}

	private void writePropertyDeprecation(ItemMetadata... items) throws Exception {
		File additionalMetadataFile = createAdditionalMetadataFile();
		JSONArray propertiesArray = new JSONArray();
		for (ItemMetadata item : items) {
			JSONObject jsonObject = new JSONObject();
			jsonObject.put("name", item.getName());
			if (item.getType() != null) {
				jsonObject.put("type", item.getType());
			}
			ItemDeprecation deprecation = item.getDeprecation();
			if (deprecation != null) {
				JSONObject deprecationJson = new JSONObject();
				if (deprecation.getReason() != null) {
					deprecationJson.put("reason", deprecation.getReason());
				}
				if (deprecation.getReplacement() != null) {
					deprecationJson.put("replacement", deprecation.getReplacement());
				}
				jsonObject.put("deprecation", deprecationJson);
			}
			propertiesArray.put(jsonObject);

		}
		JSONObject additionalMetadata = new JSONObject();
		additionalMetadata.put("properties", propertiesArray);
		writeMetadata(additionalMetadataFile, additionalMetadata);
	}

	private File createAdditionalMetadataFile() throws IOException {
		File metaInfFolder = new File(this.compiler.getOutputLocation(), "META-INF");
		metaInfFolder.mkdirs();
		File additionalMetadataFile = new File(metaInfFolder,
				"additional-spring-configuration-metadata.json");
		additionalMetadataFile.createNewFile();
		return additionalMetadataFile;
	}

	private void writeMetadata(File metadataFile, JSONObject metadata) throws Exception {
		FileWriter writer = new FileWriter(metadataFile);
		try {
			writer.append(metadata.toString(2));
		}
		finally {
			writer.close();
		}
	}

	private static class AdditionalMetadata {

	}

}
