/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.jpa.test.graphs;

import java.util.Iterator;
import java.util.Set;
import javax.persistence.ElementCollection;
import javax.persistence.Embeddable;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;


import org.junit.Test;

import static org.junit.Assert.*;

import org.hibernate.LockMode;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;
import org.hibernate.loader.plan.build.internal.FetchGraphLoadPlanBuildingStrategy;
import org.hibernate.loader.plan.build.internal.LoadGraphLoadPlanBuildingStrategy;
import org.hibernate.loader.plan.build.internal.AbstractLoadPlanBuildingAssociationVisitationStrategy;
import org.hibernate.loader.plan.build.spi.LoadPlanTreePrinter;
import org.hibernate.loader.plan.build.spi.MetamodelDrivenLoadPlanBuilder;
import org.hibernate.loader.plan.exec.internal.AliasResolutionContextImpl;
import org.hibernate.loader.plan.spi.Join;
import org.hibernate.loader.plan.spi.LoadPlan;
import org.hibernate.loader.plan.spi.QuerySpace;
import org.hibernate.persister.entity.EntityPersister;

/**
 * @author Strong Liu <stliu@hibernate.org>
 */
public class EntityGraphLoadPlanBuilderTest extends BaseEntityManagerFunctionalTestCase {
	@Override
	protected Class<?>[] getAnnotatedClasses() {
		return new Class[] { Cat.class, Person.class, Country.class, Dog.class, ExpressCompany.class };
	}

	@Entity
	public static class Dog {
		@Id
		String name;
		@ElementCollection
		Set<String> favorites;
	}

	@Entity
	public static class Cat {
		@Id
		String name;
		@ManyToOne(fetch = FetchType.LAZY)
		Person owner;

	}

	@Entity
	public static class Person {
		@Id
		String name;
		@OneToMany(mappedBy = "owner")
		Set<Cat> pets;
		@Embedded
		Address homeAddress;
	}

	@Embeddable
	public static class Address {
		@ManyToOne
		Country country;
	}

	@Entity
	public static class ExpressCompany {
		@Id
		String name;
		@ElementCollection
		Set<Address> shipAddresses;
	}

	@Entity
	public static class Country {
		@Id
		String name;
	}

	/**
	 * EntityGraph(1):
	 *
	 * Cat
	 *
	 * LoadPlan:
	 *
	 * Cat
	 *
	 * ---------------------
	 *
	 * EntityGraph(2):
	 *
	 * Cat
	 * owner -- Person
	 *
	 * LoadPlan:
	 *
	 * Cat
	 * owner -- Person
	 * address --- Address
	 */
	@Test
	public void testBasicFetchLoadPlanBuilding() {
		EntityManager em = getOrCreateEntityManager();
		EntityGraph eg = em.createEntityGraph( Cat.class );
		LoadPlan plan = buildLoadPlan( eg, Mode.FETCH, Cat.class );
		LoadPlanTreePrinter.INSTANCE.logTree( plan, new AliasResolutionContextImpl( sfi() ) );
		QuerySpace rootQuerySpace = plan.getQuerySpaces().getRootQuerySpaces().get( 0 );
		assertFalse(
				"With fetchgraph property and an empty EntityGraph, there should be no join at all",
				rootQuerySpace.getJoins().iterator().hasNext()
		);
		// -------------------------------------------------- another a little more complicated case
		eg = em.createEntityGraph( Cat.class );
		eg.addSubgraph( "owner", Person.class );
		plan = buildLoadPlan( eg, Mode.FETCH, Cat.class );
		LoadPlanTreePrinter.INSTANCE.logTree( plan, new AliasResolutionContextImpl( sfi() ) );
		rootQuerySpace = plan.getQuerySpaces().getRootQuerySpaces().get( 0 );
		Iterator<Join> iterator = rootQuerySpace.getJoins().iterator();
		assertTrue(
				"With fetchgraph property and an empty EntityGraph, there should be no join at all", iterator.hasNext()
		);
		Join personJoin = iterator.next();
		assertNotNull( personJoin );
		QuerySpace.Disposition disposition = personJoin.getRightHandSide().getDisposition();
		assertEquals(
				"This should be an entity join which fetches Person", QuerySpace.Disposition.ENTITY, disposition
		);

		iterator = personJoin.getRightHandSide().getJoins().iterator();
		assertTrue( "The composite address should be fetched", iterator.hasNext() );
		Join addressJoin = iterator.next();
		assertNotNull( addressJoin );
		disposition = addressJoin.getRightHandSide().getDisposition();
		assertEquals( QuerySpace.Disposition.COMPOSITE, disposition );
		assertFalse( iterator.hasNext() );
		assertFalse(
				"The ManyToOne attribute in composite should not be fetched",
				addressJoin.getRightHandSide().getJoins().iterator().hasNext()
		);
		em.close();
	}

	/**
	 * EntityGraph(1):
	 *
	 * Cat
	 *
	 * LoadPlan:
	 *
	 * Cat
	 *
	 * ---------------------
	 *
	 * EntityGraph(2):
	 *
	 * Cat
	 * owner -- Person
	 *
	 * LoadPlan:
	 *
	 * Cat
	 * owner -- Person
	 * address --- Address
	 * country -- Country
	 */
	@Test
	public void testBasicLoadLoadPlanBuilding() {
		EntityManager em = getOrCreateEntityManager();
		EntityGraph eg = em.createEntityGraph( Cat.class );
		LoadPlan plan = buildLoadPlan( eg, Mode.LOAD, Cat.class );
		LoadPlanTreePrinter.INSTANCE.logTree( plan, new AliasResolutionContextImpl( sfi() ) );
		QuerySpace rootQuerySpace = plan.getQuerySpaces().getRootQuerySpaces().get( 0 );
		assertFalse(
				"With fetchgraph property and an empty EntityGraph, there should be no join at all",
				rootQuerySpace.getJoins().iterator().hasNext()
		);
		// -------------------------------------------------- another a little more complicated case
		eg = em.createEntityGraph( Cat.class );
		eg.addSubgraph( "owner", Person.class );
		plan = buildLoadPlan( eg, Mode.LOAD, Cat.class );
		LoadPlanTreePrinter.INSTANCE.logTree( plan, new AliasResolutionContextImpl( sfi() ) );
		rootQuerySpace = plan.getQuerySpaces().getRootQuerySpaces().get( 0 );
		Iterator<Join> iterator = rootQuerySpace.getJoins().iterator();
		assertTrue(
				"With fetchgraph property and an empty EntityGraph, there should be no join at all", iterator.hasNext()
		);
		Join personJoin = iterator.next();
		assertNotNull( personJoin );
		QuerySpace.Disposition disposition = personJoin.getRightHandSide().getDisposition();
		assertEquals(
				"This should be an entity join which fetches Person", QuerySpace.Disposition.ENTITY, disposition
		);

		iterator = personJoin.getRightHandSide().getJoins().iterator();
		assertTrue( "The composite address should be fetched", iterator.hasNext() );
		Join addressJoin = iterator.next();
		assertNotNull( addressJoin );
		disposition = addressJoin.getRightHandSide().getDisposition();
		assertEquals( QuerySpace.Disposition.COMPOSITE, disposition );
		iterator = addressJoin.getRightHandSide().getJoins().iterator();
		assertTrue( iterator.hasNext() );
		Join countryJoin = iterator.next();
		assertNotNull( countryJoin );
		disposition = countryJoin.getRightHandSide().getDisposition();
		assertEquals( QuerySpace.Disposition.ENTITY, disposition );
		assertFalse(
				"The ManyToOne attribute in composite should not be fetched",
				countryJoin.getRightHandSide().getJoins().iterator().hasNext()
		);
		em.close();
	}


	@Test
	public void testBasicElementCollections() {
		EntityManager em = getOrCreateEntityManager();
		EntityGraph eg = em.createEntityGraph( Dog.class );
		eg.addAttributeNodes( "favorites" );
		LoadPlan loadLoadPlan = buildLoadPlan( eg, Mode.LOAD, Dog.class ); //WTF name!!!
		LoadPlanTreePrinter.INSTANCE.logTree( loadLoadPlan, new AliasResolutionContextImpl( sfi() ) );
		QuerySpace querySpace = loadLoadPlan.getQuerySpaces().getRootQuerySpaces().iterator().next();
		Iterator<Join> iterator = querySpace.getJoins().iterator();
		assertTrue( iterator.hasNext() );
		Join collectionJoin = iterator.next();
		assertEquals( QuerySpace.Disposition.COLLECTION, collectionJoin.getRightHandSide().getDisposition() );
		assertFalse( iterator.hasNext() );
		//----------------------------------------------------------------
		LoadPlan fetchLoadPlan = buildLoadPlan( eg, Mode.FETCH, Dog.class );
		LoadPlanTreePrinter.INSTANCE.logTree( fetchLoadPlan, new AliasResolutionContextImpl( sfi() ) );
		querySpace = fetchLoadPlan.getQuerySpaces().getRootQuerySpaces().iterator().next();
		iterator = querySpace.getJoins().iterator();
		assertTrue( iterator.hasNext() );
		collectionJoin = iterator.next();
		assertEquals( QuerySpace.Disposition.COLLECTION, collectionJoin.getRightHandSide().getDisposition() );
		assertFalse( iterator.hasNext() );
		em.close();
	}


	@Test
	public void testEmbeddedCollection() {
		EntityManager em = getOrCreateEntityManager();
		EntityGraph eg = em.createEntityGraph( ExpressCompany.class );
		eg.addAttributeNodes( "shipAddresses" );

		LoadPlan loadLoadPlan = buildLoadPlan( eg, Mode.LOAD, ExpressCompany.class ); //WTF name!!!
		LoadPlanTreePrinter.INSTANCE.logTree( loadLoadPlan, new AliasResolutionContextImpl( sfi() ) );

		QuerySpace querySpace = loadLoadPlan.getQuerySpaces().getRootQuerySpaces().iterator().next();
		Iterator<Join> iterator = querySpace.getJoins().iterator();
		assertTrue( iterator.hasNext() );
		Join collectionJoin = iterator.next();
		assertEquals( QuerySpace.Disposition.COLLECTION, collectionJoin.getRightHandSide().getDisposition() );
		assertFalse( iterator.hasNext() );

		iterator = collectionJoin.getRightHandSide().getJoins().iterator();
		assertTrue( iterator.hasNext() );
		Join collectionElementJoin = iterator.next();
		assertFalse( iterator.hasNext() );
		assertEquals( QuerySpace.Disposition.COMPOSITE, collectionElementJoin.getRightHandSide().getDisposition() );

		iterator = collectionElementJoin.getRightHandSide().getJoins().iterator();
		assertTrue( iterator.hasNext() );
		Join countryJoin = iterator.next();
		assertFalse( iterator.hasNext() );
		assertEquals( QuerySpace.Disposition.ENTITY, countryJoin.getRightHandSide().getDisposition() );

		//----------------------------------------------------------------
		LoadPlan fetchLoadPlan = buildLoadPlan( eg, Mode.FETCH, ExpressCompany.class );
		LoadPlanTreePrinter.INSTANCE.logTree( fetchLoadPlan, new AliasResolutionContextImpl( sfi() ) );


		querySpace = fetchLoadPlan.getQuerySpaces().getRootQuerySpaces().iterator().next();
		iterator = querySpace.getJoins().iterator();
		assertTrue( iterator.hasNext() );
		collectionJoin = iterator.next();
		assertEquals( QuerySpace.Disposition.COLLECTION, collectionJoin.getRightHandSide().getDisposition() );
		assertFalse( iterator.hasNext() );

		iterator = collectionJoin.getRightHandSide().getJoins().iterator();
		assertTrue( iterator.hasNext() );
		collectionElementJoin = iterator.next();
		assertFalse( iterator.hasNext() );
		assertEquals( QuerySpace.Disposition.COMPOSITE, collectionElementJoin.getRightHandSide().getDisposition() );

		iterator = collectionElementJoin.getRightHandSide().getJoins().iterator();
		assertFalse( iterator.hasNext() );
		//----------------------------------------------------------------
		em.close();
	}


	private SessionFactoryImplementor sfi() {
		return entityManagerFactory().unwrap( SessionFactoryImplementor.class );
	}

	private LoadPlan buildLoadPlan(EntityGraph entityGraph, Mode mode, Class clazz) {

		LoadQueryInfluencers loadQueryInfluencers = new LoadQueryInfluencers( sfi() );
		if ( Mode.FETCH == mode ) {
			loadQueryInfluencers.setFetchGraph( entityGraph );
		}
		else {
			loadQueryInfluencers.setLoadGraph( entityGraph );
		}
		EntityPersister ep = (EntityPersister) sfi().getClassMetadata( clazz );
		AbstractLoadPlanBuildingAssociationVisitationStrategy strategy = Mode.FETCH == mode ? new FetchGraphLoadPlanBuildingStrategy(
				sfi(), loadQueryInfluencers, LockMode.NONE
		) : new LoadGraphLoadPlanBuildingStrategy( sfi(), loadQueryInfluencers, LockMode.NONE );
		return MetamodelDrivenLoadPlanBuilder.buildRootEntityLoadPlan( strategy, ep );
	}

	public static enum Mode {FETCH, LOAD}
}
