/*
 * 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.envers.test.integration.collection;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.transaction.Status;
import javax.transaction.TransactionManager;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.dialect.CockroachDB192Dialect;
import org.hibernate.dialect.Oracle8iDialect;
import org.hibernate.envers.RevisionType;
import org.hibernate.envers.enhanced.SequenceIdRevisionEntity;
import org.hibernate.envers.test.BaseEnversJPAFunctionalTestCase;
import org.hibernate.envers.test.Priority;
import org.hibernate.envers.test.entities.collection.MultipleCollectionEntity;
import org.hibernate.envers.test.entities.collection.MultipleCollectionRefEntity1;
import org.hibernate.envers.test.entities.collection.MultipleCollectionRefEntity2;
import org.hibernate.testing.SkipForDialect;
import org.hibernate.testing.SkipForDialects;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.jta.TestingJtaBootstrap;
import org.hibernate.testing.jta.TestingJtaPlatformImpl;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

/**
 * Test the audit history of a detached entity with multiple collections that is
 * merged back into the persistence context.
 *
 * @author Erik-Berndt Scheper
 */
@TestForIssue(jiraKey = "HHH-6349")
@SkipForDialect(value = Oracle8iDialect.class,
				comment = "Oracle does not support identity key generation")
public class DetachedMultipleCollectionChangeTest extends BaseEnversJPAFunctionalTestCase {
	private TransactionManager tm = null;

	private Long mceId1 = null;
	private Long re1Id1 = null;
	private Long re1Id2 = null;
	private Long re1Id3 = null;
	private Long re2Id1 = null;
	private Long re2Id2 = null;
	private Long re2Id3 = null;

	@Override
	protected Class<?>[] getAnnotatedClasses() {
		return new Class<?>[] {
				MultipleCollectionEntity.class,
				MultipleCollectionRefEntity1.class,
				MultipleCollectionRefEntity2.class
		};
	}

	@Override
	protected void addConfigOptions(Map options) {
		super.addConfigOptions( options );
		TestingJtaBootstrap.prepare( options );
		options.put( AvailableSettings.ALLOW_JTA_TRANSACTION_ACCESS, "true" );
		tm = TestingJtaPlatformImpl.INSTANCE.getTransactionManager();
	}

	@Test
	@Priority(10)
	public void initData() throws Exception {
		EntityManager em;
		MultipleCollectionEntity mce;
		MultipleCollectionRefEntity1 re1_1, updatedRe1_1, re1_2, re1_3;
		MultipleCollectionRefEntity2 re2_1, updatedRe2_1, re2_2, re2_3;

		tm.begin();
		try {
			em = createIsolatedEntityManager();
			em.joinTransaction();
			mce = new MultipleCollectionEntity();
			mce.setText( "MultipleCollectionEntity-1" );
			em.persist( mce );
			mceId1 = mce.getId();
		}
		finally {
			tryCommit( tm );
		}
		assertNotNull( mceId1 );

		tm.begin();
		try {
			em = createIsolatedEntityManager();
			em.joinTransaction();

			re1_1 = new MultipleCollectionRefEntity1();
			re1_1.setText( "MultipleCollectionRefEntity1-1" );
			re1_1.setMultipleCollectionEntity( mce );

			re1_2 = new MultipleCollectionRefEntity1();
			re1_2.setText( "MultipleCollectionRefEntity1-2" );
			re1_2.setMultipleCollectionEntity( mce );

			mce.addRefEntity1( re1_1 );
			mce.addRefEntity1( re1_2 );

			re2_1 = new MultipleCollectionRefEntity2();
			re2_1.setText( "MultipleCollectionRefEntity2-1" );
			re2_1.setMultipleCollectionEntity( mce );

			re2_2 = new MultipleCollectionRefEntity2();
			re2_2.setText( "MultipleCollectionRefEntity2-2" );
			re2_2.setMultipleCollectionEntity( mce );

			mce.addRefEntity2( re2_1 );
			mce.addRefEntity2( re2_2 );

			mce = em.merge( mce );
		}
		finally {
			tryCommit( tm );
		}
		for ( MultipleCollectionRefEntity1 refEnt1 : mce.getRefEntities1() ) {
			if ( refEnt1.equals( re1_1 ) ) {
				re1Id1 = refEnt1.getId();
			}
			else if ( refEnt1.equals( re1_2 ) ) {
				re1Id2 = refEnt1.getId();
			}
			else {
				throw new IllegalStateException( "unexpected instance" );
			}
		}
		for ( MultipleCollectionRefEntity2 refEnt2 : mce.getRefEntities2() ) {
			if ( refEnt2.equals( re2_1 ) ) {
				re2Id1 = refEnt2.getId();
			}
			else if ( refEnt2.equals( re2_2 ) ) {
				re2Id2 = refEnt2.getId();
			}
			else {
				throw new IllegalStateException( "unexpected instance" );
			}
		}
		assertNotNull( re1Id1 );
		assertNotNull( re1Id2 );
		assertNotNull( re2Id1 );
		assertNotNull( re2Id2 );

		tm.begin();
		try {
			em = createIsolatedEntityManager();
			em.joinTransaction();

			assertEquals( 2, mce.getRefEntities1().size() );

			mce.removeRefEntity1( re1_2 );
			assertEquals( 1, mce.getRefEntities1().size() );

			updatedRe1_1 = mce.getRefEntities1().get( 0 );
			assertEquals( re1_1, updatedRe1_1 );
			updatedRe1_1.setText( "MultipleCollectionRefEntity1-1-updated" );

			re1_3 = new MultipleCollectionRefEntity1();
			re1_3.setText( "MultipleCollectionRefEntity1-3" );
			re1_3.setMultipleCollectionEntity( mce );
			mce.addRefEntity1( re1_3 );
			assertEquals( 2, mce.getRefEntities1().size() );

			assertEquals( 2, mce.getRefEntities2().size() );

			mce.removeRefEntity2( re2_2 );
			assertEquals( 1, mce.getRefEntities2().size() );

			updatedRe2_1 = mce.getRefEntities2().get( 0 );
			assertEquals( re2_1, updatedRe2_1 );
			updatedRe2_1.setText( "MultipleCollectionRefEntity2-1-updated" );

			re2_3 = new MultipleCollectionRefEntity2();
			re2_3.setText( "MultipleCollectionRefEntity2-3" );
			re2_3.setMultipleCollectionEntity( mce );
			mce.addRefEntity2( re2_3 );
			assertEquals( 2, mce.getRefEntities2().size() );

			mce = em.merge( mce );

		}
		finally {
			tryCommit( tm );
		}
		for ( MultipleCollectionRefEntity1 adres : mce.getRefEntities1() ) {
			if ( adres.equals( re1_3 ) ) {
				re1Id3 = adres.getId();
			}
		}
		for ( MultipleCollectionRefEntity2 partner : mce.getRefEntities2() ) {
			if ( partner.equals( re2_3 ) ) {
				re2Id3 = partner.getId();
			}
		}
		assertNotNull( re1Id3 );
		assertNotNull( re2Id3 );
	}

	@Test
	public void testRevisionsCounts() throws Exception {
		List<Number> mceId1Revs = getAuditReader().getRevisions( MultipleCollectionEntity.class, mceId1 );
		List<Number> re1Id1Revs = getAuditReader().getRevisions( MultipleCollectionRefEntity1.class, re1Id1 );
		List<Number> re1Id2Revs = getAuditReader().getRevisions( MultipleCollectionRefEntity1.class, re1Id2 );
		List<Number> re1Id3Revs = getAuditReader().getRevisions( MultipleCollectionRefEntity1.class, re1Id3 );
		List<Number> re2Id1Revs = getAuditReader().getRevisions( MultipleCollectionRefEntity2.class, re2Id1 );
		List<Number> re2Id2Revs = getAuditReader().getRevisions( MultipleCollectionRefEntity2.class, re2Id2 );
		List<Number> re2Id3Revs = getAuditReader().getRevisions( MultipleCollectionRefEntity2.class, re2Id3 );

		assertEquals( Arrays.asList( 1, 2, 3 ), mceId1Revs );
		assertEquals( Arrays.asList( 2, 3 ), re1Id1Revs );
		assertEquals( Arrays.asList( 2, 3 ), re1Id2Revs );
		assertEquals( Arrays.asList( 3 ), re1Id3Revs );
		assertEquals( Arrays.asList( 2, 3 ), re2Id1Revs );
		assertEquals( Arrays.asList( 2, 3 ), re2Id2Revs );
		assertEquals( Arrays.asList( 3 ), re2Id3Revs );
	}

	@Test
	@SkipForDialects( value = {
			@SkipForDialect(value = CockroachDB192Dialect.class,
					comment = "requires serial_normalization=sql_sequence setting"),
			@SkipForDialect(value = Oracle8iDialect.class,
					comment = "Oracle does not support identity key generation")
	})
	public void testAuditJoinTable() throws Exception {
		List<AuditJoinTableInfo> mceRe1AuditJoinTableInfos = getAuditJoinTableRows(
				"MCE_RE1_AUD", "MCE_ID",
				"aud.originalId.MultipleCollectionEntity_id", "RE1_ID",
				"aud.originalId.refEntities1_id", "aud.originalId.REV",
				"aud.originalId.REV.id", "aud.REVTYPE"
		);
		List<AuditJoinTableInfo> mceRe2AuditJoinTableInfos = getAuditJoinTableRows(
				"MCE_RE2_AUD", "MCE_ID",
				"aud.originalId.MultipleCollectionEntity_id", "RE2_ID",
				"aud.originalId.refEntities2_id", "aud.originalId.REV",
				"aud.originalId.REV.id", "aud.REVTYPE"
		);

		assertEquals( 4, mceRe1AuditJoinTableInfos.size() );
		assertEquals( 4, mceRe2AuditJoinTableInfos.size() );

		SequenceIdRevisionEntity rev2 = new SequenceIdRevisionEntity();
		rev2.setId( 2 );
		SequenceIdRevisionEntity rev3 = new SequenceIdRevisionEntity();
		rev3.setId( 3 );

		assertEquals(
				new AuditJoinTableInfo( "MCE_RE1_AUD", rev2, RevisionType.ADD, "MCE_ID", 1L, "RE1_ID", 1L ),
				mceRe1AuditJoinTableInfos.get( 0 )
		);
		assertEquals(
				new AuditJoinTableInfo( "MCE_RE1_AUD", rev2, RevisionType.ADD, "MCE_ID", 1L, "RE1_ID", 2L ),
				mceRe1AuditJoinTableInfos.get( 1 )
		);
		assertEquals(
				new AuditJoinTableInfo( "MCE_RE1_AUD", rev3, RevisionType.DEL, "MCE_ID", 1L, "RE1_ID", 2L ),
				mceRe1AuditJoinTableInfos.get( 2 )
		);
		assertEquals(
				new AuditJoinTableInfo( "MCE_RE1_AUD", rev3, RevisionType.ADD, "MCE_ID", 1L, "RE1_ID", 3L ),
				mceRe1AuditJoinTableInfos.get( 3 )
		);

		assertEquals(
				new AuditJoinTableInfo( "MCE_RE2_AUD", rev2, RevisionType.ADD, "MCE_ID", 1L, "RE2_ID", 1L ),
				mceRe2AuditJoinTableInfos.get( 0 )
		);
		assertEquals(
				new AuditJoinTableInfo( "MCE_RE2_AUD", rev2, RevisionType.ADD, "MCE_ID", 1L, "RE2_ID", 2L ),
				mceRe2AuditJoinTableInfos.get( 1 )
		);
		assertEquals(
				new AuditJoinTableInfo( "MCE_RE2_AUD", rev3, RevisionType.DEL, "MCE_ID", 1L, "RE2_ID", 2L ),
				mceRe2AuditJoinTableInfos.get( 2 )
		);
		assertEquals(
				new AuditJoinTableInfo( "MCE_RE2_AUD", rev3, RevisionType.ADD, "MCE_ID", 1L, "RE2_ID", 3L ),
				mceRe2AuditJoinTableInfos.get( 3 )
		);
	}

	private List<AuditJoinTableInfo> getAuditJoinTableRows(
			String middleEntityName, String joinColumnIdName,
			String joinColumnIdProp, String inverseJoinColumnIdName,
			String inverseJoinColumnIdProp, String revProp, String revIdProp,
			String revTypeProp) throws Exception {
		StringBuilder qryBuilder = new StringBuilder( "select " );
		qryBuilder.append( "aud " );
		qryBuilder.append( ", " ).append( joinColumnIdProp ).append( " as joinColumnId" );
		qryBuilder.append( ", " ).append( inverseJoinColumnIdProp ).append( " as inverseJoinColumnId" );
		qryBuilder.append( ", " ).append( revProp ).append( " as rev" );
		qryBuilder.append( ", " ).append( revIdProp ).append( " as revId" );
		qryBuilder.append( ", " ).append( revTypeProp ).append( " as revType" );
		qryBuilder.append( " from " ).append( middleEntityName ).append( " aud " );
		qryBuilder.append( " order by joinColumnId asc, inverseJoinColumnId asc, revId asc" );

		String query = qryBuilder.toString();

		EntityManager em = createIsolatedEntityManager();
		Query qry = em.createQuery( query );

		@SuppressWarnings("unchecked")
		List<Object[]> auditJoinTableRows = qry.getResultList();
		List<AuditJoinTableInfo> result = new ArrayList<AuditJoinTableInfo>( auditJoinTableRows.size() );

		for ( Object[] auditJoinTableRow : auditJoinTableRows ) {
			Long joinColumnId = (Long) auditJoinTableRow[1];
			Long inverseJoinColumnId = (Long) auditJoinTableRow[2];
			SequenceIdRevisionEntity rev = (SequenceIdRevisionEntity) auditJoinTableRow[3];
			RevisionType revType = (RevisionType) auditJoinTableRow[5];

			AuditJoinTableInfo info = new AuditJoinTableInfo(
					middleEntityName, rev, revType, joinColumnIdName, joinColumnId,
					inverseJoinColumnIdName, inverseJoinColumnId
			);
			result.add( info );
		}

		return result;
	}

	private static class AuditJoinTableInfo {
		private final String name;
		private final Integer revId;
		private final RevisionType revType;
		private final String joinColumnName;
		private final Long joinColumnId;
		private final String inverseJoinColumnName;
		private final Long inverseJoinColumnId;

		private AuditJoinTableInfo(
				String name, SequenceIdRevisionEntity rev,
				RevisionType revType, String joinColumnName, Long joinColumnId,
				String inverseJoinColumnName, Long inverseJoinColumnId) {
			this.name = name;
			this.revId = rev.getId();
			this.revType = revType;
			this.joinColumnName = joinColumnName;
			this.joinColumnId = joinColumnId;
			this.inverseJoinColumnName = inverseJoinColumnName;
			this.inverseJoinColumnId = inverseJoinColumnId;
		}

		@Override
		public String toString() {
			return "AuditJoinTableInfo [name=" + name + ", revId=" + revId
					+ ", revType=" + revType + ", " + joinColumnName + "="
					+ joinColumnId + ", " + inverseJoinColumnName + "="
					+ inverseJoinColumnId + "]";
		}

		@Override
		public boolean equals(Object o) {
			if ( this == o ) {
				return true;
			}
			if ( !(o instanceof AuditJoinTableInfo) ) {
				return false;
			}

			AuditJoinTableInfo that = (AuditJoinTableInfo) o;

			if ( inverseJoinColumnId != null ?
					!inverseJoinColumnId.equals( that.inverseJoinColumnId ) :
					that.inverseJoinColumnId != null ) {
				return false;
			}
			if ( joinColumnId != null ? !joinColumnId.equals( that.joinColumnId ) : that.joinColumnId != null ) {
				return false;
			}
			if ( name != null ? !name.equals( that.name ) : that.name != null ) {
				return false;
			}
			if ( revId != null ? !revId.equals( that.revId ) : that.revId != null ) {
				return false;
			}
			if ( revType != that.revType ) {
				return false;
			}

			return true;
		}

		@Override
		public int hashCode() {
			int result = name != null ? name.hashCode() : 0;
			result = 31 * result + (revId != null ? revId.hashCode() : 0);
			result = 31 * result + (revType != null ? revType.hashCode() : 0);
			result = 31 * result + (joinColumnId != null ? joinColumnId.hashCode() : 0);
			result = 31 * result + (inverseJoinColumnId != null ? inverseJoinColumnId.hashCode() : 0);
			return result;
		}
	}

	public static void tryCommit(TransactionManager tm) throws Exception {
		if ( tm.getStatus() == Status.STATUS_MARKED_ROLLBACK ) {
			tm.rollback();
		}
		else {
			tm.commit();
		}
	}
}