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

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import javax.persistence.EntityManager;

import org.hibernate.criterion.MatchMode;
import org.hibernate.envers.RevisionType;
import org.hibernate.envers.enhanced.SequenceIdRevisionEntity;
import org.hibernate.envers.query.AuditEntity;
import org.hibernate.envers.test.BaseEnversJPAFunctionalTestCase;
import org.hibernate.envers.test.Priority;
import org.hibernate.envers.test.entities.StrIntTestEntity;
import org.hibernate.envers.test.entities.ids.EmbId;
import org.hibernate.envers.test.entities.ids.EmbIdTestEntity;
import org.hibernate.envers.test.entities.ids.MulId;
import org.hibernate.envers.test.entities.ids.MulIdTestEntity;
import org.hibernate.envers.test.tools.TestTools;

import org.hibernate.testing.TestForIssue;
import org.junit.Assert;
import org.junit.Test;

/**
 * @author Adam Warski (adam at warski dot org)
 * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
 */
@SuppressWarnings({"unchecked"})
public class SimpleQuery extends BaseEnversJPAFunctionalTestCase {
	private Integer id1;
	private Integer id2;
	private Integer id3;
	private MulId mulId1;
	private EmbId embId1;

	@Override
	protected Class<?>[] getAnnotatedClasses() {
		return new Class[] { StrIntTestEntity.class, MulIdTestEntity.class, EmbIdTestEntity.class };
	}

	@Test
	@Priority(10)
	public void initData() {
		// Revision 1
		EntityManager em = getEntityManager();
		em.getTransaction().begin();

		StrIntTestEntity site1 = new StrIntTestEntity( "a", 10 );
		StrIntTestEntity site2 = new StrIntTestEntity( "a", 10 );
		StrIntTestEntity site3 = new StrIntTestEntity( "b", 5 );

		em.persist( site1 );
		em.persist( site2 );
		em.persist( site3 );

		id1 = site1.getId();
		id2 = site2.getId();
		id3 = site3.getId();

		em.getTransaction().commit();

		// Revision 2
		em.getTransaction().begin();

		mulId1 = new MulId( 1, 2 );
		em.persist( new MulIdTestEntity( mulId1.getId1(), mulId1.getId2(), "data" ) );

		embId1 = new EmbId( 3, 4 );
		em.persist( new EmbIdTestEntity( embId1, "something" ) );

		site1 = em.find( StrIntTestEntity.class, id1 );
		site2 = em.find( StrIntTestEntity.class, id2 );

		site1.setStr1( "aBc" );
		site2.setNumber( 20 );

		em.getTransaction().commit();

		// Revision 3
		em.getTransaction().begin();

		site3 = em.find( StrIntTestEntity.class, id3 );

		site3.setStr1( "a" );

		em.getTransaction().commit();

		// Revision 4
		em.getTransaction().begin();

		site1 = em.find( StrIntTestEntity.class, id1 );

		em.remove( site1 );

		em.getTransaction().commit();
	}

	@Test
	public void testEntitiesIdQuery() {
		StrIntTestEntity ver2 = (StrIntTestEntity) getAuditReader().createQuery()
				.forEntitiesAtRevision( StrIntTestEntity.class, 2 )
				.add( AuditEntity.id().eq( id2 ) )
				.getSingleResult();

		assert ver2.equals( new StrIntTestEntity( "a", 20, id2 ) );
	}

	@Test
	public void testEntitiesPropertyEqualsQuery() {
		List ver1 = getAuditReader().createQuery()
				.forEntitiesAtRevision( StrIntTestEntity.class, 1 )
				.add( AuditEntity.property( "str1" ).eq( "a" ) )
				.getResultList();

		List ver2 = getAuditReader().createQuery()
				.forEntitiesAtRevision( StrIntTestEntity.class, 2 )
				.add( AuditEntity.property( "str1" ).eq( "a" ) )
				.getResultList();

		List ver3 = getAuditReader().createQuery()
				.forEntitiesAtRevision( StrIntTestEntity.class, 3 )
				.add( AuditEntity.property( "str1" ).eq( "a" ) )
				.getResultList();

		assert new HashSet( ver1 ).equals(
				TestTools.makeSet(
						new StrIntTestEntity( "a", 10, id1 ),
						new StrIntTestEntity( "a", 10, id2 )
				)
		);
		assert new HashSet( ver2 ).equals( TestTools.makeSet( new StrIntTestEntity( "a", 20, id2 ) ) );
		assert new HashSet( ver3 ).equals(
				TestTools.makeSet(
						new StrIntTestEntity( "a", 20, id2 ),
						new StrIntTestEntity( "a", 5, id3 )
				)
		);
	}

	@Test
	public void testEntitiesPropertyLeQuery() {
		List ver1 = getAuditReader().createQuery()
				.forEntitiesAtRevision( StrIntTestEntity.class, 1 )
				.add( AuditEntity.property( "number" ).le( 10 ) )
				.getResultList();

		List ver2 = getAuditReader().createQuery()
				.forEntitiesAtRevision( StrIntTestEntity.class, 2 )
				.add( AuditEntity.property( "number" ).le( 10 ) )
				.getResultList();

		List ver3 = getAuditReader().createQuery()
				.forEntitiesAtRevision( StrIntTestEntity.class, 3 )
				.add( AuditEntity.property( "number" ).le( 10 ) )
				.getResultList();

		assert new HashSet( ver1 ).equals(
				TestTools.makeSet(
						new StrIntTestEntity( "a", 10, id1 ),
						new StrIntTestEntity( "a", 10, id2 ), new StrIntTestEntity( "b", 5, id3 )
				)
		);
		assert new HashSet( ver2 ).equals(
				TestTools.makeSet(
						new StrIntTestEntity( "aBc", 10, id1 ),
						new StrIntTestEntity( "b", 5, id3 )
				)
		);
		assert new HashSet( ver3 ).equals(
				TestTools.makeSet(
						new StrIntTestEntity( "aBc", 10, id1 ),
						new StrIntTestEntity( "a", 5, id3 )
				)
		);
	}

	@Test
	public void testRevisionsPropertyEqQuery() {
		List revs_id1 = getAuditReader().createQuery()
				.forRevisionsOfEntity( StrIntTestEntity.class, false, true )
				.addProjection( AuditEntity.revisionNumber() )
				.add( AuditEntity.property( "str1" ).le( "a" ) )
				.add( AuditEntity.id().eq( id1 ) )
				.getResultList();

		List revs_id2 = getAuditReader().createQuery()
				.forRevisionsOfEntity( StrIntTestEntity.class, false, true )
				.addProjection( AuditEntity.revisionNumber() )
				.add( AuditEntity.property( "str1" ).le( "a" ) )
				.add( AuditEntity.id().eq( id2 ) )
				.getResultList();

		List revs_id3 = getAuditReader().createQuery()
				.forRevisionsOfEntity( StrIntTestEntity.class, false, true )
				.addProjection( AuditEntity.revisionNumber() )
				.add( AuditEntity.property( "str1" ).le( "a" ) )
				.add( AuditEntity.id().eq( id3 ) )
				.getResultList();

		assert Arrays.asList( 1 ).equals( revs_id1 );
		assert Arrays.asList( 1, 2 ).equals( revs_id2 );
		assert Arrays.asList( 3 ).equals( revs_id3 );
	}

	@Test
	public void testSelectEntitiesQuery() {
		List result = getAuditReader().createQuery()
				.forRevisionsOfEntity( StrIntTestEntity.class, true, false )
				.add( AuditEntity.id().eq( id1 ) )
				.getResultList();

		assert result.size() == 2;

		assert result.get( 0 ).equals( new StrIntTestEntity( "a", 10, id1 ) );
		assert result.get( 1 ).equals( new StrIntTestEntity( "aBc", 10, id1 ) );
	}

	@Test
	public void testSelectEntitiesAndRevisionsQuery() {
		List result = getAuditReader().createQuery()
				.forRevisionsOfEntity( StrIntTestEntity.class, false, true )
				.add( AuditEntity.id().eq( id1 ) )
				.getResultList();

		assert result.size() == 3;

		assert ((Object[]) result.get( 0 ))[0].equals( new StrIntTestEntity( "a", 10, id1 ) );
		assert ((Object[]) result.get( 1 ))[0].equals( new StrIntTestEntity( "aBc", 10, id1 ) );
		assert ((Object[]) result.get( 2 ))[0].equals( new StrIntTestEntity( null, null, id1 ) );

		assert ((SequenceIdRevisionEntity) ((Object[]) result.get( 0 ))[1]).getId() == 1;
		assert ((SequenceIdRevisionEntity) ((Object[]) result.get( 1 ))[1]).getId() == 2;
		assert ((SequenceIdRevisionEntity) ((Object[]) result.get( 2 ))[1]).getId() == 4;

		assert ((Object[]) result.get( 0 ))[2].equals( RevisionType.ADD );
		assert ((Object[]) result.get( 1 ))[2].equals( RevisionType.MOD );
		assert ((Object[]) result.get( 2 ))[2].equals( RevisionType.DEL );
	}

	@Test
	public void testSelectRevisionTypeQuery() {
		List result = getAuditReader().createQuery()
				.forRevisionsOfEntity( StrIntTestEntity.class, false, true )
				.addProjection( AuditEntity.revisionType() )
				.add( AuditEntity.id().eq( id1 ) )
				.getResultList();

		assert result.size() == 3;

		assert result.get( 0 ).equals( RevisionType.ADD );
		assert result.get( 1 ).equals( RevisionType.MOD );
		assert result.get( 2 ).equals( RevisionType.DEL );
	}

	@Test
	public void testEmptyRevisionOfEntityQuery() {
		List result = getAuditReader().createQuery()
				.forRevisionsOfEntity( StrIntTestEntity.class, false, true )
				.getResultList();

		assert result.size() == 7;
	}

	@Test
	public void testEmptyConjunctionRevisionOfEntityQuery() {
		List result = getAuditReader().createQuery()
				.forRevisionsOfEntity( StrIntTestEntity.class, false, true )
				.add( AuditEntity.conjunction() )
				.getResultList();

		assert result.size() == 7;
	}

	@Test
	public void testEmptyDisjunctionRevisionOfEntityQuery() {
		List result = getAuditReader().createQuery()
				.forRevisionsOfEntity( StrIntTestEntity.class, false, true )
				.add( AuditEntity.disjunction() )
				.getResultList();

		assert result.size() == 0;
	}

	@Test
	public void testEntitiesAddedAtRevision() {
		StrIntTestEntity site1 = new StrIntTestEntity( "a", 10, id1 );
		StrIntTestEntity site2 = new StrIntTestEntity( "a", 10, id2 );
		StrIntTestEntity site3 = new StrIntTestEntity( "b", 5, id3 );

		List result = getAuditReader().createQuery().forEntitiesModifiedAtRevision(
				StrIntTestEntity.class,
				StrIntTestEntity.class.getName(),
				1
		).getResultList();
		RevisionType revisionType = (RevisionType) getAuditReader().createQuery().forEntitiesModifiedAtRevision(
				StrIntTestEntity.class,
				1
		)
				.addProjection( AuditEntity.revisionType() ).add( AuditEntity.id().eq( id1 ) )
				.getSingleResult();

		Assert.assertTrue( TestTools.checkCollection( result, site1, site2, site3 ) );
		Assert.assertEquals( revisionType, RevisionType.ADD );
	}

	@Test
	public void testEntitiesChangedAtRevision() {
		StrIntTestEntity site1 = new StrIntTestEntity( "aBc", 10, id1 );
		StrIntTestEntity site2 = new StrIntTestEntity( "a", 20, id2 );

		List result = getAuditReader().createQuery()
				.forEntitiesModifiedAtRevision( StrIntTestEntity.class, 2 )
				.getResultList();
		RevisionType revisionType = (RevisionType) getAuditReader().createQuery().forEntitiesModifiedAtRevision(
				StrIntTestEntity.class,
				2
		)
				.addProjection( AuditEntity.revisionType() ).add( AuditEntity.id().eq( id1 ) )
				.getSingleResult();

		Assert.assertTrue( TestTools.checkCollection( result, site1, site2 ) );
		Assert.assertEquals( revisionType, RevisionType.MOD );
	}

	@Test
	public void testEntitiesRemovedAtRevision() {
		StrIntTestEntity site1 = new StrIntTestEntity( null, null, id1 );

		List result = getAuditReader().createQuery()
				.forEntitiesModifiedAtRevision( StrIntTestEntity.class, 4 )
				.getResultList();
		RevisionType revisionType = (RevisionType) getAuditReader().createQuery().forEntitiesModifiedAtRevision(
				StrIntTestEntity.class,
				4
		)
				.addProjection( AuditEntity.revisionType() ).add( AuditEntity.id().eq( id1 ) )
				.getSingleResult();

		Assert.assertTrue( TestTools.checkCollection( result, site1 ) );
		Assert.assertEquals( revisionType, RevisionType.DEL );
	}

	@Test
	public void testEntityNotModifiedAtRevision() {
		List result = getAuditReader().createQuery().forEntitiesModifiedAtRevision( StrIntTestEntity.class, 3 )
				.add( AuditEntity.id().eq( id1 ) ).getResultList();
		Assert.assertTrue( result.isEmpty() );
	}

	@Test
	public void testNoEntitiesModifiedAtRevision() {
		List result = getAuditReader().createQuery()
				.forEntitiesModifiedAtRevision( StrIntTestEntity.class, 5 )
				.getResultList();
		Assert.assertTrue( result.isEmpty() );
	}

	@Test
	@TestForIssue(jiraKey = "HHH-7800")
	public void testBetweenInsideDisjunction() {
		List result = getAuditReader().createQuery()
				.forRevisionsOfEntity( StrIntTestEntity.class, true, true )
				.add(
						AuditEntity.disjunction()
								.add( AuditEntity.property( "number" ).between( 0, 5 ) )
								.add( AuditEntity.property( "number" ).between( 20, 100 ) )
				)
				.getResultList();

		for ( Object o : result ) {
			StrIntTestEntity entity = (StrIntTestEntity) o;
			int number = entity.getNumber();
			Assert.assertTrue( (number >= 0 && number <= 5) || (number >= 20 && number <= 100) );
		}
	}
	
	@Test
	@TestForIssue(jiraKey = "HHH-8495")
	public void testIlike() {
		StrIntTestEntity site1 = new StrIntTestEntity( "aBc", 10, id1 );
		
		StrIntTestEntity result = (StrIntTestEntity) getAuditReader().createQuery()
				.forRevisionsOfEntity( StrIntTestEntity.class, true, true )
				.add( AuditEntity.property( "str1" ).ilike( "abc" ) )
				.getSingleResult();
		
		Assert.assertEquals( site1, result );
	}
	
	@Test
	@TestForIssue(jiraKey = "HHH-8495")
	public void testIlikeWithMatchMode() {
		StrIntTestEntity site1 = new StrIntTestEntity( "aBc", 10, id1 );
		
		StrIntTestEntity result = (StrIntTestEntity) getAuditReader().createQuery()
				.forRevisionsOfEntity( StrIntTestEntity.class, true, true )
				.add( AuditEntity.property( "str1" ).ilike( "BC", MatchMode.ANYWHERE ) )
				.getSingleResult();
		
		Assert.assertEquals( site1, result );
	}

	@Test
	@TestForIssue(jiraKey = "HHH-8567")
	public void testIdPropertyRestriction() {
		StrIntTestEntity ver2 = (StrIntTestEntity) getAuditReader().createQuery()
				.forEntitiesAtRevision( StrIntTestEntity.class, 2 )
				.add( AuditEntity.property( "id" ).eq( id2 ) )
				.getSingleResult();

		Assert.assertEquals( new StrIntTestEntity( "a", 20, id2 ), ver2 );
	}

	@Test
	@TestForIssue(jiraKey = "HHH-8567")
	public void testMultipleIdPropertyRestriction() {
		MulIdTestEntity ver2 = (MulIdTestEntity) getAuditReader().createQuery()
				.forEntitiesAtRevision( MulIdTestEntity.class, 2 )
				.add( AuditEntity.property( "id1" ).eq( mulId1.getId1() ) )
				.add( AuditEntity.property( "id2" ).eq( mulId1.getId2() ) )
				.getSingleResult();

		Assert.assertEquals( new MulIdTestEntity( mulId1.getId1(), mulId1.getId2(), "data" ), ver2 );
	}

	@Test
	@TestForIssue(jiraKey = "HHH-8567")
	public void testEmbeddedIdPropertyRestriction() {
		EmbIdTestEntity ver2 = (EmbIdTestEntity) getAuditReader().createQuery()
				.forEntitiesAtRevision( EmbIdTestEntity.class, 2 )
				.add( AuditEntity.property( "id.x" ).eq( embId1.getX() ) )
				.add( AuditEntity.property( "id.y" ).eq( embId1.getY() ) )
				.getSingleResult();

		Assert.assertEquals( new EmbIdTestEntity( embId1, "something" ), ver2 );
	}
}
