/*
 * Copyright 2014-2018 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.data.mongodb.core.index;

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.springframework.data.mongodb.test.util.IsBsonObject.*;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collections;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.annotation.Id;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.DocumentTestUtils;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.IndexDefinitionHolder;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.CompoundIndexResolutionTests;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.GeoSpatialIndexResolutionTests;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.IndexResolutionTests;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.MixedIndexResolutionTests;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.TextIndexedResolutionTests;
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.Language;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.util.ClassTypeInformation;

/**
 * @author Christoph Strobl
 * @author Mark Paluch
 */
@RunWith(Suite.class)
@SuiteClasses({ IndexResolutionTests.class, GeoSpatialIndexResolutionTests.class, CompoundIndexResolutionTests.class,
		TextIndexedResolutionTests.class, MixedIndexResolutionTests.class })
public class MongoPersistentEntityIndexResolverUnitTests {

	/**
	 * Test resolution of {@link Indexed}.
	 *
	 * @author Christoph Strobl
	 */
	public static class IndexResolutionTests {

		@Test // DATAMONGO-899
		public void indexPathOnLevelZeroIsResolvedCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					IndexOnLevelZero.class);

			assertThat(indexDefinitions, hasSize(1));
			assertIndexPathAndCollection("indexedProperty", "Zero", indexDefinitions.get(0));
		}

		@Test // DATAMONGO-899
		public void indexPathOnLevelOneIsResolvedCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(IndexOnLevelOne.class);

			assertThat(indexDefinitions, hasSize(1));
			assertIndexPathAndCollection("zero.indexedProperty", "One", indexDefinitions.get(0));
		}

		@Test // DATAMONGO-899
		public void depplyNestedIndexPathIsResolvedCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(IndexOnLevelTwo.class);

			assertThat(indexDefinitions, hasSize(1));
			assertIndexPathAndCollection("one.zero.indexedProperty", "Two", indexDefinitions.get(0));
		}

		@Test // DATAMONGO-899
		public void resolvesIndexPathNameForNamedPropertiesCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					IndexOnLevelOneWithExplicitlyNamedField.class);

			assertThat(indexDefinitions, hasSize(1));
			assertIndexPathAndCollection("customZero.customFieldName", "indexOnLevelOneWithExplicitlyNamedField",
					indexDefinitions.get(0));
		}

		@Test // DATAMONGO-899
		public void resolvesIndexDefinitionCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					IndexOnLevelZero.class);

			IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
			assertThat(indexDefinition.getIndexOptions(), equalTo(new org.bson.Document().append("name", "indexedProperty")));
		}

		@Test // DATAMONGO-899
		public void resolvesIndexDefinitionOptionsCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					WithOptionsOnIndexedProperty.class);

			IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
			assertThat(indexDefinition.getIndexOptions(),
					equalTo(
							new org.bson.Document().append("name", "indexedProperty").append("unique", true)
									.append("sparse", true).append("background", true).append("expireAfterSeconds", 10L)));
		}

		@Test // DATAMONGO-1297
		public void resolvesIndexOnDbrefWhenDefined() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(WithDbRef.class);

			assertThat(indexDefinitions, hasSize(1));
			assertThat(indexDefinitions.get(0).getCollection(), equalTo("withDbRef"));
			assertThat(indexDefinitions.get(0).getIndexKeys(), equalTo(new org.bson.Document().append("indexedDbRef", 1)));
		}

		@Test // DATAMONGO-1297
		public void resolvesIndexOnDbrefWhenDefinedOnNestedElement() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					WrapperOfWithDbRef.class);

			assertThat(indexDefinitions, hasSize(1));
			assertThat(indexDefinitions.get(0).getCollection(), equalTo("wrapperOfWithDbRef"));
			assertThat(indexDefinitions.get(0).getIndexKeys(),
					equalTo(new org.bson.Document().append("nested.indexedDbRef", 1)));
		}

		@Test // DATAMONGO-1163
		public void resolveIndexDefinitionInMetaAnnotatedFields() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					IndexOnMetaAnnotatedField.class);

			assertThat(indexDefinitions, hasSize(1));
			assertThat(indexDefinitions.get(0).getCollection(), equalTo("indexOnMetaAnnotatedField"));
			assertThat(indexDefinitions.get(0).getIndexOptions(), equalTo(new org.bson.Document().append("name", "_name")));
		}

		@Test // DATAMONGO-1373
		public void resolveIndexDefinitionInComposedAnnotatedFields() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					IndexedDocumentWithComposedAnnotations.class);

			assertThat(indexDefinitions, hasSize(2));

			IndexDefinitionHolder indexDefinitionHolder = indexDefinitions.get(1);

			assertThat(indexDefinitionHolder.getIndexKeys(), isBsonObject().containing("fieldWithMyIndexName", 1));
			assertThat(indexDefinitionHolder.getIndexOptions(),
					isBsonObject().containing("sparse", true).containing("unique", true).containing("name", "my_index_name"));
		}

		@Test // DATAMONGO-1373
		public void resolveIndexDefinitionInCustomComposedAnnotatedFields() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					IndexedDocumentWithComposedAnnotations.class);

			assertThat(indexDefinitions, hasSize(2));

			IndexDefinitionHolder indexDefinitionHolder = indexDefinitions.get(0);

			assertThat(indexDefinitionHolder.getIndexKeys(), isBsonObject().containing("fieldWithDifferentIndexName", 1));
			assertThat(indexDefinitionHolder.getIndexOptions(),
					isBsonObject().containing("sparse", true).containing("name", "different_name").notContaining("unique"));
		}

		@Document("Zero")
		static class IndexOnLevelZero {
			@Indexed String indexedProperty;
		}

		@Document("One")
		static class IndexOnLevelOne {
			IndexOnLevelZero zero;
		}

		@Document("Two")
		static class IndexOnLevelTwo {
			IndexOnLevelOne one;
		}

		@Document("WithOptionsOnIndexedProperty")
		static class WithOptionsOnIndexedProperty {

			@Indexed(background = true, direction = IndexDirection.DESCENDING,
					dropDups = true, expireAfterSeconds = 10, sparse = true, unique = true) //
			String indexedProperty;
		}

		@Document
		static class IndexOnLevelOneWithExplicitlyNamedField {

			@Field("customZero") IndexOnLevelZeroWithExplicityNamedField zero;
		}

		static class IndexOnLevelZeroWithExplicityNamedField {

			@Indexed @Field("customFieldName") String namedProperty;
		}

		@Document
		static class WrapperOfWithDbRef {
			WithDbRef nested;
		}

		@Document
		static class WithDbRef {

			@Indexed //
			@DBRef //
			NoIndex indexedDbRef;
		}

		@Document("no-index")
		static class NoIndex {
			@Id String id;
		}

		@Document
		static class IndexedDocumentWithComposedAnnotations {

			@Id String id;
			@CustomIndexedAnnotation String fieldWithDifferentIndexName;
			@ComposedIndexedAnnotation String fieldWithMyIndexName;
		}

		@Retention(RetentionPolicy.RUNTIME)
		@Target({ ElementType.FIELD })
		@ComposedIndexedAnnotation(indexName = "different_name", beUnique = false)
		static @interface CustomIndexedAnnotation {
		}

		@Retention(RetentionPolicy.RUNTIME)
		@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
		@Indexed
		static @interface ComposedIndexedAnnotation {

			@AliasFor(annotation = Indexed.class, attribute = "unique")
			boolean beUnique() default true;

			@AliasFor(annotation = Indexed.class, attribute = "sparse")
			boolean beSparse() default true;

			@AliasFor(annotation = Indexed.class, attribute = "name")
			String indexName() default "my_index_name";
		}

		@Retention(RetentionPolicy.RUNTIME)
		@Target(ElementType.FIELD)
		@org.springframework.data.mongodb.core.mapping.Field
		static @interface ComposedFieldAnnotation {

			@AliasFor(annotation = org.springframework.data.mongodb.core.mapping.Field.class, attribute = "value")
			String name() default "_id";
		}
	}

	@Target({ ElementType.FIELD })
	@Retention(RetentionPolicy.RUNTIME)
	@Indexed
	@interface IndexedFieldAnnotation {

	}

	@Document
	static class IndexOnMetaAnnotatedField {
		@Field("_name") @IndexedFieldAnnotation String lastname;
	}

	/**
	 * Test resolution of {@link GeoSpatialIndexed}.
	 *
	 * @author Christoph Strobl
	 */
	public static class GeoSpatialIndexResolutionTests {

		@Test // DATAMONGO-899
		public void geoSpatialIndexPathOnLevelZeroIsResolvedCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					GeoSpatialIndexOnLevelZero.class);

			assertThat(indexDefinitions, hasSize(1));
			assertIndexPathAndCollection("geoIndexedProperty", "Zero", indexDefinitions.get(0));
		}

		@Test // DATAMONGO-899
		public void geoSpatialIndexPathOnLevelOneIsResolvedCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					GeoSpatialIndexOnLevelOne.class);

			assertThat(indexDefinitions, hasSize(1));
			assertIndexPathAndCollection("zero.geoIndexedProperty", "One", indexDefinitions.get(0));
		}

		@Test // DATAMONGO-899
		public void depplyNestedGeoSpatialIndexPathIsResolvedCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					GeoSpatialIndexOnLevelTwo.class);

			assertThat(indexDefinitions, hasSize(1));
			assertIndexPathAndCollection("one.zero.geoIndexedProperty", "Two", indexDefinitions.get(0));
		}

		@Test // DATAMONGO-899
		public void resolvesIndexDefinitionOptionsCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					WithOptionsOnGeoSpatialIndexProperty.class);

			IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();

			assertThat(indexDefinition.getIndexOptions(), equalTo(
					new org.bson.Document().append("name", "location").append("min", 1).append("max", 100).append("bits", 2)));
		}

		@Test // DATAMONGO-1373
		public void resolvesComposedAnnotationIndexDefinitionOptionsCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					GeoSpatialIndexedDocumentWithComposedAnnotation.class);

			IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();

			assertThat(indexDefinition.getIndexKeys(),
					isBsonObject().containing("location", "geoHaystack").containing("What light?", 1));
			assertThat(indexDefinition.getIndexOptions(),
					isBsonObject().containing("name", "my_geo_index_name").containing("bucketSize", 2.0));
		}

		@Document("Zero")
		static class GeoSpatialIndexOnLevelZero {
			@GeoSpatialIndexed Point geoIndexedProperty;
		}

		@Document("One")
		static class GeoSpatialIndexOnLevelOne {
			GeoSpatialIndexOnLevelZero zero;
		}

		@Document("Two")
		static class GeoSpatialIndexOnLevelTwo {
			GeoSpatialIndexOnLevelOne one;
		}

		@Document("WithOptionsOnGeoSpatialIndexProperty")
		static class WithOptionsOnGeoSpatialIndexProperty {

			@GeoSpatialIndexed(bits = 2, max = 100, min = 1,
					type = GeoSpatialIndexType.GEO_2D) //
			Point location;
		}

		@Document("WithComposedAnnotation")
		static class GeoSpatialIndexedDocumentWithComposedAnnotation {

			@ComposedGeoSpatialIndexed //
			Point location;
		}

		@Retention(RetentionPolicy.RUNTIME)
		@Target({ ElementType.FIELD })
		@GeoSpatialIndexed
		@interface ComposedGeoSpatialIndexed {

			@AliasFor(annotation = GeoSpatialIndexed.class, attribute = "name")
			String indexName() default "my_geo_index_name";

			@AliasFor(annotation = GeoSpatialIndexed.class, attribute = "additionalField")
			String theAdditionalFieldINeedToDefine() default "What light?";

			@AliasFor(annotation = GeoSpatialIndexed.class, attribute = "bucketSize")
			double size() default 2;

			@AliasFor(annotation = GeoSpatialIndexed.class, attribute = "type")
			GeoSpatialIndexType indexType() default GeoSpatialIndexType.GEO_HAYSTACK;
		}

	}

	/**
	 * Test resolution of {@link CompoundIndexes}.
	 *
	 * @author Christoph Strobl
	 */
	public static class CompoundIndexResolutionTests {

		@Test // DATAMONGO-899
		public void compoundIndexPathOnLevelZeroIsResolvedCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					CompoundIndexOnLevelZero.class);

			assertThat(indexDefinitions, hasSize(1));
			assertIndexPathAndCollection(new String[] { "foo", "bar" }, "CompoundIndexOnLevelZero", indexDefinitions.get(0));
		}

		@Test // DATAMONGO-899
		public void compoundIndexOptionsResolvedCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					CompoundIndexOnLevelZero.class);

			IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
			assertThat(indexDefinition.getIndexOptions(), equalTo(new org.bson.Document().append("name", "compound_index")
					.append("unique", true).append("sparse", true).append("background", true)));
			assertThat(indexDefinition.getIndexKeys(), equalTo(new org.bson.Document().append("foo", 1).append("bar", -1)));
		}

		@Test // DATAMONGO-909
		public void compoundIndexOnSuperClassResolvedCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					IndexDefinedOnSuperClass.class);

			IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
			assertThat(indexDefinition.getIndexOptions(), equalTo(new org.bson.Document().append("name", "compound_index")
					.append("unique", true).append("sparse", true).append("background", true)));
			assertThat(indexDefinition.getIndexKeys(), equalTo(new org.bson.Document().append("foo", 1).append("bar", -1)));
		}

		@Test // DATAMONGO-827
		public void compoundIndexDoesNotSpecifyNameWhenUsingGenerateName() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					ComountIndexWithAutogeneratedName.class);

			IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
			assertThat(indexDefinition.getIndexOptions(), equalTo(new org.bson.Document().append("unique", true)
					.append("sparse", true).append("background", true)));
			assertThat(indexDefinition.getIndexKeys(), equalTo(new org.bson.Document().append("foo", 1).append("bar", -1)));
		}

		@Test // DATAMONGO-929
		public void compoundIndexPathOnLevelOneIsResolvedCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					CompoundIndexOnLevelOne.class);

			assertThat(indexDefinitions, hasSize(1));
			assertIndexPathAndCollection(new String[] { "zero.foo", "zero.bar" }, "CompoundIndexOnLevelOne",
					indexDefinitions.get(0));
		}

		@Test // DATAMONGO-929
		public void emptyCompoundIndexPathOnLevelOneIsResolvedCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					CompoundIndexOnLevelOneWithEmptyIndexDefinition.class);

			assertThat(indexDefinitions, hasSize(1));
			assertIndexPathAndCollection(new String[] { "zero" }, "CompoundIndexOnLevelZeroWithEmptyIndexDef",
					indexDefinitions.get(0));
		}

		@Test // DATAMONGO-929
		public void singleCompoundIndexPathOnLevelZeroIsResolvedCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					SingleCompoundIndex.class);

			assertThat(indexDefinitions, hasSize(1));
			assertIndexPathAndCollection(new String[] { "foo", "bar" }, "CompoundIndexOnLevelZero", indexDefinitions.get(0));
		}

		@Test // DATAMONGO-1373
		public void singleCompoundIndexUsingComposedAnnotationsOnTypeResolvedCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					CompoundIndexDocumentWithComposedAnnotation.class);

			assertThat(indexDefinitions, hasSize(1));
			assertThat(indexDefinitions.get(0).getIndexKeys(), isBsonObject().containing("foo", 1).containing("bar", -1));
			assertThat(indexDefinitions.get(0).getIndexOptions(), isBsonObject().containing("name", "my_compound_index_name")
					.containing("unique", true).containing("background", true));
		}

		@Document("CompoundIndexOnLevelOne")
		static class CompoundIndexOnLevelOne {

			CompoundIndexOnLevelZero zero;
		}

		@Document("CompoundIndexOnLevelZeroWithEmptyIndexDef")
		static class CompoundIndexOnLevelOneWithEmptyIndexDefinition {

			CompoundIndexOnLevelZeroWithEmptyIndexDef zero;
		}

		@Document("CompoundIndexOnLevelZero")
		@CompoundIndexes({ @CompoundIndex(name = "compound_index", def = "{'foo': 1, 'bar': -1}", background = true,
				dropDups = true, sparse = true, unique = true) })
		static class CompoundIndexOnLevelZero {}

		@CompoundIndexes({
				@CompoundIndex(name = "compound_index", background = true, dropDups = true, sparse = true, unique = true) })
		static class CompoundIndexOnLevelZeroWithEmptyIndexDef {}

		@Document("CompoundIndexOnLevelZero")
		@CompoundIndex(name = "compound_index", def = "{'foo': 1, 'bar': -1}", background = true, dropDups = true,
				sparse = true, unique = true)
		static class SingleCompoundIndex {}

		static class IndexDefinedOnSuperClass extends CompoundIndexOnLevelZero {

		}

		@Document("ComountIndexWithAutogeneratedName")
		@CompoundIndexes({ @CompoundIndex(useGeneratedName = true, def = "{'foo': 1, 'bar': -1}", background = true,
				dropDups = true, sparse = true, unique = true) })
		static class ComountIndexWithAutogeneratedName {

		}

		@Document("WithComposedAnnotation")
		@ComposedCompoundIndex
		static class CompoundIndexDocumentWithComposedAnnotation {

		}

		@Retention(RetentionPolicy.RUNTIME)
		@Target({ ElementType.TYPE })
		@CompoundIndex
		@interface ComposedCompoundIndex {

			@AliasFor(annotation = CompoundIndex.class, attribute = "def")
			String fields() default "{'foo': 1, 'bar': -1}";

			@AliasFor(annotation = CompoundIndex.class, attribute = "background")
			boolean inBackground() default true;

			@AliasFor(annotation = CompoundIndex.class, attribute = "name")
			String indexName() default "my_compound_index_name";

			@AliasFor(annotation = CompoundIndex.class, attribute = "useGeneratedName")
			boolean useGeneratedName() default false;

			@AliasFor(annotation = CompoundIndex.class, attribute = "unique")
			boolean isUnique() default true;

		}

	}

	public static class TextIndexedResolutionTests {

		@Test // DATAMONGO-937
		public void shouldResolveSingleFieldTextIndexCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					TextIndexOnSinglePropertyInRoot.class);
			assertThat(indexDefinitions.size(), equalTo(1));
			assertIndexPathAndCollection("bar", "textIndexOnSinglePropertyInRoot", indexDefinitions.get(0));
		}

		@Test // DATAMONGO-937
		public void shouldResolveMultiFieldTextIndexCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					TextIndexOnMutiplePropertiesInRoot.class);
			assertThat(indexDefinitions.size(), equalTo(1));
			assertIndexPathAndCollection(new String[] { "foo", "bar" }, "textIndexOnMutiplePropertiesInRoot",
					indexDefinitions.get(0));
		}

		@Test // DATAMONGO-937
		public void shouldResolveTextIndexOnElementCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					TextIndexOnNestedRoot.class);
			assertThat(indexDefinitions.size(), equalTo(1));
			assertIndexPathAndCollection(new String[] { "nested.foo" }, "textIndexOnNestedRoot", indexDefinitions.get(0));
		}

		@Test // DATAMONGO-937
		public void shouldResolveTextIndexOnElementWithWeightCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					TextIndexOnNestedWithWeightRoot.class);
			assertThat(indexDefinitions.size(), equalTo(1));
			assertIndexPathAndCollection(new String[] { "nested.foo" }, "textIndexOnNestedWithWeightRoot",
					indexDefinitions.get(0));

			org.bson.Document weights = DocumentTestUtils.getAsDocument(indexDefinitions.get(0).getIndexOptions(), "weights");
			assertThat(weights.get("nested.foo"), is((Object) 5F));
		}

		@Test // DATAMONGO-937
		public void shouldResolveTextIndexOnElementWithMostSpecificWeightCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					TextIndexOnNestedWithMostSpecificValueRoot.class);
			assertThat(indexDefinitions.size(), equalTo(1));
			assertIndexPathAndCollection(new String[] { "nested.foo", "nested.bar" },
					"textIndexOnNestedWithMostSpecificValueRoot", indexDefinitions.get(0));

			org.bson.Document weights = DocumentTestUtils.getAsDocument(indexDefinitions.get(0).getIndexOptions(), "weights");
			assertThat(weights.get("nested.foo"), is((Object) 5F));
			assertThat(weights.get("nested.bar"), is((Object) 10F));
		}

		@Test // DATAMONGO-937
		public void shouldSetDefaultLanguageCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					DocumentWithDefaultLanguage.class);
			assertThat(indexDefinitions.get(0).getIndexOptions().get("default_language"), is((Object) "spanish"));
		}

		@Test // DATAMONGO-937, DATAMONGO-1049
		public void shouldResolveTextIndexLanguageOverrideCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					DocumentWithLanguageOverride.class);
			assertThat(indexDefinitions.get(0).getIndexOptions().get("language_override"), is((Object) "lang"));
		}

		@Test // DATAMONGO-1049
		public void shouldIgnoreTextIndexLanguageOverrideOnNestedElements() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					DocumentWithLanguageOverrideOnNestedElement.class);
			assertThat(indexDefinitions.get(0).getIndexOptions().get("language_override"), is(nullValue()));
		}

		@Test // DATAMONGO-1049
		public void shouldNotCreateIndexDefinitionWhenOnlyLanguageButNoTextIndexPresent() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					DocumentWithNoTextIndexPropertyButReservedFieldLanguage.class);
			assertThat(indexDefinitions, is(empty()));
		}

		@Test // DATAMONGO-1049
		public void shouldNotCreateIndexDefinitionWhenOnlyAnnotatedLanguageButNoTextIndexPresent() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					DocumentWithNoTextIndexPropertyButReservedFieldLanguageAnnotated.class);
			assertThat(indexDefinitions, is(empty()));
		}

		@Test // DATAMONGO-1049
		public void shouldPreferExplicitlyAnnotatedLanguageProperty() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					DocumentWithOverlappingLanguageProps.class);
			assertThat(indexDefinitions.get(0).getIndexOptions().get("language_override"), is((Object) "lang"));
		}

		@Test // DATAMONGO-1373
		public void shouldResolveComposedAnnotationCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					TextIndexedDocumentWithComposedAnnotation.class);

			org.bson.Document weights = DocumentTestUtils.getAsDocument(indexDefinitions.get(0).getIndexOptions(), "weights");
			assertThat(weights, isBsonObject().containing("foo", 99f));
		}

		@Document
		static class TextIndexOnSinglePropertyInRoot {

			String foo;

			@TextIndexed String bar;
		}

		@Document
		static class TextIndexOnMutiplePropertiesInRoot {

			@TextIndexed String foo;

			@TextIndexed(weight = 5) String bar;
		}

		@Document
		static class TextIndexOnNestedRoot {

			String bar;

			@TextIndexed TextIndexOnNested nested;
		}

		static class TextIndexOnNested {

			String foo;
		}

		@Document
		static class TextIndexOnNestedWithWeightRoot {

			@TextIndexed(weight = 5) TextIndexOnNested nested;
		}

		@Document
		static class TextIndexOnNestedWithMostSpecificValueRoot {
			@TextIndexed(weight = 5) TextIndexOnNestedWithMostSpecificValue nested;
		}

		static class TextIndexOnNestedWithMostSpecificValue {

			String foo;
			@TextIndexed(weight = 10) String bar;
		}

		@Document(language = "spanish")
		static class DocumentWithDefaultLanguage {
			@TextIndexed String foo;
		}

		@Document
		static class DocumentWithLanguageOverrideOnNestedElement {

			DocumentWithLanguageOverride nested;
		}

		@Document
		static class DocumentWithLanguageOverride {

			@TextIndexed String foo;

			@Language String lang;
		}

		@Document
		static class DocumentWithNoTextIndexPropertyButReservedFieldLanguage {

			String language;
		}

		@Document
		static class DocumentWithNoTextIndexPropertyButReservedFieldLanguageAnnotated {

			@Field("language") String lang;
		}

		@Document
		static class DocumentWithOverlappingLanguageProps {

			@TextIndexed String foo;
			String language;
			@Language String lang;
		}

		@Document
		static class TextIndexedDocumentWithComposedAnnotation {

			@ComposedTextIndexedAnnotation String foo;
			String lang;
		}

		@Retention(RetentionPolicy.RUNTIME)
		@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
		@TextIndexed
		static @interface ComposedTextIndexedAnnotation {

			@AliasFor(annotation = TextIndexed.class, attribute = "weight")
			float heavyweight() default 99f;
		}
	}

	public static class MixedIndexResolutionTests {

		@Test // DATAMONGO-899
		public void multipleIndexesResolvedCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(MixedIndexRoot.class);

			assertThat(indexDefinitions, hasSize(2));
			assertThat(indexDefinitions.get(0).getIndexDefinition(), instanceOf(Index.class));
			assertThat(indexDefinitions.get(1).getIndexDefinition(), instanceOf(GeospatialIndex.class));
		}

		@Test // DATAMONGO-899
		public void cyclicPropertyReferenceOverDBRefShouldNotBeTraversed() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(Inner.class);
			assertThat(indexDefinitions, hasSize(1));
			assertThat(indexDefinitions.get(0).getIndexDefinition().getIndexKeys(),
					equalTo(new org.bson.Document().append("outer", 1)));
		}

		@Test // DATAMONGO-899
		public void associationsShouldNotBeTraversed() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(Outer.class);
			assertThat(indexDefinitions, empty());
		}

		@Test // DATAMONGO-926
		public void shouldNotRunIntoStackOverflow() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					CycleStartingInBetween.class);
			assertThat(indexDefinitions, hasSize(1));
		}

		@Test // DATAMONGO-926
		public void indexShouldBeFoundEvenForCyclePropertyReferenceOnLevelZero() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(CycleLevelZero.class);
			assertIndexPathAndCollection("indexedProperty", "cycleLevelZero", indexDefinitions.get(0));
			assertIndexPathAndCollection("cyclicReference.indexedProperty", "cycleLevelZero", indexDefinitions.get(1));
			assertThat(indexDefinitions, hasSize(2));
		}

		@Test // DATAMONGO-926
		public void indexShouldBeFoundEvenForCyclePropertyReferenceOnLevelOne() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(CycleOnLevelOne.class);
			assertIndexPathAndCollection("reference.indexedProperty", "cycleOnLevelOne", indexDefinitions.get(0));
			assertThat(indexDefinitions, hasSize(1));
		}

		@Test // DATAMONGO-926
		public void indexBeResolvedCorrectlyWhenPropertiesOfDifferentTypesAreNamedEqually() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					NoCycleButIdenticallyNamedProperties.class);
			assertIndexPathAndCollection("foo", "noCycleButIdenticallyNamedProperties", indexDefinitions.get(0));
			assertIndexPathAndCollection("reference.foo", "noCycleButIdenticallyNamedProperties", indexDefinitions.get(1));
			assertIndexPathAndCollection("reference.deep.foo", "noCycleButIdenticallyNamedProperties",
					indexDefinitions.get(2));
			assertThat(indexDefinitions, hasSize(3));
		}

		@Test // DATAMONGO-949
		public void shouldNotDetectCycleInSimilarlyNamedProperties() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					SimilarityHolingBean.class);
			assertIndexPathAndCollection("norm", "similarityHolingBean", indexDefinitions.get(0));
			assertThat(indexDefinitions, hasSize(1));
		}

		@Test // DATAMONGO-962
		public void shouldDetectSelfCycleViaCollectionTypeCorrectly() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					SelfCyclingViaCollectionType.class);
			assertThat(indexDefinitions, empty());
		}

		@Test // DATAMONGO-962
		public void shouldNotDetectCycleWhenTypeIsUsedMoreThanOnce() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					MultipleObjectsOfSameType.class);
			assertThat(indexDefinitions, empty());
		}

		@Test // DATAMONGO-962
		@SuppressWarnings({ "rawtypes", "unchecked" })
		public void shouldCatchCyclicReferenceExceptionOnRoot() {

			MongoPersistentEntity entity = new BasicMongoPersistentEntity<Object>(ClassTypeInformation.from(Object.class));

			MongoPersistentProperty propertyMock = mock(MongoPersistentProperty.class);
			when(propertyMock.isEntity()).thenReturn(true);
			when(propertyMock.getOwner()).thenReturn(entity);
			when(propertyMock.getActualType()).thenThrow(
					new MongoPersistentEntityIndexResolver.CyclicPropertyReferenceException("foo", Object.class, "bar"));

			MongoPersistentEntity<SelfCyclingViaCollectionType> selfCyclingEntity = new BasicMongoPersistentEntity<SelfCyclingViaCollectionType>(
					ClassTypeInformation.from(SelfCyclingViaCollectionType.class));

			new MongoPersistentEntityIndexResolver(prepareMappingContext(SelfCyclingViaCollectionType.class))
					.resolveIndexForEntity(selfCyclingEntity);
		}

		@Test // DATAMONGO-1782
		public void shouldAllowMultiplePathsToDeeplyType() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					NoCycleManyPathsToDeepValueObject.class);

			assertIndexPathAndCollection("l3.valueObject.value", "rules", indexDefinitions.get(0));
			assertIndexPathAndCollection("l2.l3.valueObject.value", "rules", indexDefinitions.get(1));
			assertThat(indexDefinitions, hasSize(2));
		}

		@Test // DATAMONGO-1025
		public void shouldUsePathIndexAsIndexNameForDocumentsHavingNamedNestedCompoundIndexFixedOnCollection() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					DocumentWithNestedDocumentHavingNamedCompoundIndex.class);
			assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"),
					equalTo("propertyOfTypeHavingNamedCompoundIndex.c_index"));
		}

		@Test // DATAMONGO-1025
		public void shouldUseIndexNameForNestedTypesWithNamedCompoundIndexDefinition() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					DocumentWithNestedTypeHavingNamedCompoundIndex.class);
			assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"),
					equalTo("propertyOfTypeHavingNamedCompoundIndex.c_index"));
		}

		@Test // DATAMONGO-1025
		public void shouldUsePathIndexAsIndexNameForDocumentsHavingNamedNestedIndexFixedOnCollection() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					DocumentWithNestedDocumentHavingNamedIndex.class);
			assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"),
					equalTo("propertyOfTypeHavingNamedIndex.property_index"));
		}

		@Test // DATAMONGO-1025
		public void shouldUseIndexNameForNestedTypesWithNamedIndexDefinition() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					DocumentWithNestedTypeHavingNamedIndex.class);
			assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"),
					equalTo("propertyOfTypeHavingNamedIndex.property_index"));
		}

		@Test // DATAMONGO-1025
		public void shouldUseIndexNameOnRootLevel() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					DocumentWithNamedIndex.class);
			assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"), equalTo("property_index"));
		}

		@Test // DATAMONGO-1087
		public void shouldAllowMultiplePropertiesOfSameTypeWithMatchingStartLettersOnRoot() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					MultiplePropertiesOfSameTypeWithMatchingStartLetters.class);

			assertThat(indexDefinitions, hasSize(2));
			assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"), equalTo("name.component"));
			assertThat((String) indexDefinitions.get(1).getIndexOptions().get("name"), equalTo("nameLast.component"));
		}

		@Test // DATAMONGO-1087
		public void shouldAllowMultiplePropertiesOfSameTypeWithMatchingStartLettersOnNestedProperty() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					MultiplePropertiesOfSameTypeWithMatchingStartLettersOnNestedProperty.class);

			assertThat(indexDefinitions, hasSize(2));
			assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"), equalTo("component.nameLast"));
			assertThat((String) indexDefinitions.get(1).getIndexOptions().get("name"), equalTo("component.name"));
		}

		@Test // DATAMONGO-1121
		public void shouldOnlyConsiderEntitiesAsPotentialCycleCandidates() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					OuterDocumentReferingToIndexedPropertyViaDifferentNonCyclingPaths.class);

			assertThat(indexDefinitions, hasSize(2));
			assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"), equalTo("path1.foo"));
			assertThat((String) indexDefinitions.get(1).getIndexOptions().get("name"),
					equalTo("path2.propertyWithIndexedStructure.foo"));

		}

		@Test // DATAMONGO-1263
		public void shouldConsiderGenericTypeArgumentsOfCollectionElements() {

			List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
					EntityWithGenericTypeWrapperAsElement.class);

			assertThat(indexDefinitions, hasSize(1));
			assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"),
					equalTo("listWithGeneircTypeElement.entity.property_index"));
		}

		@Document
		static class MixedIndexRoot {

			@Indexed String first;
			NestedGeoIndex nestedGeo;
		}

		static class NestedGeoIndex {

			@GeoSpatialIndexed Point location;
		}

		@Document
		static class Outer {

			@DBRef Inner inner;
		}

		@Document
		static class Inner {

			@Indexed Outer outer;
		}

		@Document
		static class CycleLevelZero {

			@Indexed String indexedProperty;
			CycleLevelZero cyclicReference;
		}

		@Document
		static class CycleOnLevelOne {

			CycleOnLevelOneReferenced reference;
		}

		static class CycleOnLevelOneReferenced {

			@Indexed String indexedProperty;
			CycleOnLevelOne cyclicReference;
		}

		@Document
		public static class CycleStartingInBetween {

			CycleOnLevelOne referenceToCycleStart;
		}

		@Document
		static class NoCycleButIdenticallyNamedProperties {

			@Indexed String foo;
			NoCycleButIdenticallyNamedPropertiesNested reference;
		}

		static class NoCycleButIdenticallyNamedPropertiesNested {

			@Indexed String foo;
			NoCycleButIndenticallNamedPropertiesDeeplyNested deep;
		}

		static class NoCycleButIndenticallNamedPropertiesDeeplyNested {

			@Indexed String foo;
		}

		@Document("rules")
		static class NoCycleManyPathsToDeepValueObject {

			private NoCycleLevel3 l3;
			private NoCycleLevel2 l2;
		}

		static class NoCycleLevel2 {
			private NoCycleLevel3 l3;
		}

		static class NoCycleLevel3 {
			private ValueObject valueObject;
		}

		static class ValueObject {
			@Indexed private String value;
		}

		@Document
		static class SimilarityHolingBean {

			@Indexed @Field("norm") String normalProperty;
			@Field("similarityL") private List<SimilaritySibling> listOfSimilarilyNamedEntities = null;
		}

		static class SimilaritySibling {
			@Field("similarity") private String similarThoughNotEqualNamedProperty;
		}

		@Document
		static class MultipleObjectsOfSameType {

			SelfCyclingViaCollectionType cycleOne;

			SelfCyclingViaCollectionType cycleTwo;
		}

		@Document
		static class SelfCyclingViaCollectionType {

			List<SelfCyclingViaCollectionType> cyclic;

		}

		@Document
		@CompoundIndex(name = "c_index", def = "{ foo:1, bar:1 }")
		static class DocumentWithNamedCompoundIndex {

			String property;
		}

		@Document
		static class DocumentWithNamedIndex {

			@Indexed(name = "property_index") String property;
		}

		static class TypeWithNamedIndex {

			@Indexed(name = "property_index") String property;
		}

		@Document
		static class DocumentWithNestedDocumentHavingNamedCompoundIndex {

			DocumentWithNamedCompoundIndex propertyOfTypeHavingNamedCompoundIndex;
		}

		@CompoundIndex(name = "c_index", def = "{ foo:1, bar:1 }")
		static class TypeWithNamedCompoundIndex {
			String property;
		}

		@Document
		static class DocumentWithNestedTypeHavingNamedCompoundIndex {

			TypeWithNamedCompoundIndex propertyOfTypeHavingNamedCompoundIndex;
		}

		@Document
		static class DocumentWithNestedDocumentHavingNamedIndex {

			DocumentWithNamedIndex propertyOfTypeHavingNamedIndex;
		}

		@Document
		static class DocumentWithNestedTypeHavingNamedIndex {

			TypeWithNamedIndex propertyOfTypeHavingNamedIndex;
		}

		@Document
		public class MultiplePropertiesOfSameTypeWithMatchingStartLetters {

			public class NameComponent {

				@Indexed String component;
			}

			NameComponent name;
			NameComponent nameLast;
		}

		@Document
		public class MultiplePropertiesOfSameTypeWithMatchingStartLettersOnNestedProperty {

			public class NameComponent {

				@Indexed String nameLast;
				@Indexed String name;
			}

			NameComponent component;
		}

		@Document
		public static class OuterDocumentReferingToIndexedPropertyViaDifferentNonCyclingPaths {

			NoCycleButIndenticallNamedPropertiesDeeplyNested path1;
			AlternatePathToNoCycleButIndenticallNamedPropertiesDeeplyNestedDocument path2;
		}

		public static class AlternatePathToNoCycleButIndenticallNamedPropertiesDeeplyNestedDocument {
			NoCycleButIndenticallNamedPropertiesDeeplyNested propertyWithIndexedStructure;
		}

		static class GenericEntityWrapper<T> {
			T entity;
		}

		@Document
		static class EntityWithGenericTypeWrapperAsElement {
			List<GenericEntityWrapper<DocumentWithNamedIndex>> listWithGeneircTypeElement;
		}
	}

	private static List<IndexDefinitionHolder> prepareMappingContextAndResolveIndexForType(Class<?> type) {

		MongoMappingContext mappingContext = prepareMappingContext(type);
		MongoPersistentEntityIndexResolver resolver = new MongoPersistentEntityIndexResolver(mappingContext);
		return resolver.resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(type));
	}

	private static MongoMappingContext prepareMappingContext(Class<?> type) {

		MongoMappingContext mappingContext = new MongoMappingContext();
		mappingContext.setInitialEntitySet(Collections.singleton(type));
		mappingContext.initialize();

		return mappingContext;
	}

	private static void assertIndexPathAndCollection(String expectedPath, String expectedCollection,
			IndexDefinitionHolder holder) {
		assertIndexPathAndCollection(new String[] { expectedPath }, expectedCollection, holder);
	}

	private static void assertIndexPathAndCollection(String[] expectedPaths, String expectedCollection,
			IndexDefinitionHolder holder) {

		for (String expectedPath : expectedPaths) {
			assertThat(holder.getIndexDefinition().getIndexKeys().containsKey(expectedPath), equalTo(true));
		}

		assertThat(holder.getCollection(), equalTo(expectedCollection));
	}
}
