package org.infinispan.server.test.client.rest;

import static org.infinispan.commons.dataconversion.MediaType.APPLICATION_OCTET_STREAM_TYPE;
import static org.infinispan.server.test.client.rest.RESTHelper.KEY_A;
import static org.infinispan.server.test.client.rest.RESTHelper.KEY_B;
import static org.infinispan.server.test.client.rest.RESTHelper.KEY_C;
import static org.infinispan.server.test.client.rest.RESTHelper.addDay;
import static org.infinispan.server.test.util.ITestUtils.sleepForSecs;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.util.EntityUtils;
import org.infinispan.commons.dataconversion.MediaType;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * Tests for the REST client. Subclasses must implement the addRestServer, which has to setup the RESTHelper
 * by calling RESTHelper.addServer method.
 *
 * @author <a href="mailto:jvilkola@redhat.com">Jozef Vilkolak</a>
 * @author <a href="mailto:mlinhard@redhat.com">Michal Linhard</a>
 */
public abstract class AbstractRESTClientIT {

   protected abstract void addRestServer();

   protected RESTHelper rest;

   protected static final String REST_NAMED_CACHE = "restNamedCache";
   protected static final String REST_NAMED_CACHE_TEXT = "restNamedCacheText";

   public static class TestSerializable implements Serializable {
      private String content;

      public TestSerializable(String content) {
         super();
         this.content = content;
      }

      public String getContent() {
         return content;
      }
   }

   protected String getDefaultCache() {
      return "default";
   }

   @Before
   public void setUp() throws Exception {
      rest = new RESTHelper(getDefaultCache());
      addRestServer();

      cleanUpEntries();

      rest.head(rest.fullPathKey(KEY_A), HttpStatus.SC_NOT_FOUND);
      rest.head(rest.fullPathKey(KEY_B), HttpStatus.SC_NOT_FOUND);
      rest.head(rest.fullPathKey(KEY_C), HttpStatus.SC_NOT_FOUND);
      rest.head(rest.fullPathKey(REST_NAMED_CACHE, KEY_A), HttpStatus.SC_NOT_FOUND);
      rest.head(rest.fullPathKey(REST_NAMED_CACHE_TEXT, KEY_A), HttpStatus.SC_NOT_FOUND);
   }

   private void cleanUpEntries() throws Exception {
      rest.delete(rest.fullPathKey(KEY_A));
      rest.delete(rest.fullPathKey(KEY_B));
      rest.delete(rest.fullPathKey(KEY_C));
      rest.delete(rest.fullPathKey(REST_NAMED_CACHE, KEY_A));
      rest.delete(rest.fullPathKey(REST_NAMED_CACHE_TEXT, KEY_A));
   }

   @After
   public void tearDown() throws Exception {
      cleanUpEntries();
      rest.clearServers();
   }

   @Test
   public void testBasicOperation() throws Exception {
      URI fullPathKey = rest.fullPathKey(KEY_A);
      String initialXML = "<hey>ho</hey>";

      HttpResponse insert = rest.put(fullPathKey, initialXML, "application/octet-stream");

      assertEquals(0, insert.getEntity().getContentLength());

      HttpResponse get = rest.get(fullPathKey, initialXML);
      assertEquals("application/octet-stream", get.getHeaders("Content-Type")[0].getValue());

      rest.delete(fullPathKey);
      rest.get(fullPathKey, HttpStatus.SC_NOT_FOUND);

      rest.put(fullPathKey, initialXML, "application/octet-stream");
      rest.get(fullPathKey, initialXML);

      rest.delete(rest.fullPathKey(null));
      rest.get(fullPathKey, HttpStatus.SC_NOT_FOUND);

      ByteArrayOutputStream bout = new ByteArrayOutputStream();
      ObjectOutputStream oo = new ObjectOutputStream(bout);
      oo.writeObject(new TestSerializable("CONTENT"));
      oo.flush();

      byte[] byteData = bout.toByteArray();
      rest.put(fullPathKey, byteData, "application/octet-stream");

      HttpResponse resp = rest.getWithoutClose(fullPathKey);
      ObjectInputStream oin = new ObjectInputStream(resp.getEntity().getContent());
      TestSerializable ts = (TestSerializable) oin.readObject();
      EntityUtils.consume(resp.getEntity());
      assertEquals("CONTENT", ts.getContent());
   }

   @Test
   public void testEmptyGet() throws Exception {
      rest.get(rest.fullPathKey("nodata"), HttpStatus.SC_NOT_FOUND);
   }

   @Test
   public void testGetWithoutMediaTypeConfig() throws Exception {
      URI fullPathKey = rest.fullPathKey(KEY_A);
      rest.post(fullPathKey, "data", "text/plain");
      HttpResponse resp = rest.get(fullPathKey, "data");
      assertNotNull(resp.getHeaders("ETag")[0].getValue());
      assertNotNull(resp.getHeaders("Last-Modified")[0].getValue());
      assertEquals(APPLICATION_OCTET_STREAM_TYPE, resp.getHeaders("Content-Type")[0].getValue());
   }

   @Test
   public void testGetWithMediaTypeConfig() throws Exception {
      URI fullPathKey = rest.fullPathKey(REST_NAMED_CACHE_TEXT, KEY_A);
      rest.post(fullPathKey, "data", "text/plain");
      HttpResponse resp = rest.get(fullPathKey, "data");
      assertNotNull(resp.getHeaders("ETag")[0].getValue());
      assertNotNull(resp.getHeaders("Last-Modified")[0].getValue());
      assertEquals("text/plain", resp.getHeaders("Content-Type")[0].getValue());
   }

   @Test
   public void testGetNamedCache() throws Exception {
      URI fullPathKey = rest.fullPathKey(REST_NAMED_CACHE_TEXT, KEY_A);
      rest.post(fullPathKey, "data", "text/plain");
      HttpResponse resp = rest.get(fullPathKey, "data");
      assertNotNull(resp.getHeaders("ETag")[0].getValue());
      assertNotNull(resp.getHeaders("Last-Modified")[0].getValue());
      assertEquals("text/plain", resp.getHeaders("Content-Type")[0].getValue());
   }

   @Test
   public void testHead() throws Exception {
      URI fullPathKey = rest.fullPathKey(REST_NAMED_CACHE_TEXT, KEY_A);
      rest.post(fullPathKey, "data", "text/plain");
      HttpResponse resp = null;
      try {
         resp = rest.headWithoutClose(fullPathKey);
         assertNotNull(resp.getHeaders("ETag")[0].getValue());
         assertNotNull(resp.getHeaders("Last-Modified")[0].getValue());
         assertEquals("text/plain", resp.getHeaders("Content-Type")[0].getValue());
         assertNull(resp.getEntity());
      } finally {
         EntityUtils.consume(resp.getEntity());
      }
   }

   @Test
   public void testPostDuplicate() throws Exception {
      URI fullPathKey = rest.fullPathKey(KEY_A);

      rest.post(fullPathKey, "data", "text/plain");
      // second post, returns 409
      rest.post(fullPathKey, "data", "text/plain", HttpStatus.SC_CONFLICT);
      // Should be all ok as its a put
      rest.put(fullPathKey, "data", "text/plain");
   }

   @Test
   public void testPutDataWithTimeToLive() throws Exception {
      URI fullPathKey = rest.fullPathKey(KEY_A);

      rest.post(fullPathKey, "data", "text/plain", HttpStatus.SC_OK,
            // headers
            "Content-Type", "text/plain", "timeToLiveSeconds", "2");

      rest.get(fullPathKey, "data");
      sleepForSecs(2.1);
      // should be evicted
      rest.head(fullPathKey, HttpStatus.SC_NOT_FOUND);
   }

   @Test
   public void testPutDataWithMaxIdleTime() throws Exception {
      URI fullPathKey = rest.fullPathKey(KEY_A);

      rest.post(fullPathKey, "data", "text/plain", HttpStatus.SC_OK,
            // headers
            "Content-Type", "text/plain", "maxIdleTimeSeconds", "2");

      rest.get(fullPathKey, "data");

      // data is not idle for next 3 seconds
      for (int i = 1; i < 3; i++) {
         sleepForSecs(1);
         rest.head(fullPathKey);
      }

      // idle for 2 seconds
      sleepForSecs(2.1);
      // should be evicted
      rest.head(fullPathKey, HttpStatus.SC_NOT_FOUND);
   }

   @Test
   public void testPutDataTTLMaxIdleCombo1() throws Exception {
      URI fullPathKey = rest.fullPathKey(KEY_A);

      rest.post(fullPathKey, "data", "text/plain", HttpStatus.SC_OK,
            // headers
            "Content-Type", "text/plain", "timeToLiveSeconds", "10", "maxIdleTimeSeconds", "2");

      rest.get(fullPathKey, "data");

      // data is not idle for next 3 seconds
      for (int i = 1; i < 3; i++) {
         sleepForSecs(1);
         rest.head(fullPathKey);
      }

      // idle for 2 seconds
      sleepForSecs(2.1);
      // should be evicted
      rest.head(fullPathKey, HttpStatus.SC_NOT_FOUND);
   }

   @Test
   public void testPutDataTTLMaxIdleCombo2() throws Exception {
      URI fullPathKey = rest.fullPathKey(KEY_A);

      rest.post(fullPathKey, "data", "text/plain", HttpStatus.SC_OK,
            // headers
            "Content-Type", "text/plain", "timeToLiveSeconds", "2", "maxIdleTimeSeconds", "10");

      rest.get(fullPathKey, "data");
      sleepForSecs(2.1);
      // should be evicted
      rest.head(fullPathKey, HttpStatus.SC_NOT_FOUND);
   }

   @Test
   public void testRemoveEntry() throws Exception {
      URI fullPathKey = rest.fullPathKey(KEY_A);
      rest.post(fullPathKey, "data", "text/plain");
      rest.head(fullPathKey);
      rest.delete(fullPathKey);
      rest.head(fullPathKey, HttpStatus.SC_NOT_FOUND);
   }

   @Test
   public void testWipeCacheBucket() throws Exception {
      rest.post(rest.fullPathKey(KEY_A), "data", "text/plain");
      rest.post(rest.fullPathKey(KEY_B), "data", "text/plain");
      rest.head(rest.fullPathKey(KEY_A));
      rest.head(rest.fullPathKey(KEY_B));
      rest.delete(rest.fullPathKey(null));
      rest.head(rest.fullPathKey(KEY_A), HttpStatus.SC_NOT_FOUND);
      rest.head(rest.fullPathKey(KEY_B), HttpStatus.SC_NOT_FOUND);
   }

   @Test
   public void testPutUnknownClass() throws Exception {
      URI fullPathKey = rest.fullPathKey("x");

      ByteArrayOutputStream bout = new ByteArrayOutputStream();
      ObjectOutputStream oo = new ObjectOutputStream(bout);
      oo.writeObject(new TestSerializable("CONTENT"));
      oo.flush();
      byte[] byteData = bout.toByteArray();
      rest.put(fullPathKey, byteData, MediaType.APPLICATION_OCTET_STREAM_TYPE);
      HttpResponse resp = rest.get(fullPathKey, null, HttpStatus.SC_OK, false, "Accept", MediaType.APPLICATION_OCTET_STREAM_TYPE);
      ObjectInputStream oin = new ObjectInputStream(resp.getEntity().getContent());
      TestSerializable ts = (TestSerializable) oin.readObject();
      EntityUtils.consume(resp.getEntity());
      assertEquals("CONTENT", ts.getContent());
   }

   @Test
   public void testPutKnownClass() throws Exception {
      URI fullPathKey = rest.fullPathKey("y");
      ByteArrayOutputStream bout = new ByteArrayOutputStream();
      ObjectOutputStream oo = new ObjectOutputStream(bout);
      Integer i1 = 42;
      oo.writeObject(i1);
      oo.flush();
      byte[] byteData = bout.toByteArray();
      rest.put(fullPathKey, byteData, MediaType.APPLICATION_OCTET_STREAM_TYPE);
      HttpResponse resp = rest.get(fullPathKey, null, HttpStatus.SC_OK, false, "Accept", MediaType.APPLICATION_OCTET_STREAM_TYPE);
      ObjectInputStream oin = new ObjectInputStream(resp.getEntity().getContent());
      Integer i2 = (Integer) oin.readObject();
      EntityUtils.consume(resp.getEntity());
      assertEquals(i1, i2);
   }

   @Test
   public void testETagChanges() throws Exception {
      URI fullPathKey = rest.fullPathKey(KEY_A);
      rest.put(fullPathKey, "data1", "text/plain");
      String eTagFirst = rest.get(fullPathKey).getHeaders("ETag")[0].getValue();
      // second get should get the same ETag
      assertEquals(eTagFirst, rest.get(fullPathKey).getHeaders("ETag")[0].getValue());
      // do second PUT
      rest.put(fullPathKey, "data2", "text/plain");
      // get ETag again
      assertFalse(eTagFirst.equals(rest.get(fullPathKey).getHeaders("ETag")[0].getValue()));
   }

   @Test
   public void testXJavaSerializedObjectPutAndDelete() throws Exception {
      //show that "text/plain" works for delete
      URI fullPathKey1 = rest.fullPathKey("j");
      rest.put(fullPathKey1, "data1", "text/plain");
      rest.head(fullPathKey1, HttpStatus.SC_OK);
      rest.delete(fullPathKey1);
      rest.head(fullPathKey1, HttpStatus.SC_NOT_FOUND);

      URI fullPathKey2 = rest.fullPathKey("k");
      ByteArrayOutputStream bout = new ByteArrayOutputStream();
      ObjectOutputStream oo = new ObjectOutputStream(bout);
      Integer i1 = 42;
      oo.writeObject(i1);
      oo.flush();
      byte[] byteData = bout.toByteArray();
      rest.put(fullPathKey2, byteData, MediaType.APPLICATION_OCTET_STREAM_TYPE);
      rest.head(fullPathKey2, HttpStatus.SC_OK);
      rest.delete(fullPathKey2);
      rest.head(fullPathKey2, HttpStatus.SC_NOT_FOUND);
   }

   @Test
   public void testIfModifiedSince() throws Exception {
      URI fullPathKey = rest.fullPathKey(KEY_A);
      rest.put(fullPathKey, "data", "text/plain");
      HttpResponse resp = rest.get(fullPathKey);
      String dateLast = resp.getHeaders("Last-Modified")[0].getValue();
      String dateMinus = addDay(dateLast, -1);
      String datePlus = addDay(dateLast, 1);

//        rest.get(fullPathKey, "data", HttpStatus.SC_OK, true,
//                // resource has been modified since
//                "If-Modified-Since", dateMinus);

      rest.get(fullPathKey, null, HttpStatus.SC_NOT_MODIFIED, true,
            // exact same date as stored one
            "If-Modified-Since", dateLast);

//        rest.get(fullPathKey, null, HttpStatus.SC_NOT_MODIFIED, true,
//                // resource hasn't been modified since
//                "If-Modified-Since", datePlus);
   }

   @Test
   public void testIfUnmodifiedSince() throws Exception {
      URI fullPathKey = rest.fullPathKey(KEY_A);
      rest.put(fullPathKey, "data", "text/plain");

      HttpResponse resp = rest.get(fullPathKey);
      String dateLast = resp.getHeaders("Last-Modified")[0].getValue();
      String dateMinus = addDay(dateLast, -1);
      String datePlus = addDay(dateLast, 1);

      rest.get(fullPathKey, "data", HttpStatus.SC_OK, true, "If-Unmodified-Since", dateLast);

      rest.get(fullPathKey, "data", HttpStatus.SC_OK, true, "If-Unmodified-Since", datePlus);

      rest.get(fullPathKey, null, HttpStatus.SC_PRECONDITION_FAILED, true, "If-Unmodified-Since", dateMinus);
   }

   @Test
   public void testIfNoneMatch() throws Exception {
      URI fullPathKey = rest.fullPathKey(KEY_A);
      rest.put(fullPathKey, "data", "text/plain");
      HttpResponse resp = rest.get(fullPathKey);
      String eTag = resp.getHeaders("ETag")[0].getValue();

      rest.get(fullPathKey, null, HttpStatus.SC_NOT_MODIFIED, true, "If-None-Match", eTag);

      rest.get(fullPathKey, "data", HttpStatus.SC_OK, true, "If-None-Match", eTag + "garbage");
   }

   @Test
   public void testIfMatch() throws Exception {
      URI fullPathKey = rest.fullPathKey(KEY_A);
      rest.put(fullPathKey, "data", "text/plain");
      HttpResponse resp = rest.get(fullPathKey);

      String eTag = resp.getHeaders("ETag")[0].getValue();
      // test GET with If-Match behaviour
      rest.get(fullPathKey, "data", HttpStatus.SC_OK, true, "If-Match", eTag);
      rest.get(fullPathKey, null, HttpStatus.SC_PRECONDITION_FAILED, true, "If-Match", eTag + "garbage");

      // test HEAD with If-Match behaviour
      rest.head(fullPathKey, HttpStatus.SC_OK, new String[][]{{"If-Match", eTag}});
      rest.head(fullPathKey, HttpStatus.SC_PRECONDITION_FAILED, new String[][]{{"If-Match", eTag + "garbage"}});
   }

   @Test
   public void testNonExistentCache() throws Exception {
      //It seems Common HTTP Client uses different way for parsing HEAD than for other methods.
      //As a consequence it doesn't properly recognise 404 WITH description (without entity it works fine)
      //See https://issues.jboss.org/browse/ISPN-7821
      //rest.head(rest.fullPathKey("nonexistentcache", "nodata"), HttpStatus.SC_NOT_FOUND);
      rest.get(rest.fullPathKey("nonexistentcache", "nodata"), HttpStatus.SC_NOT_FOUND);
      rest.put(rest.fullPathKey("nonexistentcache", "nodata"), "data", "text/plain", HttpStatus.SC_NOT_FOUND);
      rest.delete(rest.fullPathKey("nonexistentcache", "nodata"), HttpStatus.SC_NOT_FOUND);
   }

   @Test
   public void testByteArrayStorage() throws Exception {
      final String KEY_Z = "z";
      byte[] data = "data".getBytes(StandardCharsets.UTF_8);

      ByteArrayOutputStream bout = new ByteArrayOutputStream();
      ObjectOutputStream oo = new ObjectOutputStream(bout);
      oo.writeObject(data);
      oo.flush();

      byte[] serializedData = bout.toByteArray();
      rest.put(rest.fullPathKey(0, KEY_Z), serializedData, MediaType.APPLICATION_OCTET_STREAM_TYPE);

      HttpResponse resp = rest.get(rest.fullPathKey(0, KEY_Z), null, HttpStatus.SC_OK, false, "Accept",
            APPLICATION_OCTET_STREAM_TYPE);
      ObjectInputStream oin = new ObjectInputStream(resp.getEntity().getContent());
      byte[] dataBack = (byte[]) oin.readObject();
      EntityUtils.consume(resp.getEntity());
      assertTrue(Arrays.equals(data, dataBack));
   }

   @Test
   public void testStoreBigObject() throws Exception {
      int SIZE = 3000000;
      byte[] bytes = new byte[SIZE];
      for (int i = 0; i < SIZE; i++) {
         bytes[i] = (byte) (i % 10);
      }
      rest.put(rest.fullPathKey("object"), bytes, "application/octet-stream");

      HttpResponse resp = rest.getWithoutClose(rest.fullPathKey("object"));
      InputStream responseStream = resp.getEntity().getContent();
      byte[] response = new byte[SIZE];
      byte data;
      int j = 0;
      while ((data = (byte) responseStream.read()) != -1) {
         response[j] = data;
         j++;
      }

      boolean correct = true;
      for (int i = 0; i < SIZE; i++) {
         if (bytes[i] != response[i]) {
            correct = false;
         }
      }
      EntityUtils.consume(resp.getEntity());
      assertTrue(correct);
   }

   //the following property is passed to the server at startup so that this test passes:
   //-Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true
   @Test
   public void testKeyIncludingSlashURLEncoded() throws Exception {
      String encodedSlashKey = URLEncoder.encode("x/y", "UTF-8");
      rest.post(rest.fullPathKey(encodedSlashKey), "data", "text/plain");
      HttpResponse get = rest.get(rest.fullPathKey(encodedSlashKey), "data");
      assertNotNull(get.getHeaders("ETag")[0].getValue());
      assertNotNull(get.getHeaders("Last-Modified")[0].getValue());
      assertEquals(APPLICATION_OCTET_STREAM_TYPE, get.getHeaders("Content-Type")[0].getValue());
   }
}
