/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, 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.test.cluster.testutil;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.management.MBeanServerConnection;
import javax.servlet.ServletException;

import org.apache.catalina.Context;
import org.apache.catalina.Manager;
import org.apache.catalina.Pipeline;
import org.apache.catalina.Valve;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.commons.httpclient.Cookie;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpState;
import org.jboss.logging.Logger;
import org.jboss.metadata.javaee.spec.EmptyMetaData;
import org.jboss.metadata.web.jboss.JBossWebMetaData;
import org.jboss.metadata.web.jboss.PassivationConfig;
import org.jboss.metadata.web.jboss.ReplicationConfig;
import org.jboss.metadata.web.jboss.ReplicationGranularity;
import org.jboss.metadata.web.jboss.ReplicationTrigger;
import org.jboss.metadata.web.jboss.SnapshotMode;
import org.jboss.test.cluster.web.CacheHelper;
import org.jboss.test.cluster.web.mocks.BasicRequestHandler;
import org.jboss.test.cluster.web.mocks.MockEngine;
import org.jboss.test.cluster.web.mocks.MockHost;
import org.jboss.test.cluster.web.mocks.MockRequest;
import org.jboss.test.cluster.web.mocks.MockValve;
import org.jboss.test.cluster.web.mocks.RequestHandler;
import org.jboss.test.cluster.web.mocks.RequestHandlerValve;
import org.jboss.web.tomcat.service.session.JBossCacheManager;
import org.jboss.web.tomcat.service.session.distributedcache.spi.ClusteringNotSupportedException;
import org.jboss.web.tomcat.service.session.distributedcache.spi.DistributedCacheManagerFactory;
import org.jboss.web.tomcat.service.session.distributedcache.spi.TestDistributedCacheManagerFactory;
import org.jboss.web.tomcat.service.session.distributedcache.spi.TestDistributedCacheManagerFactoryFactory;

/**
 * Utilities for session testing.
 * 
 * @author <a href="mailto://brian.stansberry@jboss.com">Brian Stansberry</a>
 * @version $Revision: 104407 $
 */
public class SessionTestUtil
{      
   public static final String CACHE_CONFIG_PROP = "jbosstest.cluster.web.cache.config";
   public static final String CACHE_TYPE_PROP = "jbosstest.cluster.web.cache.pojo";
   
   private static final Logger log = Logger.getLogger(SessionTestUtil.class);
   private static final String[] STRING_ONLY_TYPES = { String.class.getName() };
   private static final String[] STRING_BOOLEAN_TYPES = { String.class.getName(), boolean.class.getName() };
   
   public static JBossCacheManager createManager(String warName, int maxInactiveInterval, 
                                                 boolean local, String passivationDir, 
                                                 boolean totalReplication, boolean marshalling, 
                                                 String jvmRoute, Set<TestDistributedCacheManagerFactory> allFactories)
   {
      return createManager(warName, maxInactiveInterval, local, passivationDir, 
                           totalReplication, marshalling, false, jvmRoute, allFactories);
   }
   
   public static JBossCacheManager createManager(String warName, int maxInactiveInterval, 
                                                 boolean local, String passivationDir, 
                                                 boolean totalReplication, boolean marshalling,
                                                 boolean purgeCacheLoader,
                                                 String jvmRoute, Set<TestDistributedCacheManagerFactory> allFactories)
   {
      TestDistributedCacheManagerFactory factory = createTestDistributedCacheManagerFactory(local, passivationDir, totalReplication, marshalling, purgeCacheLoader, allFactories);
      return createManager(warName, maxInactiveInterval, factory, jvmRoute);
   }
   
   public static JBossCacheManager createManager(String warName, 
                                                 int maxInactiveInterval, 
                                                 DistributedCacheManagerFactory factory, 
                                                 String jvmRoute)
   {
      if (factory == null)
         throw new IllegalStateException("Failed to initialize distributedManagerFactory");
      
      JBossCacheManager jbcm = new JBossCacheManager(factory);
      jbcm.setSnapshotMode(SnapshotMode.INSTANT);
      
      setupContainer(warName, jvmRoute, jbcm);
      
      // Do this after assigning the manager to the container, or else
      // the container's setting will override ours
      // Can't just set the container as their config is per minute not per second
      jbcm.setMaxInactiveInterval(maxInactiveInterval);
   
      return jbcm;      
   }
   
   public static TestDistributedCacheManagerFactory createTestDistributedCacheManagerFactory(boolean local, String passivationDir, 
         boolean totalReplication, boolean marshalling, boolean purgeCacheLoader, Set<TestDistributedCacheManagerFactory> allFactories)
   {

      try
      {
         TestDistributedCacheManagerFactory factory = new TestDistributedCacheManagerFactoryFactory().getDistributedCacheManagerFactory(local, passivationDir, totalReplication, marshalling, purgeCacheLoader);
         if (allFactories != null)
         {
            allFactories.add(factory);
         }
         return factory;
      }
      catch (ClusteringNotSupportedException e)
      {
         throw new RuntimeException(e);
      }
   }

   public static void setupContainer(String warName, String jvmRoute, Manager mgr)
   {
      MockEngine engine = new MockEngine();
      engine.setJvmRoute(jvmRoute);
      MockHost host = new MockHost();
      host.setName("localhost");
      engine.addChild(host);
      StandardContext container = new StandardContext();
      container.setName(warName);
      host.addChild(container);
      container.setManager(mgr);
   }
   
   public static JBossWebMetaData createWebMetaData(int maxSessions)
   {
      return createWebMetaData(ReplicationGranularity.SESSION,
                               ReplicationTrigger.SET_AND_NON_PRIMITIVE_GET,
                               maxSessions, false, -1, -1, false, 0);
   } 
   
   public static JBossWebMetaData createWebMetaData(int maxSessions, boolean passivation,
         int maxIdle, int minIdle)
   {
      return createWebMetaData(ReplicationGranularity.SESSION,
                               ReplicationTrigger.SET_AND_NON_PRIMITIVE_GET,
                               maxSessions, passivation, maxIdle, minIdle, false, 60);
   } 
   
   public static JBossWebMetaData createWebMetaData(ReplicationGranularity granularity,
                                                    ReplicationTrigger trigger,boolean batchMode,
                                                    int maxUnreplicated)
   {
      return createWebMetaData(granularity, trigger, -1, false, 
                               -1, -1, batchMode, maxUnreplicated);
   } 
   
   public static JBossWebMetaData createWebMetaData(ReplicationGranularity granularity,
                                              ReplicationTrigger trigger,
                                              int maxSessions, boolean passivation,
                                              int maxIdle, int minIdle,
                                              boolean batchMode,
                                              int maxUnreplicated)
   {
      JBossWebMetaData webMetaData = new JBossWebMetaData();
      webMetaData.setDistributable(new EmptyMetaData());
      webMetaData.setMaxActiveSessions(new Integer(maxSessions));
      PassivationConfig pcfg = new PassivationConfig();
      pcfg.setUseSessionPassivation(Boolean.valueOf(passivation));
      pcfg.setPassivationMaxIdleTime(new Integer(maxIdle));
      pcfg.setPassivationMinIdleTime(new Integer(minIdle));
      webMetaData.setPassivationConfig(pcfg);
      ReplicationConfig repCfg = new ReplicationConfig();
      repCfg.setReplicationGranularity(granularity);
      repCfg.setReplicationTrigger(trigger);
      repCfg.setReplicationFieldBatchMode(Boolean.valueOf(batchMode));
      repCfg.setMaxUnreplicatedInterval(Integer.valueOf(maxUnreplicated));
      repCfg.setSnapshotMode(SnapshotMode.INSTANT);
      webMetaData.setReplicationConfig(repCfg);
      
      return webMetaData;
   }
   
   public static void invokeRequest(Manager manager, RequestHandler handler, String sessionId)
      throws ServletException, IOException
   {
      Valve valve = setupPipeline(manager, handler);
      Request request = setupRequest(manager, sessionId);
      invokeRequest(valve, request);
   }
   
   public static void invokeRequest(Valve pipelineHead, Request request)
      throws ServletException, IOException
   {
      pipelineHead.invoke(request, request.getResponse());
      // StandardHostValve calls request.getSession(false) on way out, so we will too
      request.getSession(false);
      request.recycle();
   }
   
   public static Valve setupPipeline(Manager manager, RequestHandler requestHandler)
   {
      Pipeline pipeline = manager.getContainer().getPipeline();
      
      // Clean out any existing request handler
      Valve[] valves = pipeline.getValves();
      RequestHandlerValve mockValve = null;
      for (Valve valve: valves)
      {
         if (valve instanceof RequestHandlerValve)         
         {
            mockValve = (RequestHandlerValve) valve;
            break;
         }
      }
      
      if (mockValve == null)
      {
         mockValve = new RequestHandlerValve(requestHandler);
         pipeline.addValve(mockValve);
      }
      else
      {
         mockValve.setRequestHandler(requestHandler);
      }
      
      return pipeline.getFirst();
   }
   
   public static Request setupRequest(Manager manager, String sessionId)
   {
      MockRequest request = new MockRequest();
      request.setRequestedSessionId(sessionId);
      request.setContext((Context) manager.getContainer());
      Response response = new Response();
      request.setResponse(response);
      return request;
      
   }
   
   public static void cleanupPipeline(Manager manager)
   {
      Pipeline pipeline = manager.getContainer().getPipeline();
      
      Valve[] valves = pipeline.getValves();
      for (Valve valve: valves)
      {
         if (valve instanceof MockValve)         
         {
            ((MockValve) valve).clear();
         }
      }
   }
   
   public static Object getSessionVersion(MBeanServerConnection adaptor, String sessionFqn) throws Exception
   {
      return adaptor.invoke(CacheHelper.OBJECT_NAME, 
                            "getSessionVersion", 
                            new Object[] { sessionFqn }, 
                            STRING_ONLY_TYPES);
   }

   public static Object getBuddySessionVersion(MBeanServerConnection adaptor, String sessionFqn) throws Exception
   {

      return adaptor.invoke(CacheHelper.OBJECT_NAME, 
                            "getBuddySessionVersion", 
                            new Object[] { sessionFqn }, 
                            STRING_ONLY_TYPES);
   }
   
   public static void setCacheConfigName(MBeanServerConnection adaptor, String cacheConfigName) throws Exception
   {
      adaptor.invoke(CacheHelper.OBJECT_NAME, 
                     "setCacheConfigName", 
                     new Object[] { cacheConfigName }, 
                     new String[]{ String.class.getName() });
   }
   
   @SuppressWarnings("unchecked")
   public static Set<String> getSessionIds(MBeanServerConnection adaptor, String warFqn) throws Exception
   {
      return (Set<String>) adaptor.invoke(CacheHelper.OBJECT_NAME, 
                           "getSessionIds", 
                           new Object[] { warFqn }, 
                           STRING_ONLY_TYPES);
   }
   
   @SuppressWarnings("unchecked")
   public static Set<String> getSessionIds(MBeanServerConnection adaptor, String warFqn, boolean includeBuddies) throws Exception
   {
      return (Set<String>) adaptor.invoke(CacheHelper.OBJECT_NAME, 
                           "getSessionIds", 
                           new Object[] { warFqn, Boolean.valueOf(includeBuddies) }, 
                           STRING_BOOLEAN_TYPES);
   }
   
   @SuppressWarnings("unchecked")
   public static Set<String> releaseCache(MBeanServerConnection adaptor) throws Exception
   {
      return (Set<String>) adaptor.invoke(CacheHelper.OBJECT_NAME, 
                           "releaseCache", 
                           new Object[0], 
                           new String[0]);
   }
   
   public static boolean isBuddyReplication() throws Exception
   {
      return Boolean.parseBoolean(System.getProperty("jbosstest.cluster.web.cache.br", "false"));
   }
   
   public static String getSessionFqn(String contextPath, String sessionId)
   {
      return "/JSESSION/" +  SessionTestUtil.getContextHostPath("localhost", contextPath) + "/" + sessionId;
   }
   
   public static String getContextHostPath(String hostname, String contextPath)
   {
      return contextPath + "_" + hostname;
   }


   /**
    * Loops, continually calling {@link #areCacheViewsComplete(TestDistributedCacheManagerFactory[])}
    * until it either returns true or <code>timeout</code> ms have elapsed.
    *
    * @param dcmFactories  caches which must all have consistent views
    * @param timeout max number of ms to loop
    * @throws RuntimeException if <code>timeout</code> ms have elapse without
    *                          all caches having the same number of members.
    */
   public static void blockUntilViewsReceived(TestDistributedCacheManagerFactory[] dcmFactories, long timeout)
   {
      long failTime = System.currentTimeMillis() + timeout;

      while (System.currentTimeMillis() < failTime)
      {
         sleepThread(100);
         if (areCacheViewsComplete(dcmFactories))
         {
            return;
         }
      }

      throw new RuntimeException("timed out before caches had complete views");
   }

   /**
    * Checks each cache to see if the number of elements in the array
    * returned by {@link TestDistributedCacheFactory#getMembers()} matches the size of
    * the <code>dcmFactories</code> parameter.
    *
    * @param dcmFactories factory whose caches should form a View
    * @return <code>true</code> if all caches have
    *         <code>factories.length</code> members; false otherwise
    * @throws IllegalStateException if any of the caches have MORE view
    *                               members than factories.length
    */
   public static boolean areCacheViewsComplete(TestDistributedCacheManagerFactory[] dcmFactories)
   {
      return areCacheViewsComplete(dcmFactories, true);
   }

   public static boolean areCacheViewsComplete(TestDistributedCacheManagerFactory[] dcmFactories, boolean barfIfTooManyMembers)
   {
      int memberCount = dcmFactories.length;

      for (int i = 0; i < memberCount; i++)
      {
         if (!isCacheViewComplete(dcmFactories[i], memberCount, barfIfTooManyMembers))
         {
            return false;
         }
      }

      return true;
   }
   
   public static boolean isCacheViewComplete(TestDistributedCacheManagerFactory factory, int memberCount, boolean barfIfTooManyMembers)
   {
      List<Object> members = factory.getMembers();
      if (members == null || memberCount > members.size())
      {
         return false;
      }
      else if (memberCount < members.size())
      {
         if (barfIfTooManyMembers)
         {
            // This is an exceptional condition
            StringBuffer sb = new StringBuffer("Cache at address ");
            sb.append(factory.getLocalAddress());
            sb.append(" had ");
            sb.append(members.size());
            sb.append(" members; expecting ");
            sb.append(memberCount);
            sb.append(". Members were (");
            for (int j = 0; j < members.size(); j++)
            {
               if (j > 0)
               {
                  sb.append(", ");
               }
               sb.append(members.get(j));
            }
            sb.append(')');

            throw new IllegalStateException(sb.toString());
         }
         else return false;
      }

      return true;
   }
   
   public static Cookie getSessionCookie(HttpClient client)
   {
      // Get the state for the JSESSIONID
      HttpState state = client.getState();
      // Get the JSESSIONID so we can reset the host
      Cookie[] cookies = state.getCookies();
      Cookie sessionID = null;
      for(int c = 0; c < cookies.length; c ++)
      {
         Cookie k = cookies[c];
         if( k.getName().equalsIgnoreCase("JSESSIONID") )
            sessionID = k;
      }
      return sessionID;
   }

   public static void setCookieDomainToThisServer(HttpClient client, String server)
   {
      // Get the session cookie
      Cookie sessionID = getSessionCookie(client);
      if (sessionID == null)
         throw new IllegalStateException("No session cookie found on " + client);
      // Reset the domain so that the cookie will be sent to server1
      sessionID.setDomain(server);
      client.getState().addCookie(sessionID);
   }
   
   public static void validateExpectedAttributes(Map<String, Object> expected, BasicRequestHandler handler)
   {
      assertFalse(handler.isNewSession());
      
      if (handler.isCheckAttributeNames())
      {
         assertEquals(expected.size(), handler.getAttributeNames().size());
      }
      Map<String, Object> checked = handler.getCheckedAttributes();
      assertEquals(expected.size(), checked.size());
      for (Map.Entry<String, Object> entry : checked.entrySet())
      {
         assertEquals(entry.getKey(), expected.get(entry.getKey()), entry.getValue());
      }
      
   }
   
   public static void validateNewSession(BasicRequestHandler handler)
   {
      assertTrue(handler.isNewSession());
      assertEquals(handler.getCreationTime(), handler.getLastAccessedTime());
      if (handler.isCheckAttributeNames())
      {
         assertEquals(0, handler.getAttributeNames().size());
      }
      Map<String, Object> checked = handler.getCheckedAttributes();
      for (Map.Entry<String, Object> entry : checked.entrySet())
         assertNull(entry.getKey(), entry.getValue());
   }
   
   public static String getPassivationDir(String rootDir, long testCount, int cacheCount)
   {
      File dir = new File(rootDir);
      dir = new File(dir, String.valueOf(testCount));
      dir.mkdirs();
      dir.deleteOnExit();
      dir = new File(dir, String.valueOf(cacheCount));
      dir.mkdirs();
      dir.deleteOnExit();
      return dir.getAbsolutePath();
   }
   
   public static void cleanFilesystem(String path)
   {
      if (path != null)
      {
         File f = new File(path);
         if (f.exists())
         {
            if (f.isDirectory())
            {
               File[] children = f.listFiles();
               for (File child : children)
               {
                  try
                  {
                     cleanFilesystem(child.getCanonicalPath());
                  }
                  catch (IOException e)
                  {
                     log.warn("Can't clean any possible children of " + f);
                  }
               }
            }
            
            if (!f.delete())
            {
               f.delete();
            }
         }
      }
   }


   /**
    * Puts the current thread to sleep for the desired number of ms, suppressing
    * any exceptions.
    *
    * @param sleeptime number of ms to sleep
    */
   public static void sleepThread(long sleeptime)
   {
      try
      {
         Thread.sleep(sleeptime);
      }
      catch (InterruptedException ie)
      {
      }
   }
   
   public static Object getAttributeValue(int value)
   {
      return Integer.valueOf(value);
   }

   private SessionTestUtil() {}
}
