/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2012, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.test.integration.jpa.secondlevelcache;

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

import java.util.List;

import javax.ejb.Stateful;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceUnit;
import javax.persistence.Query;

import org.hibernate.Session;
import org.hibernate.stat.QueryStatistics;
import org.hibernate.stat.SecondLevelCacheStatistics;
import org.hibernate.stat.Statistics;

/**
 * SFSB for Second level cache tests
 * 
 * @author Zbynek Roubalik
 */
@Stateful
@TransactionManagement(TransactionManagementType.CONTAINER)
public class SFSB2LC {
	@PersistenceUnit(unitName = "mypc")
	EntityManagerFactory emf;

	@PersistenceUnit(unitName = "mypc_no_2lc")
	EntityManagerFactory emfNo2LC;
	
	
	/**
	 * Check if disabling 2LC works as expected
	 */
	public String disabled2LCCheck() {
		
		EntityManager em = emfNo2LC.createEntityManager();
		Statistics stats = em.unwrap(Session.class).getSessionFactory().getStatistics();
		stats.clear();
		
		try {
			// check if entities are NOT cached in 2LC
			String names[] = stats.getSecondLevelCacheRegionNames();
			assertEquals("There aren't any 2LC regions.", 0, names.length);

			createEmployee(em, "Martin", "Prague 132", 1);
			assertEquals("There aren't any puts in the 2LC.", 0,stats.getSecondLevelCachePutCount());

			// check if queries are NOT cached in 2LC
			Employee emp = getEmployeeQuery(em, 1);
			assertNotNull("Employee returned", emp);
			assertEquals("There aren't any query puts in the 2LC.", 0,stats.getQueryCachePutCount());
			
			// cleanup
			em.remove(emp);
			
		}catch (AssertionError e) {
			return e.getMessage();
		}	finally{
			em.close();
		}
		return "OK";
	}
	
	
	/**
	 *  Checking entity 2LC in one EntityManager session
	 */
	public String sameSessionCheck(String CACHE_REGION_NAME) {
		
		EntityManager em = emf.createEntityManager();
		Statistics stats = em.unwrap(Session.class).getSessionFactory().getStatistics();
		stats.clear();
		SecondLevelCacheStatistics emp2LCStats = stats.getSecondLevelCacheStatistics(CACHE_REGION_NAME+"Employee");
			
		try{
			// add new entities and check if they are put in 2LC
			createEmployee(em, "Peter", "Ostrava", 2);
			createEmployee(em, "Tom", "Brno", 3);
			assertEquals("There are 2 puts in the 2LC"+generateEntityCacheStats(emp2LCStats), 2, emp2LCStats.getPutCount());
			
			// loading all Employee entities should put in 2LC all Employee 
			List<?> empList = getAllEmployeesQuery(em);
			assertEquals("There are 2 entities.", empList.size(), 2);
			assertEquals("There are 2 entities in the 2LC"+generateEntityCacheStats(emp2LCStats), 2, emp2LCStats.getElementCountInMemory());
			
			// clear session
			em.clear();
			
			// entity should be loaded from 2L cache, we'are expecting hit in 2L cache
			Employee emp = getEmployee(em, 2);
			assertNotNull("Employee returned", emp);
			assertEquals("Expected 1 hit in cache"+generateEntityCacheStats(emp2LCStats), 1,  emp2LCStats.getHitCount());
			
		}catch (AssertionError e) {
			return e.getMessage();
		}	finally{
			em.close();
		}
		return "OK";
	}
	
	
	/**
	 *  Checking entity 2LC in a different EntityManager session
	 */
	public String secondSessionCheck(String CACHE_REGION_NAME) {
		
		EntityManager em = emf.createEntityManager();
		Statistics stats = em.unwrap(Session.class).getSessionFactory().getStatistics();
		stats.clear();
		SecondLevelCacheStatistics emp2LCStats = stats.getSecondLevelCacheStatistics(CACHE_REGION_NAME+"Employee");
			
		try{
			// add new entity
			createEmployee(em, "David", "Praha", 10);
			assertEquals("There is 1 put in the 2LC"+generateEntityCacheStats(emp2LCStats), 1, emp2LCStats.getPutCount());
			
		}catch (AssertionError e) {
			return e.getMessage();
		}	finally{
			em.close();
		}
		
		
		EntityManager em2 = emf.createEntityManager();
		try{	
			// loading entity stored in previous session, we'are expecting hit in cache
			Employee emp = getEmployee(em2, 10);
			assertNotNull("Employee returned", emp);
			assertEquals("Expected 1 hit in 2LC"+generateEntityCacheStats(emp2LCStats), 1,  emp2LCStats.getHitCount());
			
		}catch (AssertionError e) {
			return e.getMessage();
		}	finally{
			em2.close();
		}
		
		return "OK";
	}
	
	
	/**
	 * Insert 2 entities and put them into the 2LC and then evicts entity cache.
	 */
	public String addEntitiesAndEvictAll(String CACHE_REGION_NAME){

		EntityManager em = emf.createEntityManager();
		Statistics stats = em.unwrap(Session.class).getSessionFactory().getStatistics();
		stats.clear();
		SecondLevelCacheStatistics emp2LCStats = stats.getSecondLevelCacheStatistics(CACHE_REGION_NAME+"Employee");
		
		try{
			createEmployee(em, "Jan", "Ostrava", 20);
			createEmployee(em, "Martin", "Brno", 30);
			assertEquals("There are 2 puts in the 2LC"+generateEntityCacheStats(emp2LCStats), 2, emp2LCStats.getPutCount());
			
			assertTrue("Expected entities stored in the cache"+generateEntityCacheStats(emp2LCStats), emp2LCStats.getElementCountInMemory() > 0);
			
			// evict entity 2lc
			emf.getCache().evictAll();
			
		}catch (AssertionError e) {
			return e.getMessage();
		}	finally{
			em.close();
		}
		
		return "OK";
	}

	
	/**
	 * Checks if entity 2LC is empty.
	 */
	public String evictedEntityCacheCheck(String CACHE_REGION_NAME){

		EntityManager em = emf.createEntityManager();
		Statistics stats = em.unwrap(Session.class).getSessionFactory().getStatistics();
		stats.clear();
		SecondLevelCacheStatistics emp2LCStats = stats.getSecondLevelCacheStatistics(CACHE_REGION_NAME+"Employee");
			
		try{	
			assertEquals("Expected no entities stored in the cache"+emp2LCStats, 0, emp2LCStats.getElementCountInMemory());
			
			// loading entity stored in previous session, we are expecting miss in 2lc
			Employee emp = getEmployee(em, 20);
			assertNotNull("Employee returned", emp);
			assertEquals("Expected 1 miss in 2LC"+generateEntityCacheStats(emp2LCStats), 1,  emp2LCStats.getMissCount());
			
		}catch (AssertionError e) {
			return e.getMessage();
		}	finally{
			em.close();
		}
	
		return "OK";	
	}

	
	/**
	 * Performs 2 query calls, first call put query in the cache and second should hit the cache
	 * @param id Employee's id in the query
	 */
	public String queryCacheCheck(String id){
		
		EntityManager em = emf.createEntityManager();
		Statistics stats = em.unwrap(Session.class).getSessionFactory().getStatistics();
		stats.clear();

		try{
	        // the nextTimestamp from infinispan is "return System.currentTimeMillis() / 100;"
	        Thread.sleep(1000);
	        
			String queryString = "from Employee e where e.id > "+id;
			QueryStatistics queryStats = stats.getQueryStatistics(queryString);
			Query query = em.createQuery(queryString);
			query.setHint("org.hibernate.cacheable", true);

			// query - this call should fill the cache
			query.getResultList();
			assertEquals("Expected 1 miss in cache"+generateQueryCacheStats(queryStats), 1,  queryStats.getCacheMissCount());
			assertEquals("Expected 1 put in cache"+generateQueryCacheStats(queryStats), 1,  queryStats.getCachePutCount());
			assertEquals("Expected no hits in cache"+generateQueryCacheStats(queryStats), 0,  queryStats.getCacheHitCount());
			
			// query - second call should hit cache
			query.getResultList();
			assertEquals("Expected 1 hit in cache"+generateQueryCacheStats(queryStats), 1,  queryStats.getCacheHitCount());
			

		}catch (AssertionError e) {
			return e.getMessage();
		} catch (InterruptedException e) {
			return e.getMessage();
		}	finally{
			em.close();
		}
		return "OK";
	}
	
	
	/**
	 * Evicts all query cache regions
	 */
	public void evictQueryCache(){
		
		EntityManager em = emf.createEntityManager();
		
		try{
			// this should evict query cache 
			em.unwrap(Session.class).getSessionFactory().getCache().evictDefaultQueryRegion();

		}catch (Exception e) {
			e.printStackTrace();
		}	finally{
			em.close();
		}

	}
	
	
	/**
	 * Checking if query cache is empty
	 * @param id Employee's id in the query
	 */
	public String queryCacheCheckIfEmpty(String id){
		
		EntityManager em = emf.createEntityManager();
		Statistics stats = em.unwrap(Session.class).getSessionFactory().getStatistics();
		stats.clear();

		try{
	        // the nextTimestamp from infinispan is "return System.currentTimeMillis() / 100;"
	        Thread.sleep(1000);
	        
			String queryString = "from Employee e where e.id > "+id;
			QueryStatistics queryStats = stats.getQueryStatistics(queryString);
			Query query = em.createQuery(queryString);
			query.setHint("org.hibernate.cacheable", true);

			// query - this call shouldn't hit the cache -> query cache is empty
			query.getResultList();
			assertEquals("Expected 1 miss in cache"+generateQueryCacheStats(queryStats), 1,  queryStats.getCacheMissCount());
			assertEquals("Expected 1 put in cache"+generateQueryCacheStats(queryStats), 1,  queryStats.getCachePutCount());
			assertEquals("Expected no hits in cache"+generateQueryCacheStats(queryStats), 0,  queryStats.getCacheHitCount());
			

		}catch (AssertionError e) {
			return e.getMessage();
		} catch (InterruptedException e) {
			return e.getMessage();
		}	finally{
			em.close();
		}
		return "OK";
	}
	
	
	/**
	 * Generate query cache statistics for put, hit and miss count as one String
	 */
	public String generateQueryCacheStats(QueryStatistics stats){
		String result = "(hitCount="+stats.getCacheHitCount()
				+", missCount="+stats.getCacheMissCount()
				+", putCount="+stats.getCachePutCount()+").";
		
		return result;
	}
	
	
	/**
	 * Generate entity cache statistics for put, hit and miss count as one String
	 */
	public String generateEntityCacheStats(SecondLevelCacheStatistics stats){
		String result = "(hitCount="+stats.getHitCount()
				+", missCount="+stats.getMissCount()
				+", putCount="+stats.getPutCount()+").";
		
		return result;
	}
	
	
	public String getCacheRegionName(){

		return (String) emf.getProperties().get("hibernate.cache.region_prefix");
	}
	
	/**
	 * Create employee in provided EntityManager
	 */
	public void createEmployee(EntityManager em, String name, String address,
			int id) {
		Employee emp = new Employee();
		emp.setId(id);
		emp.setAddress(address);
		emp.setName(name);
		try {
			em.persist(emp);
			em.flush();
		} catch (Exception e) {
			throw new RuntimeException(	"transactional failure while persisting employee entity", e);
		}
	}

    /**
     * Create employee in provided EntityManager
     */
    public void createEmployee(String name, String address,
                               int id) {
        EntityManager em = emf.createEntityManager();
        Employee emp = new Employee();
        emp.setId(id);
        emp.setAddress(address);
        emp.setName(name);
        try {
            em.persist(emp);
        } catch (Exception e) {
            throw new RuntimeException(	"transactional failure while persisting employee entity", e);
        } finally {
            em.close();
        }
    }

	
	/**
	 * Load employee from provided EntityManager
	 */
	public Employee getEmployee(EntityManager em, int id) {
		Employee emp = em.find(Employee.class, id);
		return emp;
	}

	
	/**
	 * Load employee using Query from provided EntityManager
	 */
	public Employee getEmployeeQuery(EntityManager em, int id) {

		Query query;

		query = em.createQuery("from Employee e where e.id=:id");

		query.setParameter("id", id);
		query.setHint("org.hibernate.cacheable", true);

		return (Employee) query.getSingleResult();
	}

	
	/**
	 * Load all employees using Query from provided EntityManager
	 */
	@SuppressWarnings("unchecked")
	public List<Employee> getAllEmployeesQuery(EntityManager em) {

		Query query;

		query = em.createQuery("from Employee");
		query.setHint("org.hibernate.cacheable", true);

		return query.getResultList();
	}

}
