package org.codefilarete.stalactite.engine.runtime.load;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.function.Function;

import org.assertj.core.presentation.StandardRepresentation;
import org.codefilarete.reflection.Accessor;
import org.codefilarete.stalactite.engine.runtime.load.EntityInflater.EntityMappingAdapter;
import org.codefilarete.stalactite.mapping.DefaultEntityMapping;
import org.codefilarete.stalactite.query.model.Fromable;
import org.codefilarete.stalactite.query.model.QueryEase;
import org.codefilarete.stalactite.query.model.QueryStatement.PseudoColumn;
import org.codefilarete.stalactite.query.model.Selectable;
import org.codefilarete.stalactite.query.model.Union;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Key;
import org.codefilarete.stalactite.sql.ddl.structure.PrimaryKey;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.tool.collection.Arrays;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.function.Predicates;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.codefilarete.stalactite.engine.runtime.load.EntityJoinTree.JoinType.INNER;
import static org.codefilarete.stalactite.engine.runtime.load.EntityJoinTree.JoinType.OUTER;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * @author Guillaume Mary
 */
class EntityJoinTreeTest {
	
	static DefaultEntityMapping buildMappingStrategyMock(String tableName) {
		return buildMappingStrategyMock(new Table(tableName));
	}
	
	static DefaultEntityMapping buildMappingStrategyMock(Table table) {
		DefaultEntityMapping mappingStrategyMock = mock(DefaultEntityMapping.class);
		when(mappingStrategyMock.getTargetTable()).thenReturn(table);
		// the selected columns are plugged on the table ones
		when(mappingStrategyMock.getSelectableColumns()).thenAnswer(invocation -> table.getColumns());
		return mappingStrategyMock;
	}
	
	@Test
	void cloneNodeForParent_relationJoinNode() {
		// Given following tree:
		// Toto.id (Root)
		// Toto.tataId = Tata.id (X)
		DefaultEntityMapping totoMappingMock = buildMappingStrategyMock("Toto");
		Table totoTable = totoMappingMock.getTargetTable();
		Column<Table, Long> tataIdColumn = totoTable.addColumn("tataId", long.class);
		Table tataTable = new Table("Tata");
		Column<Table, Long> dummyColumn = tataTable.addColumn("dummyColumn", long.class);
		tataTable.addColumn("id", long.class).primaryKey();
		EntityJoinTree totoEntityJoinTree = new EntityJoinTree(totoMappingMock);

		// Creating the relation join
		String relationJoinName = totoEntityJoinTree.addRelationJoin(EntityJoinTree.ROOT_JOIN_NAME,
				mock(EntityInflater.class),
				mock(Accessor.class),
				Key.ofSingleColumn(tataIdColumn),
				tataTable.<Long>getPrimaryKey(),
				"dummyTableAlias",
				INNER,
				null,
				Arrays.asSet(dummyColumn));
		AbstractJoinNode tataJoinInToto = (AbstractJoinNode) totoEntityJoinTree.getJoin(relationJoinName);

		// and another given tree:
		// Tutu.id (Root)
		DefaultEntityMapping tutuMappingMock = buildMappingStrategyMock("Tutu");
		EntityJoinTree tutuEntityJoinTree = new EntityJoinTree(tutuMappingMock);
		Table<?> tutuTable = (Table<?>) tutuEntityJoinTree.getRoot().getTable();
		tutuTable.addColumn("id", long.class).primaryKey();

		// When we clone:
		// Toto.tataId = Tata.id (X)
		// to
		// Tutu.id (Root)
		PrimaryKey<?, Object> targetJoinLink = tutuTable.getPrimaryKey();
		AbstractJoinNode<?, ?, ?, ?> tataJoinInTutu = EntityJoinTree.cloneNodeForParent(tataJoinInToto, tutuEntityJoinTree.getRoot(), targetJoinLink);

		// Then we should get:
		// Tutu.id (Root)
		// Tutu.id = Tata.id (X clone)
		assertThat(tataJoinInTutu.getLeftJoinLink()).isNotSameAs(targetJoinLink);
		assertThat(tataJoinInTutu.getLeftJoinLink()).usingRecursiveComparison().isEqualTo(targetJoinLink);
		assertThat(tutuEntityJoinTree.getRoot().getJoins()).hasSize(1);
		assertThat(tutuEntityJoinTree.getRoot().getJoins().get(0)).isInstanceOf(RelationJoinNode.class);
		// right table must be cloned ...
		assertThat(tataJoinInTutu.getTable()).isNotSameAs(tataTable);
		// ... and its columns must be the same as the original one
		Function<Selectable, String> getExpression = Selectable::getExpression;
		Function<Selectable, Class> getJavaType = Selectable::getJavaType;
		assertThat(tataJoinInTutu.getRightJoinLink().getColumns())
				.usingElementComparator(Predicates.toComparator(Predicates.and(getExpression, getExpression).and(Predicates.and(getJavaType, getJavaType))))
				.isEqualTo(tataTable.getPrimaryKey().getColumns());

		// we check that the join is well-formed
		Function<AbstractJoinNode, Collection<Column>> getLeftJoinColumns = joinNode -> joinNode.getLeftJoinLink().getColumns();
		Function<AbstractJoinNode, Collection<Column>> getRightJoinColumns = joinNode -> joinNode.getRightJoinLink().getColumns();
		
		assertThat((Iterable<AbstractJoinNode<?, ?, ?, ?>>)() -> tutuEntityJoinTree.joinIterator())
				.usingElementComparator(Predicates.toComparator(Predicates.and(getLeftJoinColumns, getLeftJoinColumns).and(Predicates.and(getRightJoinColumns, getRightJoinColumns))))
				.withRepresentation(new Printer<>(AbstractJoinNode.class, joinNode -> joinNode.getLeftJoinLink().getColumns() + " = " + joinNode.getRightJoinLink().getColumns()))
				.containsExactly(tataJoinInTutu);

		// we check that the join has the right attributes
		assertThat(tataJoinInTutu.getJoinType()).isEqualTo(tataJoinInToto.getJoinType());
		assertThat(tataJoinInTutu.getTableAlias()).isEqualTo(tataJoinInToto.getTableAlias());
		// selectable columns must be the same (to let them be available as key in ColumnedRow, thus, making them accessible by end user)
		assertThat(tataJoinInTutu.getColumnsToSelect())
				.usingElementComparator(Predicates.toComparator(Predicates.and(getExpression, getExpression).and(Predicates.and(getJavaType, getJavaType))))
				.isEqualTo(tataJoinInToto.getColumnsToSelect());
	}

	@Test
	void cloneNodeForParent_mergeJoinNode() {
		// Given following tree:
		// Toto.id (Root)
		// Toto.tataId = Tata.id (X)
		DefaultEntityMapping totoMappingMock = buildMappingStrategyMock("Toto");
		Table totoTable = totoMappingMock.getTargetTable();
		Column<Table, Long> tataIdColumn = totoTable.addColumn("tataId", long.class);
		Table tataTable = new Table("Tata");
		Column<Table, Long> dummyColumn = tataTable.addColumn("dummyColumn", long.class);
		tataTable.addColumn("id", long.class).primaryKey();
		EntityJoinTree totoEntityJoinTree = new EntityJoinTree(totoMappingMock);
		
		// Creating the merge join. We must make it have some selectable columns because we expect them to be cloned and returned by the newly created join (clone) 
		EntityMerger entityMergerMock = mock(EntityMerger.class);
		when(entityMergerMock.getSelectableColumns()).thenReturn(Arrays.asSet(dummyColumn));
		
		String relationJoinName = totoEntityJoinTree.addMergeJoin(EntityJoinTree.ROOT_JOIN_NAME,
				entityMergerMock,
				Key.ofSingleColumn(tataIdColumn),
				tataTable.<Long>getPrimaryKey());
		AbstractJoinNode tataJoinInToto = (AbstractJoinNode) totoEntityJoinTree.getJoin(relationJoinName);

		// and another given tree:
		// Tutu.id (Root)
		DefaultEntityMapping tutuMappingMock = buildMappingStrategyMock("Tutu");
		EntityJoinTree tutuEntityJoinTree = new EntityJoinTree(tutuMappingMock);
		Table tutuTable = (Table) tutuEntityJoinTree.getRoot().getTable();
		tutuTable.addColumn("id", long.class).primaryKey();

		// When we clone:
		// Toto.tataId = Tata.id (X)
		// to
		// Tutu.id (Root)
		PrimaryKey<?, Object> targetJoinLink = tutuTable.getPrimaryKey();
		AbstractJoinNode<?, ?, ?, ?> tataJoinInTutu = EntityJoinTree.cloneNodeForParent(tataJoinInToto, tutuEntityJoinTree.getRoot(), targetJoinLink);

		// Then we should get:
		// Tutu.id (Root)
		// Tutu.id = Tata.id (X clone)
		assertThat(tataJoinInTutu.getParent()).isSameAs(tutuEntityJoinTree.getRoot());
		assertThat(tataJoinInTutu.getLeftJoinLink()).isNotSameAs(targetJoinLink);
		assertThat(tataJoinInTutu.getLeftJoinLink()).usingRecursiveComparison().isEqualTo(targetJoinLink);
		assertThat(tutuEntityJoinTree.getRoot().getJoins()).hasSize(1);
		assertThat(tutuEntityJoinTree.getRoot().getJoins().get(0)).isInstanceOf(MergeJoinNode.class);
		// right table must be cloned ...
		assertThat(tataJoinInTutu.getTable()).isNotSameAs(tataTable);
		// ... and its columns must be the same as the original one
		Function<Selectable, String> getExpression = Selectable::getExpression;
		Function<Selectable, Class> getJavaType = Selectable::getJavaType;
		assertThat(tataJoinInTutu.getRightJoinLink().getColumns())
				.usingElementComparator(Predicates.toComparator(Predicates.and(getExpression, getExpression).and(Predicates.and(getJavaType, getJavaType))))
				.isEqualTo(tataTable.getPrimaryKey().getColumns());

		// we check that the join is well-formed
		Function<AbstractJoinNode, Collection<Column>> getLeftJoinColumns = joinNode -> joinNode.getLeftJoinLink().getColumns();
		Function<AbstractJoinNode, Collection<Column>> getRightJoinColumns = joinNode -> joinNode.getRightJoinLink().getColumns();

		assertThat((Iterable<AbstractJoinNode<?, ?, ?, ?>>)() -> tutuEntityJoinTree.joinIterator())
				.usingElementComparator(Predicates.toComparator(Predicates.and(getLeftJoinColumns, getLeftJoinColumns).and(Predicates.and(getRightJoinColumns, getRightJoinColumns))))
				.withRepresentation(new Printer<>(AbstractJoinNode.class, joinNode -> joinNode.getLeftJoinLink().getColumns() + " = " + joinNode.getRightJoinLink().getColumns()))
				.containsExactly(tataJoinInTutu);

		// we check that the join has the right attributes
		assertThat(tataJoinInTutu.getJoinType()).isEqualTo(tataJoinInToto.getJoinType());
		assertThat(tataJoinInTutu.getTableAlias()).isEqualTo(tataJoinInToto.getTableAlias());
		// selectable columns must be the same (to let them be available as key in ColumnedRow, thus, making them accessible by end user)
		assertThat(tataJoinInTutu.getColumnsToSelect())
				.usingElementComparator(Predicates.toComparator(Predicates.and(getExpression, getExpression).and(Predicates.and(getJavaType, getJavaType))))
				.isEqualTo(tataJoinInToto.getColumnsToSelect());
	}

	@Test
	void cloneNodeForParent_passiveJoinNode() {
		// Given following tree:
		// Toto.id (Root)
		// Toto.tataId = Tata.id (X)
		DefaultEntityMapping totoMappingMock = buildMappingStrategyMock("Toto");
		Table totoTable = totoMappingMock.getTargetTable();
		Column<Table, Long> tataIdColumn = totoTable.addColumn("tataId", long.class);
		Table tataTable = new Table("Tata");
		Column<Table, Long> dummyColumn = tataTable.addColumn("dummyColumn", long.class);
		tataTable.addColumn("id", long.class).primaryKey();
		EntityJoinTree totoEntityJoinTree = new EntityJoinTree(totoMappingMock);

		// Creating the passive join
		String relationJoinName = totoEntityJoinTree.addPassiveJoin(EntityJoinTree.ROOT_JOIN_NAME,
				Key.ofSingleColumn(tataIdColumn),
				tataTable.<Long>getPrimaryKey(),
				OUTER,
				Arrays.asSet(dummyColumn));
		AbstractJoinNode tataJoinInToto = (AbstractJoinNode) totoEntityJoinTree.getJoin(relationJoinName);

		// and another given tree:
		// Tutu.id (Root)
		DefaultEntityMapping tutuMappingMock = buildMappingStrategyMock("Tutu");
		EntityJoinTree tutuEntityJoinTree = new EntityJoinTree(tutuMappingMock);
		Table tutuTable = (Table) tutuEntityJoinTree.getRoot().getTable();
		tutuTable.addColumn("id", long.class).primaryKey();

		// When we clone:
		// Toto.tataId = Tata.id (X)
		// to
		// Tutu.id (Root)
		PrimaryKey<?, Object> targetJoinLink = ((Table<?>) tutuEntityJoinTree.getRoot().getTable()).getPrimaryKey();
		AbstractJoinNode<?, ?, ?, ?> tataJoinInTutu = EntityJoinTree.cloneNodeForParent(tataJoinInToto, tutuEntityJoinTree.getRoot(), targetJoinLink);

		// Then we should get:
		// Tutu.id (Root)
		// Tutu.id = Tata.id (X clone)
		assertThat(tataJoinInTutu.getParent()).isSameAs(tutuEntityJoinTree.getRoot());
		assertThat(tataJoinInTutu.getLeftJoinLink()).isNotSameAs(targetJoinLink);
		assertThat(tataJoinInTutu.getLeftJoinLink()).usingRecursiveComparison().isEqualTo(targetJoinLink);
		assertThat(tutuEntityJoinTree.getRoot().getJoins()).hasSize(1);
		assertThat(tutuEntityJoinTree.getRoot().getJoins().get(0)).isInstanceOf(PassiveJoinNode.class);
		// right table must be cloned ...
		assertThat(tataJoinInTutu.getTable()).isNotSameAs(tataTable);
		// ... and its columns must be the same as the original one
		Function<Selectable, String> getExpression = Selectable::getExpression;
		Function<Selectable, Class> getJavaType = Selectable::getJavaType;
		assertThat(tataJoinInTutu.getRightJoinLink().getColumns())
				.usingElementComparator(Predicates.toComparator(Predicates.and(getExpression, getExpression).and(Predicates.and(getJavaType, getJavaType))))
				.isEqualTo(tataTable.getPrimaryKey().getColumns());

		// we check that the join is well-formed
		Function<AbstractJoinNode, Collection<Column>> getLeftJoinColumns = joinNode -> joinNode.getLeftJoinLink().getColumns();
		Function<AbstractJoinNode, Collection<Column>> getRightJoinColumns = joinNode -> joinNode.getRightJoinLink().getColumns();

		assertThat((Iterable<AbstractJoinNode<?, ?, ?, ?>>)() -> tutuEntityJoinTree.joinIterator())
				.usingElementComparator(Predicates.toComparator(Predicates.and(getLeftJoinColumns, getLeftJoinColumns).and(Predicates.and(getRightJoinColumns, getRightJoinColumns))))
				.withRepresentation(new Printer<>(AbstractJoinNode.class, joinNode -> joinNode.getLeftJoinLink().getColumns() + " = " + joinNode.getRightJoinLink().getColumns()))
				.containsExactly(tataJoinInTutu);

		// we check that the join has the right attributes
		assertThat(tataJoinInTutu.getJoinType()).isEqualTo(tataJoinInToto.getJoinType());
		assertThat(tataJoinInTutu.getTableAlias()).isEqualTo(tataJoinInToto.getTableAlias());
		// selectable columns must be the same (to let them be available as key in ColumnedRow, thus, making them accessible by end user)
		assertThat(tataJoinInTutu.getColumnsToSelect())
				.usingElementComparator(Predicates.toComparator(Predicates.and(getExpression, getExpression).and(Predicates.and(getJavaType, getJavaType))))
				.isEqualTo(tataJoinInToto.getColumnsToSelect());
	}
	
	static Fromable getOwner(Selectable<?> selectable) {
		if (selectable instanceof Column) {
			return ((Column<?, ?>) selectable).getOwner();
		} else if (selectable instanceof PseudoColumn) {
			return ((PseudoColumn) selectable).getOwner();
		} else {
			throw new IllegalArgumentException("Selectable " + selectable + " is not a JoinLink or a Key");
		}
	}
	
	@Test
	void projectTo() {
		DefaultEntityMapping totoMappingMock = buildMappingStrategyMock("Toto");
		Table totoTable = totoMappingMock.getTargetTable();
		totoTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> totoPrimaryKey = totoTable.getPrimaryKey();
		// column for "noise" in select
		Column totoNameColumn = totoTable.addColumn("name", String.class);
		
		DefaultEntityMapping tataMappingMock = buildMappingStrategyMock("Tata");
		Table tataTable = tataMappingMock.getTargetTable();
		tataTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> tataPrimaryKey = tataTable.getPrimaryKey();
		// column for "noise" in select
		Column tataNameColumn = tataTable.addColumn("name", String.class);
		
		DefaultEntityMapping tutuMappingMock = buildMappingStrategyMock("Tutu");
		Table tutuTable = tutuMappingMock.getTargetTable();
		tutuTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> tutuPrimaryKey = tutuTable.getPrimaryKey();
		// column for "noise" in select
		Column tutuNameColumn = tutuTable.addColumn("name", String.class);
		
		DefaultEntityMapping titiMappingMock = buildMappingStrategyMock("Titi");
		Table titiTable = titiMappingMock.getTargetTable();
		titiTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> titiPrimaryKey = titiTable.getPrimaryKey();
		// column for "noise" in select
		Column titiNameColumn = titiTable.addColumn("name", String.class);
		
		// Given following tree:
		// Toto.id = Tata.id (X)
		//   Tata.id = Tutu.id (Y)
		EntityJoinTree entityJoinTree1 = new EntityJoinTree(new EntityMappingAdapter(totoMappingMock), totoMappingMock.getTargetTable());
		String tataAddKey = entityJoinTree1.addRelationJoin(EntityJoinTree.ROOT_JOIN_NAME, new EntityMappingAdapter(tataMappingMock), mock(Accessor.class), totoPrimaryKey, tataPrimaryKey, null, INNER, null, Collections.emptySet());
		String tutuAddKey = entityJoinTree1.addRelationJoin(tataAddKey, new EntityMappingAdapter(tutuMappingMock), mock(Accessor.class), tataPrimaryKey, tutuPrimaryKey, null, INNER, null, Collections.emptySet());
		
		// and following second one:
		// Tata.id = Titi.id (Z)
		EntityJoinTree entityJoinTree2 = new EntityJoinTree(new EntityMappingAdapter(tataMappingMock), tataMappingMock.getTargetTable());
		String titiAddKey = entityJoinTree2.addRelationJoin(EntityJoinTree.ROOT_JOIN_NAME, new EntityMappingAdapter(titiMappingMock), mock(Accessor.class), tataPrimaryKey, titiPrimaryKey, null, INNER, null, Collections.emptySet());
		
		// projecting second one to first one on X
		entityJoinTree2.projectTo(entityJoinTree1, tataAddKey);
		
		// we expect to have:
		// Toto.id = Tata.id (X)
		//   Tata.id = Tutu.id (Y)
		//   Tata.id = Titi.id (Z)
		JoinNode tataJoinClone = entityJoinTree1.getJoin(tataAddKey);
		assertThat(tataJoinClone).isNotNull();
		// by checking node count we ensure that node was added 
		assertThat(Iterables.stream(entityJoinTree1.joinIterator()).count()).isEqualTo(3);
		// and there was no removal
		assertThat(entityJoinTree2.getJoin(titiAddKey)).isNotNull();
		// we check that a copy was made, not a node move
		AbstractJoinNode<?, ?, ?, ?> abstractJoinNode = (AbstractJoinNode) entityJoinTree1.getJoin(titiAddKey);
		assertThat(abstractJoinNode).isNotSameAs(entityJoinTree2.getJoin(titiAddKey));

		// selectable columns must be cloned ...
		Function<Selectable, String> getExpression = Selectable::getExpression;
		Function<Selectable, Class> getJavaType = Selectable::getJavaType;
		assertThat(abstractJoinNode.getColumnsToSelect())
				.usingElementComparator(Predicates.toComparator(Predicates.and(getExpression, getExpression).and(Predicates.and(getJavaType, getJavaType))))
				.isEqualTo(titiMappingMock.getSelectableColumns());
		// ... but different from the original one
		assertThat(abstractJoinNode.getColumnsToSelect())
				.allSatisfy(column -> assertThat(EntityJoinTreeTest.getOwner(column)).isNotSameAs(tataTable));
		
		// copy must be put at the right place 
		assertThat(abstractJoinNode.getParent()).isEqualTo(tataJoinClone);
		// we check that join indexes were updated: since it's difficult to check detailed content because of index naming strategy, we fallback to count them (far from perfect) 
		assertThat(entityJoinTree1.getJoinIndex()).hasSize(4);
		assertThat(entityJoinTree2.getJoinIndex()).hasSize(2);
	}
	
	@Test
	void joinIterator() {
		DefaultEntityMapping totoMappingMock = buildMappingStrategyMock("Toto");
		Table totoTable = totoMappingMock.getTargetTable();
		totoTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> totoPrimaryKey = totoTable.getPrimaryKey();
		// column for "noise" in select
		Column totoNameColumn = totoTable.addColumn("name", String.class);
		
		DefaultEntityMapping tataMappingMock = buildMappingStrategyMock("Tata");
		Table tataTable = tataMappingMock.getTargetTable();
		tataTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> tataPrimaryKey = tataTable.getPrimaryKey();
		// column for "noise" in select
		Column tataNameColumn = tataTable.addColumn("name", String.class);
		
		DefaultEntityMapping tutuMappingMock = buildMappingStrategyMock("Tutu");
		Table tutuTable = tutuMappingMock.getTargetTable();
		tutuTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> tutuPrimaryKey = tutuTable.getPrimaryKey();
		// column for "noise" in select
		Column tutuNameColumn = tutuTable.addColumn("name", String.class);
		
		DefaultEntityMapping titiMappingMock = buildMappingStrategyMock("Titi");
		Table titiTable = titiMappingMock.getTargetTable();
		titiTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> titiPrimaryKey = titiTable.getPrimaryKey();
		// column for "noise" in select
		Column titiNameColumn = titiTable.addColumn("name", String.class);
		
		Table tataTableClone = new Table("tata2");
		DefaultEntityMapping tataCloneMappingMock = buildMappingStrategyMock(tataTableClone);
		tataTableClone.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> tataClonePrimaryKey = tataTableClone.getPrimaryKey();
		// column for "noise" in select
		Column tataCloneNameColumn = tataTableClone.addColumn("name", String.class);
		
		Table tutuTableClone = new Table("tutu2");
		DefaultEntityMapping tutuCloneMappingMock = buildMappingStrategyMock(tutuTableClone);
		tutuTableClone.addColumn("id", long.class).primaryKey();
		// column for "noise" in select
		Column tutuCloneNameColumn = tutuTableClone.addColumn("name", String.class);
		PrimaryKey<?, Long> tutuClonePrimaryKey = tutuTableClone.getPrimaryKey();
		
		// Given following tree:
		// Toto.id = Tata.id (X)
		//   Tata.id = Tutu.id (Y)
		//   Tata.id = Titi.id (Z)
		// Toto.id = tata2.id (X')
		//   tata2.id = tutu2.id (Y')
		
		EntityJoinTree entityJoinTree = new EntityJoinTree(new EntityMappingAdapter(totoMappingMock), totoMappingMock.getTargetTable());
		String tataAddKey = entityJoinTree.addRelationJoin(EntityJoinTree.ROOT_JOIN_NAME, new EntityMappingAdapter(tataMappingMock), mock(Accessor.class), totoPrimaryKey, tataPrimaryKey, null, INNER, null, Collections.emptySet());
		String tutuAddKey = entityJoinTree.addRelationJoin(tataAddKey, new EntityMappingAdapter(tutuMappingMock), mock(Accessor.class), tataPrimaryKey, tutuPrimaryKey, null, INNER, null, Collections.emptySet());
		String titiAddKey = entityJoinTree.addRelationJoin(tataAddKey, new EntityMappingAdapter(titiMappingMock), mock(Accessor.class), tataPrimaryKey, titiPrimaryKey, null, INNER, null, Collections.emptySet());
		String tataAddKey2 = entityJoinTree.addRelationJoin(EntityJoinTree.ROOT_JOIN_NAME, new EntityMappingAdapter(tataCloneMappingMock), mock(Accessor.class), totoPrimaryKey, tataClonePrimaryKey, null, INNER, null, Collections.emptySet());
		String tutuAddKey2 = entityJoinTree.addRelationJoin(tataAddKey2, new EntityMappingAdapter(tutuCloneMappingMock), mock(Accessor.class), tataClonePrimaryKey, tutuClonePrimaryKey, null, INNER, null, Collections.emptySet());
		
		Iterator<AbstractJoinNode<?, ?, ?, ?>> actual = entityJoinTree.joinIterator();
		assertThat(Iterables.copy(actual))
				.usingElementComparator(Predicates.toComparator(Predicates.and(AbstractJoinNode::getLeftJoinLink, AbstractJoinNode::getRightJoinLink)))
				.withRepresentation(new Printer<>(AbstractJoinNode.class, joinNode -> joinNode.getLeftJoinLink() + " = " + joinNode.getRightJoinLink()))
				.isEqualTo(Arrays.asList(
						entityJoinTree.getJoin(tataAddKey),	// X
						entityJoinTree.getJoin(tataAddKey2),	// X'
						entityJoinTree.getJoin(tutuAddKey),	// Y
						entityJoinTree.getJoin(titiAddKey),	// Z
						entityJoinTree.getJoin(tutuAddKey2)	// Y'
				));
	}
	
	@Test
	void foreachJoinWithDepth() {
		DefaultEntityMapping totoMappingMock = buildMappingStrategyMock("Toto");
		Table totoTable = totoMappingMock.getTargetTable();
		totoTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> totoPrimaryKey = totoTable.getPrimaryKey();
		// column for "noise" in select
		Column totoNameColumn = totoTable.addColumn("name", String.class);
		
		DefaultEntityMapping tataMappingMock = buildMappingStrategyMock("Tata");
		Table tataTable = tataMappingMock.getTargetTable();
		tataTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> tataPrimaryKey = tataTable.getPrimaryKey();
		// column for "noise" in select
		Column tataNameColumn = tataTable.addColumn("name", String.class);
		
		DefaultEntityMapping tutuMappingMock = buildMappingStrategyMock("Tutu");
		Table tutuTable = tutuMappingMock.getTargetTable();
		tutuTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> tutuPrimaryKey = tutuTable.getPrimaryKey();
		// column for "noise" in select
		Column tutuNameColumn = tutuTable.addColumn("name", String.class);
		
		DefaultEntityMapping titiMappingMock = buildMappingStrategyMock("Titi");
		Table titiTable = titiMappingMock.getTargetTable();
		titiTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> titiPrimaryKey = titiTable.getPrimaryKey();
		// column for "noise" in select
		Column titiNameColumn = titiTable.addColumn("name", String.class);
		
		Table tataTableClone = new Table("tata2");
		DefaultEntityMapping tataCloneMappingMock = buildMappingStrategyMock(tataTableClone);
		tataTableClone.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> tataClonePrimaryKey = tataTableClone.getPrimaryKey();
		// column for "noise" in select
		Column tataCloneNameColumn = tataTableClone.addColumn("name", String.class);
		
		Table tutuTableClone = new Table("tutu2");
		DefaultEntityMapping tutuCloneMappingMock = buildMappingStrategyMock(tutuTableClone);
		tutuTableClone.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> tutuClonePrimaryKey = tutuTableClone.getPrimaryKey();
		// column for "noise" in select
		Column tutuCloneNameColumn = tutuTableClone.addColumn("name", String.class);
		
		// Given following tree:
		// Toto.id = Tata.id (X)
		//   Tata.id = Tutu.id (Y)
		//   Tata.id = Titi.id (Z)
		// Toto.id = tata2.id (X')
		//   tata2.id = tutu2.id (Y')
		
		EntityJoinTree<?, ?> entityJoinTree = new EntityJoinTree(new EntityMappingAdapter(totoMappingMock), totoMappingMock.getTargetTable());
		String tataAddKey = entityJoinTree.addRelationJoin(EntityJoinTree.ROOT_JOIN_NAME, new EntityMappingAdapter(tataMappingMock), mock(Accessor.class), totoPrimaryKey, tataPrimaryKey, null, INNER, null, Collections.emptySet());
		String tutuAddKey = entityJoinTree.addRelationJoin(tataAddKey, new EntityMappingAdapter(tutuMappingMock), mock(Accessor.class), tataPrimaryKey, tutuPrimaryKey, null, INNER, null, Collections.emptySet());
		String titiAddKey = entityJoinTree.addRelationJoin(tataAddKey, new EntityMappingAdapter(titiMappingMock), mock(Accessor.class), tataPrimaryKey, titiPrimaryKey, null, INNER, null, Collections.emptySet());
		String tataAddKey2 = entityJoinTree.addRelationJoin(EntityJoinTree.ROOT_JOIN_NAME, new EntityMappingAdapter(tataCloneMappingMock), mock(Accessor.class), totoPrimaryKey, tataClonePrimaryKey, null, INNER, null, Collections.emptySet());
		String tutuAddKey2 = entityJoinTree.addRelationJoin(tataAddKey2, new EntityMappingAdapter(tutuCloneMappingMock), mock(Accessor.class), tataClonePrimaryKey, tutuClonePrimaryKey, null, INNER, null, Collections.emptySet());
		
		List<Integer> depth = new ArrayList<>();
		List<AbstractJoinNode> collectedNodes = new ArrayList<>();
		entityJoinTree.foreachJoinWithDepth(1, (o, abstractJoinNode) -> {
			depth.add(o);
			collectedNodes.add(abstractJoinNode);
			return ++o;
		});
		
		assertThat(depth).isEqualTo(Arrays.asList(1, 1, 2, 2, 2));
		
		assertThat(collectedNodes)
				.withRepresentation(new Printer<>(AbstractJoinNode.class, joinNode -> joinNode.getLeftJoinLink() + " = " + joinNode.getRightJoinLink()))
				.isEqualTo(Arrays.asList(
						entityJoinTree.getJoin(tataAddKey),    // X
						entityJoinTree.getJoin(tataAddKey2),    // X'
						entityJoinTree.getJoin(tutuAddKey),    // Y
						entityJoinTree.getJoin(titiAddKey),    // Z
						entityJoinTree.getJoin(tutuAddKey2)    // Y'
				));
		
	}
	
	@Test
	void giveTables() {
		DefaultEntityMapping totoMappingMock = buildMappingStrategyMock("Toto");
		Table totoTable = totoMappingMock.getTargetTable();
		totoTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> totoPrimaryKey = totoTable.getPrimaryKey();
		// column for "noise" in select
		Column totoNameColumn = totoTable.addColumn("name", String.class);
		
		DefaultEntityMapping tataMappingMock = buildMappingStrategyMock("Tata");
		Table tataTable = tataMappingMock.getTargetTable();
		tataTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> tataPrimaryKey = tataTable.getPrimaryKey();
		// column for "noise" in select
		Column tataNameColumn = tataTable.addColumn("name", String.class);
		
		DefaultEntityMapping tutuMappingMock = buildMappingStrategyMock("Tutu");
		Table tutuTable = tutuMappingMock.getTargetTable();
		tutuTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> tutuPrimaryKey = tutuTable.getPrimaryKey();
		// column for "noise" in select
		Column tutuNameColumn = tutuTable.addColumn("name", String.class);
		
		DefaultEntityMapping titiMappingMock = buildMappingStrategyMock("Titi");
		Table titiTable = titiMappingMock.getTargetTable();
		titiTable.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> titiPrimaryKey = titiTable.getPrimaryKey();
		// column for "noise" in select
		Column titiNameColumn = titiTable.addColumn("name", String.class);
		
		Table tataTableClone = new Table("tata2");
		DefaultEntityMapping tataCloneMappingMock = buildMappingStrategyMock(tataTableClone);
		tataTableClone.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> tataClonePrimaryKey = tataTableClone.getPrimaryKey();
		// column for "noise" in select
		Column tataCloneNameColumn = tataTableClone.addColumn("name", String.class);
		
		Table tutuTableClone = new Table("tutu2");
		DefaultEntityMapping tutuCloneMappingMock = buildMappingStrategyMock(tutuTableClone);
		tutuTableClone.addColumn("id", long.class).primaryKey();
		PrimaryKey<?, Long> tutuClonePrimaryKey = tutuTableClone.getPrimaryKey();
		// column for "noise" in select
		Column tutuCloneNameColumn = tutuTableClone.addColumn("name", String.class);
		
		// Given following tree:
		// Toto.id = Tata.id (X)
		//   Tata.id = Tutu.id (Y)
		//   Tata.id = Titi.id (Z)
		// Toto.id = tata2.id (X')
		//   tata2.id = tutu2.id (Y')
		
		EntityJoinTree<?, ?> entityJoinTree = new EntityJoinTree(new EntityMappingAdapter(totoMappingMock), totoMappingMock.getTargetTable());
		String tataAddKey = entityJoinTree.addRelationJoin(EntityJoinTree.ROOT_JOIN_NAME, new EntityMappingAdapter(tataMappingMock), mock(Accessor.class), totoPrimaryKey, tataPrimaryKey, null, INNER, null, Collections.emptySet());
		String tutuAddKey = entityJoinTree.addRelationJoin(tataAddKey, new EntityMappingAdapter(tutuMappingMock), mock(Accessor.class), tataPrimaryKey, tutuPrimaryKey, null, INNER, null, Collections.emptySet());
		String titiAddKey = entityJoinTree.addRelationJoin(tataAddKey, new EntityMappingAdapter(titiMappingMock), mock(Accessor.class), tataPrimaryKey, titiPrimaryKey, null, INNER, null, Collections.emptySet());
		String tataAddKey2 = entityJoinTree.addRelationJoin(EntityJoinTree.ROOT_JOIN_NAME, new EntityMappingAdapter(tataCloneMappingMock), mock(Accessor.class), totoPrimaryKey, tataClonePrimaryKey, null, INNER, null, Collections.emptySet());
		String tutuAddKey2 = entityJoinTree.addRelationJoin(tataAddKey2, new EntityMappingAdapter(tutuCloneMappingMock), mock(Accessor.class), tataClonePrimaryKey, tutuClonePrimaryKey, null, INNER, null, Collections.emptySet());

		assertThat(entityJoinTree.giveTables())
				.usingElementComparator(Table.COMPARATOR_ON_SCHEMA_AND_NAME)
				.containsExactlyInAnyOrder(
				totoTable, tataTable, tataTableClone, titiTable, tutuTable, tutuTableClone);
		
	}
	
	private static class Printer<E> extends StandardRepresentation {
		
		private final Class<E> elementType;
		private final Function<E, String> printingFunction;
		
		private Printer(Class<E> elementType, Function<E, String> printingFunction) {
			this.elementType = elementType;
			this.printingFunction = printingFunction;
		}
		
		@Override
		public String toStringOf(Object o) {
			if (elementType.isInstance(o)) {
				return printingFunction.apply((E) o);
			} else {
				return super.toStringOf(o);
			}
		}
	}
	
}
