/*
 * Copyright 2013-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.convert;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.any;
import static org.springframework.data.mongodb.core.convert.LazyLoadingTestUtils.*;

import java.io.Serializable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.annotation.AccessType;
import org.springframework.data.annotation.AccessType.Type;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoExceptionTranslator;
import org.springframework.data.mongodb.core.convert.MappingMongoConverterUnitTests.Person;
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.mongodb.util.MongoClientVersion;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.SerializationUtils;

import com.mongodb.DBRef;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;

/**
 * Unit tests for {@link DbRefMappingMongoConverter}.
 *
 * @author Oliver Gierke
 * @author Thomas Darimont
 * @author Christoph Strobl
 * @author Mark Paluch
 */
@RunWith(MockitoJUnitRunner.class)
public class DbRefMappingMongoConverterUnitTests {

	MappingMongoConverter converter;
	MongoMappingContext mappingContext;

	@Mock MongoDbFactory dbFactory;
	DefaultDbRefResolver dbRefResolver;

	@Before
	public void setUp() {

		when(dbFactory.getExceptionTranslator()).thenReturn(new MongoExceptionTranslator());

		this.dbRefResolver = spy(new DefaultDbRefResolver(dbFactory));
		this.mappingContext = new MongoMappingContext();
		this.converter = new MappingMongoConverter(dbRefResolver, mappingContext);
	}

	@Test // DATAMONGO-347
	public void createsSimpleDBRefCorrectly() {

		Person person = new Person();
		person.id = "foo";

		DBRef dbRef = converter.toDBRef(person, null);
		assertThat(dbRef.getId(), is("foo"));
		assertThat(dbRef.getCollectionName(), is("person"));
	}

	@Test // DATAMONGO-657
	public void convertDocumentWithMapDBRef() {

		Document mapValDocument = new Document();
		mapValDocument.put("_id", BigInteger.ONE);

		DBRef dbRef = mock(DBRef.class);
		when(dbRef.getId()).thenReturn(BigInteger.ONE);
		when(dbRef.getCollectionName()).thenReturn("collection-1");

		if (MongoClientVersion.isMongo3Driver()) {
			MongoDatabase dbMock = mock(MongoDatabase.class);
			MongoCollection collectionMock = mock(MongoCollection.class);
			when(dbFactory.getDb()).thenReturn(dbMock);
			when(dbMock.getCollection(anyString(), eq(Document.class))).thenReturn(collectionMock);

			FindIterable fi = mock(FindIterable.class);
			when(fi.first()).thenReturn(mapValDocument);
			when(collectionMock.find(Mockito.any(Bson.class))).thenReturn(fi);
		} else {
			when(dbRefResolver.fetch(dbRef)).thenReturn(mapValDocument);
		}

		MapDBRef mapDBRef = new MapDBRef();

		MapDBRefVal val = new MapDBRefVal();
		val.id = BigInteger.ONE;

		Map<String, MapDBRefVal> mapVal = new HashMap<>();
		mapVal.put("test", val);

		mapDBRef.map = mapVal;

		Document document = new Document();
		converter.write(mapDBRef, document);

		Document map = (Document) document.get("map");

		assertThat(map.get("test"), instanceOf(DBRef.class));

		((Document) document.get("map")).put("test", dbRef);

		MapDBRef read = converter.read(MapDBRef.class, document);

		assertThat(read.map.get("test").id, is(BigInteger.ONE));
	}

	@Test // DATAMONGO-347
	public void createsDBRefWithClientSpecCorrectly() {

		PropertyPath path = PropertyPath.from("person", PersonClient.class);
		MongoPersistentProperty property = mappingContext.getPersistentPropertyPath(path).getLeafProperty();

		Person person = new Person();
		person.id = "foo";

		DBRef dbRef = converter.toDBRef(person, property);
		assertThat(dbRef.getId(), is("foo"));
		assertThat(dbRef.getCollectionName(), is("person"));
	}

	@Test // DATAMONGO-348
	public void lazyLoadingProxyForLazyDbRefOnInterface() {

		String id = "42";
		String value = "bubu";
		MappingMongoConverter converterSpy = spy(converter);
		doReturn(new Document("_id", id).append("value", value)).when(converterSpy).readRef(any());

		Document document = new Document();
		ClassWithLazyDbRefs lazyDbRefs = new ClassWithLazyDbRefs();
		lazyDbRefs.dbRefToInterface = new LinkedList<>(Collections.singletonList(new LazyDbRefTarget("1")));
		converterSpy.write(lazyDbRefs, document);

		ClassWithLazyDbRefs result = converterSpy.read(ClassWithLazyDbRefs.class, document);

		assertProxyIsResolved(result.dbRefToInterface, false);
		assertThat(result.dbRefToInterface.get(0).getId(), is(id));
		assertProxyIsResolved(result.dbRefToInterface, true);
		assertThat(result.dbRefToInterface.get(0).getValue(), is(value));
	}

	@Test // DATAMONGO-348
	public void lazyLoadingProxyForLazyDbRefOnConcreteCollection() {

		String id = "42";
		String value = "bubu";
		MappingMongoConverter converterSpy = spy(converter);
		doReturn(new Document("_id", id).append("value", value)).when(converterSpy).readRef(any());

		Document document = new Document();
		ClassWithLazyDbRefs lazyDbRefs = new ClassWithLazyDbRefs();
		lazyDbRefs.dbRefToConcreteCollection = new ArrayList<>(Collections.singletonList(new LazyDbRefTarget(id, value)));
		converterSpy.write(lazyDbRefs, document);

		ClassWithLazyDbRefs result = converterSpy.read(ClassWithLazyDbRefs.class, document);

		assertProxyIsResolved(result.dbRefToConcreteCollection, false);
		assertThat(result.dbRefToConcreteCollection.get(0).getId(), is(id));
		assertProxyIsResolved(result.dbRefToConcreteCollection, true);
		assertThat(result.dbRefToConcreteCollection.get(0).getValue(), is(value));
	}

	@Test // DATAMONGO-348
	public void lazyLoadingProxyForLazyDbRefOnConcreteType() {

		String id = "42";
		String value = "bubu";
		MappingMongoConverter converterSpy = spy(converter);
		doReturn(new Document("_id", id).append("value", value)).when(converterSpy).readRef(any());

		Document document = new Document();
		ClassWithLazyDbRefs lazyDbRefs = new ClassWithLazyDbRefs();
		lazyDbRefs.dbRefToConcreteType = new LazyDbRefTarget(id, value);
		converterSpy.write(lazyDbRefs, document);

		ClassWithLazyDbRefs result = converterSpy.read(ClassWithLazyDbRefs.class, document);

		assertProxyIsResolved(result.dbRefToConcreteType, false);
		assertThat(result.dbRefToConcreteType.getId(), is(id));
		assertProxyIsResolved(result.dbRefToConcreteType, true);
		assertThat(result.dbRefToConcreteType.getValue(), is(value));
	}

	@Test // DATAMONGO-348
	public void lazyLoadingProxyForLazyDbRefOnConcreteTypeWithPersistenceConstructor() {

		String id = "42";
		String value = "bubu";
		MappingMongoConverter converterSpy = spy(converter);
		doReturn(new Document("_id", id).append("value", value)).when(converterSpy).readRef(any());

		Document document = new Document();
		ClassWithLazyDbRefs lazyDbRefs = new ClassWithLazyDbRefs();
		lazyDbRefs.dbRefToConcreteTypeWithPersistenceConstructor = new LazyDbRefTargetWithPeristenceConstructor(id, value);
		converterSpy.write(lazyDbRefs, document);

		ClassWithLazyDbRefs result = converterSpy.read(ClassWithLazyDbRefs.class, document);

		assertProxyIsResolved(result.dbRefToConcreteTypeWithPersistenceConstructor, false);
		assertThat(result.dbRefToConcreteTypeWithPersistenceConstructor.getId(), is(id));
		assertProxyIsResolved(result.dbRefToConcreteTypeWithPersistenceConstructor, true);
		assertThat(result.dbRefToConcreteTypeWithPersistenceConstructor.getValue(), is(value));
	}

	@Test // DATAMONGO-348
	public void lazyLoadingProxyForLazyDbRefOnConcreteTypeWithPersistenceConstructorButWithoutDefaultConstructor() {

		String id = "42";
		String value = "bubu";
		MappingMongoConverter converterSpy = spy(converter);
		doReturn(new Document("_id", id).append("value", value)).when(converterSpy).readRef(any());

		Document document = new Document();
		ClassWithLazyDbRefs lazyDbRefs = new ClassWithLazyDbRefs();
		lazyDbRefs.dbRefToConcreteTypeWithPersistenceConstructorWithoutDefaultConstructor = new LazyDbRefTargetWithPeristenceConstructorWithoutDefaultConstructor(
				id, value);
		converterSpy.write(lazyDbRefs, document);

		ClassWithLazyDbRefs result = converterSpy.read(ClassWithLazyDbRefs.class, document);

		assertProxyIsResolved(result.dbRefToConcreteTypeWithPersistenceConstructorWithoutDefaultConstructor, false);
		assertThat(result.dbRefToConcreteTypeWithPersistenceConstructorWithoutDefaultConstructor.getId(), is(id));
		assertProxyIsResolved(result.dbRefToConcreteTypeWithPersistenceConstructorWithoutDefaultConstructor, true);
		assertThat(result.dbRefToConcreteTypeWithPersistenceConstructorWithoutDefaultConstructor.getValue(), is(value));
	}

	@Test // DATAMONGO-348
	public void lazyLoadingProxyForSerializableLazyDbRefOnConcreteType() {

		String id = "42";
		String value = "bubu";
		MappingMongoConverter converterSpy = spy(converter);
		doReturn(new Document("_id", id).append("value", value)).when(converterSpy).readRef(any());

		Document document = new Document();
		SerializableClassWithLazyDbRefs lazyDbRefs = new SerializableClassWithLazyDbRefs();
		lazyDbRefs.dbRefToSerializableTarget = new SerializableLazyDbRefTarget(id, value);
		converterSpy.write(lazyDbRefs, document);

		SerializableClassWithLazyDbRefs result = converterSpy.read(SerializableClassWithLazyDbRefs.class, document);

		SerializableClassWithLazyDbRefs deserializedResult = (SerializableClassWithLazyDbRefs) transport(result);

		assertThat(deserializedResult.dbRefToSerializableTarget.getId(), is(id));
		assertProxyIsResolved(deserializedResult.dbRefToSerializableTarget, true);
		assertThat(deserializedResult.dbRefToSerializableTarget.getValue(), is(value));
	}

	@Test // DATAMONGO-884
	public void lazyLoadingProxyForToStringObjectMethodOverridingDbref() {

		String id = "42";
		String value = "bubu";
		MappingMongoConverter converterSpy = spy(converter);
		doReturn(new Document("_id", id).append("value", value)).when(converterSpy).readRef(any());

		Document document = new Document();
		WithObjectMethodOverrideLazyDbRefs lazyDbRefs = new WithObjectMethodOverrideLazyDbRefs();
		lazyDbRefs.dbRefToToStringObjectMethodOverride = new ToStringObjectMethodOverrideLazyDbRefTarget(id, value);
		converterSpy.write(lazyDbRefs, document);

		WithObjectMethodOverrideLazyDbRefs result = converterSpy.read(WithObjectMethodOverrideLazyDbRefs.class, document);

		assertThat(result.dbRefToToStringObjectMethodOverride, is(notNullValue()));
		assertProxyIsResolved(result.dbRefToToStringObjectMethodOverride, false);
		assertThat(result.dbRefToToStringObjectMethodOverride.toString(), is(id + ":" + value));
		assertProxyIsResolved(result.dbRefToToStringObjectMethodOverride, true);
	}

	@Test // DATAMONGO-884
	public void callingToStringObjectMethodOnLazyLoadingDbrefShouldNotInitializeProxy() {

		String id = "42";
		String value = "bubu";
		MappingMongoConverter converterSpy = spy(converter);
		doReturn(new Document("_id", id).append("value", value)).when(converterSpy).readRef(any());

		Document document = new Document();
		WithObjectMethodOverrideLazyDbRefs lazyDbRefs = new WithObjectMethodOverrideLazyDbRefs();
		lazyDbRefs.dbRefToPlainObject = new LazyDbRefTarget(id, value);
		converterSpy.write(lazyDbRefs, document);

		WithObjectMethodOverrideLazyDbRefs result = converterSpy.read(WithObjectMethodOverrideLazyDbRefs.class, document);

		assertThat(result.dbRefToPlainObject, is(notNullValue()));
		assertProxyIsResolved(result.dbRefToPlainObject, false);

		// calling Object#toString does not initialize the proxy.
		String proxyString = result.dbRefToPlainObject.toString();
		assertThat(proxyString, is("lazyDbRefTarget" + ":" + id + "$LazyLoadingProxy"));
		assertProxyIsResolved(result.dbRefToPlainObject, false);

		// calling another method not declared on object triggers proxy initialization.
		assertThat(result.dbRefToPlainObject.getValue(), is(value));
		assertProxyIsResolved(result.dbRefToPlainObject, true);
	}

	@Test // DATAMONGO-884
	public void equalsObjectMethodOnLazyLoadingDbrefShouldNotInitializeProxy() {

		String id = "42";
		String value = "bubu";
		MappingMongoConverter converterSpy = spy(converter);

		Document document = new Document();
		WithObjectMethodOverrideLazyDbRefs lazyDbRefs = new WithObjectMethodOverrideLazyDbRefs();
		lazyDbRefs.dbRefToPlainObject = new LazyDbRefTarget(id, value);
		lazyDbRefs.dbRefToToStringObjectMethodOverride = new ToStringObjectMethodOverrideLazyDbRefTarget(id, value);
		converterSpy.write(lazyDbRefs, document);

		WithObjectMethodOverrideLazyDbRefs result = converterSpy.read(WithObjectMethodOverrideLazyDbRefs.class, document);

		assertThat(result.dbRefToPlainObject, is(notNullValue()));
		assertProxyIsResolved(result.dbRefToPlainObject, false);

		assertThat(result.dbRefToPlainObject, is(equalTo(result.dbRefToPlainObject)));
		assertThat(result.dbRefToPlainObject, is(not(equalTo(null))));
		assertThat(result.dbRefToPlainObject, is(not(equalTo((Object) lazyDbRefs.dbRefToToStringObjectMethodOverride))));

		assertProxyIsResolved(result.dbRefToPlainObject, false);
	}

	@Test // DATAMONGO-884
	public void hashcodeObjectMethodOnLazyLoadingDbrefShouldNotInitializeProxy() {

		String id = "42";
		String value = "bubu";
		MappingMongoConverter converterSpy = spy(converter);

		Document document = new Document();
		WithObjectMethodOverrideLazyDbRefs lazyDbRefs = new WithObjectMethodOverrideLazyDbRefs();
		lazyDbRefs.dbRefToPlainObject = new LazyDbRefTarget(id, value);
		lazyDbRefs.dbRefToToStringObjectMethodOverride = new ToStringObjectMethodOverrideLazyDbRefTarget(id, value);
		converterSpy.write(lazyDbRefs, document);

		WithObjectMethodOverrideLazyDbRefs result = converterSpy.read(WithObjectMethodOverrideLazyDbRefs.class, document);

		assertThat(result.dbRefToPlainObject, is(notNullValue()));
		assertProxyIsResolved(result.dbRefToPlainObject, false);

		assertThat(result.dbRefToPlainObject.hashCode(), is(311365444));

		assertProxyIsResolved(result.dbRefToPlainObject, false);
	}

	@Test // DATAMONGO-884
	public void lazyLoadingProxyForEqualsAndHashcodeObjectMethodOverridingDbref() {

		String id = "42";
		String value = "bubu";
		MappingMongoConverter converterSpy = spy(converter);
		doReturn(new Document("_id", id).append("value", value)).when(converterSpy).readRef(any());

		Document document = new Document();
		WithObjectMethodOverrideLazyDbRefs lazyDbRefs = new WithObjectMethodOverrideLazyDbRefs();
		lazyDbRefs.dbRefEqualsAndHashcodeObjectMethodOverride1 = new EqualsAndHashCodeObjectMethodOverrideLazyDbRefTarget(
				id, value);
		lazyDbRefs.dbRefEqualsAndHashcodeObjectMethodOverride2 = new EqualsAndHashCodeObjectMethodOverrideLazyDbRefTarget(
				id, value);
		converterSpy.write(lazyDbRefs, document);

		WithObjectMethodOverrideLazyDbRefs result = converterSpy.read(WithObjectMethodOverrideLazyDbRefs.class, document);

		assertProxyIsResolved(result.dbRefEqualsAndHashcodeObjectMethodOverride1, false);
		assertThat(result.dbRefEqualsAndHashcodeObjectMethodOverride1, is(notNullValue()));
		result.dbRefEqualsAndHashcodeObjectMethodOverride1.equals(null);
		assertProxyIsResolved(result.dbRefEqualsAndHashcodeObjectMethodOverride1, true);

		assertProxyIsResolved(result.dbRefEqualsAndHashcodeObjectMethodOverride2, false);
		assertThat(result.dbRefEqualsAndHashcodeObjectMethodOverride2, is(notNullValue()));
		result.dbRefEqualsAndHashcodeObjectMethodOverride2.hashCode();
		assertProxyIsResolved(result.dbRefEqualsAndHashcodeObjectMethodOverride2, true);
	}

	@Test // DATAMONGO-987
	public void shouldNotGenerateLazyLoadingProxyForNullValues() {

		Document document = new Document();
		ClassWithLazyDbRefs lazyDbRefs = new ClassWithLazyDbRefs();
		lazyDbRefs.id = "42";
		converter.write(lazyDbRefs, document);

		ClassWithLazyDbRefs result = converter.read(ClassWithLazyDbRefs.class, document);

		assertThat(result.id, is(lazyDbRefs.id));
		assertThat(result.dbRefToInterface, is(nullValue()));
		assertThat(result.dbRefToConcreteCollection, is(nullValue()));
		assertThat(result.dbRefToConcreteType, is(nullValue()));
		assertThat(result.dbRefToConcreteTypeWithPersistenceConstructor, is(nullValue()));
		assertThat(result.dbRefToConcreteTypeWithPersistenceConstructorWithoutDefaultConstructor, is(nullValue()));
	}

	@Test // DATAMONGO-1005
	public void shouldBeAbleToStoreDirectReferencesToSelf() {

		Document document = new Document();

		ClassWithDbRefField o = new ClassWithDbRefField();
		o.id = "123";
		o.reference = o;
		converter.write(o, document);

		ClassWithDbRefField found = converter.read(ClassWithDbRefField.class, document);

		assertThat(found, is(notNullValue()));
		assertThat(found.reference, is(found));
	}

	@Test // DATAMONGO-1005
	public void shouldBeAbleToStoreNestedReferencesToSelf() {

		Document document = new Document();

		ClassWithNestedDbRefField o = new ClassWithNestedDbRefField();
		o.id = "123";
		o.nested = new NestedReferenceHolder();
		o.nested.reference = o;

		converter.write(o, document);

		ClassWithNestedDbRefField found = converter.read(ClassWithNestedDbRefField.class, document);

		assertThat(found, is(notNullValue()));
		assertThat(found.nested, is(notNullValue()));
		assertThat(found.nested.reference, is(found));
	}

	@Test // DATAMONGO-1012
	public void shouldEagerlyResolveIdPropertyWithFieldAccess() {

		MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(ClassWithLazyDbRefs.class);
		MongoPersistentProperty property = entity.getRequiredPersistentProperty("dbRefToConcreteType");
		MongoPersistentEntity<?> propertyEntity = mappingContext.getRequiredPersistentEntity(property);

		String idValue = new ObjectId().toString();
		DBRef dbRef = converter.toDBRef(new LazyDbRefTarget(idValue), property);

		Document object = new Document("dbRefToConcreteType", dbRef);

		ClassWithLazyDbRefs result = converter.read(ClassWithLazyDbRefs.class, object);

		PersistentPropertyAccessor accessor = propertyEntity.getPropertyAccessor(result.dbRefToConcreteType);
		MongoPersistentProperty idProperty = mappingContext.getRequiredPersistentEntity(LazyDbRefTarget.class)
				.getIdProperty();

		assertThat(accessor.getProperty(idProperty), is(notNullValue()));
		assertProxyIsResolved(result.dbRefToConcreteType, false);
	}

	@Test // DATAMONGO-1012
	public void shouldNotEagerlyResolveIdPropertyWithPropertyAccess() {

		MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(ClassWithLazyDbRefs.class);
		MongoPersistentProperty property = entity.getRequiredPersistentProperty("dbRefToConcreteTypeWithPropertyAccess");

		String idValue = new ObjectId().toString();
		DBRef dbRef = converter.toDBRef(new LazyDbRefTargetPropertyAccess(idValue), property);

		Document object = new Document("dbRefToConcreteTypeWithPropertyAccess", dbRef);

		ClassWithLazyDbRefs result = converter.read(ClassWithLazyDbRefs.class, object);

		LazyDbRefTargetPropertyAccess proxy = result.dbRefToConcreteTypeWithPropertyAccess;
		assertThat(ReflectionTestUtils.getField(proxy, "id"), is(nullValue()));
		assertProxyIsResolved(proxy, false);
	}

	@Test // DATAMONGO-1076
	public void shouldNotTriggerResolvingOfLazyLoadedProxyWhenFinalizeMethodIsInvoked() throws Exception {

		MongoPersistentEntity<?> entity = mappingContext
				.getRequiredPersistentEntity(WithObjectMethodOverrideLazyDbRefs.class);
		MongoPersistentProperty property = entity.getRequiredPersistentProperty("dbRefToPlainObject");

		String idValue = new ObjectId().toString();
		DBRef dbRef = converter.toDBRef(new LazyDbRefTargetPropertyAccess(idValue), property);

		WithObjectMethodOverrideLazyDbRefs result = converter.read(WithObjectMethodOverrideLazyDbRefs.class,
				new Document("dbRefToPlainObject", dbRef));

		ReflectionTestUtils.invokeMethod(result.dbRefToPlainObject, "finalize");

		assertProxyIsResolved(result.dbRefToPlainObject, false);
	}

	@Test // DATAMONGO-1194
	public void shouldBulkFetchListOfReferences() {

		String id1 = "1";
		String id2 = "2";
		String value = "val";

		MappingMongoConverter converterSpy = spy(converter);
		doReturn(
				Arrays.asList(new Document("_id", id1).append("value", value), new Document("_id", id2).append("value", value)))
						.when(converterSpy).bulkReadRefs(anyList());

		Document document = new Document();
		ClassWithLazyDbRefs lazyDbRefs = new ClassWithLazyDbRefs();
		lazyDbRefs.dbRefToConcreteCollection = new ArrayList<>(
				Arrays.asList(new LazyDbRefTarget(id1, value), new LazyDbRefTarget(id2, value)));
		converterSpy.write(lazyDbRefs, document);

		ClassWithLazyDbRefs result = converterSpy.read(ClassWithLazyDbRefs.class, document);

		assertProxyIsResolved(result.dbRefToConcreteCollection, false);
		assertThat(result.dbRefToConcreteCollection.get(0).getId(), is(id1));
		assertProxyIsResolved(result.dbRefToConcreteCollection, true);
		assertThat(result.dbRefToConcreteCollection.get(1).getId(), is(id2));

		verify(converterSpy, never()).readRef(Mockito.any(DBRef.class));
	}

	@Test // DATAMONGO-1666
	public void shouldBulkFetchSetOfReferencesForConstructorCreation() {

		String id1 = "1";
		String id2 = "2";
		String value = "val";

		MappingMongoConverter converterSpy = spy(converter);
		doReturn(
				Arrays.asList(new Document("_id", id1).append("value", value), new Document("_id", id2).append("value", value)))
						.when(converterSpy).bulkReadRefs(anyList());

		Document document = new Document("dbRefToInterface",
				Arrays.asList(new DBRef("lazyDbRefTarget", "1"), new DBRef("lazyDbRefTarget", "2")));

		ClassWithDbRefSetConstructor result = converterSpy.read(ClassWithDbRefSetConstructor.class, document);

		assertThat(result.dbRefToInterface, is(instanceOf(Set.class)));

		verify(converterSpy, never()).readRef(Mockito.any(DBRef.class));
	}

	@Test // DATAMONGO-1194
	public void shouldFallbackToOneByOneFetchingWhenElementsInListOfReferencesPointToDifferentCollections() {

		String id1 = "1";
		String id2 = "2";
		String value = "val";

		MappingMongoConverter converterSpy = spy(converter);
		doReturn(new Document("_id", id1).append("value", value)).doReturn(new Document("_id", id2).append("value", value))
				.when(converterSpy).readRef(Mockito.any(DBRef.class));

		Document document = new Document();
		ClassWithLazyDbRefs lazyDbRefs = new ClassWithLazyDbRefs();
		lazyDbRefs.dbRefToConcreteCollection = new ArrayList<>(
				Arrays.asList(new LazyDbRefTarget(id1, value), new SerializableLazyDbRefTarget(id2, value)));
		converterSpy.write(lazyDbRefs, document);

		ClassWithLazyDbRefs result = converterSpy.read(ClassWithLazyDbRefs.class, document);

		assertProxyIsResolved(result.dbRefToConcreteCollection, false);
		assertThat(result.dbRefToConcreteCollection.get(0).getId(), is(id1));
		assertProxyIsResolved(result.dbRefToConcreteCollection, true);
		assertThat(result.dbRefToConcreteCollection.get(1).getId(), is(id2));

		verify(converterSpy, times(2)).readRef(Mockito.any(DBRef.class));
		verify(converterSpy, never()).bulkReadRefs(anyList());
	}

	@Test // DATAMONGO-1194
	public void shouldBulkFetchMapOfReferences() {

		MapDBRefVal val1 = new MapDBRefVal();
		val1.id = BigInteger.ONE;

		MapDBRefVal val2 = new MapDBRefVal();
		val2.id = BigInteger.ZERO;

		MappingMongoConverter converterSpy = spy(converter);
		doReturn(Arrays.asList(new Document("_id", val1.id), new Document("_id", val2.id))).when(converterSpy)
				.bulkReadRefs(anyList());

		Document document = new Document();
		MapDBRef mapDBRef = new MapDBRef();
		mapDBRef.map = new LinkedHashMap<>();
		mapDBRef.map.put("one", val1);
		mapDBRef.map.put("two", val2);

		converterSpy.write(mapDBRef, document);

		MapDBRef result = converterSpy.read(MapDBRef.class, document);

		// assertProxyIsResolved(result.map, false);
		assertThat(result.map.get("one").id, is(val1.id));
		// assertProxyIsResolved(result.map, true);
		assertThat(result.map.get("two").id, is(val2.id));

		verify(converterSpy, times(1)).bulkReadRefs(anyList());
		verify(converterSpy, never()).readRef(Mockito.any(DBRef.class));
	}

	@Test // DATAMONGO-1194
	public void shouldBulkFetchLazyMapOfReferences() {

		MapDBRefVal val1 = new MapDBRefVal();
		val1.id = BigInteger.ONE;

		MapDBRefVal val2 = new MapDBRefVal();
		val2.id = BigInteger.ZERO;

		MappingMongoConverter converterSpy = spy(converter);
		doReturn(Arrays.asList(new Document("_id", val1.id), new Document("_id", val2.id))).when(converterSpy)
				.bulkReadRefs(anyList());

		Document document = new Document();
		MapDBRef mapDBRef = new MapDBRef();
		mapDBRef.lazyMap = new LinkedHashMap<>();
		mapDBRef.lazyMap.put("one", val1);
		mapDBRef.lazyMap.put("two", val2);

		converterSpy.write(mapDBRef, document);

		MapDBRef result = converterSpy.read(MapDBRef.class, document);

		assertProxyIsResolved(result.lazyMap, false);
		assertThat(result.lazyMap.get("one").id, is(val1.id));
		assertProxyIsResolved(result.lazyMap, true);
		assertThat(result.lazyMap.get("two").id, is(val2.id));

		verify(converterSpy, times(1)).bulkReadRefs(anyList());
		verify(converterSpy, never()).readRef(any());
	}

	private Object transport(Object result) {
		return SerializationUtils.deserialize(SerializationUtils.serialize(result));
	}

	class MapDBRef {
		@org.springframework.data.mongodb.core.mapping.DBRef Map<String, MapDBRefVal> map;
		@org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) Map<String, MapDBRefVal> lazyMap;
	}

	class MapDBRefVal {
		BigInteger id;
	}

	class PersonClient {
		@org.springframework.data.mongodb.core.mapping.DBRef Person person;
	}

	static class ClassWithLazyDbRefs {

		@Id String id;
		@org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) List<LazyDbRefTarget> dbRefToInterface;
		@org.springframework.data.mongodb.core.mapping.DBRef(
				lazy = true) ArrayList<LazyDbRefTarget> dbRefToConcreteCollection;
		@org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) LazyDbRefTarget dbRefToConcreteType;
		@org.springframework.data.mongodb.core.mapping.DBRef(
				lazy = true) LazyDbRefTargetPropertyAccess dbRefToConcreteTypeWithPropertyAccess;
		@org.springframework.data.mongodb.core.mapping.DBRef(
				lazy = true) LazyDbRefTargetWithPeristenceConstructor dbRefToConcreteTypeWithPersistenceConstructor;
		@org.springframework.data.mongodb.core.mapping.DBRef(
				lazy = true) LazyDbRefTargetWithPeristenceConstructorWithoutDefaultConstructor dbRefToConcreteTypeWithPersistenceConstructorWithoutDefaultConstructor;
	}

	static class ClassWithDbRefSetConstructor {

		final @org.springframework.data.mongodb.core.mapping.DBRef Set<LazyDbRefTarget> dbRefToInterface;

		public ClassWithDbRefSetConstructor(Set<LazyDbRefTarget> dbRefToInterface) {
			this.dbRefToInterface = dbRefToInterface;
		}
	}

	static class SerializableClassWithLazyDbRefs implements Serializable {

		private static final long serialVersionUID = 1L;

		@org.springframework.data.mongodb.core.mapping.DBRef(
				lazy = true) SerializableLazyDbRefTarget dbRefToSerializableTarget;
	}

	static class LazyDbRefTarget implements Serializable {

		private static final long serialVersionUID = 1L;

		@Id String id;
		String value;

		public LazyDbRefTarget() {
			this(null);
		}

		public LazyDbRefTarget(String id) {
			this(id, null);
		}

		public LazyDbRefTarget(String id, String value) {
			this.id = id;
			this.value = value;
		}

		public String getId() {
			return id;
		}

		public String getValue() {
			return value;
		}
	}

	static class LazyDbRefTargetPropertyAccess implements Serializable {

		private static final long serialVersionUID = 1L;

		@Id @AccessType(Type.PROPERTY) String id;

		public LazyDbRefTargetPropertyAccess(String id) {
			this.id = id;
		}

		public String getId() {
			return id;
		}
	}

	@SuppressWarnings("serial")
	static class LazyDbRefTargetWithPeristenceConstructor extends LazyDbRefTarget {

		boolean persistenceConstructorCalled;

		public LazyDbRefTargetWithPeristenceConstructor() {}

		@PersistenceConstructor
		public LazyDbRefTargetWithPeristenceConstructor(String id, String value) {
			super(id, value);
			this.persistenceConstructorCalled = true;
		}

		public LazyDbRefTargetWithPeristenceConstructor(Object id, Object value) {
			super(id.toString(), value.toString());
		}
	}

	@SuppressWarnings("serial")
	static class LazyDbRefTargetWithPeristenceConstructorWithoutDefaultConstructor extends LazyDbRefTarget {

		boolean persistenceConstructorCalled;

		@PersistenceConstructor
		public LazyDbRefTargetWithPeristenceConstructorWithoutDefaultConstructor(String id, String value) {
			super(id, value);
			this.persistenceConstructorCalled = true;
		}

		public LazyDbRefTargetWithPeristenceConstructorWithoutDefaultConstructor(Object id, Object value) {
			super(id.toString(), value.toString());
		}
	}

	static class SerializableLazyDbRefTarget extends LazyDbRefTarget implements Serializable {

		public SerializableLazyDbRefTarget() {}

		public SerializableLazyDbRefTarget(String id, String value) {
			super(id, value);
		}

		private static final long serialVersionUID = 1L;
	}

	static class ToStringObjectMethodOverrideLazyDbRefTarget extends LazyDbRefTarget {

		private static final long serialVersionUID = 1L;

		public ToStringObjectMethodOverrideLazyDbRefTarget() {}

		public ToStringObjectMethodOverrideLazyDbRefTarget(String id, String value) {
			super(id, value);
		}

		/*
		 * (non-Javadoc)
		 * @see java.lang.Object#toString()
		 */
		@Override
		public String toString() {
			return this.id + ":" + this.value;
		}
	}

	static class EqualsAndHashCodeObjectMethodOverrideLazyDbRefTarget extends LazyDbRefTarget {

		private static final long serialVersionUID = 1L;

		public EqualsAndHashCodeObjectMethodOverrideLazyDbRefTarget() {}

		public EqualsAndHashCodeObjectMethodOverrideLazyDbRefTarget(String id, String value) {
			super(id, value);
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((id == null) ? 0 : id.hashCode());
			result = prime * result + ((value == null) ? 0 : value.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			EqualsAndHashCodeObjectMethodOverrideLazyDbRefTarget other = (EqualsAndHashCodeObjectMethodOverrideLazyDbRefTarget) obj;
			if (id == null) {
				if (other.id != null)
					return false;
			} else if (!id.equals(other.id))
				return false;
			if (value == null) {
				if (other.value != null)
					return false;
			} else if (!value.equals(other.value))
				return false;
			return true;
		}
	}

	static class WithObjectMethodOverrideLazyDbRefs {

		@org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) LazyDbRefTarget dbRefToPlainObject;
		@org.springframework.data.mongodb.core.mapping.DBRef(
				lazy = true) ToStringObjectMethodOverrideLazyDbRefTarget dbRefToToStringObjectMethodOverride;
		@org.springframework.data.mongodb.core.mapping.DBRef(
				lazy = true) EqualsAndHashCodeObjectMethodOverrideLazyDbRefTarget dbRefEqualsAndHashcodeObjectMethodOverride2;
		@org.springframework.data.mongodb.core.mapping.DBRef(
				lazy = true) EqualsAndHashCodeObjectMethodOverrideLazyDbRefTarget dbRefEqualsAndHashcodeObjectMethodOverride1;
	}

	class ClassWithDbRefField {

		String id;
		@org.springframework.data.mongodb.core.mapping.DBRef ClassWithDbRefField reference;
	}

	static class NestedReferenceHolder {

		String id;
		@org.springframework.data.mongodb.core.mapping.DBRef ClassWithNestedDbRefField reference;
	}

	static class ClassWithNestedDbRefField {

		String id;
		NestedReferenceHolder nested;
	}
}
