/*
 * 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 test.compliance.timer;

import junit.framework.TestCase;

import java.util.ArrayList;
import java.util.Date;

import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.Notification;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.management.timer.TimerNotification;

/**
 * Basic timer test.<p>
 *
 * The aim of these tests is to check the most common uses of the timer
 * service.
 *
 * @author  <a href="mailto:Adrian.Brock@HappeningTimes.com">Adrian Brock</a>.
 */
public class TimerTest
  extends TestCase
  implements NotificationListener
{
/**
 * The period for a timer notification. This needs to be small so the tests * don't take too long. * The wait needs to be long enough to be sure the monitor has enough time * to send the notification and switch the context to the handler. */ 
public static final long PERIOD = 100; 
public static final long WAIT = 10000; 
/**
 * The number of repeats for occurances tests */
 public static final long REPEATS = 2; 

  // Attributes ----------------------------------------------------------------

  /**
   * The object name of the timer service
   */
  ObjectName timerName;

  /**
   * The MBean server
   */
  MBeanServer server;

  /**
   * The received notifications
   */
  ArrayList receivedNotifications = new ArrayList();

  // Constructor ---------------------------------------------------------------

  public TimerTest(String s)
  {
    super(s);
  }

  // Tests ---------------------------------------------------------------------

  /**
   * Test a single notification works.
   */
  public void testSingleNotification()
     throws Exception
  {
    try
    {
      startTimerService();

      Integer id = addNotification("test", "hello", "data", calcTime(PERIOD),
                                   0, 1);
      expectNotifications(1);

// TODO
//      if (getNotificationType(id) != null)
//        fail("Single notification still registered");
    }
    finally
    {
      stopTimerService();
    }
  }

  /**
   * Test a repeated notification works.
   */
  public void testRepeatedNotification()
     throws Exception
  {
    try
    {
      startTimerService();
      Integer id = addNotification("test", "hello", "data", calcTime(PERIOD),
                                   PERIOD, REPEATS);
      expectNotifications(1);
      expectNotifications(2);

// TODO
//      if (getNotificationType(id) != null)
//        fail("Repeated notification still registered");
    }
    finally
    {
      stopTimerService();
    }
  }

  /**
   * Test infinite notification works.
   */
  public void testInfiniteNotification()
     throws Exception
  {
    try
    {
      startTimerService();

      Integer id = addNotification("test", "hello", "data", calcTime(PERIOD),
                                   PERIOD, 0);
      expectNotifications(1);
      expectNotifications(2);

      if (getNotificationType(id) == null)
        fail("Infinite notification not registered");
    }
    finally
    {
      stopTimerService();
    }
  }

   /**
    * Test two infinite notification works.
    */
   public void testTwoNotificationProducers()
      throws Exception
   {
      try
      {
         startTimerService();
         long lTimeOne = 5 * 1000;
         long lTimeTwo = 12 * 1000;
         long lWait = 2 * lTimeTwo;
         long lStart = calcTime( lTimeOne );
         Integer lIdOne = addNotification( "test-2", "hello", "data", lStart + lTimeOne,
                                           lTimeOne, 0);
         Integer lIdTwo = addNotification( "test-2", "hello", "data", lStart + lTimeTwo,
                                           lTimeTwo, 0);
         
         expectNotifications( 1, lWait );
         expectNotifications( 2, lWait );
         // Check time differences which should be around TIME ONE
         TimerNotification lNotificationOne = (TimerNotification) receivedNotifications.get( 0 );
         TimerNotification lNotificationTwo = (TimerNotification) receivedNotifications.get( 1 );
         checkNotificationID( lNotificationOne, lIdOne );
         checkNotificationID( lNotificationTwo, lIdOne );
         checkTimeDifference( lNotificationOne, lNotificationTwo, lTimeOne );
         
         expectNotifications( 3, lWait );
         lNotificationOne = lNotificationTwo;
         lNotificationTwo = (TimerNotification) receivedNotifications.get( 2 );
         checkNotificationID( lNotificationTwo, lIdTwo );
         checkTimeDifference( lNotificationOne, lNotificationTwo, ( lTimeTwo - ( 2 * lTimeOne ) ) );
         
         expectNotifications( 4, lWait );
         lNotificationOne = lNotificationTwo;
         lNotificationTwo = (TimerNotification) receivedNotifications.get( 3 );
         checkNotificationID( lNotificationTwo, lIdOne );
         checkTimeDifference( lNotificationOne, lNotificationTwo, ( ( 3 * lTimeOne ) - lTimeTwo ) );
         
         expectNotifications( 5, lWait );
         lNotificationOne = lNotificationTwo;
         lNotificationTwo = (TimerNotification) receivedNotifications.get( 4 );
         checkNotificationID( lNotificationTwo, lIdOne );
         checkTimeDifference( lNotificationOne, lNotificationTwo, lTimeOne );
         
         expectNotifications( 6, lWait );
         lNotificationOne = lNotificationTwo;
         lNotificationTwo = (TimerNotification) receivedNotifications.get( 5 );
         checkNotificationID( lNotificationTwo, lIdTwo );
         checkTimeDifference( lNotificationOne, lNotificationTwo, ( ( 2 * lTimeTwo ) - ( 4 * lTimeOne ) ) );
      }
      finally
      {
         stopTimerService();
      }
   }

  // Support functions ---------------------------------------------------------

  /**
   * Get an MBeanServer, install the timer service and a notification
   * listener.
   */
  private void startTimerService()
    throws Exception
  {
    server = MBeanServerFactory.createMBeanServer("Timer");

    timerName = new ObjectName("Timer:type=TimerService");
    server.createMBean("javax.management.timer.Timer", timerName,
                       new Object[0], new String[0]);
    server.invoke(timerName, "start", new Object[0], new String[0]);

    receivedNotifications.clear();
    server.addNotificationListener(timerName, this, null, null);
  }

  /**
   * Remove everything used by this test. Cannot report failures because
   * the test might have failed earlier. All notifications are removed,
   * the RI hangs otherwise.
   */
  private void stopTimerService()
  {
    try
    {
      server.invoke(timerName, "removeAllNotifications", new Object[0], new String[0]);
      server.invoke(timerName, "stop", new Object[0], new String[0]);
      server.unregisterMBean(timerName);
      MBeanServerFactory.releaseMBeanServer(server);
    }
    catch (Exception ignored) {}
  }

  /**
   * Handle a notification, just add it to the list
   *
   * @param notification the notification received
   * @param handback not used
   */
  public void handleNotification(Notification notification, Object handback)
  {
    synchronized (receivedNotifications)
    {
      receivedNotifications.add(notification);
      receivedNotifications.notifyAll();
    }
  }

  /**
   * Wait for the timer notification and see if we have the correct number
   * hopefully this should synchronize this test with the timer thread.
   *
   * @param expected the number of notifications expected
   * @throws Exception when the notifications are incorrect
   */
  public void expectNotifications(int expected)
    throws Exception
  {
     expectNotifications( expected, WAIT );
  }

  /**
   * Wait for the timer notification and see if we have the correct number
   * hopefully this should synchronize this test with the timer thread.
   *
   * @param expected the number of notifications expected
   * @param wait time in milli seconds to wait for the notification
   * @throws Exception when the notifications are incorrect
   */
  public void expectNotifications(int expected, long wait)
    throws Exception
  {
     synchronized (receivedNotifications)
     {
       if (receivedNotifications.size() > expected)
         fail("too many notifications");
       if (receivedNotifications.size() < expected)
       {
         receivedNotifications.wait( wait );
       }
       assertEquals(expected, receivedNotifications.size());
     }
  }

  /**
   * Checks if the given Notification ID is the same as the
   * one of the given Notification
   *
   * @param pNotification Notification to be tested
   * @param pNotificationID Id the Notification should have
   **/
   public void checkNotificationID( TimerNotification pNotification, Integer pNotificationID ) {
      if( pNotification == null ) {
         fail( "Notification is null" );
      }
      if( !pNotification.getNotificationID().equals( pNotificationID ) ) {
         fail( "Wrong Notification ID received: " + pNotification.getNotificationID() +
            ", expected: " + pNotificationID );
      }
  }
  
  /**
   * Checks if the time between the two Notification is in a
   * +- 10% limit
   *
   * @param pNotificationOne First Notification to be tested
   * @param pNotificationTwo Second Notification to be tested
   * @param pTimeDiffernce Expected Time Difference
   **/
   public void checkTimeDifference(
      TimerNotification pNotificationOne,
      TimerNotification pNotificationTwo,
      long pTimeDiffernce
   ) {
      long lDiff = pNotificationTwo.getTimeStamp() - pNotificationOne.getTimeStamp();
      if( lDiff < ( pTimeDiffernce - ( pTimeDiffernce / 10 ) ) ||
         lDiff > ( pTimeDiffernce + ( pTimeDiffernce / 10 ) )
      ) {
         fail( "Time between first two notification is too small or too big: " + pTimeDiffernce );
      }
   }

  /**
   * Add a timer notification
   *
   * @param type the type of the notification
   * @param message the message
   * @param data the user data
   * @param time the time of the notification
   * @param period the period of notification
   * @param occurs the number of occurances
   * @return the id of the notfication
   */
  private Integer addNotification(String type, String message, String data,
                                  long time, long period, long occurs)
    throws Exception
  {
    return (Integer) server.invoke(timerName, "addNotification",
      new Object[] { type, message, data, new Date(time), new Long(period), 
                     new Long(occurs) },
      new String[] { "java.lang.String", "java.lang.String", "java.lang.Object",
                     "java.util.Date", "long", "long" } );
  }

  /**
   * Get the notification type for an id
   *
   * @param id the id of the notification
   * @return the type of the notification
   */
  private String getNotificationType(Integer id)
    throws Exception
  {
    // This is called after the last expected notification
    // The timer thread has notified us, but hasn't had time
    // to remove the notification, give it chance, before
    // checking for correct behaviour.
    Thread.yield();
    
    return (String) server.invoke(timerName, "getNotificationType",
      new Object[] { id },
      new String[] { "java.lang.Integer" });
  }

  /**
   * Calculate the time using an offset from the current time.
   * @param offset the offset from the current time
   * @return the calculated time
   */
  private long calcTime(long offset)
  {
    return System.currentTimeMillis() + offset;
  }
}
