/*
 * Copyright (c) 2007, Red Hat Middleware, LLC. All rights reserved.
 *
 * 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, v. 2.1. This program is distributed in the
 * hope that it will be useful, but WITHOUT A 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, v.2.1 along with this
 * distribution; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * Red Hat Author(s): Brian Stansberry
 */

package org.hibernate.test.cache.jbc2.timestamp;

import java.util.Properties;
import java.util.Random;

import junit.framework.AssertionFailedError;

import org.hibernate.cache.UpdateTimestampsCache;
import org.hibernate.cache.jbc2.CacheInstanceManager;
import org.hibernate.cache.jbc2.JBossCacheRegionFactory;
import org.hibernate.cache.jbc2.MultiplexedJBossCacheRegionFactory;
import org.hibernate.cache.jbc2.timestamp.ClusteredConcurrentTimestampsRegionImpl;
import org.hibernate.cfg.Configuration;
import org.hibernate.test.cache.jbc2.AbstractJBossCacheTestCase;
import org.hibernate.test.util.CacheTestUtil;
import org.jboss.cache.Cache;

/**
 * A ClusteredConcurrentTimestampCacheTestCase.
 * 
 * @author <a href="brian.stansberry@jboss.com">Brian Stansberry</a>
 * @version $Revision: 1 $
 */
public class ClusteredConcurrentTimestampRegionTestCase extends AbstractJBossCacheTestCase {

    private static final String KEY1 = "com.foo.test.Entity1";
    private static final String KEY2 = "com.foo.test.Entity2";
    
    private static final Long ONE = new Long(1);
    private static final Long TWO = new Long(2);
    private static final Long THREE = new Long(3);
    private static final Long TEN = new Long(10);
    private static final Long ELEVEN = new Long(11);
    
    private static Cache cache;
    private static Properties properties;
    private ClusteredConcurrentTimestampsRegionImpl region;
    
    /**
     * Create a new ClusteredConcurrentTimestampCacheTestCase.
     * 
     * @param name
     */
    public ClusteredConcurrentTimestampRegionTestCase(String name) {
        super(name);
    }
    
    

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        
        if (cache == null) {
            Configuration cfg = CacheTestUtil.buildConfiguration("test", MultiplexedJBossCacheRegionFactory.class, false, true);
            properties = cfg.getProperties();
            cache = createCache();
            
            // Sleep a bit to avoid concurrent FLUSH problem
            avoidConcurrentFlush();
        }
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
        
        if (region != null) {
            region.destroy();
        }
    }
    
    private Cache createCache() throws Exception {
        Configuration cfg = CacheTestUtil.buildConfiguration("test", MultiplexedJBossCacheRegionFactory.class, false, true);
        JBossCacheRegionFactory regionFactory = CacheTestUtil.startRegionFactory(cfg);
        CacheInstanceManager mgr = regionFactory.getCacheInstanceManager();
        return mgr.getTimestampsCacheInstance();
    }

    protected ClusteredConcurrentTimestampsRegionImpl getTimestampRegion(Cache cache)  throws Exception {        
        
        return new ClusteredConcurrentTimestampsRegionImpl(cache, "test/" + UpdateTimestampsCache.class.getName(), "test", properties);
    }
    
    public void testSimplePreinvalidate() throws Exception {
        
        region = getTimestampRegion(cache);
        
        assertEquals(null, region.get(KEY1));
        region.preInvalidate(KEY1, TWO);
        assertEquals(TWO, region.get(KEY1));
        region.preInvalidate(KEY1, ONE);
        assertEquals(TWO, region.get(KEY1));
        region.preInvalidate(KEY1, TWO);
        assertEquals(TWO, region.get(KEY1));
        region.preInvalidate(KEY1, THREE);
        assertEquals(THREE, region.get(KEY1));
    }
    
    public void testInitialState() throws Exception {
        
        region = getTimestampRegion(cache);
        region.preInvalidate(KEY1, TEN);
        region.preInvalidate(KEY2, ELEVEN);
        region.invalidate(KEY1, ONE, TEN);
        
        Cache cache2 = createCache();
        registerCache(cache2);
        
        // Sleep a bit to avoid concurrent FLUSH problem
        avoidConcurrentFlush();
        
        ClusteredConcurrentTimestampsRegionImpl region2 = getTimestampRegion(cache2);
        assertEquals(ONE, region2.get(KEY1));
        assertEquals(ELEVEN, region2.get(KEY2));
    }
    
    public void testSimpleInvalidate() throws Exception {
        
        region = getTimestampRegion(cache);
        
        assertEquals(null, region.get(KEY1));
        region.preInvalidate(KEY1, TWO);
        assertEquals(TWO, region.get(KEY1));
        region.invalidate(KEY1, ONE, TWO);
        assertEquals(ONE, region.get(KEY1));
        region.preInvalidate(KEY1, TEN);
        region.preInvalidate(KEY1, ELEVEN);
        assertEquals(ELEVEN, region.get(KEY1));
        region.invalidate(KEY1, TWO, TEN);
        assertEquals(ELEVEN, region.get(KEY1));
        region.invalidate(KEY1, TWO, ELEVEN);
        assertEquals(TWO, region.get(KEY1));
        region.preInvalidate(KEY1, TEN);
        assertEquals(TEN, region.get(KEY1));
        region.invalidate(KEY1, THREE, TEN);
        assertEquals(THREE, region.get(KEY1));        
    }
    
    public void testConcurrentActivityClustered() throws Exception {
        concurrentActivityTest(true);
    }
    
    public void testConcurrentActivityNonClustered() throws Exception {
        concurrentActivityTest(false);
    }
    
    private void concurrentActivityTest(boolean clustered) throws Exception {
        
        region = getTimestampRegion(cache);
        ClusteredConcurrentTimestampsRegionImpl region2 = region;
        
        if (clustered) {
            Cache cache2 = createCache();
            registerCache(cache2);
            
            // Sleep a bit to avoid concurrent FLUSH problem
            avoidConcurrentFlush();
            
            region2 = getTimestampRegion(cache2);
        }
        
        Tester[] testers = new Tester[20];
        for (int i = 0; i < testers.length; i++) {
            testers[i] = new Tester((i % 2 == 0) ? region : region2);
            testers[i].start();
        }
        
        for (int j = 0; j < 10; j++) {
            sleep(2000);
            
            log.info("Running for " + ((j + 1) * 2) + " seconds");
            
            for (int i = 0; i < testers.length; i++) {
                if (testers[i].assertionFailure != null)
                    throw testers[i].assertionFailure;
            }
            
            for (int i = 0; i < testers.length; i++) {
                if (testers[i].exception != null)
                    throw testers[i].exception;
            }
        }
        
        for (int i = 0; i < testers.length; i++) {
            testers[i].stop();
        }
        
        for (int i = 0; i < testers.length; i++) {
            if (testers[i].assertionFailure != null)
                throw testers[i].assertionFailure;
        }
        
        for (int i = 0; i < testers.length; i++) {
            if (testers[i].exception != null)
                throw testers[i].exception;
        }
    }
    
    
    
    private class Tester implements Runnable {
        
        ClusteredConcurrentTimestampsRegionImpl region;
        Exception exception;
        AssertionFailedError assertionFailure;
        boolean stopped = true;
        Thread thread;
        Random random = new Random();

        Tester(ClusteredConcurrentTimestampsRegionImpl region) {
            this.region = region;
        }
        
        public void run() {
            stopped = false;
        
            while (!stopped) {
                try {
                    Long pre = new Long(region.nextTimestamp() + region.getTimeout());
                    region.preInvalidate(KEY1, pre);
                    sleep(random.nextInt(1));
                    Long post = new Long(region.nextTimestamp());
                    region.invalidate(KEY1, post, pre);
                    Long ts = (Long) region.get(KEY1);
                    assertTrue(ts + " >= " + post, ts.longValue() >= post.longValue());
                    sleep(random.nextInt(1));
                }
                catch (AssertionFailedError e) {
                    assertionFailure = e;
                }
                catch (Exception e) {
                    if (!stopped)
                        exception = e;
                }
                finally {
                    stopped = true;
                }
            }
        }
        
        void start() {
            if (stopped) {
                if (thread == null) {
                    thread = new Thread(this);
                    thread.setDaemon(true);
                }
                thread.start();
            }            
        }
        
        void stop() {
            if (!stopped) {
                stopped = true;
                try {
                    thread.join(100);
                }
                catch (InterruptedException ignored) {}
                
                if (thread.isAlive())
                    thread.interrupt();
            }
        }
    }
}
