/*
 * 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.cascade.multicircle;

import javax.persistence.EntityManager;
import javax.persistence.RollbackException;

import org.hibernate.TransientPropertyValueException;
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;

import org.hibernate.testing.FailureExpected;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import junit.framework.Assert;

import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * This test uses a complicated model that requires Hibernate to delay
 * inserts until non-nullable transient entity dependencies are resolved.
 *
 * All IDs are generated from a sequence.
 *
 * JPA cascade types are used (javax.persistence.CascadeType)..
 *
 * This test uses the following model:
 *
 * <code>
 *     ------------------------------ N G
 *     |
 *     |                                1
 *     |                                |
 *     |                                |
 *     |                                N
 *     |
 *     |         E N--------------0,1 * F
 *     |
 *     |         1                      N
 *     |         |                      |
 *     |         |                      |
 *     1         N                      |
 *     *                                |
 *     B * N---1 D * 1------------------
 *     *
 *     N         N
 *     |         |
 *     |         |
 *     1         |
 *               |
 *     C * 1-----
 *</code>
 *
 * In the diagram, all associations are bidirectional;
 * assocations marked with '*' cascade persist, save, merge operations to the
 * associated entities (e.g., B cascades persist to D, but D does not cascade
 * persist to B);
 *
 * b, c, d, e, f, and g are all transient unsaved that are associated with each other.
 *
 * When saving b, the entities are added to the ActionQueue in the following order:
 * c, d (depends on e), f (depends on d, g), e, b, g.
 *
 * Entities are inserted in the following order:
 * c, e, d, b, g, f.
 */
public class MultiCircleJpaCascadeTest extends BaseEntityManagerFunctionalTestCase {
	private B b;
	private C c;
	private D d;
	private E e;
	private F f;
	private G g;
	private boolean skipCleanup;

	@Before
	public void setup() {
		b = new B();
		c = new C();
		d = new D();
		e = new E();
		f = new F();
		g = new G();

		b.getGCollection().add( g );
		b.setC( c );
		b.setD( d );

		c.getBCollection().add( b );
		c.getDCollection().add( d );

		d.getBCollection().add( b );
		d.setC( c );
		d.setE( e );
		d.getFCollection().add( f );

		e.getDCollection().add( d );
		e.setF( f );

		f.getECollection().add( e );
		f.setD( d );
		f.setG( g );

		g.setB( b );
		g.getFCollection().add( f );

		skipCleanup = false;
	}

	@After
	public void cleanup() {
		if ( ! skipCleanup ) {
			b.setC( null );
			b.setD( null );
			b.getGCollection().remove( g );

			c.getBCollection().remove( b );
			c.getDCollection().remove( d );

			d.getBCollection().remove( b );
			d.setC( null );
			d.setE( null );
			d.getFCollection().remove( f );

			e.getDCollection().remove( d );
			e.setF( null );

			f.setD( null );
			f.getECollection().remove( e );
			f.setG( null );

			g.setB( null );
			g.getFCollection().remove( f );

			EntityManager em = getOrCreateEntityManager();
			em.getTransaction().begin();
			b = em.merge( b );
			c = em.merge( c );
			d = em.merge( d );
			e = em.merge( e );
			f = em.merge( f );
			g = em.merge( g );
			em.remove( f );
			em.remove( g );
			em.remove( b );
			em.remove( d );
			em.remove( e );
			em.remove( c );
			em.getTransaction().commit();
			em.close();
		}
	}

	@Test
	public void testPersist() {
		EntityManager em = getOrCreateEntityManager();
		em.getTransaction().begin();
		em.persist( b );
		em.getTransaction().commit();
		em.close();

		check();
	}

	@Test
	public void testPersistNoCascadeToTransient() {
		skipCleanup = true;
		EntityManager em = getOrCreateEntityManager();
		em.getTransaction().begin();
		try {
			em.persist( c );
			fail( "should have failed." );
		}
		catch( IllegalStateException ex ) {
			assertTrue( TransientPropertyValueException.class.isInstance( ex.getCause() ) );
			TransientPropertyValueException pve = (TransientPropertyValueException) ex.getCause();
			assertEquals( G.class.getName(), pve.getTransientEntityName() );
			assertEquals( F.class.getName(),  pve.getPropertyOwnerEntityName() );
			assertEquals( "g", pve.getPropertyName() );
		}
		em.getTransaction().rollback();
		em.close();
	}

	@Test
	@FailureExpected( jiraKey = "HHH-6999" )
	// fails on d.e; should pass
	public void testPersistThenUpdate() {
		EntityManager em = getOrCreateEntityManager();
		em.getTransaction().begin();
		em.persist( b );
		// remove old e from associations
		e.getDCollection().remove( d );
		d.setE( null );
		f.getECollection().remove( e );
		e.setF( null );
		// add new e to associations
		e = new E();
		e.getDCollection().add( d );
		f.getECollection().add( e );
		d.setE( e );
		e.setF( f );
		em.getTransaction().commit();
		em.close();

		check();
	}

	@Test
	public void testPersistThenUpdateNoCascadeToTransient() {
		// expected to fail, so nothing will be persisted.
		skipCleanup = true;

		// remove elements from collections and persist
		c.getBCollection().clear();
		c.getDCollection().clear();

		EntityManager em = getOrCreateEntityManager();
		em.getTransaction().begin();
		em.persist( c );
		// now add the elements back
		c.getBCollection().add( b );
		c.getDCollection().add( d );
		try {
			em.getTransaction().commit();
			fail( "should have thrown IllegalStateException");
		}
		catch ( RollbackException ex ) {
			assertTrue( ex.getCause() instanceof IllegalStateException );
			IllegalStateException ise = ( IllegalStateException ) ex.getCause();
			// should fail on entity g (due to no cascade to f.g);
			// instead it fails on entity e ( due to no cascade to d.e)
			// because e is not in the process of being saved yet.
			// when HHH-6999 is fixed, this test should be changed to
			// check for g and f.g
			//noinspection ThrowableResultOfMethodCallIgnored
			TransientPropertyValueException tpve = assertTyping( TransientPropertyValueException.class, ise.getCause() );
			assertEquals( E.class.getName(), tpve.getTransientEntityName() );
			assertEquals( D.class.getName(), tpve.getPropertyOwnerEntityName() );
			assertEquals( "e", tpve.getPropertyName() );
		}
		em.close();
	}

	@Test
	public void testMerge() {
		EntityManager em = getOrCreateEntityManager();
		em.getTransaction().begin();
		b = em.merge( b );
		c = b.getC();
		d = b.getD();
		e = d.getE();
		f = e.getF();
		g = f.getG();
		em.getTransaction().commit();
		em.close();

		check();
	}

	private void check() {
		EntityManager em = getOrCreateEntityManager();
		em.getTransaction().begin();
		B bRead = em.find( B.class, b.getId() );
		Assert.assertEquals( b, bRead );

		G gRead = bRead.getGCollection().iterator().next();
		Assert.assertEquals( g, gRead );
		C cRead = bRead.getC();
		Assert.assertEquals( c, cRead );
		D dRead = bRead.getD();
		Assert.assertEquals( d, dRead );

		Assert.assertSame( bRead, cRead.getBCollection().iterator().next() );
		Assert.assertSame( dRead, cRead.getDCollection().iterator().next() );

		Assert.assertSame( bRead, dRead.getBCollection().iterator().next() );
		Assert.assertEquals( cRead, dRead.getC() );
		E eRead = dRead.getE();
		Assert.assertEquals( e, eRead );
		F fRead = dRead.getFCollection().iterator().next();
		Assert.assertEquals( f, fRead );

		Assert.assertSame( dRead, eRead.getDCollection().iterator().next() );
		Assert.assertSame( fRead, eRead.getF() );

		Assert.assertSame( eRead, fRead.getECollection().iterator().next() );
		Assert.assertSame( dRead, fRead.getD() );
		Assert.assertSame( gRead, fRead.getG());

		Assert.assertSame( bRead, gRead.getB() );
		Assert.assertSame( fRead, gRead.getFCollection().iterator().next() );

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

	@Override
	protected Class[] getAnnotatedClasses() {
		return new Class[]{
				B.class,
				C.class,
				D.class,
				E.class,
				F.class,
				G.class
		};
	}

}
