/*
 * Copyright (C) The MX4J Contributors.
 * All rights reserved.
 *
 * This software is distributed under the terms of the MX4J License version 1.0.
 * See the terms of the MX4J License in the documentation provided with this software.
 */

package test.javax.management.timer;

import java.util.Date;
import java.util.Vector;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanServer;
import javax.management.MBeanServerInvocationHandler;
import javax.management.Notification;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.management.timer.Timer;
import javax.management.timer.TimerMBean;
import javax.management.timer.TimerNotification;

import test.MX4JTestCase;
import test.MutableBoolean;
import test.MutableInteger;
import test.MutableLong;

/**
 * @version $Revision: 1.13 $
 */
public class TimerTest extends MX4JTestCase
{
   private MBeanServer m_server;
   private ObjectName m_timerName;
   private TimerMBean m_timer;

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

   protected void setUp() throws Exception
   {
      m_server = newMBeanServer();
      m_timerName = new ObjectName("Service:type=Timer");
      m_server.createMBean("javax.management.timer.Timer", m_timerName, null);
      m_timer = (TimerMBean)MBeanServerInvocationHandler.newProxyInstance(m_server, m_timerName, TimerMBean.class, false);
   }

   protected void tearDown() throws Exception
   {
      m_server.unregisterMBean(m_timerName);
   }

   public void testStartStop() throws Exception
   {
      m_timer.start();
      assertTrue(m_timer.isActive());
      m_timer.stop();
      assertFalse(m_timer.isActive());
   }

   public void testStartStopStart() throws Exception
   {
      m_timer.start();
      assertTrue(m_timer.isActive());
      m_timer.stop();
      assertFalse(m_timer.isActive());
      m_timer.start();
      assertTrue(m_timer.isActive());
      // Will be stopped during unregistration
   }

   public void testOneShotNotification() throws Exception
   {
      m_timer.start();

      final long now = System.currentTimeMillis();
      final MutableInteger mid = new MutableInteger(-1);
      final MutableInteger occurrencesCount = new MutableInteger(0);

      final String notifType = "timer-test";
      final long delay = 3 * Timer.ONE_SECOND;

      NotificationListener listener = new NotificationListener()
      {
         public void handleNotification(Notification notification, Object handback)
         {
            // Test that the listener has been called at the specified time
            long elapsed = System.currentTimeMillis() - now;
            assertTrue(elapsed >= delay);
            assertFalse(elapsed - delay > 50);

            assertTrue(notification instanceof TimerNotification);

            Integer id = ((TimerNotification)notification).getNotificationID();
            assertEquals(mid.get(), id.intValue());

            occurrencesCount.set(occurrencesCount.get() + 1);
         }
      };

      m_server.addNotificationListener(m_timerName, listener, new NotificationFilter()
      {
         public boolean isNotificationEnabled(Notification notification)
         {
            return notification.getType().equals(notifType);
         }
      }, null);

      // Notify after a while
      Date date = new Date(now + delay);
      // One shot notification at the specified time
      Integer id = m_timer.addNotification(notifType, "timer-message", "user-data", date);
      mid.set(id.intValue());

      // Sleep to wait for the notification to happen
      sleep(delay * 2);

      // Check notification arrived
      assertTrue(occurrencesCount.get() == 1);

      // Check that it won't be notified again
      assertTrue(m_timer.getNbNotifications() == 0);
   }

   public void testPeriodicNotification() throws Exception
   {
      m_timer.start();

      final String notifType = "timer-test";
      final String periodicNotifType = "timer-test-periodic";

      final MutableInteger occurrencesCount = new MutableInteger(0);

      NotificationListener listener = new NotificationListener()
      {
         public void handleNotification(Notification notification, Object handback)
         {
            occurrencesCount.set(occurrencesCount.get() + 1);
         }
      };

      final MutableInteger periodicOccurrences = new MutableInteger(0);
      NotificationListener periodicListener = new NotificationListener()
      {
         public void handleNotification(Notification notification, Object handback)
         {
            periodicOccurrences.set(periodicOccurrences.get() + 1);
         }
      };

      m_server.addNotificationListener(m_timerName, listener, new NotificationFilter()
      {
         public boolean isNotificationEnabled(Notification notification)
         {
            return notification.getType().equals(notifType);
         }
      }, null);
      m_server.addNotificationListener(m_timerName, periodicListener, new NotificationFilter()
      {
         public boolean isNotificationEnabled(Notification notification)
         {
            return notification.getType().equals(periodicNotifType);
         }
      }, null);

      // Register to happen 3 times on the first listener
      long now = System.currentTimeMillis();
      // Notify in one second
      Date date = new Date(now + Timer.ONE_SECOND);
      String message = "timer-message";
      Integer id = m_timer.addNotification(notifType, message, "user-data", date, Timer.ONE_SECOND, 3L);

      // Register to happen periodically
      // Notify in one second
      date = new Date(now + Timer.ONE_SECOND);
      String userDataPeriodic = "user-data-periodic";
      Integer periodicID = m_timer.addNotification(periodicNotifType, "timer-message-periodic", userDataPeriodic, date, Timer.ONE_SECOND);

      // Sleep some time
      sleep(Timer.ONE_SECOND);

      Vector v = m_timer.getAllNotificationIDs();
      assertEquals(v.size(), 2);
      assertTrue(v.contains(id));
      assertTrue(v.contains(periodicID));

      v = m_timer.getNotificationIDs(periodicNotifType);
      assertEquals(v.size(), 1);
      assertTrue(v.contains(periodicID));

      assertEquals(m_timer.getNotificationMessage(id), message);

      assertEquals(m_timer.getNotificationUserData(periodicID), userDataPeriodic);

      // Sleep till the end of the three-time notification
      sleep(Timer.ONE_SECOND * 6);

      // Check that was called the right number of times
      assertEquals(occurrencesCount.get(), 3);

      // The three-time notification is expired now
      v = m_timer.getAllNotificationIDs();
      assertEquals(v.size(), 1);
      assertTrue(v.contains(periodicID));

      Long p = m_timer.getPeriod(periodicID);
      assertEquals(p.longValue(), Timer.ONE_SECOND);

      assertEquals(m_timer.getNotificationType(periodicID), periodicNotifType);

      // Removing non existing notification
      try
      {
         m_timer.removeNotifications("dummy");
         fail("Removed non-existing notification");
      }
      catch (InstanceNotFoundException ignored)
      {
      }

      // Should have already been removed, was the three-shot notification
      try
      {
         m_timer.removeNotification(id);
         fail("Removed non-existing notification");
      }
      catch (InstanceNotFoundException ignored)
      {
      }

      // Some more wait
      sleep(Timer.ONE_SECOND * 3);

      // Removing existing notification
      m_timer.removeNotification(periodicID);

      // Check that none are still present
      assertTrue(m_timer.isEmpty());

      // Wait some more to be sure the periodic listener is not notified anymore
      int periodTimes = periodicOccurrences.get();
      assertTrue(periodTimes > 0);

      sleep(Timer.ONE_SECOND * 5);

      assertEquals(periodicOccurrences.get(), periodTimes);
   }

   public void testTimerNotStarted() throws Exception
   {
      // Don't start the Timer. Notification should not be emitted
//      m_timer.start();

      final MutableBoolean bool = new MutableBoolean(false);
      NotificationListener listener = new NotificationListener()
      {
         public void handleNotification(Notification notification, Object handback)
         {
            bool.set(true);
         }
      };
      m_server.addNotificationListener(m_timerName, listener, null, null);

      long now = System.currentTimeMillis();
      m_timer.addNotification("timer-notif", "Must not be emitted", null, new Date(now + Timer.ONE_SECOND));

      // Sleep to wait for the notification to happen
      sleep(Timer.ONE_SECOND * 2);

      assertFalse(bool.get());
   }

   public void testAddStopRemoveNotification() throws Exception
   {
      // Check that add + stop + remove behaves correctly

      final MutableInteger count = new MutableInteger(0);
      NotificationListener listener = new NotificationListener()
      {
         public void handleNotification(Notification notification, Object handback)
         {
            count.set(count.get() + 1);
         }
      };
      m_server.addNotificationListener(m_timerName, listener, null, null);

      long now = System.currentTimeMillis();
      Date date = new Date(now + Timer.ONE_SECOND);

      // Periodic notification
      Integer id = m_timer.addNotification("notif-type", "notif-message", "notif-data", date, Timer.ONE_SECOND);
      m_timer.start();

      // Wait for the notifications to arrive...
      sleep(Timer.ONE_SECOND * 2);

      m_timer.stop();

      int counted = count.get();

      assertEquals(m_timer.getNbNotifications(), 1);

      m_timer.removeNotification(id);
      assertTrue(m_timer.isEmpty());

      // Wait some more to be sure that there are no more notifications
      Thread.sleep(Timer.ONE_SECOND * 5);

      assertEquals(counted, count.get());
   }

   public void testSendPastNotifications1() throws Exception
   {
      final MutableBoolean bool = new MutableBoolean(false);
      NotificationListener listener = new NotificationListener()
      {
         public void handleNotification(Notification notification, Object handback)
         {
            bool.set(true);
         }
      };
      m_server.addNotificationListener(m_timerName, listener, null, null);

      long now = System.currentTimeMillis();

      // This one-shot notification is already passed, sendPastNotifications is false
      // so the notification must not be emitted
      Date date = new Date(now - Timer.ONE_SECOND);
      m_timer.setSendPastNotifications(false);
      m_timer.addNotification("notif-type", "notif-message", "notif-data", date);
      m_timer.start();

      // Wait that the notification arrives
      sleep(Timer.ONE_SECOND);

      assertFalse(bool.get());
      assertTrue(m_timer.isEmpty());
   }

   public void testNotificationsWithOldDate() throws Exception
   {
      final MutableBoolean bool = new MutableBoolean(false);
      NotificationListener listener = new NotificationListener()
      {
         public void handleNotification(Notification notification, Object handback)
         {
            bool.set(true);
         }
      };
      m_server.addNotificationListener(m_timerName, listener, null, null);

      long now = System.currentTimeMillis();

      m_timer.start();
      Date date = new Date(now - Timer.ONE_SECOND);
      m_timer.addNotification("notif-type", "notif-message", "notif-data", date);

      // Wait that the notification arrives
      sleep(Timer.ONE_SECOND);

      assertTrue(bool.get());
      assertTrue(m_timer.isEmpty());
   }

   public void testSendPastNotifications2() throws Exception
   {
      final MutableBoolean bool = new MutableBoolean(false);
      NotificationListener listener = new NotificationListener()
      {
         public void handleNotification(Notification notification, Object handback)
         {
            bool.set(true);
         }
      };
      m_server.addNotificationListener(m_timerName, listener, null, null);

      long now = System.currentTimeMillis();

      // This one-shot notification is already passed, sendPastNotifications is true
      // so the notification must be emitted
      Date date = new Date(now - Timer.ONE_SECOND);
      m_timer.setSendPastNotifications(true);
      m_timer.addNotification("notif-type", "notif-message", "notif-data", date);
      m_timer.start();

      // Wait that the notification arrives
      sleep(Timer.ONE_SECOND);

      assertTrue(bool.get());
      assertTrue(m_timer.isEmpty());
   }

   public void testSendPastNotifications3() throws Exception
   {
      final MutableInteger count = new MutableInteger(0);
      NotificationListener listener = new NotificationListener()
      {
         public void handleNotification(Notification notification, Object handback)
         {
            count.set(count.get() + 1);
         }
      };
      m_server.addNotificationListener(m_timerName, listener, null, null);

      long now = System.currentTimeMillis();

      // This periodic notification started in the past, sendPastNotifications is false
      // so only some notification must be emitted
      long occurrences = 10;
      long skip = 4;
      Date date = new Date(now - Timer.ONE_SECOND * skip);
      m_timer.setSendPastNotifications(false);
      m_timer.addNotification("notif-type", "notif-message", "notif-data", date, Timer.ONE_SECOND, occurrences);
      m_timer.start();

      // Wait for the notifications to happen
      sleep(Timer.ONE_SECOND * (occurrences + 1));

      // Sometimes we loose one notification because we're not that fast, it's ok.
      long expected = occurrences - skip;
      if (count.get() != expected && count.get() != expected - 1)
         fail("Expected notifications not emitted: expecting " + expected + " got " + count.get());
      assertTrue(m_timer.isEmpty());
   }

   public void testSendPastNotifications4() throws Exception
   {
      final MutableInteger count = new MutableInteger(0);
      NotificationListener listener = new NotificationListener()
      {
         public void handleNotification(Notification notification, Object handback)
         {
            count.set(count.get() + 1);
         }
      };
      m_server.addNotificationListener(m_timerName, listener, null, null);

      long now = System.currentTimeMillis();

      // This periodic notification started in the past, sendPastNotifications is true
      // so all notifications must be emitted
      long occurrences = 10;
      long skip = 4;
      Date date = new Date(now - Timer.ONE_SECOND * skip);
      m_timer.setSendPastNotifications(true);
      m_timer.addNotification("notif-type", "notif-message", "notif-data", date, Timer.ONE_SECOND, occurrences);
      m_timer.start();

      // Wait for the notifications to happen
      sleep(Timer.ONE_SECOND * (occurrences + 1));

      assertEquals(count.get(), occurrences);
      assertTrue(m_timer.isEmpty());
   }

   public void testSendPastNotifications5() throws Exception
   {
      final MutableInteger count = new MutableInteger(0);
      NotificationListener listener = new NotificationListener()
      {
         public void handleNotification(Notification notification, Object handback)
         {
            count.set(count.get() + 1);
         }
      };
      m_server.addNotificationListener(m_timerName, listener, null, null);

      long now = System.currentTimeMillis();

      // This periodic notification is started, sendPastNotifications is false
      // the Timer is started, then stopped, then restarted
      long occurrences = 10;
      long pre = 2;
      long skip = 4;
      Date date = new Date(now + Timer.ONE_SECOND);
      m_timer.setSendPastNotifications(false);
      m_timer.addNotification("notif-type", "notif-message", "notif-data", date, Timer.ONE_SECOND, occurrences);
      m_timer.start();

      // Wait for the notifications to happen
      sleep(Timer.ONE_SECOND * pre);
      m_timer.stop();

      // Sometimes we loose one notification because we're not that fast, it's ok.
      if (count.get() != pre && count.get() != pre - 1)
         fail("Expected notifications not emitted: expecting " + pre + " got " + count.get());
      assertEquals(m_timer.getNbNotifications(), 1);

      // Wait to skip some notification
      sleep(Timer.ONE_SECOND * skip);

      // Restart the Timer
      m_timer.start();

      // Wait for the remaining notifications to happen
      sleep(Timer.ONE_SECOND * (occurrences - pre - skip + 1));

      m_timer.stop();

      // Sometimes we loose one notification because we're not that fast, it's ok.
      long expected = occurrences - skip;
      if (count.get() != expected && count.get() != expected - 1)
         fail("Expected notifications not emitted.  Expected " + expected + " or " + (expected - 1) + ". got " + count.get());
      assertTrue(m_timer.isEmpty());
   }

   public void testSendPastNotifications6() throws Exception
   {
      final MutableInteger count = new MutableInteger(0);
      NotificationListener listener = new NotificationListener()
      {
         public void handleNotification(Notification notification, Object handback)
         {
            count.set(count.get() + 1);
         }
      };
      m_server.addNotificationListener(m_timerName, listener, null, null);

      long now = System.currentTimeMillis();

      // This periodic notification is started, sendPastNotifications is true
      // the Timer is started, then stopped, then restarted
      long occurrences = 10;
      long pre = 2;
      long skip = 4;
      Date date = new Date(now + Timer.ONE_SECOND);
      m_timer.setSendPastNotifications(true);
      m_timer.addNotification("notif-type", "notif-message", "notif-data", date, Timer.ONE_SECOND, occurrences, true);
      m_timer.start();

      // Wait for the notifications to happen
      sleep(Timer.ONE_SECOND * pre);
      m_timer.stop();

      // Sometimes we loose one notification because we're not that fast, it's ok.
      if (count.get() != pre && count.get() != pre - 1)
         fail("Expected notifications not emitted: expecting " + pre + " got " + count.get());
      assertFalse(m_timer.isEmpty());

      // Wait to skip some notification
      sleep(Timer.ONE_SECOND * skip);

      // Restart the Timer
      m_timer.start();

      // Wait for the remaining notifications to happen
      sleep(Timer.ONE_SECOND * (occurrences - pre - skip + 1));

      m_timer.stop();
      assertEquals(count.get(), occurrences);
      assertTrue(m_timer.isEmpty());
   }

   public void testFixedDelay() throws Exception
   {
      m_timer.start();

      final int occurrences = 100;
      final String fdNotifType = "timer-test-fixed-delay";
      final String frNotifType = "timer-test-fixed-rate";

      final MutableInteger frOccurrences = new MutableInteger(0);
      final MutableLong frElapsedTime = new MutableLong(0);
      final MutableLong frLastTime = new MutableLong(System.currentTimeMillis());

      NotificationListener frListener = new NotificationListener()
      {
         public void handleNotification(Notification notification, Object handback)
         {
            if (frOccurrences.get() < occurrences)
            {
               long now = System.currentTimeMillis();
               frElapsedTime.set(frElapsedTime.get() + (now - frLastTime.get()));
               frLastTime.set(now);
               frOccurrences.set(frOccurrences.get() + 1);
            }
         }
      };

      final MutableInteger fdOccurrences = new MutableInteger(0);
      final MutableLong fdElapsedTime = new MutableLong(0);
      final MutableLong fdLastTime = new MutableLong(System.currentTimeMillis());

      NotificationListener fdListener = new NotificationListener()
      {
         public void handleNotification(Notification notification, Object handback)
         {
            if (fdOccurrences.get() < occurrences)
            {
               long now = System.currentTimeMillis();
               fdElapsedTime.set(fdElapsedTime.get() + (now - fdLastTime.get()));
               fdLastTime.set(now);
               fdOccurrences.set(fdOccurrences.get() + 1);
            }
         }
      };

      m_server.addNotificationListener(m_timerName, fdListener, new NotificationFilter()
      {
         public boolean isNotificationEnabled(Notification notification)
         {
            return notification.getType().equals(fdNotifType);
         }
      }, null);

      m_server.addNotificationListener(m_timerName, frListener, new NotificationFilter()
      {
         public boolean isNotificationEnabled(Notification notification)
         {
            return notification.getType().equals(frNotifType);
         }
      }, null);

      // Testing fixed delay/fixed rate
      long now = System.currentTimeMillis();
      // Notify in one second
      Date date = new Date(now + Timer.ONE_SECOND);
      // Register to happen 10 times
      m_timer.addNotification(fdNotifType, "timer-message", "user-data", date, 10, occurrences, false);
      m_timer.addNotification(frNotifType, "timer-message", "user-data", date, 10, occurrences, true);

      // Sleep some time
      while (frOccurrences.get() < occurrences || fdOccurrences.get() < occurrences)
      {
         sleep(10);
         System.gc();
      }

      assertEquals(frOccurrences.get(), occurrences);
      assertEquals(fdOccurrences.get(), occurrences);

      if (((1.0f * frElapsedTime.get()) / fdElapsedTime.get()) > 0.95)
         fail("Fixed rate and fixed delay exhibit no execution rate differences");
   }

   public void testRemoveNotifications() throws Exception
   {
      m_timer.addNotification("mx4j.timer.test", "test notification", null, new Date(System.currentTimeMillis()), 8);
      m_timer.addNotification("mx4j.timer.ignore", "ignore me", null, new Date(System.currentTimeMillis()), 4);
      m_timer.addNotification("mx4j.timer.test", "another test", null, new Date(System.currentTimeMillis()), 8);
      assertEquals(m_timer.getNbNotifications(), 3);
      m_timer.start();
      m_timer.removeNotifications("mx4j.timer.ignore");
      assertEquals(m_timer.getNbNotifications(), 2);
   }

   public void testRemoveNonexistentNotifications() throws Exception
   {
      m_timer.addNotification("mx4j.timer.test", "test notification", null, new Date(System.currentTimeMillis()), 4);
      m_timer.start();
      try
      {
         m_timer.removeNotifications("mx4j.timer.bogus");
         fail("Expecting InstanceNotFoundException");
      }
      catch (InstanceNotFoundException x)
      {
      }
   }
}
