package org.infinispan.commons.util;

import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
import java.util.UUID;

import org.infinispan.commons.marshall.MarshallUtil;
import org.junit.Assert;
import org.junit.Test;

/**
 * Unit tests for {@link org.infinispan.commons.marshall.MarshallUtil}
 *
 * @author Pedro Ruivo
 * @since 8.2
 */
public class MarshallUtilTest {

   private static final int NR_RANDOM = 1000;

   private static void positiveRange(int min, int max, int bytesExpected, ObjectInputOutput io) throws IOException {
      for (int i = min; i > 0 && i < max; i <<= 1) {
         checkIntAndByteArray(i, bytesExpected, io);
      }
   }

   private static void checkIntAndByteArray(int i, int bytesExpected, ObjectInputOutput io) throws IOException {
      io.reset();
      MarshallUtil.marshallSize(io, i);
      Assert.assertEquals("Error for i=" + i, bytesExpected, io.buffer.size());
      Assert.assertEquals("Error for i=" + i, i, MarshallUtil.unmarshallSize(io));
      Assert.assertEquals("Error for i=" + i, 0, io.buffer.size());
   }

   private static void checkNegativeInt(int i, ObjectInputOutput io) throws IOException {
      io.reset();
      MarshallUtil.marshallSize(io, i);
      Assert.assertEquals("Error for i=" + i, 1, io.buffer.size());
      Assert.assertEquals("Error for i=" + i, -1, MarshallUtil.unmarshallSize(io));
      Assert.assertEquals("Error for i=" + i, 0, io.buffer.size());
   }

   @Test
   public void testPositiveRange() throws IOException {
      ObjectInputOutput io = new ObjectInputOutput();
      checkIntAndByteArray(0, 1, io); //zero
      positiveRange(1, 1 << 6, 1, io);
      positiveRange(1 << 6, 1 << 13, 2, io);
      positiveRange(1 << 13, 1 << 20, 3, io);
      positiveRange(1 << 20, 1 << 27, 4, io);
      positiveRange(1 << 27, Integer.MAX_VALUE, 5, io);
      checkIntAndByteArray(Integer.MAX_VALUE, 5, io);
   }

   @Test
   public void testNegativeRange() throws IOException {
      ObjectInputOutput io = new ObjectInputOutput();
      for (int i = 1; i < Integer.MAX_VALUE; i <<= 1) {
         int v = -i;
         if (v >= 0) {
            break;
         }
         checkNegativeInt(v, io);
      }
      checkNegativeInt(Integer.MIN_VALUE, io);
   }

   @Test
   public void testRandomPositiveInt() throws IOException {
      Random random = new Random(System.nanoTime());
      ObjectInputOutput io = new ObjectInputOutput();
      for (int i = 0; i < NR_RANDOM; ++i) {
         int v = random.nextInt();
         if (v < 0) {
            v = -v;
         }
         io.reset();
         MarshallUtil.marshallSize(io, v);
         Assert.assertEquals("Error for v=" + v, v, MarshallUtil.unmarshallSize(io));
      }
   }

   @Test
   public void testRandomNegativeInt() throws IOException {
      Random random = new Random(System.nanoTime());
      ObjectInputOutput io = new ObjectInputOutput();
      for (int i = 0; i < NR_RANDOM; ++i) {
         int v = random.nextInt();
         if (v > 0) {
            v = -v;
         } else if (v == 0) {
            i--;
            continue;
         }
         io.reset();
         MarshallUtil.marshallSize(io, v);
         Assert.assertEquals("Error for v=" + v, -1, MarshallUtil.unmarshallSize(io));
      }
   }

   @Test
   public void testEnum() throws IOException {
      ObjectInputOutput io = new ObjectInputOutput();
      MarshallUtil.marshallEnum(null, io);
      Assert.assertNull(MarshallUtil.unmarshallEnum(io, ordinal -> TestEnum.values()[ordinal]));
      Assert.assertEquals(0, io.buffer.size());

      for (TestEnum e : TestEnum.values()) {
         io.reset();
         MarshallUtil.marshallEnum(e, io);
         Assert.assertEquals(e, MarshallUtil.unmarshallEnum(io, ordinal -> TestEnum.values()[ordinal]));
         Assert.assertEquals(0, io.buffer.size());
      }
   }

   @Test
   public void testUUID() throws IOException {
      ObjectInputOutput io = new ObjectInputOutput();
      MarshallUtil.marshallUUID(null, io, true);
      Assert.assertNull(MarshallUtil.unmarshallUUID(io, true));
      Assert.assertEquals(0, io.buffer.size());

      for (int i = 0; i < NR_RANDOM; ++i) {
         io.reset();
         UUID uuid = UUID.randomUUID();
         MarshallUtil.marshallUUID(uuid, io, false);
         Assert.assertEquals(uuid, MarshallUtil.unmarshallUUID(io, false));
         Assert.assertEquals(0, io.buffer.size());
      }

      for (int i = 0; i < NR_RANDOM; ++i) {
         io.reset();
         UUID uuid = UUID.randomUUID();
         MarshallUtil.marshallUUID(uuid, io, true);
         Assert.assertEquals(uuid, MarshallUtil.unmarshallUUID(io, true));
         Assert.assertEquals(0, io.buffer.size());
      }
   }

   @Test
   public void testArray() throws IOException, ClassNotFoundException {
      ObjectInputOutput io = new ObjectInputOutput();
      MarshallUtil.marshallArray(null, io);
      Assert.assertNull(MarshallUtil.unmarshallArray(io, null));
      Assert.assertEquals(0, io.buffer.size());
      io.reset();

      String[] array = new String[0];
      MarshallUtil.marshallArray(array, io);
      Assert.assertTrue(Arrays.equals(array, MarshallUtil.unmarshallArray(io, Util::stringArray)));
      Assert.assertEquals(0, io.buffer.size());
      io.reset();

      array = new String[] {"a", "b", "c"};
      MarshallUtil.marshallArray(array, io);
      Assert.assertTrue(Arrays.equals(array, MarshallUtil.unmarshallArray(io, Util::stringArray)));
      Assert.assertEquals(0, io.buffer.size());
      io.reset();
   }

   @Test
   public void testByteArray() throws IOException, ClassNotFoundException {
      ObjectInputOutput io = new ObjectInputOutput();
      MarshallUtil.marshallByteArray(null, io);
      Assert.assertNull(MarshallUtil.unmarshallByteArray(io));
      Assert.assertEquals(0, io.buffer.size());
      io.reset();

      byte[] array = Util.EMPTY_BYTE_ARRAY;
      MarshallUtil.marshallByteArray(array, io);
      Assert.assertTrue(Arrays.equals(array, MarshallUtil.unmarshallByteArray(io)));
      Assert.assertEquals(0, io.buffer.size());
      io.reset();

      array = new byte[] {1, 2, 3};
      MarshallUtil.marshallByteArray(array, io);
      Assert.assertTrue(Arrays.equals(array, MarshallUtil.unmarshallByteArray(io)));
      Assert.assertEquals(0, io.buffer.size());
      io.reset();

   }

   private enum TestEnum {
      ONE, TWO, THREE
   }

   private static class ObjectInputOutput implements ObjectOutput, ObjectInput {

      private final Queue<Object> buffer = new LinkedList<>();

      @Override
      public void writeByte(int v) throws IOException {
         buffer.add((byte) v);
      }

      @Override
      public void writeShort(int v) throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public void writeChar(int v) throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public void writeInt(int v) throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public void writeLong(long v) throws IOException {
         buffer.add(v);
      }

      @Override
      public void writeFloat(float v) throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public void writeDouble(double v) throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public void writeBytes(String s) throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public void writeChars(String s) throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public void writeUTF(String s) throws IOException {
         throw new UnsupportedOperationException();
      }

      public void reset() {
         buffer.clear();
      }

      @Override
      public Object readObject() throws ClassNotFoundException, IOException {
         return buffer.poll();
      }

      @Override
      public int read() throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public int read(byte[] b) throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public int read(byte[] b, int off, int len) throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public long skip(long n) throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public int available() throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public void readFully(byte[] b) throws IOException {
         byte[] array = (byte[]) buffer.poll();
         Assert.assertEquals(array.length, b.length);
         System.arraycopy(array, 0, b, 0, b.length);
      }

      @Override
      public void readFully(byte[] b, int off, int len) throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public int skipBytes(int n) throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public boolean readBoolean() throws IOException {
         return (boolean) buffer.poll();
      }

      @Override
      public byte readByte() throws IOException {
         return (byte) buffer.poll();
      }

      @Override
      public int readUnsignedByte() throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public short readShort() throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public int readUnsignedShort() throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public char readChar() throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public int readInt() throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public long readLong() throws IOException {
         return (long) buffer.poll();
      }

      @Override
      public float readFloat() throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public double readDouble() throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public String readLine() throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public String readUTF() throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public void writeObject(Object obj) throws IOException {
         buffer.add(obj);
      }

      @Override
      public void write(int b) throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public void write(byte[] b) throws IOException {
         buffer.add(b);
      }

      @Override
      public void write(byte[] b, int off, int len) throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public void writeBoolean(boolean v) throws IOException {
         buffer.add(v);
      }

      @Override
      public void flush() throws IOException {
         throw new UnsupportedOperationException();
      }

      @Override
      public void close() throws IOException {
         throw new UnsupportedOperationException();
      }
   }

}
