/*
 * Copyright (c) 2019-2020 "Neo4j,"
 * Neo4j Sweden AB [https://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * 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
 *
 *     https://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.neo4j.springframework.data.integration.imperative;

import static java.util.Collections.*;
import static java.util.stream.Collectors.*;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.domain.Range.Bound.*;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;

import org.assertj.core.data.MapEntry;
import org.assertj.core.groups.Tuple;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Record;
import org.neo4j.driver.Session;
import org.neo4j.driver.SessionConfig;
import org.neo4j.driver.Transaction;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.types.Node;
import org.neo4j.driver.types.Point;
import org.neo4j.driver.types.Relationship;
import org.neo4j.springframework.data.config.AbstractNeo4jConfig;
import org.neo4j.springframework.data.core.DatabaseSelection;
import org.neo4j.springframework.data.core.DatabaseSelectionProvider;
import org.neo4j.springframework.data.core.convert.Neo4jConversions;
import org.neo4j.springframework.data.integration.imperative.repositories.PersonRepository;
import org.neo4j.springframework.data.integration.imperative.repositories.ThingRepository;
import org.neo4j.springframework.data.integration.shared.*;
import org.neo4j.springframework.data.repository.Neo4jRepository;
import org.neo4j.springframework.data.repository.config.EnableNeo4jRepositories;
import org.neo4j.springframework.data.repository.query.BoundingBox;
import org.neo4j.springframework.data.repository.query.Query;
import org.neo4j.springframework.data.test.Neo4jExtension;
import org.neo4j.springframework.data.types.CartesianPoint2d;
import org.neo4j.springframework.data.types.GeographicPoint2d;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.ExampleMatcher.StringMatcher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.Range.Bound;
import org.springframework.data.domain.Sort;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Polygon;
import org.springframework.data.repository.query.Param;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * @author Michael J. Simons
 * @author Gerrit Meier
 * @author Ján Šúr
 * @author Philipp Tölle
 */
@ExtendWith(Neo4jExtension.class)
@SpringJUnitConfig
@DirtiesContext
class RepositoryIT {

	protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
	protected static DatabaseSelection databaseSelection = DatabaseSelection.undecided();

	private static final String TEST_PERSON1_NAME = "Test";
	private static final String TEST_PERSON2_NAME = "Test2";
	private static final String TEST_PERSON1_FIRST_NAME = "Ernie";
	private static final String TEST_PERSON2_FIRST_NAME = "Bert";
	private static final LocalDate TEST_PERSON1_BORN_ON = LocalDate.of(2019, 1, 1);
	private static final LocalDate TEST_PERSON2_BORN_ON = LocalDate.of(2019, 2, 1);
	private static final String TEST_PERSON_SAMEVALUE = "SameValue";
	private static final Point NEO4J_HQ = Values.point(4326, 12.994823, 55.612191).asPoint();
	private static final Point SFO = Values.point(4326, -122.38681, 37.61649).asPoint();
	private static final Point CLARION = Values.point(4326, 12.994243, 55.607726).asPoint();
	private static final Point MINC = Values.point(4326, 12.994039, 55.611496).asPoint();

	static PersonWithAllConstructor personExample(String sameValue) {
		return new PersonWithAllConstructor(null, null, null, sameValue, null, null, null, null, null, null, null);
	}

	Long id1;
	Long id2;
	PersonWithAllConstructor person1;
	PersonWithAllConstructor person2;

	RepositoryIT() {
		databaseSelection = DatabaseSelection.undecided();
	}

	@Nested
	class Find extends IntegrationTestBase {

		@Override
		void setupData(Transaction transaction) {
			ZonedDateTime createdAt = LocalDateTime.of(2019, 1, 1, 23, 23, 42, 0).atZone(ZoneOffset.UTC.normalized());
			id1 = transaction.run("" +
					"CREATE (n:PersonWithAllConstructor) " +
					"  SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place, n.createdAt = $createdAt "
					+
					"RETURN id(n)",
				Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName",
					TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place",
					NEO4J_HQ, "createdAt", createdAt)
			).next().get(0).asLong();
			id2 = transaction.run(
				"CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)",
				Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName",
					TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place",
					SFO)
			).next().get(0).asLong();
			transaction.run("CREATE (n:PersonWithNoConstructor) SET n.name = $name, n.first_name = $firstName",
				Values.parameters("name", TEST_PERSON1_NAME, "firstName", TEST_PERSON1_FIRST_NAME));
			transaction.run("CREATE (n:PersonWithWither) SET n.name = '" + TEST_PERSON1_NAME + "'");
			transaction.run("CREATE (n:KotlinPerson) SET n.name = '" + TEST_PERSON1_NAME + "'");
			transaction
				.run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})-[:Has]->(b:Thing2{theId: 4711, name: 'Bart'})");

			IntStream.rangeClosed(1, 20).forEach(i ->
				transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})",
					Values.parameters("i", String.format("%02d", i))));

			person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME,
				TEST_PERSON_SAMEVALUE,
				true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, createdAt.toInstant());
			person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME,
				TEST_PERSON_SAMEVALUE,
				false, 2L, TEST_PERSON2_BORN_ON, null, emptyList(), SFO, null);
		}

		@Test
		void findAll(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> people = repository.findAll();
			assertThat(people).hasSize(2);
			assertThat(people).extracting("name").containsExactlyInAnyOrder(TEST_PERSON1_NAME, TEST_PERSON2_NAME);
		}

		@Test
		void findAllWithoutResultDoesNotThrowAnException(@Autowired PersonRepository repository) {

			try (Session session = createSession()) {
				session.run("MATCH (n:PersonWithAllConstructor) DETACH DELETE n;");
			}

			List<PersonWithAllConstructor> people = repository.findAll();
			assertThat(people).hasSize(0);
		}

		@Test
		void findById(@Autowired PersonRepository repository) {
			Optional<PersonWithAllConstructor> person = repository.findById(id1);
			assertThat(person).isPresent();
			assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME);
		}

		@Test
		void dontFindById(@Autowired PersonRepository repository) {
			Optional<PersonWithAllConstructor> person = repository.findById(-4711L);
			assertThat(person).isNotPresent();
		}

		@Test
		void dontFindOneByDerivedFinderMethodReturningOptional(@Autowired PersonRepository repository) {
			Optional<PersonWithAllConstructor> person = repository.findOneByNameAndFirstName("A", "BB");
			assertThat(person).isNotPresent();
		}

		@Test
		void dontFindOneByDerivedFinderMethodReturning(@Autowired PersonRepository repository) {
			PersonWithAllConstructor person = repository.findOneByName("A");
			assertThat(person).isNull();

			person = repository.findOneByName(TEST_PERSON1_NAME);
			assertThat(person).extracting(PersonWithAllConstructor::getName).isEqualTo(TEST_PERSON1_NAME);
		}

		@Test
		void findAllById(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAllById(Arrays.asList(id1, id2));
			assertThat(persons).hasSize(2);
		}

		@Test
		void findByAssignedId(@Autowired ThingRepository repository) {

			Optional<ThingWithAssignedId> optionalThing = repository.findById("anId");
			assertThat(optionalThing).isPresent();
			assertThat(optionalThing).map(ThingWithAssignedId::getTheId).contains("anId");
			assertThat(optionalThing).map(ThingWithAssignedId::getName).contains("Homer");

			AnotherThingWithAssignedId anotherThing = new AnotherThingWithAssignedId(4711L);
			anotherThing.setName("Bart");
			assertThat(optionalThing).map(ThingWithAssignedId::getThings)
				.contains(singletonList(anotherThing));
		}

		@Test
		void findByConvertedId(@Autowired EntityWithConvertedIdRepository repository) {
			try (Session session = createSession()) {
				session.run("CREATE (:EntityWithConvertedId{identifyingEnum:'A'})");
			}

			Optional<EntityWithConvertedId> entity = repository.findById(EntityWithConvertedId.IdentifyingEnum.A);
			assertThat(entity).isPresent();
			assertThat(entity.get().getIdentifyingEnum()).isEqualTo(EntityWithConvertedId.IdentifyingEnum.A);
		}

		@Test
		void findAllByConvertedId(@Autowired EntityWithConvertedIdRepository repository) {
			try (Session session = createSession()) {
				session.run("CREATE (:EntityWithConvertedId{identifyingEnum:'A'})");
			}

			List<EntityWithConvertedId> entities = repository
				.findAllById(singleton(EntityWithConvertedId.IdentifyingEnum.A));

			assertThat(entities).hasSize(1);
			assertThat(entities.get(0).getIdentifyingEnum()).isEqualTo(EntityWithConvertedId.IdentifyingEnum.A);
		}

		@Test
		void findWithAssignedIdViaQuery(@Autowired ThingRepository repository) {

			ThingWithAssignedId thing = repository.getViaQuery();
			assertThat(thing.getTheId()).isEqualTo("anId");
			assertThat(thing.getName()).isEqualTo("Homer");

			AnotherThingWithAssignedId anotherThing = new AnotherThingWithAssignedId(4711L);
			anotherThing.setName("Bart");
			assertThat(thing.getThings()).containsExactly(anotherThing);
		}

		@Test
		void findAllWithSortByOrderDefault(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAll(Sort.by("name"));

			assertThat(persons).containsExactly(person1, person2);
		}

		@Test
		void findAllWithSortByOrderAsc(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAll(Sort.by(Sort.Order.asc("name")));

			assertThat(persons).containsExactly(person1, person2);
		}

		@Test
		void findAllWithSortByOrderDesc(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAll(Sort.by(Sort.Order.desc("name")));

			assertThat(persons).containsExactly(person2, person1);
		}

		@Test
		void findAllWithPageable(@Autowired PersonRepository repository) {

			Sort sort = Sort.by("name");
			int page = 0;
			int limit = 1;
			Page<PersonWithAllConstructor> persons = repository.findAll(PageRequest.of(page, limit, sort));

			assertThat(persons).containsExactly(person1);

			page = 1;
			persons = repository.findAll(PageRequest.of(page, limit, sort));
			assertThat(persons).containsExactly(person2);
		}

		@Test
		void loadAllPersonsWithAllConstructorViaCustomQuery(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.getAllPersonsViaQuery();

			assertThat(persons).anyMatch(person -> person.getName().equals(TEST_PERSON1_NAME));
		}

		@Test
		void loadOnePersonWithAllConstructor(@Autowired PersonRepository repository) {

			PersonWithAllConstructor person = repository.getOnePersonViaQuery();
			assertThat(person.getName()).isEqualTo(TEST_PERSON1_NAME);
		}

		@Test
		void loadOptionalPersonWithAllConstructor(@Autowired PersonRepository repository) {

			Optional<PersonWithAllConstructor> person = repository.getOptionalPersonViaQuery();
			assertThat(person).isPresent();
			assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME);
		}

		@Test
		void loadOptionalPersonWithAllConstructorWithParameter(@Autowired PersonRepository repository) {

			Optional<PersonWithAllConstructor> person = repository.getOptionalPersonViaQuery(TEST_PERSON1_NAME);
			assertThat(person).isPresent();
			assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME);
		}

		@Test
		void loadNoPersonsWithAllConstructorViaCustomQueryWithoutException(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.getNobodyViaQuery();
			assertThat(persons).hasSize(0);
		}

		@Test
		void loadOptionalPersonWithAllConstructorWithSpelParameters(@Autowired PersonRepository repository) {

			Optional<PersonWithAllConstructor> person = repository
				.getOptionalPersonViaQuery(TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2));
			assertThat(person).isPresent();
			assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME);
		}

		@Test
		void loadOptionalPersonWithAllConstructorWithSpelParametersAndNamedQuery(
			@Autowired PersonRepository repository) {

			Optional<PersonWithAllConstructor> person = repository
				.getOptionalPersonViaNamedQuery(TEST_PERSON1_NAME.substring(0, 2), TEST_PERSON1_NAME.substring(2));
			assertThat(person).isPresent();
			assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME);
		}

		@Test
		void loadAllPersonsWithNoConstructor(@Autowired PersonRepository repository) {

			List<PersonWithNoConstructor> persons = repository.getAllPersonsWithNoConstructorViaQuery();

			assertThat(persons)
				.extracting(PersonWithNoConstructor::getName, PersonWithNoConstructor::getFirstName)
				.containsExactlyInAnyOrder(
					Tuple.tuple(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME)
				);
		}

		@Test
		void loadOnePersonWithNoConstructor(@Autowired PersonRepository repository) {

			PersonWithNoConstructor person = repository.getOnePersonWithNoConstructorViaQuery();
			assertThat(person.getName()).isEqualTo(TEST_PERSON1_NAME);
			assertThat(person.getFirstName()).isEqualTo(TEST_PERSON1_FIRST_NAME);
		}

		@Test
		void loadOptionalPersonWithNoConstructor(@Autowired PersonRepository repository) {

			Optional<PersonWithNoConstructor> person = repository.getOptionalPersonWithNoConstructorViaQuery();
			assertThat(person).isPresent();
			assertThat(person).map(PersonWithNoConstructor::getName).contains(TEST_PERSON1_NAME);
			assertThat(person).map(PersonWithNoConstructor::getFirstName).contains(TEST_PERSON1_FIRST_NAME);
		}

		@Test
		void loadAllPersonsWithWither(@Autowired PersonRepository repository) {

			List<PersonWithWither> persons = repository.getAllPersonsWithWitherViaQuery();

			assertThat(persons).anyMatch(person -> person.getName().equals(TEST_PERSON1_NAME));
		}

		@Test
		void loadOnePersonWithWither(@Autowired PersonRepository repository) {

			PersonWithWither person = repository.getOnePersonWithWitherViaQuery();
			assertThat(person.getName()).isEqualTo(TEST_PERSON1_NAME);
		}

		@Test
		void loadOptionalPersonWithWither(@Autowired PersonRepository repository) {

			Optional<PersonWithWither> person = repository.getOptionalPersonWithWitherViaQuery();
			assertThat(person).isPresent();
			assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME);
		}

		@Test
		void loadAllKotlinPersons(@Autowired PersonRepository repository) {

			List<KotlinPerson> persons = repository.getAllKotlinPersonsViaQuery();
			assertThat(persons).anyMatch(person -> person.getName().equals(TEST_PERSON1_NAME));
		}

		@Test
		void loadOneKotlinPerson(@Autowired PersonRepository repository) {

			KotlinPerson person = repository.getOneKotlinPersonViaQuery();
			assertThat(person.getName()).isEqualTo(TEST_PERSON1_NAME);
		}

		@Test
		void loadOptionalKotlinPerson(@Autowired PersonRepository repository) {

			Optional<KotlinPerson> person = repository.getOptionalKotlinPersonViaQuery();
			assertThat(person).isPresent();
			assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME);
		}

		@Test
		void callCustomCypher(@Autowired PersonRepository repository) {

			Long fixedLong = repository.customQuery();
			assertThat(fixedLong).isEqualTo(1L);
		}

		@Test
		void findBySimpleProperty(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons;

			persons = repository.findAllBySameValue(TEST_PERSON_SAMEVALUE);
			assertThat(persons).containsExactlyInAnyOrder(person1, person2);

			persons = repository.findAllBySameValueIgnoreCase(TEST_PERSON_SAMEVALUE.toUpperCase());
			assertThat(persons).containsExactlyInAnyOrder(person1, person2);

			persons = repository.findAllByBornOn(TEST_PERSON1_BORN_ON);
			assertThat(persons)
				.hasSize(1)
				.contains(person1);
		}

		@Test
		void findBySimplePropertyByEqualsWithNullShouldWork(@Autowired PersonRepository repository) {

			int emptyResultSize = 0;
			assertThat(repository.findAllBySameValue(null)).hasSize(emptyResultSize);
		}

		@Test
		void findByPropertyThatNeedsConversion(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> people = repository
				.findAllByPlace(new GeographicPoint2d(NEO4J_HQ.y(), NEO4J_HQ.x()));

			assertThat(people).hasSize(1);
		}

		@Test
		void findByPropertyFailsIfNoConverterIsAvailable(@Autowired PersonRepository repository) {
			assertThatExceptionOfType(ConverterNotFoundException.class)
				.isThrownBy(() -> repository.findAllByPlace(new ThingWithGeneratedId("hello")))
				.withMessageStartingWith("No converter found capable of converting from type");
		}

		@Test
		void findBySimplePropertiesAnded(@Autowired PersonRepository repository) {

			Optional<PersonWithAllConstructor> optionalPerson;

			optionalPerson = repository.findOneByNameAndFirstName(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME);
			assertThat(optionalPerson).isPresent().contains(person1);

			optionalPerson = repository.findOneByNameAndFirstNameAllIgnoreCase(TEST_PERSON1_NAME.toUpperCase(),
				TEST_PERSON1_FIRST_NAME.toUpperCase());
			assertThat(optionalPerson).isPresent().contains(person1);
		}

		@Test // GH-112
		void findByPropertyWithPageable(@Autowired PersonRepository repository) {

			Page<PersonWithAllConstructor> people;

			Sort sort = Sort.by("name").descending();
			people = repository.findAllByNameOrName(PageRequest.of(0, 1, sort), TEST_PERSON1_NAME, TEST_PERSON2_NAME);
			assertThat(people.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON2_NAME);
			assertThat(people.getTotalPages()).isEqualTo(2);

			people = repository.findAllByNameOrName(PageRequest.of(1, 1, sort), TEST_PERSON1_NAME, TEST_PERSON2_NAME);
			assertThat(people.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON1_NAME);
			assertThat(people.getTotalPages()).isEqualTo(2);

			people = repository.findAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME, PageRequest.of(1, 1, sort));
			assertThat(people.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON1_NAME);
			assertThat(people.getTotalPages()).isEqualTo(2);
		}

		@Test
		void findBySimplePropertiesOred(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository
				.findAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME);
			assertThat(persons).containsExactlyInAnyOrder(person1, person2);
		}
	}

	@Nested
	class FindWithRelationships extends IntegrationTestBase {

		@Test
		void findEntityWithRelationship(@Autowired RelationshipRepository repository) {

			long personId;
			long clubId;
			long hobbyNode1Id;
			long hobbyNode2Id;
			long petNode1Id;
			long petNode2Id;

			try (Session session = createSession()) {
				Record record = session
					.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), "
						+ "(n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}), "
						+ "(n)<-[:Has]-(c:Club{name:'ClownsClub'}), "
						+ "(p1)-[:Has]->(h2:Hobby{name:'sleeping'}), "
						+ "(p1)-[:Has]->(p2)"
						+ "RETURN n, h1, h2, p1, p2, c").single();

				Node personNode = record.get("n").asNode();
				Node clubNode = record.get("c").asNode();
				Node hobbyNode1 = record.get("h1").asNode();
				Node hobbyNode2 = record.get("h2").asNode();
				Node petNode1 = record.get("p1").asNode();
				Node petNode2 = record.get("p2").asNode();

				personId = personNode.id();
				clubId = clubNode.id();
				hobbyNode1Id = hobbyNode1.id();
				hobbyNode2Id = hobbyNode2.id();
				petNode1Id = petNode1.id();
				petNode2Id = petNode2.id();
			}

			PersonWithRelationship loadedPerson = repository.findById(personId).get();
			assertThat(loadedPerson.getName()).isEqualTo("Freddie");
			Hobby hobby = loadedPerson.getHobbies();
			assertThat(hobby).isNotNull();
			assertThat(hobby.getId()).isEqualTo(hobbyNode1Id);
			assertThat(hobby.getName()).isEqualTo("Music");

			Club club = loadedPerson.getClub();
			assertThat(club).isNotNull();
			assertThat(club.getId()).isEqualTo(clubId);
			assertThat(club.getName()).isEqualTo("ClownsClub");

			List<Pet> pets = loadedPerson.getPets();
			Pet comparisonPet1 = new Pet(petNode1Id, "Jerry");
			Pet comparisonPet2 = new Pet(petNode2Id, "Tom");
			assertThat(pets).containsExactlyInAnyOrder(comparisonPet1, comparisonPet2);

			Pet pet1 = pets.get(pets.indexOf(comparisonPet1));
			Pet pet2 = pets.get(pets.indexOf(comparisonPet2));
			Hobby petHobby = pet1.getHobbies().iterator().next();
			assertThat(petHobby.getId()).isEqualTo(hobbyNode2Id);
			assertThat(petHobby.getName()).isEqualTo("sleeping");

			assertThat(pet1.getFriends()).containsExactly(pet2);

		}

		@Test
		void findDeepSameLabelsAndTypeRelationships(@Autowired PetRepository repository) {

			long petNode1Id;
			long petNode2Id;
			long petNode3Id;

			try (Session session = createSession()) {
				Record record = session
					.run("CREATE "
						+ "(p1:Pet{name: 'Pet1'})-[:Has]->(p2:Pet{name: 'Pet2'}), "
						+ "(p2)-[:Has]->(p3:Pet{name: 'Pet3'}) "
						+ "RETURN p1, p2, p3").single();

				petNode1Id = record.get("p1").asNode().id();
				petNode2Id = record.get("p2").asNode().id();
				petNode3Id = record.get("p3").asNode().id();
			}

			Pet loadedPet = repository.findById(petNode1Id).get();

			Pet comparisonPet2 = new Pet(petNode2Id, "Pet2");
			Pet comparisonPet3 = new Pet(petNode3Id, "Pet3");
			assertThat(loadedPet.getFriends()).containsExactlyInAnyOrder(comparisonPet2);

			Pet pet2 = loadedPet.getFriends().get(loadedPet.getFriends().indexOf(comparisonPet2));
			assertThat(pet2.getFriends()).containsExactly(comparisonPet3);

		}

		@Test
		void findDeepRelationships(@Autowired DeepRelationshipRepository deepRelationshipRepository) {

			long type1Id;

			try (Session session = createSession()) {
				Record record = session
					.run("CREATE "
						+ "(t1:Type1)-[:NEXT_TYPE]->(t2:Type2)-[:NEXT_TYPE]->(:Type3)-[:NEXT_TYPE]->(t4:Type4)-"
						+ "[:NEXT_TYPE]->(:Type5)-[:NEXT_TYPE]->(:Type6)-[:NEXT_TYPE]->(:Type7), "
						+ "(t2)-[:SAME_TYPE]->"
						+ "(:Type2)-[:SAME_TYPE]->(:Type2)-[:SAME_TYPE]->(:Type2)-[:SAME_TYPE]->"
						+ "(:Type2)-[:SAME_TYPE]->(:Type2)-[:SAME_TYPE]->(:Type2)-[:SAME_TYPE]->"
						+ "(:Type2) "
						+ "RETURN t1").single();

				type1Id = record.get("t1").asNode().id();
			}

			DeepRelationships.Type1 type1 = deepRelationshipRepository.findById(type1Id).get();

			// ensures that the virtual limit for same relationships does not affect distinct relationships
			assertThat(type1.nextType.nextType.nextType.nextType.nextType.nextType).isNotNull();

			// assert that same type relationships not cause stack overflow
			DeepRelationships.Type2 type2 = type1.nextType;
			assertThat(type2.sameType.sameType.sameType).isNotNull();
			assertThat(type2.sameType.sameType.sameType.sameType).isNull();

		}

		@Test
		void findLoopingDeepRelationships(@Autowired LoopingRelationshipRepository loopingRelationshipRepository) {

			long type1Id;

			try (Session session = createSession()) {
				Record record = session
					.run("CREATE "
						+ "(t1:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->"
						+ "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->"
						+ "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->"
						+ "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->"
						+ "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->"
						+ "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->"
						+ "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->"
						+ "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->"
						+ "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->"
						+ "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->"
						+ "(:LoopingType1)"
						+ "RETURN t1").single();

				type1Id = record.get("t1").asNode().id();
			}

			DeepRelationships.LoopingType1 type1 = loopingRelationshipRepository.findById(type1Id).get();

			DeepRelationships.LoopingType1 iteration1 = type1.nextType.nextType.nextType;
			assertThat(iteration1).isNotNull();
			DeepRelationships.LoopingType1 iteration2 = iteration1.nextType.nextType.nextType;
			assertThat(iteration2).isNotNull();
			DeepRelationships.LoopingType1 iteration3 = iteration2.nextType.nextType.nextType;
			assertThat(iteration3).isNotNull();
			assertThat(iteration3.nextType).isNull();

		}

		@Test
		void findEntityWithRelationshipToTheSameNode(@Autowired RelationshipRepository repository) {

			long personId;
			long hobbyNode1Id;
			long petNode1Id;

			try (Session session = createSession()) {
				Record record = session
					.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), "
						+ "(n)-[:Has]->(p1:Pet{name: 'Jerry'}), "
						+ "(p1)-[:Has]->(h1)"
						+ "RETURN n, h1, p1").single();

				Node personNode = record.get("n").asNode();
				Node hobbyNode1 = record.get("h1").asNode();
				Node petNode1 = record.get("p1").asNode();

				personId = personNode.id();
				hobbyNode1Id = hobbyNode1.id();
				petNode1Id = petNode1.id();
			}

			PersonWithRelationship loadedPerson = repository.findById(personId).get();
			assertThat(loadedPerson.getName()).isEqualTo("Freddie");
			Hobby hobby = loadedPerson.getHobbies();
			assertThat(hobby).isNotNull();
			assertThat(hobby.getId()).isEqualTo(hobbyNode1Id);
			assertThat(hobby.getName()).isEqualTo("Music");

			List<Pet> pets = loadedPerson.getPets();
			Pet comparisonPet1 = new Pet(petNode1Id, "Jerry");
			assertThat(pets).containsExactlyInAnyOrder(comparisonPet1);

			Pet pet1 = pets.get(pets.indexOf(comparisonPet1));
			Hobby petHobby = pet1.getHobbies().iterator().next();
			assertThat(petHobby.getName()).isEqualTo("Music");

			assertThat(petHobby).isSameAs(hobby);

		}

		@Test
		void findEntityWithBidirectionalRelationship(@Autowired BidirectionalStartRepository repository) {

			long startId;

			try (Session session = createSession()) {
				Record record = session
					.run("CREATE (n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}), "
						+ "(e)<-[:ANOTHER_CONNECTION]-(anotherStart:BidirectionalStart{name:'Elmo'})"
						+ "RETURN n").single();

				Node startNode = record.get("n").asNode();
				startId = startNode.id();
			}

			Optional<BidirectionalStart> entityOptional = repository.findById(startId);
			assertThat(entityOptional).isPresent();
			BidirectionalStart entity = entityOptional.get();
			assertThat(entity.getEnds()).hasSize(1);

			BidirectionalEnd end = entity.getEnds().iterator().next();
			assertThat(end.getAnotherStart()).isNotNull();
			assertThat(end.getAnotherStart().getName()).isEqualTo("Elmo");

		}

		@Test
		void findEntityWithSelfReferencesInBothDirections(@Autowired PetRepository repository) {
			long petId;
			try (Session session = createSession()) {
				petId = session.run("CREATE (luna:Pet{name:'Luna'})-[:Has]->(daphne:Pet{name:'Daphne'})"
					+ "-[:Has]->(luna2:Pet{name:'Luna'})"
					+ "RETURN id(luna) as id").single().get("id").asLong();
			}
			Pet loadedPet = repository.findById(petId).get();

			assertThat(loadedPet.getFriends().get(0).getName()).isEqualTo("Daphne");
			assertThat(loadedPet.getFriends().get(0).getFriends().get(0).getName()).isEqualTo("Luna");

		}

		@Test
		void findEntityWithBidirectionalRelationshipFromIncomingSide(@Autowired BidirectionalEndRepository repository) {

			long endId;

			try (Session session = createSession()) {
				Record record = session
					.run("CREATE (n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}) "
						+ "RETURN e").single();

				Node endNode = record.get("e").asNode();
				endId = endNode.id();
			}

			Optional<BidirectionalEnd> entityOptional = repository.findById(endId);
			assertThat(entityOptional).isPresent();
			BidirectionalEnd entity = entityOptional.get();
			assertThat(entity.getStart()).isNotNull();

		}

		@Test
		void findMultipleEntitiesWithRelationship(@Autowired RelationshipRepository repository) {

			long hobbyNode1Id;
			long hobbyNode2Id;
			long petNode1Id;
			long petNode2Id;

			try (Session session = createSession()) {
				Record record = session
					.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h:Hobby{name:'Music'}), "
						+ "(n)-[:Has]->(p:Pet{name: 'Jerry'}) "
						+ "RETURN n, h, p").single();

				hobbyNode1Id = record.get("h").asNode().id();
				petNode1Id = record.get("p").asNode().id();

				record = session
					.run("CREATE (n:PersonWithRelationship{name:'SomeoneElse'})-[:Has]->(h:Hobby{name:'Music2'}), "
						+ "(n)-[:Has]->(p:Pet{name: 'Jerry2'}) "
						+ "RETURN n, h, p").single();

				hobbyNode2Id = record.get("h").asNode().id();
				petNode2Id = record.get("p").asNode().id();
			}

			List<PersonWithRelationship> loadedPersons = repository.findAll();

			Hobby hobby1 = new Hobby();
			hobby1.setId(hobbyNode1Id);
			hobby1.setName("Music");

			Hobby hobby2 = new Hobby();
			hobby2.setId(hobbyNode2Id);
			hobby2.setName("Music2");

			Pet pet1 = new Pet(petNode1Id, "Jerry");
			Pet pet2 = new Pet(petNode2Id, "Jerry2");

			assertThat(loadedPersons).extracting("name").containsExactlyInAnyOrder("Freddie", "SomeoneElse");
			assertThat(loadedPersons).extracting("hobbies").containsExactlyInAnyOrder(hobby1, hobby2);
			assertThat(loadedPersons).flatExtracting("pets").containsExactlyInAnyOrder(pet1, pet2);
		}

		@Test
		void findEntityWithRelationshipViaQuery(@Autowired RelationshipRepository repository) {

			long personId;
			long hobbyNodeId;
			long petNode1Id;
			long petNode2Id;

			try (Session session = createSession()) {
				Record record = session
					.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), "
						+ "(n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}) "
						+ "RETURN n, h1, p1, p2").single();

				Node personNode = record.get("n").asNode();
				Node hobbyNode1 = record.get("h1").asNode();
				Node petNode1 = record.get("p1").asNode();
				Node petNode2 = record.get("p2").asNode();

				personId = personNode.id();
				hobbyNodeId = hobbyNode1.id();
				petNode1Id = petNode1.id();
				petNode2Id = petNode2.id();
			}

			PersonWithRelationship loadedPerson = repository.getPersonWithRelationshipsViaQuery();
			assertThat(loadedPerson.getName()).isEqualTo("Freddie");
			assertThat(loadedPerson.getId()).isEqualTo(personId);
			Hobby hobby = loadedPerson.getHobbies();
			assertThat(hobby).isNotNull();
			assertThat(hobby.getId()).isEqualTo(hobbyNodeId);
			assertThat(hobby.getName()).isEqualTo("Music");

			List<Pet> pets = loadedPerson.getPets();
			Pet comparisonPet1 = new Pet(petNode1Id, "Jerry");
			Pet comparisonPet2 = new Pet(petNode2Id, "Tom");
			assertThat(pets).containsExactlyInAnyOrder(comparisonPet1, comparisonPet2);

		}

		@Test
		void findEntityWithRelationshipWithAssignedId(@Autowired PetRepository repository) {

			long petNodeId;

			try (Session session = createSession()) {
				Record record = session
					.run("CREATE (p:Pet{name:'Jerry'})-[:Has]->(t:Thing{theId:'t1', name:'Thing1'}) "
						+ "RETURN p, t").single();

				Node petNode = record.get("p").asNode();
				petNodeId = petNode.id();
			}

			Pet pet = repository.findById(petNodeId).get();
			ThingWithAssignedId relatedThing = pet.getThings().get(0);
			assertThat(relatedThing.getTheId()).isEqualTo("t1");
			assertThat(relatedThing.getName()).isEqualTo("Thing1");
		}

	}

	@Nested
	class RelationshipProperties extends IntegrationTestBase {

		@Test
		void findEntityWithRelationshipWithProperties(
			@Autowired PersonWithRelationshipWithPropertiesRepository repository) {

			long personId;
			long hobbyNode1Id;
			long hobbyNode2Id;

			try (Session session = createSession()) {
				Record record = session
					.run("CREATE (n:PersonWithRelationshipWithProperties{name:'Freddie'}),"
						+ " (n)-[l1:LIKES"
						+ "{since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}"
						+ "]->(h1:Hobby{name:'Music'}),"
						+ " (n)-[l2:LIKES"
						+ "{since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}"
						+ "]->(h2:Hobby{name:'Something else'})"
						+ "RETURN n, h1, h2").single();

				Node personNode = record.get("n").asNode();
				Node hobbyNode1 = record.get("h1").asNode();
				Node hobbyNode2 = record.get("h2").asNode();

				personId = personNode.id();
				hobbyNode1Id = hobbyNode1.id();
				hobbyNode2Id = hobbyNode2.id();
			}

			Optional<PersonWithRelationshipWithProperties> optionalPerson = repository.findById(personId);
			assertThat(optionalPerson).isPresent();
			PersonWithRelationshipWithProperties person = optionalPerson.get();
			assertThat(person.getName()).isEqualTo("Freddie");

			Hobby hobby1 = new Hobby();
			hobby1.setName("Music");
			hobby1.setId(hobbyNode1Id);
			LikesHobbyRelationship rel1 = new LikesHobbyRelationship(1995);
			rel1.setActive(true);
			rel1.setLocalDate(LocalDate.of(1995, 2, 26));
			rel1.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING);
			rel1.setPoint(new CartesianPoint2d(0d, 1d));

			Hobby hobby2 = new Hobby();
			hobby2.setName("Something else");
			hobby2.setId(hobbyNode2Id);
			LikesHobbyRelationship rel2 = new LikesHobbyRelationship(2000);
			rel2.setActive(false);
			rel2.setLocalDate(LocalDate.of(2000, 6, 28));
			rel2.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING_DIFFERENT);
			rel2.setPoint(new CartesianPoint2d(2d, 3d));

			assertThat(person.getHobbies()).contains(MapEntry.entry(hobby1, rel1), MapEntry.entry(hobby2, rel2));
		}

		@Test
		void saveEntityWithRelationshipWithProperties(
			@Autowired PersonWithRelationshipWithPropertiesRepository repository) {
			// given
			Hobby h1 = new Hobby();
			h1.setName("Music");

			int rel1Since = 1995;
			boolean rel1Active = true;
			LocalDate rel1LocalDate = LocalDate.of(1995, 2, 26);
			LikesHobbyRelationship.MyEnum rel1MyEnum = LikesHobbyRelationship.MyEnum.SOMETHING;
			CartesianPoint2d rel1Point = new CartesianPoint2d(0.0, 1.0);

			LikesHobbyRelationship rel1 = new LikesHobbyRelationship(rel1Since);
			rel1.setActive(rel1Active);
			rel1.setLocalDate(rel1LocalDate);
			rel1.setMyEnum(rel1MyEnum);
			rel1.setPoint(rel1Point);

			Hobby h2 = new Hobby();
			h2.setName("Something else");
			int rel2Since = 2000;
			boolean rel2Active = false;
			LocalDate rel2LocalDate = LocalDate.of(2000, 6, 28);
			LikesHobbyRelationship.MyEnum rel2MyEnum = LikesHobbyRelationship.MyEnum.SOMETHING_DIFFERENT;
			CartesianPoint2d rel2Point = new CartesianPoint2d(2.0, 3.0);

			LikesHobbyRelationship rel2 = new LikesHobbyRelationship(rel2Since);
			rel2.setActive(rel2Active);
			rel2.setLocalDate(rel2LocalDate);
			rel2.setMyEnum(rel2MyEnum);
			rel2.setPoint(rel2Point);

			Map<Hobby, LikesHobbyRelationship> hobbies = new HashMap<>();
			hobbies.put(h1, rel1);
			hobbies.put(h2, rel2);
			PersonWithRelationshipWithProperties clonePerson = new PersonWithRelationshipWithProperties(
				"Freddie clone");
			clonePerson.setHobbies(hobbies);

			// when
			PersonWithRelationshipWithProperties shouldBeDifferentPerson = repository
				.save(clonePerson);

			// then
			assertThat(shouldBeDifferentPerson)
				.isNotNull()
				.isEqualToComparingOnlyGivenFields(clonePerson, "hobbies");

			assertThat(shouldBeDifferentPerson.getName()).isEqualToIgnoringCase("Freddie clone");

			try (Session session = createSession()) {
				Record record = session.run(
					"MATCH (n:PersonWithRelationshipWithProperties {name:'Freddie clone'}) "
						+ "RETURN n, "
						+ "[(n) -[:LIKES]->(h:Hobby) |h] as Hobbies, "
						+ "[(n) -[r:LIKES]->(:Hobby) |r] as rels"
				).single();

				assertThat(record.containsKey("n")).isTrue();
				assertThat(record.containsKey("Hobbies")).isTrue();
				assertThat(record.containsKey("rels")).isTrue();
				assertThat(record.values()).hasSize(3);
				assertThat(record.get("Hobbies").values()).hasSize(2);
				assertThat(record.get("rels").values()).hasSize(2);

				assertThat(record.get("rels").values(Value::asRelationship)).
					extracting(
						Relationship::type,
						rel -> rel.get("active"),
						rel -> rel.get("localDate"),
						rel -> rel.get("point"),
						rel -> rel.get("myEnum"),
						rel -> rel.get("since")
					)
					.containsExactlyInAnyOrder(
						tuple(
							"LIKES", Values.value(rel1Active), Values.value(rel1LocalDate),
							Values.point(rel1Point.getSrid(), rel1Point.getX(), rel1Point.getY()),
							Values.value(rel1MyEnum.name()), Values.value(rel1Since)
						),
						tuple(
							"LIKES", Values.value(rel2Active), Values.value(rel2LocalDate),
							Values.point(rel2Point.getSrid(), rel2Point.getX(), rel2Point.getY()),
							Values.value(rel2MyEnum.name()), Values.value(rel2Since)
						)
					);
			}
		}

		@Test
		void findEntityWithRelationshipWithPropertiesFromCustomQuery(
			@Autowired PersonWithRelationshipWithPropertiesRepository repository) {

			long personId;
			long hobbyNode1Id;
			long hobbyNode2Id;

			try (Session session = createSession()) {
				Record record = session
					.run("CREATE (n:PersonWithRelationshipWithProperties{name:'Freddie'}),"
						+ " (n)-[l1:LIKES"
						+ "{since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}"
						+ "]->(h1:Hobby{name:'Music'}),"
						+ " (n)-[l2:LIKES"
						+ "{since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}"
						+ "]->(h2:Hobby{name:'Something else'})"
						+ "RETURN n, h1, h2").single();

				Node personNode = record.get("n").asNode();
				Node hobbyNode1 = record.get("h1").asNode();
				Node hobbyNode2 = record.get("h2").asNode();

				personId = personNode.id();
				hobbyNode1Id = hobbyNode1.id();
				hobbyNode2Id = hobbyNode2.id();
			}

			PersonWithRelationshipWithProperties person = repository.loadFromCustomQuery(personId);
			assertThat(person.getName()).isEqualTo("Freddie");

			Hobby hobby1 = new Hobby();
			hobby1.setName("Music");
			hobby1.setId(hobbyNode1Id);
			LikesHobbyRelationship rel1 = new LikesHobbyRelationship(1995);
			rel1.setActive(true);
			rel1.setLocalDate(LocalDate.of(1995, 2, 26));
			rel1.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING);
			rel1.setPoint(new CartesianPoint2d(0d, 1d));

			Hobby hobby2 = new Hobby();
			hobby2.setName("Something else");
			hobby2.setId(hobbyNode2Id);
			LikesHobbyRelationship rel2 = new LikesHobbyRelationship(2000);
			rel2.setActive(false);
			rel2.setLocalDate(LocalDate.of(2000, 6, 28));
			rel2.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING_DIFFERENT);
			rel2.setPoint(new CartesianPoint2d(2d, 3d));

			assertThat(person.getHobbies()).contains(MapEntry.entry(hobby1, rel1), MapEntry.entry(hobby2, rel2));
		}
	}

	@Nested
	class Save extends IntegrationTestBase {

		@Override
		void setupData(Transaction transaction) {
			ZonedDateTime createdAt = LocalDateTime.of(2019, 1, 1, 23, 23, 42, 0).atZone(ZoneOffset.UTC.normalized());
			id1 = transaction.run("" +
					"CREATE (n:PersonWithAllConstructor) " +
					"  SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place, n.createdAt = $createdAt "
					+
					"RETURN id(n)",
				Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName",
					TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place",
					NEO4J_HQ, "createdAt", createdAt)
			).next().get(0).asLong();
			transaction
				.run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})-[:Has]->(b:Thing2{theId: 4711, name: 'Bart'})");
			IntStream.rangeClosed(1, 20).forEach(i ->
				transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})",
					Values.parameters("i", String.format("%02d", i))));

			person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME,
				TEST_PERSON_SAMEVALUE,
				true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, createdAt.toInstant());
		}

		@Test
		void saveSingleEntity(@Autowired PersonRepository repository) {

			PersonWithAllConstructor person = new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true,
				1509L,
				LocalDate.of(1946, 9, 15), null, Arrays.asList("b", "a"), null, null);
			PersonWithAllConstructor savedPerson = repository.save(person);
			try (Session session = createSession()) {
				Record record = session
					.run("MATCH (n:PersonWithAllConstructor) WHERE n.first_name = $first_name RETURN n",
						Values.parameters("first_name", "Freddie")).single();

				assertThat(record.containsKey("n")).isTrue();
				Node node = record.get("n").asNode();
				assertThat(savedPerson.getId()).isEqualTo(node.id());
				assertThat(node.get("things").asList()).containsExactly("b", "a");
			}
		}

		@Test
		void saveAll(@Autowired PersonRepository repository) {

			PersonWithAllConstructor newPerson = new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true,
				1509L,
				LocalDate.of(1946, 9, 15), null, emptyList(), null, null);

			PersonWithAllConstructor existingPerson = repository.findById(id1).get();
			existingPerson.setFirstName("Updated first name");
			existingPerson.setNullable("Updated nullable field");

			assertThat(repository.count()).isEqualTo(1);

			List<Long> ids = StreamSupport
				.stream(repository.saveAll(Arrays.asList(existingPerson, newPerson)).spliterator(), false)
				.map(PersonWithAllConstructor::getId)
				.collect(toList());

			assertThat(repository.count()).isEqualTo(2);

			try (Session session = createSession()) {

				Record record = session
					.run(
						"MATCH (n:PersonWithAllConstructor) WHERE id(n) IN ($ids) WITH n ORDER BY n.name ASC RETURN COLLECT(n.name) as names",
						Values.parameters("ids", ids))
					.single();

				assertThat(record.containsKey("names")).isTrue();
				List<String> names = record.get("names").asList(Value::asString);
				assertThat(names).contains("Mercury", TEST_PERSON1_NAME);
			}
		}

		@Test
		void updateSingleEntity(@Autowired PersonRepository repository) {

			PersonWithAllConstructor originalPerson = repository.findById(id1).get();
			originalPerson.setFirstName("Updated first name");
			originalPerson.setNullable("Updated nullable field");
			assertThat(originalPerson.getThings()).isNotEmpty();
			originalPerson.setThings(emptyList());

			PersonWithAllConstructor savedPerson = repository.save(originalPerson);
			try (Session session = createSession()) {
				session.readTransaction(tx -> {
					Record record = tx.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) = $id RETURN n",
						Values.parameters("id", id1)).single();

					assertThat(record.containsKey("n")).isTrue();
					Node node = record.get("n").asNode();

					assertThat(node.id()).isEqualTo(savedPerson.getId());
					assertThat(node.get("first_name").asString()).isEqualTo(savedPerson.getFirstName());
					assertThat(node.get("nullable").asString()).isEqualTo(savedPerson.getNullable());
					assertThat(node.get("things").asList()).isEmpty();

					return null;
				});
			}
		}

		@Test
		void saveWithAssignedId(@Autowired ThingRepository repository) {

			assertThat(repository.count()).isEqualTo(21);

			ThingWithAssignedId thing = new ThingWithAssignedId("aaBB");
			thing.setName("That's the thing.");
			thing = repository.save(thing);

			try (Session session = createSession()) {
				Record record = session
					.run("MATCH (n:Thing) WHERE n.theId = $id RETURN n", Values.parameters("id", thing.getTheId()))
					.single();

				assertThat(record.containsKey("n")).isTrue();
				Node node = record.get("n").asNode();
				assertThat(node.get("theId").asString()).isEqualTo(thing.getTheId());
				assertThat(node.get("name").asString()).isEqualTo(thing.getName());

				assertThat(repository.count()).isEqualTo(22);
			}
		}

		@Test
		void saveAllWithAssignedId(@Autowired ThingRepository repository) {

			assertThat(repository.count()).isEqualTo(21);

			ThingWithAssignedId newThing = new ThingWithAssignedId("aaBB");
			newThing.setName("That's the thing.");

			ThingWithAssignedId existingThing = repository.findById("anId").get();
			existingThing.setName("Updated name.");

			repository.saveAll(Arrays.asList(newThing, existingThing));

			try (Session session = createSession()) {
				Record record = session
					.run(
						"MATCH (n:Thing) WHERE n.theId IN ($ids) WITH n ORDER BY n.name ASC RETURN COLLECT(n.name) as names",
						Values.parameters("ids", Arrays.asList(newThing.getTheId(), existingThing.getTheId())))
					.single();

				assertThat(record.containsKey("names")).isTrue();
				List<String> names = record.get("names").asList(Value::asString);
				assertThat(names).containsExactly(newThing.getName(), existingThing.getName());

				assertThat(repository.count()).isEqualTo(22);
			}
		}

		@Test
		void updateWithAssignedId(@Autowired ThingRepository repository) {

			assertThat(repository.count()).isEqualTo(21);

			ThingWithAssignedId thing = new ThingWithAssignedId("id07");
			thing.setName("An updated thing");
			repository.save(thing);

			thing = repository.findById("id15").get();
			thing.setName("Another updated thing");
			repository.save(thing);

			try (Session session = createSession()) {
				Record record = session
					.run(
						"MATCH (n:Thing) WHERE n.theId IN ($ids) WITH n ORDER BY n.name ASC RETURN COLLECT(n.name) as names",
						Values.parameters("ids", Arrays.asList("id07", "id15")))
					.single();

				assertThat(record.containsKey("names")).isTrue();
				List<String> names = record.get("names").asList(Value::asString);
				assertThat(names).containsExactly("An updated thing", "Another updated thing");

				assertThat(repository.count()).isEqualTo(21);
			}
		}

		@Test
		void saveWithConvertedId(@Autowired EntityWithConvertedIdRepository repository) {
			EntityWithConvertedId entity = new EntityWithConvertedId();
			entity.setIdentifyingEnum(EntityWithConvertedId.IdentifyingEnum.A);
			repository.save(entity);

			try (Session session = createSession()) {
				Record node = session.run("MATCH (e:EntityWithConvertedId) return e").next();
				assertThat(node.get("e").get("identifyingEnum").asString()).isEqualTo("A");
			}
		}

		@Test
		void saveAllWithConvertedId(@Autowired EntityWithConvertedIdRepository repository) {
			EntityWithConvertedId entity = new EntityWithConvertedId();
			entity.setIdentifyingEnum(EntityWithConvertedId.IdentifyingEnum.A);
			repository.saveAll(Collections.singleton(entity));

			try (Session session = createSession()) {
				Record node = session.run("MATCH (e:EntityWithConvertedId) return e").next();
				assertThat(node.get("e").get("identifyingEnum").asString()).isEqualTo("A");
			}
		}
	}

	@Nested
	class SaveWithRelationships extends IntegrationTestBase {

		@Test
		void saveSingleEntityWithRelationships(@Autowired RelationshipRepository repository) {

			PersonWithRelationship person = new PersonWithRelationship();
			person.setName("Freddie");
			Hobby hobby = new Hobby();
			hobby.setName("Music");
			person.setHobbies(hobby);
			Club club = new Club();
			club.setName("ClownsClub");
			person.setClub(club);
			Pet pet1 = new Pet("Jerry");
			Pet pet2 = new Pet("Tom");
			Hobby petHobby = new Hobby();
			petHobby.setName("sleeping");
			pet1.setHobbies(singleton(petHobby));
			person.setPets(Arrays.asList(pet1, pet2));

			PersonWithRelationship savedPerson = repository.save(person);
			try (Session session = createSession()) {

				Record record = session.run("MATCH (n:PersonWithRelationship)"
						+ " RETURN n,"
						+ " [(n)-[:Has]->(p:Pet) | [ p , [ (p)-[:Has]-(h:Hobby) | h ] ] ] as petsWithHobbies,"
						+ " [(n)-[:Has]->(h:Hobby) | h] as hobbies, "
						+ " [(n)<-[:Has]-(c:Club) | c] as clubs",
					Values.parameters("name", "Freddie")).single();

				assertThat(record.containsKey("n")).isTrue();
				Node rootNode = record.get("n").asNode();
				assertThat(savedPerson.getId()).isEqualTo(rootNode.id());
				assertThat(savedPerson.getName()).isEqualTo("Freddie");

				List<List<Object>> petsWithHobbies = record.get("petsWithHobbies").asList(Value::asList);

				Map<Object, List<Node>> pets = new HashMap<>();
				for (List<Object> petWithHobbies : petsWithHobbies) {
					pets.put(petWithHobbies.get(0), ((List<Node>) petWithHobbies.get(1)));
				}

				assertThat(pets.keySet().stream().map(pet -> ((Node) pet).get("name").asString()).collect(toList()))
					.containsExactlyInAnyOrder("Jerry", "Tom");

				assertThat(pets.values().stream()
					.flatMap(petHobbies -> petHobbies.stream().map(node -> node.get("name").asString()))
					.collect(toList()))
					.containsExactlyInAnyOrder("sleeping");

				assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString()))
					.containsExactlyInAnyOrder("Music");

				assertThat(record.get("clubs").asList(entry -> entry.asNode().get("name").asString()))
					.containsExactlyInAnyOrder("ClownsClub");
			}
		}

		@Test
		void saveSingleEntityWithRelationshipsTwiceDoesNotCreateMoreRelationships(
			@Autowired RelationshipRepository repository) {

			PersonWithRelationship person = new PersonWithRelationship();
			person.setName("Freddie");
			Hobby hobby = new Hobby();
			hobby.setName("Music");
			person.setHobbies(hobby);
			Pet pet1 = new Pet("Jerry");
			Pet pet2 = new Pet("Tom");
			Hobby petHobby = new Hobby();
			petHobby.setName("sleeping");
			pet1.setHobbies(singleton(petHobby));
			person.setPets(Arrays.asList(pet1, pet2));

			PersonWithRelationship savedPerson = repository.save(person);
			savedPerson = repository.save(savedPerson);
			try (Session session = createSession()) {

				List<Record> recordList = session.run("MATCH (n:PersonWithRelationship)"
						+ " RETURN n,"
						+ " [(n)-[:Has]->(p:Pet) | [ p , [ (p)-[:Has]-(h:Hobby) | h ] ] ] as petsWithHobbies,"
						+ " [(n)-[:Has]->(h:Hobby) | h] as hobbies",
					Values.parameters("name", "Freddie")).list();

				// assert that there is only one record in the returned list
				assertThat(recordList).hasSize(1);

				Record record = recordList.get(0);

				assertThat(record.containsKey("n")).isTrue();
				Node rootNode = record.get("n").asNode();
				assertThat(savedPerson.getId()).isEqualTo(rootNode.id());
				assertThat(savedPerson.getName()).isEqualTo("Freddie");

				List<List<Object>> petsWithHobbies = record.get("petsWithHobbies").asList(Value::asList);

				Map<Object, List<Node>> pets = new HashMap<>();
				for (List<Object> petWithHobbies : petsWithHobbies) {
					pets.put(petWithHobbies.get(0), ((List<Node>) petWithHobbies.get(1)));
				}

				assertThat(pets.keySet().stream().map(pet -> ((Node) pet).get("name").asString()).collect(toList()))
					.containsExactlyInAnyOrder("Jerry", "Tom");

				assertThat(pets.values().stream()
					.flatMap(petHobbies -> petHobbies.stream().map(node -> node.get("name").asString()))
					.collect(toList()))
					.containsExactlyInAnyOrder("sleeping");

				assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString()))
					.containsExactlyInAnyOrder("Music");

				// assert that only two hobbies is stored
				recordList = session.run("MATCH (h:Hobby) RETURN h").list();
				assertThat(recordList).hasSize(2);

				// assert that only two pets is stored
				recordList = session.run("MATCH (p:Pet) RETURN p").list();
				assertThat(recordList).hasSize(2);
			}
		}

		@Test
		void saveEntityWithAlreadyExistingTargetNode(@Autowired RelationshipRepository repository) {

			Long hobbyId;
			try (Session session = createSession()) {
				hobbyId = session.run("CREATE (h:Hobby{name: 'Music'}) return id(h) as hId").single().get("hId")
					.asLong();
			}

			PersonWithRelationship person = new PersonWithRelationship();
			person.setName("Freddie");
			Hobby hobby = new Hobby();
			hobby.setId(hobbyId);
			hobby.setName("Music");
			person.setHobbies(hobby);

			PersonWithRelationship savedPerson = repository.save(person);
			try (Session session = createSession()) {

				List<Record> recordList = session.run("MATCH (n:PersonWithRelationship)"
						+ " RETURN n,"
						+ " [(n)-[:Has]->(h:Hobby) | h] as hobbies",
					Values.parameters("name", "Freddie")).list();

				assertThat(recordList).hasSize(1);

				Record record = recordList.get(0);

				assertThat(record.containsKey("n")).isTrue();
				Node rootNode = record.get("n").asNode();
				assertThat(savedPerson.getId()).isEqualTo(rootNode.id());
				assertThat(savedPerson.getName()).isEqualTo("Freddie");

				assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString()))
					.containsExactlyInAnyOrder("Music");

				// assert that only one hobby is stored
				recordList = session.run("MATCH (h:Hobby) RETURN h").list();
				assertThat(recordList).hasSize(1);
			}
		}

		@Test
		void saveEntityWithAlreadyExistingSourceAndTargetNode(@Autowired RelationshipRepository repository) {

			Long hobbyId;
			Long personId;

			try (Session session = createSession()) {
				Record record = session.run(
					"CREATE (p:PersonWithRelationship{name: 'Freddie'}), (h:Hobby{name: 'Music'}) return id(h) as hId, id(p) as pId")
					.single();

				personId = record.get("pId").asLong();
				hobbyId = record.get("hId").asLong();
			}

			PersonWithRelationship person = new PersonWithRelationship();
			person.setName("Freddie");
			person.setId(personId);
			Hobby hobby = new Hobby();
			hobby.setId(hobbyId);
			hobby.setName("Music");
			person.setHobbies(hobby);

			PersonWithRelationship savedPerson = repository.save(person);
			try (Session session = createSession()) {

				List<Record> recordList = session.run("MATCH (n:PersonWithRelationship)"
						+ " RETURN n,"
						+ " [(n)-[:Has]->(h:Hobby) | h] as hobbies",
					Values.parameters("name", "Freddie")).list();

				assertThat(recordList).hasSize(1);

				Record record = recordList.get(0);

				assertThat(record.containsKey("n")).isTrue();
				Node rootNode = record.get("n").asNode();
				assertThat(savedPerson.getId()).isEqualTo(rootNode.id());
				assertThat(savedPerson.getName()).isEqualTo("Freddie");

				assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString()))
					.containsExactlyInAnyOrder("Music");

				// assert that only one hobby is stored
				recordList = session.run("MATCH (h:Hobby) RETURN h").list();
				assertThat(recordList).hasSize(1);
			}
		}

		@Test
		void saveEntityWithDeepSelfReferences(@Autowired PetRepository repository) {
			Pet rootPet = new Pet("Luna");
			Pet petOfRootPet = new Pet("Daphne");
			Pet petOfChildPet = new Pet("Mucki");
			Pet petOfGrandChildPet = new Pet("Blacky");

			rootPet.setFriends(singletonList(petOfRootPet));
			petOfRootPet.setFriends(singletonList(petOfChildPet));
			petOfChildPet.setFriends(singletonList(petOfGrandChildPet));

			repository.save(rootPet);

			try (Session session = createSession()) {
				Record record = session.run("MATCH (rootPet:Pet)-[:Has]->(petOfRootPet:Pet)-[:Has]->(petOfChildPet:Pet)"
					+ "-[:Has]->(petOfGrandChildPet:Pet) "
					+ "RETURN rootPet, petOfRootPet, petOfChildPet, petOfGrandChildPet", emptyMap()).single();

				assertThat(record.get("rootPet").asNode().get("name").asString()).isEqualTo("Luna");
				assertThat(record.get("petOfRootPet").asNode().get("name").asString()).isEqualTo("Daphne");
				assertThat(record.get("petOfChildPet").asNode().get("name").asString()).isEqualTo("Mucki");
				assertThat(record.get("petOfGrandChildPet").asNode().get("name").asString()).isEqualTo("Blacky");
			}
		}

		@Test
		void saveEntityWithSelfReferencesInBothDirections(@Autowired PetRepository repository) {
			Pet luna = new Pet("Luna");
			Pet daphne = new Pet("Daphne");

			luna.setFriends(singletonList(daphne));
			daphne.setFriends(singletonList(luna));

			repository.save(luna);

			try (Session session = createSession()) {
				Record record = session.run("MATCH (luna:Pet{name:'Luna'})-[:Has]->(daphne:Pet{name:'Daphne'})"
					+ "-[:Has]->(luna2:Pet{name:'Luna'})"
					+ "RETURN luna, daphne, luna2").single();

				assertThat(record.get("luna").asNode().get("name").asString()).isEqualTo("Luna");
				assertThat(record.get("daphne").asNode().get("name").asString()).isEqualTo("Daphne");
				assertThat(record.get("luna2").asNode().get("name").asString()).isEqualTo("Luna");
			}
		}

		@Test
		void saveEntityGraphWithSelfInverseRelationshipDefined(@Autowired SimilarThingRepository repository) {
			SimilarThing originalThing = new SimilarThing().withName("Original");
			SimilarThing similarThing = new SimilarThing().withName("Similar");

			originalThing.setSimilar(similarThing);
			similarThing.setSimilarOf(originalThing);
			repository.save(originalThing);

			try (Session session = createSession()) {
				Record record = session.run(
					"MATCH (ot:SimilarThing{name:'Original'})-[r:SimilarTo]->(st:SimilarThing {name:'Similar'})"
						+ " RETURN r").single();

				assertThat(record.keys()).isNotEmpty();
				assertThat(record.containsKey("r")).isTrue();
				assertThat(record.get("r").asRelationship().type()).isEqualToIgnoringCase("SimilarTo");
			}
		}

		@Test
		void saveWithAssignedIdAndRelationship(@Autowired ThingRepository repository) {

			ThingWithAssignedId thing = new ThingWithAssignedId("aaBB");
			thing.setName("That's the thing.");
			AnotherThingWithAssignedId anotherThing = new AnotherThingWithAssignedId(4711L);
			anotherThing.setName("AnotherThing");
			thing.setThings(singletonList(anotherThing));
			thing = repository.save(thing);

			try (Session session = createSession()) {
				Record record = session
					.run("MATCH (n:Thing)-[:Has]->(t:Thing2) WHERE n.theId = $id RETURN n, t",
						Values.parameters("id", thing.getTheId()))
					.single();

				assertThat(record.containsKey("n")).isTrue();
				assertThat(record.containsKey("t")).isTrue();
				Node node = record.get("n").asNode();
				assertThat(node.get("theId").asString()).isEqualTo(thing.getTheId());
				assertThat(node.get("name").asString()).isEqualTo(thing.getName());

				Node relatedNode = record.get("t").asNode();
				assertThat(relatedNode.get("theId").asLong()).isEqualTo(anotherThing.getTheId());
				assertThat(relatedNode.get("name").asString()).isEqualTo(anotherThing.getName());
				assertThat(repository.count()).isEqualTo(1);
			}
		}

		@Test
		void saveAllWithAssignedIdAndRelationship(@Autowired ThingRepository repository) {

			ThingWithAssignedId thing = new ThingWithAssignedId("aaBB");
			thing.setName("That's the thing.");
			AnotherThingWithAssignedId anotherThing = new AnotherThingWithAssignedId(4711L);
			anotherThing.setName("AnotherThing");
			thing.setThings(singletonList(anotherThing));
			repository.saveAll(singletonList(thing));

			try (Session session = createSession()) {
				Record record = session
					.run("MATCH (n:Thing)-[:Has]->(t:Thing2) WHERE n.theId = $id RETURN n, t",
						Values.parameters("id", thing.getTheId()))
					.single();

				assertThat(record.containsKey("n")).isTrue();
				assertThat(record.containsKey("t")).isTrue();
				Node node = record.get("n").asNode();
				assertThat(node.get("theId").asString()).isEqualTo(thing.getTheId());
				assertThat(node.get("name").asString()).isEqualTo(thing.getName());

				Node relatedNode = record.get("t").asNode();
				assertThat(relatedNode.get("theId").asLong()).isEqualTo(anotherThing.getTheId());
				assertThat(relatedNode.get("name").asString()).isEqualTo(anotherThing.getName());
				assertThat(repository.count()).isEqualTo(1);
			}
		}

		@Test
		void createComplexSameClassRelationshipsBeforeRootObject(@Autowired
			ImmutablePersonRepository immutablePersonRepository) {

			ImmutablePerson p1 = new ImmutablePerson("Person1", Collections.emptyList());
			ImmutablePerson p2 = new ImmutablePerson("Person2", Arrays.asList(p1));
			ImmutablePerson p3 = new ImmutablePerson("Person3", Arrays.asList(p2));
			ImmutablePerson p4 = new ImmutablePerson("Person4", Arrays.asList(p1, p3));

			immutablePersonRepository.saveAll(Arrays.asList(p4));

			List<ImmutablePerson> people = immutablePersonRepository.findAll();

			assertThat(people).hasSize(4);

		}
	}

	@Nested
	class Delete extends IntegrationTestBase {

		@Override
		void setupData(Transaction transaction) {
			id1 = transaction.run("CREATE (n:PersonWithAllConstructor) RETURN id(n)").next().get(0).asLong();
			id2 = transaction.run("CREATE (n:PersonWithAllConstructor) RETURN id(n)").next().get(0).asLong();

			person1 = new PersonWithAllConstructor(id1, null, null, null, null, null, null, null, null, null, null);
			person2 = new PersonWithAllConstructor(id2, null, null, null, null, null, null, null, null, null, null);
		}

		@Test
		void delete(@Autowired PersonRepository repository) {

			repository.delete(person1);

			assertThat(repository.existsById(id1)).isFalse();
			assertThat(repository.existsById(id2)).isTrue();
		}

		@Test
		void deleteById(@Autowired PersonRepository repository) {

			repository.deleteById(id1);

			assertThat(repository.existsById(id1)).isFalse();
			assertThat(repository.existsById(id2)).isTrue();
		}

		@Test
		void deleteAllEntities(@Autowired PersonRepository repository) {

			repository.deleteAll(Arrays.asList(person1, person2));

			assertThat(repository.existsById(id1)).isFalse();
			assertThat(repository.existsById(id2)).isFalse();
		}

		@Test
		void deleteAll(@Autowired PersonRepository repository) {

			repository.deleteAll();
			assertThat(repository.count()).isEqualTo(0L);
		}

		@Test
		void deleteSimpleRelationship(@Autowired RelationshipRepository repository) {
			try (Session session = createSession()) {
				session.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'})");
			}

			PersonWithRelationship person = repository.getPersonWithRelationshipsViaQuery();
			person.setHobbies(null);
			repository.save(person);
			person = repository.getPersonWithRelationshipsViaQuery();

			assertThat(person.getHobbies()).isNull();
		}

		@Test
		void deleteCollectionRelationship(@Autowired RelationshipRepository repository) {
			try (Session session = createSession()) {
				session.run("CREATE (n:PersonWithRelationship{name:'Freddie'}), "
					+ "(n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'})");
			}

			PersonWithRelationship person = repository.getPersonWithRelationshipsViaQuery();
			person.getPets().remove(0);
			repository.save(person);
			person = repository.getPersonWithRelationshipsViaQuery();

			assertThat(person.getPets()).hasSize(1);
		}

	}

	@Nested
	class ByExample extends IntegrationTestBase {

		@Override
		void setupData(Transaction transaction) {
			ZonedDateTime createdAt = LocalDateTime.of(2019, 1, 1, 23, 23, 42, 0).atZone(ZoneOffset.UTC.normalized());
			id1 = transaction.run("" +
					"CREATE (n:PersonWithAllConstructor) " +
					"  SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place, n.createdAt = $createdAt "
					+
					"RETURN id(n)",
				Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName",
					TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place",
					NEO4J_HQ, "createdAt", createdAt)
			).next().get(0).asLong();
			id2 = transaction.run(
				"CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)",
				Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName",
					TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place",
					SFO)
			).next().get(0).asLong();

			person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME,
				TEST_PERSON_SAMEVALUE,
				true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, createdAt.toInstant());
			person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME,
				TEST_PERSON_SAMEVALUE,
				false, 2L, TEST_PERSON2_BORN_ON, null, emptyList(), SFO, null);
		}

		@Test
		void findOneByExample(@Autowired PersonRepository repository) {

			Example<PersonWithAllConstructor> example = Example
				.of(person1, ExampleMatcher.matchingAll().withIgnoreNullValues());
			Optional<PersonWithAllConstructor> person = repository.findOne(example);

			assertThat(person).isPresent();
			assertThat(person.get()).isEqualTo(person1);
		}

		@Test
		void findAllByExample(@Autowired PersonRepository repository) {

			Example<PersonWithAllConstructor> example = Example
				.of(person1, ExampleMatcher.matchingAll().withIgnoreNullValues());
			List<PersonWithAllConstructor> persons = repository.findAll(example);

			assertThat(persons).containsExactly(person1);
		}

		@Test
		void findAllByExampleWithDifferentMatchers(@Autowired PersonRepository repository) {

			PersonWithAllConstructor person;
			Example<PersonWithAllConstructor> example;
			List<PersonWithAllConstructor> persons;

			person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON2_FIRST_NAME, null, null, null,
				null,
				null, null, null, null);
			example = Example.of(person, ExampleMatcher.matchingAny());

			persons = repository.findAll(example);
			assertThat(persons).containsExactlyInAnyOrder(person1, person2);

			person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME.toUpperCase(), TEST_PERSON2_FIRST_NAME, null,
				null, null, null, null, null, null, null);
			example = Example.of(person, ExampleMatcher.matchingAny().withIgnoreCase("name"));

			persons = repository.findAll(example);
			assertThat(persons).containsExactlyInAnyOrder(person1, person2);

			person = new PersonWithAllConstructor(null,
				TEST_PERSON2_NAME.substring(TEST_PERSON2_NAME.length() - 2).toUpperCase(),
				TEST_PERSON2_FIRST_NAME.substring(0, 2), TEST_PERSON_SAMEVALUE.substring(3, 5), null, null, null, null,
				null, null, null);
			example = Example.of(person, ExampleMatcher
				.matchingAll()
				.withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(StringMatcher.ENDING, true))
				.withMatcher("firstName", ExampleMatcher.GenericPropertyMatcher.of(StringMatcher.STARTING))
				.withMatcher("sameValue", ExampleMatcher.GenericPropertyMatcher.of(StringMatcher.CONTAINING))
			);

			persons = repository.findAll(example);
			assertThat(persons).containsExactlyInAnyOrder(person2);

			person = new PersonWithAllConstructor(null, null, "(?i)ern.*", null, null, null, null, null, null, null,
				null);
			example = Example.of(person, ExampleMatcher.matchingAll().withStringMatcher(StringMatcher.REGEX));

			persons = repository.findAll(example);
			assertThat(persons).containsExactlyInAnyOrder(person1);

			example = Example
				.of(person,
					ExampleMatcher.matchingAll().withStringMatcher(StringMatcher.REGEX).withIncludeNullValues());

			persons = repository.findAll(example);
			assertThat(persons).isEmpty();
		}

		@Test
		void findAllByExampleWithSort(@Autowired PersonRepository repository) {

			Example<PersonWithAllConstructor> example = Example.of(personExample(TEST_PERSON_SAMEVALUE));
			List<PersonWithAllConstructor> persons = repository.findAll(example, Sort.by(Sort.Direction.DESC, "name"));

			assertThat(persons).containsExactly(person2, person1);
		}

		@Test
		void findAllByExampleWithPagination(@Autowired PersonRepository repository) {

			Example<PersonWithAllConstructor> example = Example.of(personExample(TEST_PERSON_SAMEVALUE));
			Iterable<PersonWithAllConstructor> persons = repository
				.findAll(example, PageRequest.of(1, 1, Sort.by("name")));

			assertThat(persons).containsExactly(person2);
		}

		@Test
		void existsByExample(@Autowired PersonRepository repository) {

			Example<PersonWithAllConstructor> example = Example.of(personExample(TEST_PERSON_SAMEVALUE));
			boolean exists = repository.exists(example);

			assertThat(exists).isTrue();
		}

		@Test
		void countByExample(@Autowired PersonRepository repository) {

			Example<PersonWithAllConstructor> example = Example.of(person1);
			long count = repository.count(example);

			assertThat(count).isEqualTo(1);
		}

	}

	@Nested
	class FinderMethodKeywords extends IntegrationTestBase {

		@Override
		void setupData(Transaction transaction) {
			ZonedDateTime createdAt = LocalDateTime.of(2019, 1, 1, 23, 23, 42, 0).atZone(ZoneOffset.UTC.normalized());
			id1 = transaction.run("" +
					"CREATE (n:PersonWithAllConstructor) " +
					"  SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place, n.createdAt = $createdAt "
					+
					"RETURN id(n)",
				Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName",
					TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place",
					NEO4J_HQ, "createdAt", createdAt)
			).next().get(0).asLong();
			id2 = transaction.run(
				"CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)",
				Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName",
					TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place",
					SFO)
			).next().get(0).asLong();

			IntStream.rangeClosed(1, 20).forEach(i ->
				transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})",
					Values.parameters("i", String.format("%02d", i))));

			person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME,
				TEST_PERSON_SAMEVALUE,
				true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, createdAt.toInstant());
			person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME,
				TEST_PERSON_SAMEVALUE,
				false, 2L, TEST_PERSON2_BORN_ON, null, emptyList(), SFO, null);
		}

		@Test
		void findByNegatedSimpleProperty(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons;

			persons = repository.findAllByNameNot(TEST_PERSON1_NAME);
			assertThat(persons).doesNotContain(person1);

			persons = repository.findAllByNameNotIgnoreCase(TEST_PERSON1_NAME.toUpperCase());
			assertThat(persons).doesNotContain(person1);
		}

		@Test
		void findByTrueAndFalse(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> coolPeople = repository.findAllByCoolTrue();
			List<PersonWithAllConstructor> theRest = repository.findAllByCoolFalse();
			assertThat(coolPeople).doesNotContain(person2);
			assertThat(theRest).doesNotContain(person1);
		}

		@Test
		void findByLike(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons;

			persons = repository.findAllByFirstNameLike("Ern");
			assertThat(persons)
				.hasSize(1)
				.contains(person1);

			persons = repository.findAllByFirstNameLikeIgnoreCase("eRN");
			assertThat(persons)
				.hasSize(1)
				.contains(person1);
		}

		@Test
		void findByMatches(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAllByFirstNameMatches("(?i)ern.*");
			assertThat(persons)
				.hasSize(1)
				.contains(person1);
		}

		@Test
		void findByNotLike(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons;

			persons = repository.findAllByFirstNameNotLike("Ern");
			assertThat(persons).doesNotContain(person1);

			persons = repository.findAllByFirstNameNotLikeIgnoreCase("eRN");
			assertThat(persons).doesNotContain(person1);
		}

		@Test
		void findByStartingWith(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons;

			persons = repository.findAllByFirstNameStartingWith("Er");
			assertThat(persons)
				.hasSize(1)
				.contains(person1);

			persons = repository.findAllByFirstNameStartingWithIgnoreCase("eRN");
			assertThat(persons)
				.hasSize(1)
				.contains(person1);
		}

		@Test
		void findByContaining(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons;

			persons = repository.findAllByFirstNameContaining("ni");
			assertThat(persons)
				.hasSize(1)
				.contains(person1);

			persons = repository.findAllByFirstNameContainingIgnoreCase("NI");
			assertThat(persons)
				.hasSize(1)
				.contains(person1);
		}

		@Test
		void findByNotContaining(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons;

			persons = repository.findAllByFirstNameNotContaining("ni");
			assertThat(persons)
				.hasSize(1)
				.contains(person2);

			persons = repository.findAllByFirstNameNotContainingIgnoreCase("NI");
			assertThat(persons)
				.hasSize(1)
				.contains(person2);
		}

		@Test
		void findByEndingWith(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons;

			persons = repository.findAllByFirstNameEndingWith("nie");
			assertThat(persons)
				.hasSize(1)
				.contains(person1);

			persons = repository.findAllByFirstNameEndingWithIgnoreCase("NIE");
			assertThat(persons)
				.hasSize(1)
				.contains(person1);
		}

		@Test
		void findByLessThan(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAllByPersonNumberIsLessThan(2L);
			assertThat(persons)
				.hasSize(1)
				.contains(person1);
		}

		@Test
		void findByLessThanEqual(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAllByPersonNumberIsLessThanEqual(2L);
			assertThat(persons)
				.containsExactlyInAnyOrder(person1, person2);
		}

		@Test
		void findByGreaterThanEqual(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAllByPersonNumberIsGreaterThanEqual(1L);
			assertThat(persons)
				.containsExactlyInAnyOrder(person1, person2);
		}

		@Test
		void findByGreaterThan(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAllByPersonNumberIsGreaterThan(1L);
			assertThat(persons)
				.hasSize(1)
				.contains(person2);
		}

		@Test
		void findByBetweenRange(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons;
			persons = repository.findAllByPersonNumberIsBetween(Range.from(inclusive(1L)).to(inclusive(2L)));
			assertThat(persons)
				.containsExactlyInAnyOrder(person1, person2);

			persons = repository.findAllByPersonNumberIsBetween(Range.from(inclusive(1L)).to(exclusive(2L)));
			assertThat(persons)
				.hasSize(1)
				.contains(person1);

			persons = repository.findAllByPersonNumberIsBetween(Range.from(inclusive(1L)).to(unbounded()));
			assertThat(persons)
				.containsExactlyInAnyOrder(person1, person2);

			persons = repository.findAllByPersonNumberIsBetween(Range.from(exclusive(1L)).to(unbounded()));
			assertThat(persons)
				.hasSize(1)
				.contains(person2);

			persons = repository.findAllByPersonNumberIsBetween(Range.from(Bound.<Long>unbounded()).to(inclusive(2L)));
			assertThat(persons)
				.containsExactlyInAnyOrder(person1, person2);

			persons = repository.findAllByPersonNumberIsBetween(Range.from(Bound.<Long>unbounded()).to(exclusive(2L)));
			assertThat(persons)
				.hasSize(1)
				.contains(person1);

			persons = repository.findAllByPersonNumberIsBetween(Range.unbounded());
			assertThat(persons)
				.containsExactlyInAnyOrder(person1, person2);
		}

		@Test
		void findByBetween(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons;
			persons = repository.findAllByPersonNumberIsBetween(1L, 2L);
			assertThat(persons)
				.containsExactlyInAnyOrder(person1, person2);

			persons = repository.findAllByPersonNumberIsBetween(3L, 5L);
			assertThat(persons).isEmpty();

			persons = repository.findAllByPersonNumberIsBetween(2L, 3L);
			assertThat(persons)
				.hasSize(1)
				.contains(person2);
		}

		@Test
		void findByAfter(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAllByBornOnAfter(TEST_PERSON1_BORN_ON);
			assertThat(persons)
				.hasSize(1)
				.contains(person2);
		}

		@Test
		void findByBefore(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAllByBornOnBefore(TEST_PERSON2_BORN_ON);
			assertThat(persons)
				.hasSize(1)
				.contains(person1);
		}

		@Test
		void findByInstant(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository
				.findAllByCreatedAtBefore(LocalDate.of(2019, 9, 25).atStartOfDay().toInstant(ZoneOffset.UTC));
			assertThat(persons)
				.hasSize(1)
				.contains(person1);
		}

		@Test
		void findByIsNotNull(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAllByNullableIsNotNull();
			assertThat(persons)
				.hasSize(1)
				.contains(person1);
		}

		@Test
		void findByIsNull(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAllByNullableIsNull();
			assertThat(persons)
				.hasSize(1)
				.contains(person2);
		}

		@Test
		void findByIn(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository
				.findAllByFirstNameIn(Arrays.asList("a", "b", TEST_PERSON2_FIRST_NAME, "c"));
			assertThat(persons)
				.hasSize(1)
				.contains(person2);
		}

		@Test
		void findByNotIn(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository
				.findAllByFirstNameNotIn(Arrays.asList("a", "b", TEST_PERSON2_FIRST_NAME, "c"));
			assertThat(persons)
				.hasSize(1)
				.contains(person1);
		}

		@Test
		void findByEmpty(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAllByThingsIsEmpty();
			assertThat(persons)
				.hasSize(1)
				.contains(person2);
		}

		@Test
		void findByNotEmpty(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAllByThingsIsNotEmpty();
			assertThat(persons)
				.hasSize(1)
				.contains(person1);
		}

		@Test
		void findByExists(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons = repository.findAllByNullableExists();
			assertThat(persons)
				.hasSize(1)
				.contains(person1);
		}

		@Test
		void shouldSupportSort(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons;

			persons = repository.findAllByOrderByFirstNameAscBornOnDesc();
			assertThat(persons)
				.containsExactly(person2, person1);
		}

		@Test
		void findByNear(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons;

			persons = repository.findAllByPlaceNear(SFO);
			assertThat(persons)
				.containsExactly(person2, person1);

			persons = repository.findAllByPlaceNearAndFirstNameIn(SFO, singletonList(TEST_PERSON1_FIRST_NAME));
			assertThat(persons)
				.containsExactly(person1);

			Distance distance = new Distance(200.0 / 1000.0, Metrics.KILOMETERS);
			persons = repository.findAllByPlaceNear(MINC, distance);
			assertThat(persons)
				.hasSize(1)
				.contains(person1);

			persons = repository.findAllByPlaceNear(CLARION, distance);
			assertThat(persons).isEmpty();

			persons = repository.findAllByPlaceNear(MINC,
				Distance.between(60.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS));
			assertThat(persons)
				.hasSize(1)
				.contains(person1);

			persons = repository.findAllByPlaceNear(MINC,
				Distance.between(100.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS));
			assertThat(persons).isEmpty();

			final Range<Distance> distanceRange = Range
				.of(inclusive(new Distance(100.0 / 1000.0, Metrics.KILOMETERS)), unbounded());
			persons = repository.findAllByPlaceNear(MINC, distanceRange);
			assertThat(persons)
				.hasSize(1)
				.contains(person2);

			persons = repository.findAllByPlaceNear(distanceRange, MINC);
			assertThat(persons)
				.hasSize(1)
				.contains(person2);

			persons = repository
				.findAllByPlaceWithin(new Circle(new org.springframework.data.geo.Point(MINC.x(), MINC.y()), distance));
			assertThat(persons)
				.hasSize(1)
				.contains(person1);

			Box b = new Box(
				new org.springframework.data.geo.Point(MINC.x() - distance.getValue(), MINC.y() - distance.getValue()),
				new org.springframework.data.geo.Point(MINC.x() + distance.getValue(), MINC.y() + distance.getValue()));
			persons = repository.findAllByPlaceWithin(b);
			assertThat(persons)
				.hasSize(1)
				.contains(person1);

			b = new Box(
				new org.springframework.data.geo.Point(NEO4J_HQ.x(), NEO4J_HQ.y()),
				new org.springframework.data.geo.Point(SFO.x(), SFO.y())
			);
			persons = repository.findAllByPlaceWithin(b);
			assertThat(persons)
				.hasSize(2);

			Polygon p = new Polygon(
				new org.springframework.data.geo.Point(12.993747, 55.6122746),
				new org.springframework.data.geo.Point(12.9927492, 55.6110566),
				new org.springframework.data.geo.Point(12.9953456, 55.6106688),
				new org.springframework.data.geo.Point(12.9946482, 55.6110505),
				new org.springframework.data.geo.Point(12.9959786, 55.6112748),
				new org.springframework.data.geo.Point(12.9951847, 55.6122261),
				new org.springframework.data.geo.Point(12.9942727, 55.6122382),
				new org.springframework.data.geo.Point(12.9937685, 55.6122685),
				new org.springframework.data.geo.Point(12.993747, 55.6122746)
			);

			persons = repository.findAllByPlaceWithin(BoundingBox.of(p));
			assertThat(persons)
				.hasSize(1)
				.contains(person1);

			assertThatIllegalArgumentException().isThrownBy(() -> repository.findAllByPlaceWithin(p))
				.withMessage(
					"The WITHIN operation does not support a class org.springframework.data.geo.Polygon. You might want to pass a bounding box instead: class org.neo4j.springframework.data.repository.query.BoundingBox.of(polygon).");

			persons = repository.findAllByPlaceNear(CLARION, distance);
			assertThat(persons).isEmpty();
		}

		@Test
		void existsById(@Autowired PersonRepository repository) {

			boolean exists = repository.existsById(id1);
			assertThat(exists).isTrue();
		}

		@Test
		void findBySomeCaseInsensitiveProperties(@Autowired PersonRepository repository) {

			List<PersonWithAllConstructor> persons;
			persons = repository
				.findAllByPlaceNearAndFirstNameAllIgnoreCase(SFO, TEST_PERSON1_FIRST_NAME.toUpperCase());
			assertThat(persons)
				.containsExactly(person1);
		}

		@Test
		void limitClauseShouldWork(@Autowired ThingRepository repository) {

			List<ThingWithAssignedId> things;

			things = repository.findTop5ByOrderByNameDesc();
			assertThat(things)
				.hasSize(5)
				.extracting(ThingWithAssignedId::getName)
				.containsExactlyInAnyOrder("name20", "name19", "name18", "name17", "name16");

			things = repository.findFirstByOrderByNameDesc();
			assertThat(things)
				.extracting(ThingWithAssignedId::getName)
				.containsExactlyInAnyOrder("name20");
		}

		@Test
		void count(@Autowired PersonRepository repository) {
			assertThat(repository.count()).isEqualTo(2);
		}

		@Test // GH-112
		void countBySimplePropertiesOred(@Autowired PersonRepository repository) {

			long count = repository.countAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME);
			assertThat(count).isEqualTo(2L);
		}
	}

	@Nested
	class Projection extends IntegrationTestBase {

		@Override
		void setupData(Transaction transaction) {
			id1 = transaction.run("CREATE (n:PersonWithAllConstructor) "
					+ "SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName "
					+ "RETURN id(n)",
				Values.parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName",
					TEST_PERSON1_FIRST_NAME)
			).next().get(0).asLong();
			id2 = transaction.run("CREATE (n:PersonWithAllConstructor) "
					+ "SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName "
					+ "RETURN id(n)",
				Values.parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName",
					TEST_PERSON2_FIRST_NAME)
			).next().get(0).asLong();
		}

		@Test
		void mapsInterfaceProjectionWithDerivedFinderMethod(@Autowired PersonRepository repository) {

			assertThat(repository.findByName(TEST_PERSON1_NAME).getName()).isEqualTo(TEST_PERSON1_NAME);
		}

		@Test
		void mapsDtoProjectionWithDerivedFinderMethod(@Autowired PersonRepository repository) {
			assertThat(repository.findByFirstName(TEST_PERSON1_FIRST_NAME)).hasSize(1);
		}

		@Test
		void mapsInterfaceProjectionWithDerivedFinderMethodWithMultipleResults(@Autowired PersonRepository repository) {
			assertThat(repository.findBySameValue(TEST_PERSON_SAMEVALUE)).hasSize(2);
		}

		@Test
		void mapsInterfaceProjectionWithCustomQueryAndMapProjection(@Autowired PersonRepository repository) {
			assertThat(repository.findByNameWithCustomQueryAndMapProjection(TEST_PERSON1_NAME).getName())
				.isEqualTo(TEST_PERSON1_NAME);
		}

		@Test
		void mapsInterfaceProjectionWithCustomQueryAndMapProjectionWithMultipleResults(
			@Autowired PersonRepository repository) {
			assertThat(repository.loadAllProjectionsWithMapProjection()).hasSize(2);
		}

		@Test
		void mapsInterfaceProjectionWithCustomQueryAndNodeReturn(@Autowired PersonRepository repository) {
			assertThat(repository.findByNameWithCustomQueryAndNodeReturn(TEST_PERSON1_NAME).getName())
				.isEqualTo(TEST_PERSON1_NAME);
		}

		@Test
		void mapsInterfaceProjectionWithCustomQueryAndNodeReturnWithMultipleResults(@Autowired PersonRepository repository) {
			assertThat(repository.loadAllProjectionsWithNodeReturn()).hasSize(2);
		}

	}

	@Nested
	class ReturnTypes extends IntegrationTestBase {

		@Override
		void setupData(Transaction transaction) {
			transaction.run("CREATE (:PersonWithAllConstructor{name: '" + TEST_PERSON1_NAME + "', first_name: '" + TEST_PERSON1_FIRST_NAME + "'}),"
				+ " (:PersonWithAllConstructor{name: '" + TEST_PERSON2_NAME + "'})");
		}

		@Test
		void streamMethodsShouldWork(@Autowired PersonRepository repository) {
			assertThat(repository.findAllByNameLike(TEST_PERSON1_NAME)).hasSize(2);
		}

//		commented see PersonRepository line 126
//		@Test
//		void asyncMethodsShouldWork(@Autowired PersonRepository repository) {
//			PersonWithAllConstructor p = repository.findOneByFirstName(TEST_PERSON1_FIRST_NAME).join();
//			assertThat(p).isNotNull();
//		}
	}

	@Nested
	class MultipleLabel extends IntegrationTestBase {

		@Test
		void createNodeWithMultipleLabels(@Autowired MultipleLabelRepository multipleLabelRepository) {
			multipleLabelRepository.save(new MultipleLabels.MultipleLabelsEntity());

			try (Session session = createSession()) {
				Node node = session.run("MATCH (n:A) return n").single().get("n").asNode();
				assertThat(node.labels()).containsExactlyInAnyOrder("A", "B", "C");
			}
		}

		@Test
		void createAllNodesWithMultipleLabels(@Autowired MultipleLabelRepository multipleLabelRepository) {
			multipleLabelRepository.saveAll(singletonList(new MultipleLabels.MultipleLabelsEntity()));

			try (Session session = createSession()) {
				Node node = session.run("MATCH (n:A) return n").single().get("n").asNode();
				assertThat(node.labels()).containsExactlyInAnyOrder("A", "B", "C");
			}
		}

		@Test
		void createNodeAndRelationshipWithMultipleLabels(@Autowired MultipleLabelRepository multipleLabelRepository) {
			MultipleLabels.MultipleLabelsEntity entity = new MultipleLabels.MultipleLabelsEntity();
			entity.otherMultipleLabelEntity = new MultipleLabels.MultipleLabelsEntity();

			multipleLabelRepository.save(entity);

			try (Session session = createSession()) {
				Record record = session.run("MATCH (n:A)-[:HAS]->(c:A) return n, c").single();
				Node parentNode = record.get("n").asNode();
				Node childNode = record.get("c").asNode();
				assertThat(parentNode.labels()).containsExactlyInAnyOrder("A", "B", "C");
				assertThat(childNode.labels()).containsExactlyInAnyOrder("A", "B", "C");
			}
		}

		@Test
		void findNodeWithMultipleLabels(@Autowired MultipleLabelRepository multipleLabelRepository) {
			long n1Id;
			long n2Id;
			long n3Id;

			try (Session session = createSession()) {
				Record record = session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single();
				n1Id = record.get("n1").asNode().id();
				n2Id = record.get("n2").asNode().id();
				n3Id = record.get("n3").asNode().id();
			}

			assertThat(multipleLabelRepository.findById(n1Id)).isPresent();
			assertThat(multipleLabelRepository.findById(n2Id)).isNotPresent();
			assertThat(multipleLabelRepository.findById(n3Id)).isNotPresent();
		}

		@Test
		void deleteNodeWithMultipleLabels(@Autowired MultipleLabelRepository multipleLabelRepository) {
			long n1Id;
			long n2Id;
			long n3Id;

			try (Session session = createSession()) {
				Record record = session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single();
				n1Id = record.get("n1").asNode().id();
				n2Id = record.get("n2").asNode().id();
				n3Id = record.get("n3").asNode().id();
			}

			multipleLabelRepository.deleteById(n1Id);
			multipleLabelRepository.deleteById(n2Id);
			multipleLabelRepository.deleteById(n3Id);

			try (Session session = createSession()) {
				assertThat(session.run("MATCH (n:A:B:C) return n").list()).hasSize(0);
				assertThat(session.run("MATCH (n:B:C) return n").list()).hasSize(1);
				assertThat(session.run("MATCH (n:A) return n").list()).hasSize(1);
			}
		}

		@Test
		void createNodeWithMultipleLabelsAndAssignedId(
			@Autowired MultipleLabelWithAssignedIdRepository multipleLabelRepository) {
			multipleLabelRepository.save(new MultipleLabels.MultipleLabelsEntityWithAssignedId(4711L));

			try (Session session = createSession()) {
				Node node = session.run("MATCH (n:X) return n").single().get("n").asNode();
				assertThat(node.labels()).containsExactlyInAnyOrder("X", "Y", "Z");
			}
		}

		@Test
		void createAllNodesWithMultipleLabels(
			@Autowired MultipleLabelWithAssignedIdRepository multipleLabelRepository) {
			multipleLabelRepository
				.saveAll(singletonList(new MultipleLabels.MultipleLabelsEntityWithAssignedId(4711L)));

			try (Session session = createSession()) {
				Node node = session.run("MATCH (n:X) return n").single().get("n").asNode();
				assertThat(node.labels()).containsExactlyInAnyOrder("X", "Y", "Z");
			}
		}

		@Test
		void createNodeAndRelationshipWithMultipleLabels(
			@Autowired MultipleLabelWithAssignedIdRepository multipleLabelRepository) {
			MultipleLabels.MultipleLabelsEntityWithAssignedId entity = new MultipleLabels.MultipleLabelsEntityWithAssignedId(
				4711L);
			entity.otherMultipleLabelEntity = new MultipleLabels.MultipleLabelsEntityWithAssignedId(42L);

			multipleLabelRepository.save(entity);

			try (Session session = createSession()) {
				Record record = session.run("MATCH (n:X)-[:HAS]->(c:X) return n, c").single();
				Node parentNode = record.get("n").asNode();
				Node childNode = record.get("c").asNode();
				assertThat(parentNode.labels()).containsExactlyInAnyOrder("X", "Y", "Z");
				assertThat(childNode.labels()).containsExactlyInAnyOrder("X", "Y", "Z");
			}
		}

		@Test
		void findNodeWithMultipleLabels(@Autowired MultipleLabelWithAssignedIdRepository multipleLabelRepository) {
			long n1Id;
			long n2Id;
			long n3Id;

			try (Session session = createSession()) {
				Record record = session
					.run("CREATE (n1:X:Y:Z{id:4711}), (n2:Y:Z{id:42}), (n3:X{id:23}) return n1, n2, n3").single();
				n1Id = record.get("n1").asNode().get("id").asLong();
				n2Id = record.get("n2").asNode().get("id").asLong();
				n3Id = record.get("n3").asNode().get("id").asLong();
			}

			assertThat(multipleLabelRepository.findById(n1Id)).isPresent();
			assertThat(multipleLabelRepository.findById(n2Id)).isNotPresent();
			assertThat(multipleLabelRepository.findById(n3Id)).isNotPresent();
		}

		@Test
		void deleteNodeWithMultipleLabels(@Autowired MultipleLabelWithAssignedIdRepository multipleLabelRepository) {
			long n1Id;
			long n2Id;
			long n3Id;

			try (Session session = createSession()) {
				Record record = session
					.run("CREATE (n1:X:Y:Z{id:4711}), (n2:Y:Z{id:42}), (n3:X{id:23}) return n1, n2, n3").single();
				n1Id = record.get("n1").asNode().get("id").asLong();
				n2Id = record.get("n2").asNode().get("id").asLong();
				n3Id = record.get("n3").asNode().get("id").asLong();
			}

			multipleLabelRepository.deleteById(n1Id);
			multipleLabelRepository.deleteById(n2Id);
			multipleLabelRepository.deleteById(n3Id);

			try (Session session = createSession()) {
				assertThat(session.run("MATCH (n:X:Y:Z) return n").list()).hasSize(0);
				assertThat(session.run("MATCH (n:Y:Z) return n").list()).hasSize(1);
				assertThat(session.run("MATCH (n:X) return n").list()).hasSize(1);
			}
		}
	}

	@Nested
	class TypeInheritanceAndGenerics extends IntegrationTestBase {

		@Test
		void findByIdWithInheritance(@Autowired BaseClassRepository baseClassRepository) {
			String someValue = "test";
			String concreteClassName = "cc1";
			Inheritance.ConcreteClassA ccA = new Inheritance.ConcreteClassA(concreteClassName, someValue);
			baseClassRepository.save(ccA);

			Inheritance.BaseClass loadedCcA = baseClassRepository.findById(ccA.getId()).get();
			assertThat(loadedCcA).isInstanceOfSatisfying(Inheritance.ConcreteClassA.class,
				o -> {
					assertThat(o.getName()).isEqualTo(concreteClassName);
					assertThat(o.getConcreteSomething()).isEqualTo(someValue);
				});
		}

		@Test
		void findAllWithInheritance(@Autowired BaseClassRepository baseClassRepository) {
			Inheritance.ConcreteClassA ccA = new Inheritance.ConcreteClassA("cc1", "test");
			Inheritance.ConcreteClassB ccB = new Inheritance.ConcreteClassB("cc2", 42);
			baseClassRepository.save(ccA);
			baseClassRepository.save(ccB);

			List<Inheritance.BaseClass> all = baseClassRepository.findAll();

			assertThat(all).containsExactlyInAnyOrder(ccA, ccB);
		}

		@Test
		void findAllWithInheritanceAndExplicitLabeling(@Autowired BaseClassWithLabelsRepository repository) {
			String classAName = "test1";
			String classBName = "test2";
			Inheritance.ExtendingClassWithLabelsA classWithLabelsA = new Inheritance.ExtendingClassWithLabelsA(
				classAName);
			Inheritance.ExtendingClassWithLabelsB classWithLabelsB = new Inheritance.ExtendingClassWithLabelsB(
				classBName);

			repository.save(classWithLabelsA);
			repository.save(classWithLabelsB);

			List<Inheritance.BaseClassWithLabels> all = repository.findAll();

			assertThat(all).containsExactlyInAnyOrder(classWithLabelsA, classWithLabelsB);
		}

		@Test
		void findByIdWithTwoLevelInheritance(@Autowired SuperBaseClassRepository superBaseClassRepository) {
			String someValue = "test";
			String concreteClassName = "cc1";
			Inheritance.ConcreteClassA ccA = new Inheritance.ConcreteClassA(concreteClassName, someValue);
			superBaseClassRepository.save(ccA);

			Inheritance.SuperBaseClass loadedCcA = superBaseClassRepository.findById(ccA.getId()).get();
			assertThat(loadedCcA).isInstanceOfSatisfying(Inheritance.ConcreteClassA.class,
				o -> {
					assertThat(o.getName()).isEqualTo(concreteClassName);
					assertThat(o.getConcreteSomething()).isEqualTo(someValue);
				});
		}

		@Test
		void findAllWithTwoLevelInheritance(@Autowired SuperBaseClassRepository superBaseClassRepository) {
			Inheritance.ConcreteClassA ccA = new Inheritance.ConcreteClassA("cc1", "test");
			Inheritance.ConcreteClassB ccB = new Inheritance.ConcreteClassB("cc2", 42);
			superBaseClassRepository.save(ccA);
			superBaseClassRepository.save(ccB);

			List<Inheritance.SuperBaseClass> all = superBaseClassRepository.findAll();

			assertThat(all).containsExactlyInAnyOrder(ccA, ccB);
		}

		@Test
		void findAllWithTwoLevelInheritanceByCustomQuery(@Autowired SuperBaseClassRepository superBaseClassRepository) {
			Inheritance.ConcreteClassA ccA = new Inheritance.ConcreteClassA("cc1", "test");
			Inheritance.ConcreteClassB ccB = new Inheritance.ConcreteClassB("cc2", 42);
			superBaseClassRepository.save(ccA);
			superBaseClassRepository.save(ccB);

			List<Inheritance.SuperBaseClass> all = superBaseClassRepository.getAllConcreteTypes();

			assertThat(all).containsExactlyInAnyOrder(ccA, ccB);
		}

		@Test
		void findAndInstantiateGenericRelationships(@Autowired RelationshipToAbstractClassRepository repository) {

			Inheritance.ConcreteClassA ccA = new Inheritance.ConcreteClassA("cc1", "test");
			Inheritance.ConcreteClassB ccB = new Inheritance.ConcreteClassB("cc2", 42);

			List<Inheritance.SuperBaseClass> things = new ArrayList<>();
			things.add(ccA);
			things.add(ccB);
			Inheritance.RelationshipToAbstractClass thing = new Inheritance.RelationshipToAbstractClass();
			thing.setThings(things);

			repository.save(thing);

			List<Inheritance.RelationshipToAbstractClass> all = repository.findAll();

			assertThat(all.get(0).getThings()).containsExactlyInAnyOrder(ccA, ccB);
		}

		@Test
		void findAndInstantiateGenericRelationshipsWithCustomQuery(
			@Autowired RelationshipToAbstractClassRepository repository) {

			Inheritance.ConcreteClassA ccA = new Inheritance.ConcreteClassA("cc1", "test");
			Inheritance.ConcreteClassB ccB = new Inheritance.ConcreteClassB("cc2", 42);

			List<Inheritance.SuperBaseClass> things = new ArrayList<>();
			things.add(ccA);
			things.add(ccB);
			Inheritance.RelationshipToAbstractClass thing = new Inheritance.RelationshipToAbstractClass();
			thing.setThings(things);

			repository.save(thing);

			Inheritance.RelationshipToAbstractClass result = repository.getAllConcreteRelationships();

			assertThat(result.getThings()).containsExactlyInAnyOrder(ccA, ccB);
		}
	}

	@Nested
	class RelatedEntityQuery extends IntegrationTestBase {

		@Test
		void findByPropertyOnRelatedEntity(@Autowired RelationshipRepository repository) {
			try (Session session = createSession()) {
				session.run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})");
			}

			assertThat(repository.findByPetsName("Jerry").getName()).isEqualTo("Freddie");
		}

		@Test
		void findByPropertyOnRelatedEntitiesOr(@Autowired RelationshipRepository repository) {
			try (Session session = createSession()) {
				session.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Tom'}),"
					+ "(n)-[:Has]->(:Hobby{name: 'Music'})");
			}

			assertThat(repository.findByHobbiesNameOrPetsName("Music", "Jerry").getName()).isEqualTo("Freddie");
			assertThat(repository.findByHobbiesNameOrPetsName("Sports", "Tom").getName()).isEqualTo("Freddie");
			assertThat(repository.findByHobbiesNameOrPetsName("Sports", "Jerry")).isNull();
		}

		@Test
		void findByPropertyOnRelatedEntitiesAnd(@Autowired RelationshipRepository repository) {
			try (Session session = createSession()) {
				session.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Tom'}),"
					+ "(n)-[:Has]->(:Hobby{name: 'Music'})");
			}

			assertThat(repository.findByHobbiesNameAndPetsName("Music", "Tom").getName()).isEqualTo("Freddie");
			assertThat(repository.findByHobbiesNameAndPetsName("Sports", "Jerry")).isNull();
		}

		@Test
		void findByPropertyOnRelatedEntityOfRelatedEntity(@Autowired RelationshipRepository repository) {
			try (Session session = createSession()) {
				session.run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})"
					+ "-[:Has]->(:Hobby{name: 'Sleeping'})");
			}

			assertThat(repository.findByPetsHobbiesName("Sleeping").getName()).isEqualTo("Freddie");
			assertThat(repository.findByPetsHobbiesName("Sports")).isNull();
		}

		@Test
		void findByPropertyOnRelatedEntityOfRelatedSameEntity(@Autowired RelationshipRepository repository) {
			try (Session session = createSession()) {
				session.run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})"
					+ "-[:Has]->(:Pet{name: 'Tom'})");
			}

			assertThat(repository.findByPetsFriendsName("Tom").getName()).isEqualTo("Freddie");
			assertThat(repository.findByPetsFriendsName("Jerry")).isNull();
		}

		@Test
		void findByPropertyOnRelationshipWithProperties(@Autowired PersonWithRelationshipWithPropertiesRepository repository) {
			try (Session session = createSession()) {
				session.run("CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020}]->(:Hobby{name: 'Bowling'})");
			}

			assertThat(repository.findByHobbiesSince(2020).getName()).isEqualTo("Freddie");
		}

		@Test
		void findByPropertyOnRelationshipWithPropertiesOr(@Autowired PersonWithRelationshipWithPropertiesRepository repository) {
			try (Session session = createSession()) {
				session.run("CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})");
			}

			assertThat(repository.findByHobbiesSinceOrHobbiesActive(2020, false).getName()).isEqualTo("Freddie");
			assertThat(repository.findByHobbiesSinceOrHobbiesActive(2019, true).getName()).isEqualTo("Freddie");
			assertThat(repository.findByHobbiesSinceOrHobbiesActive(2019, false)).isNull();
		}

		@Test
		void findByPropertyOnRelationshipWithPropertiesAnd(@Autowired PersonWithRelationshipWithPropertiesRepository repository) {
			try (Session session = createSession()) {
				session.run("CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})");
			}

			assertThat(repository.findByHobbiesSinceAndHobbiesActive(2020, true).getName()).isEqualTo("Freddie");
			assertThat(repository.findByHobbiesSinceAndHobbiesActive(2019, true)).isNull();
			assertThat(repository.findByHobbiesSinceAndHobbiesActive(2020, false)).isNull();
		}
	}

	@Nested
	class Converter extends IntegrationTestBase {

		@Override void setupData(Transaction transaction) {
			transaction.run("CREATE (:CustomTypes{customType:'XYZ'})");
		}

		@Test
		void findByConvertedCustomType(@Autowired EntityWithCustomTypePropertyRepository repository) {

			assertThat(repository.findByCustomType(ThingWithCustomTypes.CustomType.of("XYZ"))).isNotNull();
		}

		@Test
		void findByConvertedCustomTypeWithCustomQuery(@Autowired EntityWithCustomTypePropertyRepository repository) {

			assertThat(repository.findByCustomTypeCustomQuery(ThingWithCustomTypes.CustomType.of("XYZ"))).isNotNull();
		}

		@Test
		void findByConvertedCustomTypeWithSpELPropertyAccessQuery(@Autowired EntityWithCustomTypePropertyRepository repository) {

			assertThat(
				repository.findByCustomTypeCustomSpELPropertyAccessQuery(ThingWithCustomTypes.CustomType.of("XYZ")))
				.isNotNull();
		}

		@Test
		void findByConvertedCustomTypeWithSpELObjectQuery(@Autowired EntityWithCustomTypePropertyRepository repository) {

			assertThat(repository.findByCustomTypeSpELObjectQuery(ThingWithCustomTypes.CustomType.of("XYZ")))
				.isNotNull();
		}

		@Test
		void findByConvertedDifferentTypeWithSpELObjectQuery(@Autowired EntityWithCustomTypePropertyRepository repository) {

			assertThat(repository.findByDifferentTypeCustomQuery(ThingWithCustomTypes.DifferentType.of("XYZ")))
				.isNotNull();
		}
	}

	interface BidirectionalStartRepository extends Neo4jRepository<BidirectionalStart, Long> {
	}

	interface BidirectionalEndRepository extends Neo4jRepository<BidirectionalEnd, Long> {
	}

	interface DeepRelationshipRepository extends Neo4jRepository<DeepRelationships.Type1, Long> {
	}

	interface LoopingRelationshipRepository extends Neo4jRepository<DeepRelationships.LoopingType1, Long> {
	}

	interface ImmutablePersonRepository extends Neo4jRepository<ImmutablePerson, String> {
	}

	interface MultipleLabelRepository extends Neo4jRepository<MultipleLabels.MultipleLabelsEntity, Long> {
	}

	interface MultipleLabelWithAssignedIdRepository extends Neo4jRepository<MultipleLabels.MultipleLabelsEntityWithAssignedId, Long> {
	}

	interface PersonWithRelationshipWithPropertiesRepository extends Neo4jRepository<PersonWithRelationshipWithProperties, Long> {

		@Query("MATCH (p:PersonWithRelationshipWithProperties)-[l:LIKES]->(h:Hobby) return p, collect(l), collect(h)")
		PersonWithRelationshipWithProperties loadFromCustomQuery(@Param("id") Long id);

		PersonWithRelationshipWithProperties findByHobbiesSince(int since);

		PersonWithRelationshipWithProperties findByHobbiesSinceOrHobbiesActive(int since1, boolean active);

		PersonWithRelationshipWithProperties findByHobbiesSinceAndHobbiesActive(int since1, boolean active);
	}

	interface PetRepository extends Neo4jRepository<Pet, Long> {
	}

	interface RelationshipRepository extends Neo4jRepository<PersonWithRelationship, Long> {

		@Query("MATCH (n:PersonWithRelationship{name:'Freddie'}) "
			+ "OPTIONAL MATCH (n)-[r1:Has]->(p:Pet) WITH n, collect(r1) as petRels, collect(p) as pets "
			+ "OPTIONAL MATCH (n)-[r2:Has]->(h:Hobby) "
			+ "return n, petRels, pets, collect(r2) as hobbyRels, collect(h) as hobbies")
		PersonWithRelationship getPersonWithRelationshipsViaQuery();

		PersonWithRelationship findByPetsName(String petName);

		PersonWithRelationship findByHobbiesNameOrPetsName(String hobbyName, String petName);

		PersonWithRelationship findByHobbiesNameAndPetsName(String hobbyName, String petName);

		PersonWithRelationship findByPetsHobbiesName(String hobbyName);

		PersonWithRelationship findByPetsFriendsName(String petName);
	}

	interface SimilarThingRepository extends Neo4jRepository<SimilarThing, Long> {
	}

	interface BaseClassRepository extends Neo4jRepository<Inheritance.BaseClass, Long> {
	}

	interface SuperBaseClassRepository extends Neo4jRepository<Inheritance.SuperBaseClass, Long> {

		@Query("MATCH (n:SuperBaseClass) return n")
		List<Inheritance.SuperBaseClass> getAllConcreteTypes();
	}

	interface RelationshipToAbstractClassRepository extends Neo4jRepository<Inheritance.RelationshipToAbstractClass, Long> {

		@Query("MATCH (n:RelationshipToAbstractClass)-[h:HAS]->(m:SuperBaseClass) return n, collect(h), collect(m)")
		Inheritance.RelationshipToAbstractClass getAllConcreteRelationships();
	}

	interface BaseClassWithLabelsRepository extends Neo4jRepository<Inheritance.BaseClassWithLabels, Long> {
	}

	interface EntityWithConvertedIdRepository extends Neo4jRepository<EntityWithConvertedId, EntityWithConvertedId.IdentifyingEnum> {
	}

	interface EntityWithCustomTypePropertyRepository extends Neo4jRepository<ThingWithCustomTypes, Long> {

		ThingWithCustomTypes findByCustomType(ThingWithCustomTypes.CustomType customType);

		@Query("MATCH (c:CustomTypes) WHERE c.customType = $customType return c")
		ThingWithCustomTypes findByCustomTypeCustomQuery(@Param("customType") ThingWithCustomTypes.CustomType customType);

		@Query("MATCH (c:CustomTypes) WHERE c.customType = $differentType return c")
		ThingWithCustomTypes findByDifferentTypeCustomQuery(@Param("differentType") ThingWithCustomTypes.DifferentType differentType);

		@Query("MATCH (c:CustomTypes) WHERE c.customType = :#{#customType.value} return c")
		ThingWithCustomTypes findByCustomTypeCustomSpELPropertyAccessQuery(@Param("customType") ThingWithCustomTypes.CustomType customType);

		@Query("MATCH (c:CustomTypes) WHERE c.customType = :#{#customType} return c")
		ThingWithCustomTypes findByCustomTypeSpELObjectQuery(@Param("customType") ThingWithCustomTypes.CustomType customType);
	}

	@SpringJUnitConfig(Config.class)
	static abstract class IntegrationTestBase {

		@Autowired
		private Driver driver;

		void setupData(Transaction transaction) {

		}

		@BeforeEach
		void before() {
			Session session = createSession();
			session.writeTransaction(tx -> {
				tx.run("MATCH (n) detach delete n").consume();
				setupData(tx);
				return null;
			});
			session.close();
		}

		Session createSession() {
			return driver.session(Optional.ofNullable(databaseSelection.getValue())
				.map(SessionConfig::forDatabase).orElseGet(SessionConfig::defaultConfig));
		}

	}

	@Configuration
	@EnableNeo4jRepositories(considerNestedRepositories = true)
	@EnableTransactionManagement
	static class Config extends AbstractNeo4jConfig {

		@Bean
		public Driver driver() {
			return neo4jConnectionSupport.getDriver();
		}

		@Override
		public Neo4jConversions neo4jConversions() {
			Set<GenericConverter> additionalConverters = new HashSet<>();
			additionalConverters.add(new ThingWithCustomTypes.CustomTypeConverter());
			additionalConverters.add(new ThingWithCustomTypes.DifferentTypeConverter());

			return new Neo4jConversions(additionalConverters);
		}

		@Override
		protected Collection<String> getMappingBasePackages() {
			return singletonList(PersonWithAllConstructor.class.getPackage().getName());
		}

		@Bean
		public DatabaseSelectionProvider databaseNameProvider() {
			return () -> databaseSelection;
		}
	}
}