/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2011, Red Hat Inc. or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat Inc.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */
package org.hibernate.test.criteria;

import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Restrictions;
import org.hibernate.sql.JoinFragment;
import org.hibernate.testing.junit.functional.FunctionalTestCase;

/**
 * @author Mattias Jiderhamn
 * @author Gail Badner
 */
public class OuterJoinCriteriaTest extends FunctionalTestCase {
	private Order order1;
	private Order order2;
	private Order order3;

	public OuterJoinCriteriaTest(String s) {
		super( s );
	}

	public String[] getMappings() {
		return new String[] { "criteria/Order.hbm.xml" };
	}

	public void testSubcriteriaWithNonNullRestrictions() {
		Session s = openSession();
		s.getTransaction().begin();

		Criteria rootCriteria = s.createCriteria( Order.class );
		Criteria subCriteria = rootCriteria.createCriteria( "orderLines", JoinFragment.LEFT_OUTER_JOIN );
		assertNotSame( rootCriteria, subCriteria );

		// add restrictions to subCriteria, ensuring we stay on subCriteria
		assertSame( subCriteria, subCriteria.add( Restrictions.eq( "articleId", "3000" ) ) );

		List orders = rootCriteria.list();

		// order1 and order3 should be returned because each has articleId == "3000"
		// both should have their full collection
		assertEquals( 2, orders.size() );
		for ( Iterator it = orders.iterator(); it.hasNext(); ) {
			Order o = (Order) it.next();
			if ( order1.getOrderId() == o.getOrderId() ) {
				assertEquals( order1.getLines().size(), o.getLines().size() );
			}
			else if ( order3.getOrderId() == o.getOrderId() ) {
				assertEquals( order3.getLines().size(), o.getLines().size() );
			}
			else {
				fail( "unknown order" );
			}
		}
		s.getTransaction().commit();
		s.close();
	}

	public void testSubcriteriaWithNonNullRestrictionsAliasToEntityMap() {
		Session s = openSession();
		s.getTransaction().begin();

		Criteria rootCriteria = s.createCriteria( Order.class, "o" );
		Criteria subCriteria = rootCriteria.createCriteria( "orderLines", "ol", JoinFragment.LEFT_OUTER_JOIN );
		assertNotSame( rootCriteria, subCriteria );

		// add restriction to subCriteria, ensuring we stay on subCriteria
		assertSame( subCriteria, subCriteria.add( Restrictions.eq( "articleId", "3000" ) ) );

		List orders = rootCriteria.setResultTransformer( Criteria.ALIAS_TO_ENTITY_MAP ).list();

		// order1 and order3 should be returned because each has articleId == "3000";
		// the orders should both should have their full collection;
		assertEquals( 2, orders.size() );
		for ( Iterator it = orders.iterator(); it.hasNext(); ) {
			Map map = (Map) it.next();
			Order o = ( Order ) map.get( "o" );
		    // the orderLine returned from the map should have articleId = "3000"
			OrderLine ol = ( OrderLine ) map.get( "ol" );
			if ( order1.getOrderId() == o.getOrderId() ) {
				assertEquals( order1.getLines().size(), o.getLines().size() );
				assertEquals( "3000", ol.getArticleId() );
			}
			else if ( order3.getOrderId() == o.getOrderId() ) {
				assertEquals( order3.getLines().size(), o.getLines().size() );
				assertEquals( "3000", ol.getArticleId() );
			}
			else {
				fail( "unknown order" );
			}
		}
		s.getTransaction().commit();
		s.close();
	}


	public void testSubcriteriaWithNullOrNonNullRestrictions() {
		Session s = openSession();
		s.getTransaction().begin();

		Criteria rootCriteria = s.createCriteria( Order.class );
		Criteria subCriteria = rootCriteria.createCriteria( "orderLines", JoinFragment.LEFT_OUTER_JOIN );
		assertNotSame( rootCriteria, subCriteria );

		// add restrictions to subCriteria, ensuring we stay on subCriteria
		// add restriction to subCriteria, ensuring we stay on subCriteria
		assertSame(
				subCriteria,
				subCriteria.add(
					Restrictions.or(
							Restrictions.isNull( "articleId" ),		  // Allow null
							Restrictions.eq( "articleId", "1000" )
					)
				)
		);

		List orders = rootCriteria.list();

		// order1 should be returned because it has an orderline with articleId == "1000";
		// order2 should be returned because it has no orderlines
		assertEquals( 2, orders.size() );
		for ( Iterator it = orders.iterator(); it.hasNext(); ) {
			Order o = ( Order ) it.next();
			if ( order1.getOrderId() == o.getOrderId() ) {
				// o.getLines() should contain all of its orderLines
				assertEquals( order1.getLines().size(), o.getLines().size() );
			}
			else if ( order2.getOrderId() == o.getOrderId() ) {
				assertEquals( order2.getLines() , o.getLines() );
				assertTrue( o.getLines().isEmpty() );
			}
			else {
				fail( "unknown order" );
			}
		}
		s.getTransaction().commit();
		s.close();
	}

	public void testSubcriteriaWithNullOrNonNullRestrictionsAliasToEntityMap() {
		Session s = openSession();
		s.getTransaction().begin();

		Criteria rootCriteria = s.createCriteria( Order.class, "o" );
		Criteria subCriteria = rootCriteria.createCriteria( "orderLines", "ol", JoinFragment.LEFT_OUTER_JOIN );
		assertNotSame( rootCriteria, subCriteria );

		// add restriction to subCriteria, ensuring we stay on subCriteria
		assertSame(
				subCriteria,
				subCriteria.add(
					Restrictions.or(
							Restrictions.isNull( "ol.articleId" ),		  // Allow null
							Restrictions.eq( "ol.articleId", "1000" )
					)
				)
		);

		List orders = rootCriteria.setResultTransformer( Criteria.ALIAS_TO_ENTITY_MAP ).list();

		// order1 should be returned because it has an orderline with articleId == "1000";
		// order2 should be returned because it has no orderlines
		assertEquals( 2, orders.size() );
		for ( Iterator it = orders.iterator(); it.hasNext(); ) {
			Map map = (Map) it.next();
			Order o = ( Order ) map.get( "o" );
		    // the orderLine returned from the map should either be null or have articleId = "1000"
			OrderLine ol = ( OrderLine ) map.get( "ol" );
			if ( order1.getOrderId() == o.getOrderId() ) {
				// o.getLines() should contain all of its orderLines
				assertEquals( order1.getLines().size(), o.getLines().size() );
				assertNotNull( ol );
				assertEquals( "1000", ol.getArticleId() );
			}
			else if ( order2.getOrderId() == o.getOrderId() ) {
				assertEquals( order2.getLines() , o.getLines() );
				assertTrue( o.getLines().isEmpty() );
				assertNull( ol );
			}
			else {
				fail( "unknown order" );
			}
		}
		s.getTransaction().commit();
		s.close();
	}

	public void testSubcriteriaWithClauseAliasToEntityMap() {
		Session s = openSession();
		s.getTransaction().begin();

		Criteria rootCriteria = s.createCriteria( Order.class, "o" );
		Criteria subCriteria = rootCriteria.createCriteria(
				"orderLines",
				"ol", JoinFragment.LEFT_OUTER_JOIN,
				Restrictions.or(
						Restrictions.isNull( "ol.articleId" ),		  // Allow null
						Restrictions.eq( "ol.articleId", "1000" )
				)
		);
		assertNotSame( rootCriteria, subCriteria );

		List orders = rootCriteria.setResultTransformer( Criteria.ALIAS_TO_ENTITY_MAP ).list();

		// all orders should be returned (via map.get( "o" )) with their full collections;
		assertEquals( 3, orders.size() );
		for ( Iterator it = orders.iterator(); it.hasNext(); ) {
			Map map = ( Map ) it.next();
			Order o = ( Order ) map.get( "o" );
		    // the orderLine returned from the map should either be null or have articleId = "1000"
			OrderLine ol = ( OrderLine ) map.get( "ol" );
			if ( order1.getOrderId() == o.getOrderId() ) {
				// o.getLines() should contain all of its orderLines
				assertEquals( order1.getLines().size(), o.getLines().size() );
				assertNotNull( ol );
				assertEquals( "1000", ol.getArticleId() );
			}
			else if ( order2.getOrderId() == o.getOrderId() ) {
				assertTrue( o.getLines().isEmpty() );
				assertNull( ol );
			}
			else if ( order3.getOrderId() == o.getOrderId() ) {
				assertEquals( order3.getLines().size(), o.getLines().size() );
				assertNull( ol);
			}
			else {
				fail( "unknown order" );
			}
		}
		s.getTransaction().commit();
		s.close();
	}

	public void testAliasWithNonNullRestrictions() {
		Session s = openSession();
		s.getTransaction().begin();

		Criteria rootCriteria = s.createCriteria( Order.class );
		// create alias, ensuring we stay on the root criteria
		assertSame( rootCriteria, rootCriteria.createAlias( "orderLines", "ol", JoinFragment.LEFT_OUTER_JOIN ) );

		// add restrictions to rootCriteria
		assertSame( rootCriteria, rootCriteria.add( Restrictions.eq( "ol.articleId", "3000" ) ) );

		List orders = rootCriteria.list();

		// order1 and order3 should be returned because each has articleId == "3000"
		// the contained collections should only have the orderLine with articleId == "3000"
		assertEquals( 2, orders.size() );
		for ( Iterator it = orders.iterator(); it.hasNext(); ) {
			Order o = (Order) it.next();
			if ( order1.getOrderId() == o.getOrderId() ) {
				assertEquals( 1, o.getLines().size() );
				assertEquals( "3000", ( ( OrderLine ) o.getLines().iterator().next() ).getArticleId() );
			}
			else if ( order3.getOrderId() == o.getOrderId() ) {
				assertEquals( 1, o.getLines().size() );
				assertEquals( "3000", ( ( OrderLine ) o.getLines().iterator().next() ).getArticleId() );
			}
			else {
				fail( "unknown order" );
			}
		}
		s.getTransaction().commit();
		s.close();
	}

	public void testAliasWithNullOrNonNullRestrictions() {
		Session s = openSession();
		s.getTransaction().begin();

		Criteria rootCriteria = s.createCriteria( Order.class );
		// create alias, ensuring we stay on the root criteria
		assertSame( rootCriteria, rootCriteria.createAlias( "orderLines", "ol", JoinFragment.LEFT_OUTER_JOIN ) );

		// add restrictions to rootCriteria
		assertSame(
				rootCriteria,
				rootCriteria.add(
						Restrictions.or(
								Restrictions.isNull( "ol.articleId" ),		  // Allow null
								Restrictions.eq( "ol.articleId", "1000" )
						)
				)
		);

		List orders = rootCriteria.list();

		// order1 should be returned because it has an orderline with articleId == "1000";
		// the contained collection for order1 should only have the orderLine with articleId == "1000";
		// order2 should be returned because it has no orderlines
		assertEquals( 2, orders.size() );
		for ( Object order : orders ) {
			Order o = (Order) order;
			if ( order1.getOrderId() == o.getOrderId() ) {
				assertEquals( "1000", ( ( OrderLine ) o.getLines().iterator().next() ).getArticleId() );
			}
			else if ( order2.getOrderId() == o.getOrderId() ) {
				assertEquals( 0, o.getLines().size() );
			}
			else {
				fail( "unknown order" );
			}
		}
		s.getTransaction().commit();
		s.close();
	}

	public void testNonNullSubcriteriaRestrictionsOnRootCriteria() {
		Session s = openSession();
		s.getTransaction().begin();

		Criteria rootCriteria = s.createCriteria( Order.class );
		Criteria subCriteria = rootCriteria.createCriteria( "orderLines", "ol", JoinFragment.LEFT_OUTER_JOIN );
		assertNotSame( rootCriteria, subCriteria );

		// add restriction to rootCriteria (NOT subcriteria)
		assertSame( rootCriteria, rootCriteria.add( Restrictions.eq( "ol.articleId", "3000" ) ) );

		List orders = rootCriteria.list();

		// results should be the same as testAliasWithNonNullRestrictions() (using Criteria.createAlias())
		// order1 and order3 should be returned because each has articleId == "3000"
		// the contained collections should only have the orderLine with articleId == "3000"
		assertEquals( 2, orders.size() );
		for ( Iterator it = orders.iterator(); it.hasNext(); ) {
			Order o = (Order) it.next();
			if ( order1.getOrderId() == o.getOrderId() ) {
				assertEquals( 1, o.getLines().size() );
				assertEquals( "3000", ( ( OrderLine ) o.getLines().iterator().next() ).getArticleId() );
			}
			else if ( order3.getOrderId() == o.getOrderId() ) {
				assertEquals( 1, o.getLines().size() );
				assertEquals( "3000", ( ( OrderLine ) o.getLines().iterator().next() ).getArticleId() );
			}
			else {
				fail( "unknown order" );
			}
		}
		s.getTransaction().commit();
		s.close();
	}

	protected void prepareTest() {
		Session s = openSession();
		s.getTransaction().begin();

		// Order with one mathing line
		order1 = new Order();
		OrderLine line = new OrderLine();
		line.setArticleId( "1000" );
		order1.addLine( line );
		line = new OrderLine();
		line.setArticleId( "3000" );
		order1.addLine( line );
		s.persist( order1 );

		// Order with no lines
		order2 = new Order();
		s.persist( order2 );

		// Order with non-matching line
		order3 = new Order();
		line = new OrderLine();
		line.setArticleId( "3000" );
		order3.addLine( line );
		s.persist( order3 );

		s.getTransaction().commit();
		s.close();
	}

	protected void cleanupTest() {
		Session s = openSession();
		s.getTransaction().begin();

		s.createQuery( "delete from OrderLine" ).executeUpdate();

		s.createQuery( "delete from Order" ).executeUpdate();

		s.getTransaction().commit();
		s.close();
	}

	private static boolean isBlank(String s) {
		return s == null || s.trim().length() == 0;
	}
}