/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt 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.test.util.test;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;

import org.jboss.util.threadpool.BasicThreadPool;
import org.jboss.util.threadpool.Task;
import org.apache.log4j.Logger;
import junit.framework.TestCase;

/**
 * Tests of thread pool with Tasks added to the pool
 *
 * @see org.jboss.util.threadpool.ThreadPool
 * @author <a href="adrian@jboss.org">Adrian.Brock</a>
 * @author Scott.Stark@jboss.org
 * @version $Revision: 1.2.2.1 $
 */
public class ThreadPoolTaskUnitTestCase extends TestCase
{
   private static Logger log = Logger.getLogger(ThreadPoolTaskUnitTestCase.class);

   /** Basic test */
   static final int BASIC = 0;

   /** Hold the thread after start */
   static final int HOLD_START = 1;

   /** The accepted stats */
   Stats accepted = new Stats("Accepted");

   /** The rejected stats */
   Stats rejected = new Stats("Rejected");

   /** The started stats */
   Stats started = new Stats("Started");

   /** The completed stats */
   Stats completed = new Stats("Completed");

   /** The stopped stats */
   Stats stopped = new Stats("Stopped");

   /** The thread names */
   HashMap threadNames = new HashMap();

   /**
    * Create a new ThreadPoolTaskUnitTestCase
    *
    * @param name the test to run
    */
   public ThreadPoolTaskUnitTestCase(String name)
   {
      super(name);
   }

   protected void setUp() throws Exception
   {
      log.debug("====> Starting test: " + getName());
   }

   protected void tearDown() throws Exception
   {
      log.debug("=====> Stopping test: " + getName());
   }

   /**
    * Basic test
    */
   public void testBasic() throws Exception
   {
      BasicThreadPool pool = new BasicThreadPool();
      try
      {
         pool.runTask(new TestTask(BASIC, "test"));
         completed.wait(1);
         HashSet expected = makeExpected(new Object[] {"test"});
         assertEquals(expected, accepted.tasks);
         assertEquals(expected, started.tasks);
         assertEquals(expected, completed.tasks);
      }
      finally
      {
         pool.stop(true);
      }
   }

   /**
    * Multiple Basic test
    */
   public void testMultipleBasic() throws Exception
   {
      BasicThreadPool pool = new BasicThreadPool();
      try
      {
         pool.runTask(new TestTask(BASIC, "test1"));
         pool.runTask(new TestTask(BASIC, "test2"));
         pool.runTask(new TestTask(BASIC, "test3"));
         completed.wait(3);
         HashSet expected = makeExpected(new Object[] {"test1", "test2", "test3"});
         assertEquals(expected, accepted.tasks);
         assertEquals(expected, started.tasks);
         assertEquals(expected, completed.tasks);
      }
      finally
      {
         pool.stop(true);
      }
   }

   /**
    * Test pooling
    */
   public void testSimplePooling() throws Exception
   {
      BasicThreadPool pool = new BasicThreadPool();
      pool.setMaximumPoolSize(1);
      try
      {
         pool.runTask(new TestTask(BASIC, "test1"));
         completed.wait(1);
         pool.runTask(new TestTask(BASIC, "test2"));
         completed.wait(2);
         assertEquals(threadNames.get("test1"), threadNames.get("test2"));
      }
      finally
      {
         pool.stop(true);
      }
   }

   /**
    * Test multiple pooling
    */
   public void testMultiplePooling() throws Exception
   {
      BasicThreadPool pool = new BasicThreadPool();
      try
      {
         pool.runTask(new TestTask(HOLD_START, "test1"));
         started.wait(1);
         pool.runTask(new TestTask(BASIC, "test2"));
         completed.wait(1);
         started.release("test1");
         completed.wait(2);
         assertTrue("Shouldn't run on the same thread", threadNames.get("test1").equals(threadNames.get("test2")) == false);
      }
      finally
      {
         pool.stop(true);
      }
   }

   /**
    * Test maximum pool
    */
   public void testMaximumPool() throws Exception
   {
      BasicThreadPool pool = new BasicThreadPool();
      pool.setMaximumPoolSize(1);
      try
      {
         pool.runTask(new TestTask(HOLD_START, "test1"));
         started.wait(1);
         pool.runTask(new TestTask(BASIC, "test2"));
         Thread.sleep(1000);
         assertEquals(0, completed.tasks.size());
         started.release("test1");
         completed.wait(2);
         assertEquals(makeExpected(new Object[] {"test1", "test2"}), completed.tasks);
      }
      finally
      {
         pool.stop(true);
      }
   }

   /**
    * Test maximum cache
    */
   public void testMaximumQueue() throws Exception
   {
      BasicThreadPool pool = new BasicThreadPool();
      pool.setMaximumQueueSize(1);
      pool.setMaximumPoolSize(1);
      try
      {
         pool.runTask(new TestTask(HOLD_START, "test1"));
         started.wait(1);
         pool.runTask(new TestTask(BASIC, "test2"));
         assertEquals(0, rejected.tasks.size());
         pool.runTask(new TestTask(BASIC, "test3"));
         assertEquals(makeExpected(new Object[] {"test3"}), rejected.tasks);

         started.release("test1");
         completed.wait(2);
         assertEquals(makeExpected(new Object[] {"test1", "test2"}), completed.tasks);
      }
      finally
      {
         pool.stop(true);
      }
   }

   /**
    * Test maximum cache
    */
   public void testCompleteTimeout() throws Exception
   {
      BasicThreadPool pool = new BasicThreadPool();
      pool.setMaximumQueueSize(1);
      pool.setMaximumPoolSize(1);
      try
      {
         /* Test that a task with a timeout that completes within its timeout
         works as expected
         */
         TestTask task = new TestTask(HOLD_START, "test1", 0, 10*1000, Task.WAIT_NONE);
         pool.runTask(task);
         started.wait(1);
         started.release("test1");
         completed.wait(1);

         /* Test a task with a timeout that does not complete within its timeout
         is stopped
         */
         task = new TestTask(HOLD_START, "test2", 0, 10*1000, Task.WAIT_NONE);
         task.setRunSleepTime(12*1000);
         pool.runTask(task);
         started.wait(1);
         started.release("test2");
         stopped.wait(1);
         completed.wait(1);

         // Test that another valid task completes as expected
         task = new TestTask(HOLD_START, "test3", 0, 0, Task.WAIT_NONE);
         pool.runTask(task);
         started.wait(1);
         started.release("test3");
         completed.wait(1);

         /* Test a task with a timeout that does not complete within its timeout
         is stopped
         */
         task = new TestTask(HOLD_START, "test4", 0, 10*1000, Task.WAIT_NONE);
         task.setRunSleepTime(12*1000);
         pool.runTask(task);
         started.wait(1);
         started.release("test4");
         stopped.wait(1);         
         completed.wait(1);
      }
      finally
      {
         pool.stop(true);
      }
   }

   public void testCompleteTimeoutWithSpinLoop() throws Exception
   {
      BasicThreadPool pool = new BasicThreadPool();
      pool.setMaximumQueueSize(1);
      pool.setMaximumPoolSize(1);
      try
      {
         /* Test that a task with a timeout that completes within its timeout
         works as expected
         */
         TestTask task = new TestTask(HOLD_START, "test1", 0, 10*1000, Task.WAIT_NONE);
         task.setRunSleepTime(Long.MAX_VALUE);
         pool.runTask(task);
         started.wait(1);
         started.release("test1");
         stopped.wait(1);         
         completed.wait(1);
      }
      finally
      {
         pool.stop(true);
      }
   }

   /**
    * Save the thread name
    *
    * @param data the test data
    * @param name the thread name
    */
   public synchronized void saveRunnableThreadName(String data, String name)
   {
      threadNames.put(data, name);
   }

   /**
    * Make the expected result
    *
    * @param expected the results as an object array
    * @return the expected result
    */
   public HashSet makeExpected(Object[] expected)
   {
      return new HashSet(Arrays.asList(expected));
   }

   /**
    * Test task
    */
   public class TestTask implements Task
   {
      /** The test to run */
      private int test;
      /** The data for the test */
      private String data;
      /** The start timeout */
      private long startTimeout;
      /** The completion timeout */
      private long completionTimeout;
      /** The time to sleep in execute */
      private long runSleepTime;
      /** The wait type */
      private int waitType;

      /**
       * Create a new TestTask
       *
       * @param test the test
       * @param data the test data
       */
      public TestTask(int test, String data)
      {
         this(test, data, 0, Task.WAIT_NONE);
      }

      /**
       * Create a new TestTask
       *
       * @param test the test
       * @param data the test data
       * @param startTimeout the start timeout
       * @param waitType the wait type
       */
      public TestTask(int test, String data, long startTimeout, int waitType)
      {
         this(test, data, startTimeout, 0, waitType);
      }
      public TestTask(int test, String data, long startTimeout,
         long completionTimeout, int waitType)
      {
         this.test = test;
         this.data = data;
         this.startTimeout = startTimeout;
         this.completionTimeout = completionTimeout;
         this.waitType = waitType;
      }

      public void execute()
      {
         saveThreadName();
         log.info("Start execute");
         if( runSleepTime > 0 )
         {
            log.info("Begin spin loop");
            if( runSleepTime == Long.MAX_VALUE )
            {
               while( true )
                  ;
            }
            else
            {
               log.info("Begin sleep");
               try
               {
                  Thread.sleep(runSleepTime);
               }
               catch(InterruptedException e)
               {
               }
            }
         }
         log.info("End execute");
      }

      public void saveThreadName()
      {
         saveRunnableThreadName(data, Thread.currentThread().getName());
      }

      public void accepted(long time)
      {
         accepted.notify(data, time);
      }

      public void rejected(long time, Throwable throwable)
      {
         rejected.notify(data, time, throwable);
      }

      public void started(long time)
      {
         started.notify(data, time);
         if (test == HOLD_START)
            started.waitForRelease(data);
      }

      public void completed(long time, Throwable throwable)
      {
         completed.notify(data, time, throwable);
      }

      public long getCompletionTimeout()
      {
         return completionTimeout;
      }

      public int getPriority()
      {
         return Thread.NORM_PRIORITY;
      }

      public long getStartTimeout()
      {
         return startTimeout;
      }

      public int getWaitType()
      {
         return waitType;
      }

      public void stop()
      {
         stopped.notify(data);
      }

      public void setRunSleepTime(long runSleepTime)
      {
         this.runSleepTime = runSleepTime;
      }
   }

   public class Stats
   {
      /**
       * The name
       */
      String name;

      /** The tasks */
      HashSet tasks = new HashSet();

      /** The times */
      HashMap times = new HashMap();

      /** The errors */
      HashMap errors = new HashMap();

      /** The releases */
      HashSet releases = new HashSet();

      public Stats(String name)
      {
         this.name = name;
      }

      /**
       * Wait for expected
       */
      public void wait(int target)
         throws InterruptedException
      {
         log.debug(Thread.currentThread().getName() + ": Waiting for " + name + " target=" + target);
         synchronized (ThreadPoolTaskUnitTestCase.this)
         {
            while (tasks.size() < target)
               ThreadPoolTaskUnitTestCase.this.wait();
            log.debug(Thread.currentThread().getName() + ": Waited for " + name + " target=" + target);
         }
      }

      /**
       * Release in waiting
       *
       * @param data the thread
       */
      public void release(String data)
      {
         log.debug(Thread.currentThread().getName() + ": Releasing " + name + " data=" + data);
         synchronized (ThreadPoolTaskUnitTestCase.this)
         {
            releases.add(data);
            ThreadPoolTaskUnitTestCase.this.notifyAll();
            log.debug(Thread.currentThread().getName() + ": Released " + name + " data=" + data);
         }
      }

      /**
       * Wait for release
       */
      public void waitForRelease(String data)
      {
         log.debug(Thread.currentThread().getName() + ": Waiting for release " + name + " data=" + data);
         synchronized (ThreadPoolTaskUnitTestCase.this)
         {
            try
            {
               while (releases.contains(data) == false)
                  ThreadPoolTaskUnitTestCase.this.wait();
            }
            catch (InterruptedException ignored)
            {
            }
            log.debug(Thread.currentThread().getName() + ": Waited for release " + name + " data=" + data);
         }
      }

      /**
       * Notify
       */
      public void notify(String data)
      {
         log.debug(Thread.currentThread().getName() + ": Notifying " + name + " data=" + data);
         synchronized (ThreadPoolTaskUnitTestCase.this)
         {
            tasks.add(data);
            ThreadPoolTaskUnitTestCase.this.notifyAll();
            log.debug(Thread.currentThread().getName() + ": Notified " + name + " data=" + data);
         }
      }

      /**
       * Notify
       */
      public void notify(String data, long time)
      {
         log.debug(Thread.currentThread().getName() + ": Notifying " + name + " data=" + data + " time=" + time);
         synchronized (ThreadPoolTaskUnitTestCase.this)
         {
            tasks.add(data);
            times.put(data, new Long(time));
            ThreadPoolTaskUnitTestCase.this.notifyAll();
         }
         log.debug(Thread.currentThread().getName() + ": Notified " + name + " data=" + data + " time=" + time);
      }

      /**
       * Notify
       */
      public void notify(String data, long time, Throwable throwable)
      {
         if (throwable != null)
            log.debug(Thread.currentThread().getName() + ": Notifying " + name + " data=" + data + " time=" + time, throwable);
         else
            log.debug(Thread.currentThread().getName() + ": Notifying " + name + " data=" + data + " time=" + time + " throwable=null");
         synchronized (ThreadPoolTaskUnitTestCase.this)
         {
            tasks.add(data);
            times.put(data, new Long(time));
            errors.put(data, throwable);
            ThreadPoolTaskUnitTestCase.this.notifyAll();
         }
         if (throwable != null)
            log.debug(Thread.currentThread().getName() + ": Notified " + name + " data=" + data + " time=" + time + " throwable=" + throwable.getMessage());
         else
            log.debug(Thread.currentThread().getName() + ": Notified " + name + " data=" + data + " time=" + time + " throwable=null");
      }

      /**
       * Clear
       */
      public void clear()
      {
         log.debug(Thread.currentThread().getName() + ": Clearing " + name);
         synchronized (ThreadPoolTaskUnitTestCase.this)
         {
            tasks.clear();
            log.debug(Thread.currentThread().getName() + ": Cleared " + name);
         }
      }
   }
}
