package org.infinispan.server.websocket;

import static org.infinispan.server.websocket.OpHandler.*;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.URI;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocketClient;
import org.eclipse.jetty.websocket.WebSocketClientFactory;
import org.infinispan.Cache;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.server.websocket.configuration.WebSocketServerConfigurationBuilder;
import org.infinispan.server.websocket.json.JsonObject;
import org.infinispan.test.SingleCacheManagerTest;
import org.infinispan.test.fwk.TestCacheManagerFactory;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import net.jcip.annotations.NotThreadSafe;

/**
 * @author gustavonalle
 */
@Test(testName = "websocket.WebSocketServerIntegrationTest", groups = "functional")
public class WebSocketServerIntegrationTest extends SingleCacheManagerTest {

   private WebSocketServer wsServer;
   private WebSocketTestClient wsClient;
   String cacheName = "testCache";

   @BeforeMethod
   public void setUp() throws Exception {
      Cache<String, String> cache = cacheManager.getCache(cacheName);
      cache.put("key", "value");

      wsServer = new WebSocketServer();
      wsServer.start(new WebSocketServerConfigurationBuilder().host("localhost").port(getFreePort()).build(), cacheManager);
      wsClient = new WebSocketTestClient(cacheName);
      wsClient.connect(new URI("ws://localhost:" + wsServer.getPort()));
   }

   @AfterMethod
   public void tearDown() throws Exception {
      wsServer.stop();
      wsClient.destroy();
   }

   @Test
   public void testGet() throws Exception {
      assertEquals(wsClient.get("key"), "value");
   }

   @Test
   public void testPut() throws Exception {
      String key = "key2";
      String value = "value2";
      wsClient.put(key, value);

      assertEquals(wsClient.get(key), value);
   }

   @Test
   public void testGarbage() throws Exception {
      JsonObject jsonObject = wsClient.sendAndWait("garbage");
      assertTrue(jsonObject.containsKey(ERROR));
   }

   @Test(expectedExceptions = Exception.class, expectedExceptionsMessageRegExp = "Cache temporarily unavailable")
   public void testDisableCache() throws Exception {
      assertEquals(wsClient.get("key"), "value");

      wsServer.ignoreCache(cacheName);

      assertEquals(wsClient.get("key"), "value");

   }

   @NotThreadSafe
   private static class WebSocketTestClient {

      private final String cacheName;
      private final WebSocketClient wsClient;
      private final WebSocketClientFactory webSocketClientFactory;
      private CountDownLatch messageLatch = new CountDownLatch(1);

      private static final int TIMEOUT = 5;
      private static final TimeUnit TIMEOUT_UNIT = TimeUnit.SECONDS;

      private WebSocket.Connection connection;
      private String lastMessage;

      public WebSocketTestClient(String cacheName) throws Exception {
         this.cacheName = cacheName;
         webSocketClientFactory = new WebSocketClientFactory();
         webSocketClientFactory.start();
         wsClient = webSocketClientFactory.newWebSocketClient();
      }

      public void connect(URI serverURI) throws Exception {
         connection = wsClient.open(serverURI, new ClientWebSocket()).get(2, TimeUnit.SECONDS);
      }

      public void destroy() throws Exception {
         webSocketClientFactory.stop();
         connection.close();
      }

      public void put(String key, String value) throws Exception {
         JsonObject jsonObject = JsonObject.createNew();
         jsonObject.put(OP_CODE, "put");
         jsonObject.put(CACHE_NAME, cacheName);
         jsonObject.put(KEY, key);
         jsonObject.put(VALUE, value);
         jsonObject.put(MIME, "text/plain");
         connection.sendMessage(jsonObject.toString());
      }

      public JsonObject sendAndWait(String message) throws Exception {
         messageLatch = new CountDownLatch(1);
         connection.sendMessage(message);
         messageLatch.await(TIMEOUT, TIMEOUT_UNIT);
         return JsonObject.fromString(lastMessage);
      }

      private class ClientWebSocket implements WebSocket.OnTextMessage {

         @Override
         public void onMessage(String data) {
            lastMessage = data;
            messageLatch.countDown();
         }

         @Override
         public void onOpen(Connection connection) {
         }

         @Override
         public void onClose(int closeCode, String message) {
         }
      }

      public String get(String key) throws Exception {
         JsonObject jsonObject = JsonObject.createNew();
         jsonObject.put(OP_CODE, "get");
         jsonObject.put(CACHE_NAME, cacheName);
         jsonObject.put(KEY, key);
         JsonObject response = sendAndWait(jsonObject.toString());
         if (response.containsKey(ERROR)) {
            throw new Exception(response.get(ERROR).toString());
         }
         return response.get("value").toString();
      }

   }

   private int getFreePort() throws IOException {
      try (ServerSocket socket = new ServerSocket(0)) {
         return socket.getLocalPort();
      }
   }

   @Override
   protected EmbeddedCacheManager createCacheManager() throws Exception {
      return TestCacheManagerFactory.createCacheManager(new GlobalConfigurationBuilder().defaultCacheName(cacheName), new ConfigurationBuilder());
   }
}
