/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License. You may obtain a
 * copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.apache.geode.internal.offheap;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.nio.ByteBuffer;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import org.apache.geode.compression.Compressor;
import org.apache.geode.internal.HeapDataOutputStream;
import org.apache.geode.internal.cache.BytesAndBitsForCompactor;
import org.apache.geode.internal.cache.CachePerfStats;
import org.apache.geode.internal.cache.EntryEventImpl;
import org.apache.geode.internal.cache.RegionEntryContext;
import org.apache.geode.internal.offheap.MemoryAllocatorImpl.DummyNonRealTimeStatsUpdater;
import org.apache.geode.internal.offheap.MemoryBlock.State;
import org.apache.geode.internal.serialization.DSCODE;
import org.apache.geode.internal.serialization.KnownVersion;

public class OffHeapStoredObjectJUnitTest extends AbstractStoredObjectTestBase {

  private MemoryAllocator ma;
  private static Boolean assertionsEnabled;

  @BeforeClass
  public static void setUpOnce() {
    try {
      assert false;
      assertionsEnabled = false;
    } catch (AssertionError e) {
      assertionsEnabled = true;
    }
    ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true);
    System.out.println("assertionsEnabled = " + assertionsEnabled);
  }

  @AfterClass
  public static void tearDownOnce() {
    ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(assertionsEnabled);
  }

  @Before
  public void setUp() {
    OutOfOffHeapMemoryListener ooohml = mock(OutOfOffHeapMemoryListener.class);
    OffHeapMemoryStats stats = mock(OffHeapMemoryStats.class);

    ma = MemoryAllocatorImpl.create(ooohml, stats, 3, OffHeapStorage.MIN_SLAB_SIZE * 3,
        OffHeapStorage.MIN_SLAB_SIZE, null, () -> new DummyNonRealTimeStatsUpdater());
  }

  @After
  public void tearDown() {
    MemoryAllocatorImpl.freeOffHeapMemory();
  }

  @Override
  public Object getValue() {
    return Long.MAX_VALUE;
  }

  @Override
  public byte[] getValueAsByteArray() {
    return convertValueToByteArray(getValue());
  }

  private byte[] convertValueToByteArray(Object value) {
    return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong((Long) value).array();
  }

  @Override
  public Object convertByteArrayToObject(byte[] valueInByteArray) {
    return ByteBuffer.wrap(valueInByteArray).getLong();
  }

  @Override
  public Object convertSerializedByteArrayToObject(byte[] valueInSerializedByteArray) {
    return EntryEventImpl.deserialize(valueInSerializedByteArray);
  }

  @Override
  public OffHeapStoredObject createValueAsUnserializedStoredObject(Object value) {
    byte[] valueInByteArray;
    if (value instanceof Long) {
      valueInByteArray = convertValueToByteArray(value);
    } else {
      valueInByteArray = (byte[]) value;
    }

    boolean isSerialized = false;
    boolean isCompressed = false;

    return createChunk(valueInByteArray, isSerialized, isCompressed);
  }

  @Override
  public OffHeapStoredObject createValueAsSerializedStoredObject(Object value) {
    byte[] valueInSerializedByteArray = EntryEventImpl.serialize(value);

    boolean isSerialized = true;
    boolean isCompressed = false;

    return createChunk(valueInSerializedByteArray, isSerialized, isCompressed);
  }

  private OffHeapStoredObject createChunk(byte[] v, boolean isSerialized, boolean isCompressed) {
    OffHeapStoredObject chunk =
        (OffHeapStoredObject) ma.allocateAndInitialize(v, isSerialized, isCompressed);
    return chunk;
  }

  @Test
  public void chunkCanBeCreatedFromAnotherChunk() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());

    OffHeapStoredObject newChunk = new OffHeapStoredObject(chunk);

    assertNotNull(newChunk);
    assertThat(newChunk.getAddress()).isEqualTo(chunk.getAddress());

    chunk.release();
  }

  @Test
  public void chunkCanBeCreatedWithOnlyMemoryAddress() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());

    OffHeapStoredObject newChunk = new OffHeapStoredObject(chunk.getAddress());

    assertNotNull(newChunk);
    assertThat(newChunk.getAddress()).isEqualTo(chunk.getAddress());

    chunk.release();
  }

  @Test
  public void chunkSliceCanBeCreatedFromAnotherChunk() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());

    int position = 1;
    int end = 2;

    OffHeapStoredObject newChunk = (OffHeapStoredObject) chunk.slice(position, end);

    assertNotNull(newChunk);
    assertThat(newChunk.getClass()).isEqualTo(OffHeapStoredObjectSlice.class);
    assertThat(newChunk.getAddress()).isEqualTo(chunk.getAddress());

    chunk.release();
  }

  @Test
  public void fillSerializedValueShouldFillWrapperWithSerializedValueIfValueIsSerialized() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());

    // mock the things
    BytesAndBitsForCompactor wrapper = mock(BytesAndBitsForCompactor.class);

    byte userBits = 0;
    byte serializedUserBits = 1;
    chunk.fillSerializedValue(wrapper, userBits);

    verify(wrapper, times(1)).setOffHeapData(chunk, serializedUserBits);

    chunk.release();
  }

  @Test
  public void fillSerializedValueShouldFillWrapperWithDeserializedValueIfValueIsNotSerialized() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());

    // mock the things
    BytesAndBitsForCompactor wrapper = mock(BytesAndBitsForCompactor.class);

    byte userBits = 1;
    chunk.fillSerializedValue(wrapper, userBits);

    verify(wrapper, times(1)).setOffHeapData(chunk, userBits);

    chunk.release();
  }

  @Test
  public void getShortClassNameShouldReturnShortClassName() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    assertThat(chunk.getShortClassName()).isEqualTo("OffHeapStoredObject");

    chunk.release();
  }

  @Test
  public void chunksAreEqualsOnlyByAddress() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());

    OffHeapStoredObject newChunk = new OffHeapStoredObject(chunk.getAddress());
    assertThat(chunk.equals(newChunk)).isTrue();

    OffHeapStoredObject chunkWithSameValue = createValueAsUnserializedStoredObject(getValue());
    assertThat(chunk.equals(chunkWithSameValue)).isFalse();

    Object someObject = getValue();
    assertThat(chunk.equals(someObject)).isFalse();

    chunk.release();
    chunkWithSameValue.release();
  }

  @Test
  public void chunksShouldBeComparedBySize() {
    OffHeapStoredObject chunk1 = createValueAsSerializedStoredObject(getValue());

    OffHeapStoredObject chunk2 = chunk1;
    assertThat(chunk1.compareTo(chunk2)).isEqualTo(0);

    OffHeapStoredObject chunkWithSameValue = createValueAsSerializedStoredObject(getValue());
    assertThat(chunk1.compareTo(chunkWithSameValue))
        .isEqualTo(Long.signum(chunk1.getAddress() - chunkWithSameValue.getAddress()));

    OffHeapStoredObject chunk3 = createValueAsSerializedStoredObject(Long.MAX_VALUE);
    OffHeapStoredObject chunk4 = createValueAsSerializedStoredObject(Long.MAX_VALUE);

    int newSizeForChunk3 = 2;
    int newSizeForChunk4 = 3;

    assertThat(chunk3.compareTo(chunk4))
        .isEqualTo(Integer.signum(newSizeForChunk3 - newSizeForChunk4));

    chunk1.release();
    chunk4.release();
  }

  @Test
  public void setSerializedShouldSetTheSerializedBit() {
    Object regionEntryValue = getValue();
    byte[] regionEntryValueAsBytes = convertValueToByteArray(regionEntryValue);

    boolean isSerialized = false;
    boolean isCompressed = false;

    OffHeapStoredObject chunk = (OffHeapStoredObject) ma
        .allocateAndInitialize(regionEntryValueAsBytes, isSerialized, isCompressed);

    int headerBeforeSerializedBitSet = AddressableMemoryManager
        .readIntVolatile(chunk.getAddress() + OffHeapStoredObject.REF_COUNT_OFFSET);

    assertThat(chunk.isSerialized()).isFalse();

    chunk.setSerialized(true); // set to true

    assertThat(chunk.isSerialized()).isTrue();

    int headerAfterSerializedBitSet = AddressableMemoryManager
        .readIntVolatile(chunk.getAddress() + OffHeapStoredObject.REF_COUNT_OFFSET);

    assertThat(headerAfterSerializedBitSet)
        .isEqualTo(headerBeforeSerializedBitSet | OffHeapStoredObject.IS_SERIALIZED_BIT);

    chunk.release();
  }

  @Test(expected = IllegalStateException.class)
  public void setSerialziedShouldThrowExceptionIfChunkIsAlreadyReleased() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    chunk.release();
    chunk.setSerialized(true);

    chunk.release();
  }

  @Test
  public void setCompressedShouldSetTheCompressedBit() {
    Object regionEntryValue = getValue();
    byte[] regionEntryValueAsBytes = convertValueToByteArray(regionEntryValue);

    boolean isSerialized = false;
    boolean isCompressed = false;

    OffHeapStoredObject chunk = (OffHeapStoredObject) ma
        .allocateAndInitialize(regionEntryValueAsBytes, isSerialized, isCompressed);

    int headerBeforeCompressedBitSet = AddressableMemoryManager
        .readIntVolatile(chunk.getAddress() + OffHeapStoredObject.REF_COUNT_OFFSET);

    assertThat(chunk.isCompressed()).isFalse();

    chunk.setCompressed(true); // set to true

    assertThat(chunk.isCompressed()).isTrue();

    int headerAfterCompressedBitSet = AddressableMemoryManager
        .readIntVolatile(chunk.getAddress() + OffHeapStoredObject.REF_COUNT_OFFSET);

    assertThat(headerAfterCompressedBitSet)
        .isEqualTo(headerBeforeCompressedBitSet | OffHeapStoredObject.IS_COMPRESSED_BIT);

    chunk.release();
  }

  @Test(expected = IllegalStateException.class)
  public void setCompressedShouldThrowExceptionIfChunkIsAlreadyReleased() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    chunk.release();
    chunk.setCompressed(true);

    chunk.release();
  }

  @Test
  public void setDataSizeShouldSetTheDataSizeBits() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());

    int beforeSize = chunk.getDataSize();

    chunk.setDataSize(2);

    int afterSize = chunk.getDataSize();

    assertThat(afterSize).isEqualTo(2);
    assertThat(afterSize).isNotEqualTo(beforeSize);

    chunk.release();
  }

  @Test(expected = IllegalStateException.class)
  public void setDataSizeShouldThrowExceptionIfChunkIsAlreadyReleased() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    chunk.release();
    chunk.setDataSize(1);

    chunk.release();
  }

  @Test(expected = IllegalStateException.class)
  public void initializeUseCountShouldThrowIllegalStateExceptionIfChunkIsAlreadyRetained() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    chunk.retain();
    chunk.initializeUseCount();

    chunk.release();
  }

  @Test(expected = IllegalStateException.class)
  public void initializeUseCountShouldThrowIllegalStateExceptionIfChunkIsAlreadyReleased() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    chunk.release();
    chunk.initializeUseCount();

    chunk.release();
  }

  @Test
  public void isSerializedPdxInstanceShouldReturnTrueIfItsPDXInstance() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());

    byte[] serailizedValue = chunk.getSerializedValue();
    serailizedValue[0] = DSCODE.PDX.toByte();
    chunk.setSerializedValue(serailizedValue);

    assertThat(chunk.isSerializedPdxInstance()).isTrue();

    serailizedValue = chunk.getSerializedValue();
    serailizedValue[0] = DSCODE.PDX_ENUM.toByte();
    chunk.setSerializedValue(serailizedValue);

    assertThat(chunk.isSerializedPdxInstance()).isTrue();

    serailizedValue = chunk.getSerializedValue();
    serailizedValue[0] = DSCODE.PDX_INLINE_ENUM.toByte();
    chunk.setSerializedValue(serailizedValue);

    assertThat(chunk.isSerializedPdxInstance()).isTrue();

    chunk.release();
  }

  @Test
  public void isSerializedPdxInstanceShouldReturnFalseIfItsNotPDXInstance() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());
    assertThat(chunk.isSerializedPdxInstance()).isFalse();

    chunk.release();
  }

  @Test
  public void checkDataEqualsByChunk() {
    OffHeapStoredObject chunk1 = createValueAsSerializedStoredObject(getValue());
    OffHeapStoredObject sameAsChunk1 = chunk1;

    assertThat(chunk1.checkDataEquals(sameAsChunk1)).isTrue();

    OffHeapStoredObject unserializedChunk = createValueAsUnserializedStoredObject(getValue());
    assertThat(chunk1.checkDataEquals(unserializedChunk)).isFalse();

    OffHeapStoredObject chunkDifferBySize = createValueAsSerializedStoredObject(getValue());
    chunkDifferBySize.setSize(0);
    assertThat(chunk1.checkDataEquals(chunkDifferBySize)).isFalse();

    OffHeapStoredObject chunkDifferByValue =
        createValueAsSerializedStoredObject(Long.MAX_VALUE - 1);
    assertThat(chunk1.checkDataEquals(chunkDifferByValue)).isFalse();

    OffHeapStoredObject newChunk1 = createValueAsSerializedStoredObject(getValue());
    assertThat(chunk1.checkDataEquals(newChunk1)).isTrue();

    chunk1.release();
    unserializedChunk.release();
    chunkDifferBySize.release();
    chunkDifferByValue.release();
    newChunk1.release();
  }

  @Test
  public void checkDataEqualsBySerializedValue() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());
    assertThat(chunk.checkDataEquals(new byte[1])).isFalse();

    OffHeapStoredObject chunkDifferByValue =
        createValueAsSerializedStoredObject(Long.MAX_VALUE - 1);
    assertThat(chunk.checkDataEquals(chunkDifferByValue.getSerializedValue())).isFalse();

    OffHeapStoredObject newChunk = createValueAsSerializedStoredObject(getValue());
    assertThat(chunk.checkDataEquals(newChunk.getSerializedValue())).isTrue();

    chunk.release();
    chunkDifferByValue.release();
    newChunk.release();
  }

  @Test
  public void getDecompressedBytesShouldReturnDecompressedBytesIfCompressed() {
    Object regionEntryValue = getValue();
    byte[] regionEntryValueAsBytes = convertValueToByteArray(regionEntryValue);

    boolean isSerialized = true;
    boolean isCompressed = true;

    OffHeapStoredObject chunk = (OffHeapStoredObject) ma
        .allocateAndInitialize(regionEntryValueAsBytes, isSerialized, isCompressed);

    RegionEntryContext regionContext = mock(RegionEntryContext.class);
    CachePerfStats cacheStats = mock(CachePerfStats.class);
    Compressor compressor = mock(Compressor.class);

    long startTime = 10000L;

    // mock required things
    when(regionContext.getCompressor()).thenReturn(compressor);
    when(compressor.decompress(regionEntryValueAsBytes)).thenReturn(regionEntryValueAsBytes);
    when(regionContext.getCachePerfStats()).thenReturn(cacheStats);
    when(cacheStats.startDecompression()).thenReturn(startTime);

    // invoke the thing
    byte[] bytes = chunk.getDecompressedBytes(regionContext);

    // verify the thing happened
    verify(cacheStats, atLeastOnce()).startDecompression();
    verify(compressor, times(1)).decompress(regionEntryValueAsBytes);
    verify(cacheStats, atLeastOnce()).endDecompression(startTime);

    assertArrayEquals(regionEntryValueAsBytes, bytes);

    chunk.release();
  }

  @Test
  public void incSizeShouldIncrementSize() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());

    int beforeSize = chunk.getSize();

    chunk.incSize(1);
    assertThat(chunk.getSize()).isEqualTo(beforeSize + 1);

    chunk.incSize(2);
    assertThat(chunk.getSize()).isEqualTo(beforeSize + 1 + 2);

    chunk.release();
  }

  @Test
  public void readyForFreeShouldResetTheRefCount() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());

    int refCountBeforeFreeing = chunk.getRefCount();
    assertThat(refCountBeforeFreeing).isEqualTo(1);

    chunk.readyForFree();

    int refCountAfterFreeing = chunk.getRefCount();
    assertThat(refCountAfterFreeing).isEqualTo(0);
  }

  @Test(expected = IllegalStateException.class)
  public void readyForAllocationShouldThrowExceptionIfAlreadyAllocated() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());

    // chunk is already allocated when we created it, so calling readyForAllocation should throw
    // exception.
    chunk.readyForAllocation();

    chunk.release();
  }

  @Test
  public void checkIsAllocatedShouldReturnIfAllocated() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());
    chunk.checkIsAllocated();

    chunk.release();
  }

  @Test(expected = IllegalStateException.class)
  public void checkIsAllocatedShouldThrowExceptionIfNotAllocated() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());
    chunk.release();
    chunk.checkIsAllocated();

    chunk.release();
  }

  @Test
  public void sendToShouldWriteSerializedValueToDataOutputIfValueIsSerialized() throws IOException {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());
    OffHeapStoredObject spyChunk = spy(chunk);

    HeapDataOutputStream dataOutput = mock(HeapDataOutputStream.class);
    ByteBuffer directByteBuffer = ByteBuffer.allocate(1024);

    doReturn(directByteBuffer).when(spyChunk).createDirectByteBuffer();
    doNothing().when(dataOutput).write(directByteBuffer);

    spyChunk.sendTo(dataOutput);

    verify(dataOutput, times(1)).write(directByteBuffer);

    chunk.release();
  }

  @Test
  public void sendToShouldWriteUnserializedValueToDataOutputIfValueIsUnserialized()
      throws IOException {
    byte[] regionEntryValue = getValueAsByteArray();
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(regionEntryValue);

    // writeByte is a final method and cannot be mocked, so creating a real one
    HeapDataOutputStream dataOutput = new HeapDataOutputStream(KnownVersion.CURRENT);

    chunk.sendTo(dataOutput);

    byte[] actual = dataOutput.toByteArray();

    byte[] expected = new byte[regionEntryValue.length + 2];
    expected[0] = DSCODE.BYTE_ARRAY.toByte();
    expected[1] = (byte) regionEntryValue.length;
    System.arraycopy(regionEntryValue, 0, expected, 2, regionEntryValue.length);

    assertNotNull(dataOutput);
    assertThat(actual).isEqualTo(expected);

    chunk.release();
  }

  @Test
  public void sendAsByteArrayShouldWriteValueToDataOutput() throws IOException {
    byte[] regionEntryValue = getValueAsByteArray();
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(regionEntryValue);

    // writeByte is a final method and cannot be mocked, so creating a real one
    HeapDataOutputStream dataOutput = new HeapDataOutputStream(KnownVersion.CURRENT);

    chunk.sendAsByteArray(dataOutput);

    byte[] actual = dataOutput.toByteArray();

    byte[] expected = new byte[regionEntryValue.length + 1];
    expected[0] = (byte) regionEntryValue.length;
    System.arraycopy(regionEntryValue, 0, expected, 1, regionEntryValue.length);

    assertNotNull(dataOutput);
    assertThat(actual).isEqualTo(expected);

    chunk.release();
  }

  @Test
  public void createDirectByteBufferShouldCreateAByteBuffer() {
    byte[] regionEntryValue = getValueAsByteArray();

    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(regionEntryValue);

    ByteBuffer buffer = chunk.createDirectByteBuffer();

    byte[] actual = new byte[regionEntryValue.length];
    buffer.get(actual);

    assertArrayEquals(regionEntryValue, actual);

    chunk.release();
  }

  @Test
  public void getDirectByteBufferShouldCreateAByteBuffer() {
    byte[] regionEntryValue = getValueAsByteArray();
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(regionEntryValue);

    ByteBuffer buffer = chunk.createDirectByteBuffer();
    long bufferAddress = AddressableMemoryManager.getDirectByteBufferAddress(buffer);

    // returned address should be starting of the value (after skipping HEADER_SIZE bytes)
    assertEquals(chunk.getAddress() + OffHeapStoredObject.HEADER_SIZE, bufferAddress);

    chunk.release();
  }

  @Test(expected = AssertionError.class)
  public void getAddressForReadingDataShouldFailIfItsOutsideOfChunk() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());
    chunk.getAddressForReadingData(0, chunk.getDataSize() + 1);

    chunk.release();
  }

  @Test
  public void getAddressForReadingDataShouldReturnDataAddressFromGivenOffset() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());

    int offset = 1;
    long requestedAddress = chunk.getAddressForReadingData(offset, 1);

    assertThat(requestedAddress).isEqualTo(chunk.getBaseDataAddress() + offset);

    chunk.release();
  }

  @Test
  public void getSizeInBytesShouldReturnSize() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());
    assertThat(chunk.getSizeInBytes()).isEqualTo(chunk.getSize());

    chunk.release();
  }

  @Test(expected = AssertionError.class)
  public void getAddressForReadingDataShouldFailIfOffsetIsNegative() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());
    chunk.getAddressForReadingData(-1, 1);

    chunk.release();
  }

  @Test(expected = AssertionError.class)
  public void getAddressForReadingDataShouldFailIfSizeIsNegative() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());
    chunk.getAddressForReadingData(1, -1);

    chunk.release();
  }

  @Test(expected = AssertionError.class)
  public void readByteAndWriteByteShouldFailIfOffsetIsOutside() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());

    chunk.readDataByte(chunk.getDataSize() + 1);

    chunk.writeDataByte(chunk.getDataSize() + 1, Byte.MAX_VALUE);

    chunk.release();
  }

  @Test
  public void writeByteShouldWriteAtCorrectLocation() {
    OffHeapStoredObject chunk = createValueAsSerializedStoredObject(getValue());

    byte valueBeforeWrite = chunk.readDataByte(2);

    Byte expected = Byte.MAX_VALUE;
    chunk.writeDataByte(2, expected);

    Byte actual = chunk.readDataByte(2);

    assertThat(actual).isNotEqualTo(valueBeforeWrite);
    assertThat(actual).isEqualTo(expected);

    chunk.release();
  }

  @Test
  public void retainShouldIncrementRefCount() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    assertThat(chunk.getRefCount()).isEqualTo(1);

    chunk.retain();
    assertThat(chunk.getRefCount()).isEqualTo(2);

    chunk.retain();
    assertThat(chunk.getRefCount()).isEqualTo(3);

    chunk.release();
    chunk.release();
    chunk.release();
    boolean retainAfterRelease = chunk.retain();

    assertThat(retainAfterRelease).isFalse();
  }

  @Test(expected = IllegalStateException.class)
  public void retainShouldThrowExceptionAfterMaxNumberOfTimesRetained() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());

    // loop though and invoke retain for MAX_REF_COUNT-1 times, as create chunk above counted as one
    // reference
    for (int i = 0; i < OffHeapStoredObject.MAX_REF_COUNT - 1; i++) {
      chunk.retain();
    }

    // invoke for the one more time should throw exception
    chunk.retain();
  }

  @Test
  public void releaseShouldDecrementRefCount() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    assertThat(chunk.getRefCount()).isEqualTo(1);

    chunk.retain();
    chunk.retain();
    assertThat(chunk.getRefCount()).isEqualTo(3);

    chunk.release();
    assertThat(chunk.getRefCount()).isEqualTo(2);

    chunk.release();
    assertThat(chunk.getRefCount()).isEqualTo(1);

    chunk.retain();
    chunk.release();
    assertThat(chunk.getRefCount()).isEqualTo(1);

    chunk.release();
    assertThat(chunk.getRefCount()).isEqualTo(0);
  }

  @Test(expected = IllegalStateException.class)
  public void releaseShouldThrowExceptionIfChunkIsAlreadyReleased() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    chunk.release();
    chunk.release();
  }

  @Test
  public void testToString() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());

    String expected = ":<dataSize=" + chunk.getDataSize() + " refCount=" + chunk.getRefCount()
        + " addr=" + Long.toHexString(chunk.getAddress()) + ">";
    assertThat(chunk.toString()).endsWith(expected);

    chunk.release();
  }

  @Test
  public void getStateShouldReturnAllocatedIfRefCountIsGreaterThanZero() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    assertEquals(State.ALLOCATED, chunk.getState());

    chunk.release();
  }

  @Test
  public void getStateShouldReturnDeallocatedIfRefCountIsZero() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    chunk.release();
    assertEquals(State.DEALLOCATED, chunk.getState());
  }

  @Test(expected = UnsupportedOperationException.class)
  public void getNextBlockShouldThrowUnSupportedOperationException() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    chunk.getNextBlock();

    chunk.release();
  }

  @Test
  public void getBlockSizeShouldBeSameSameGetSize() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    assertEquals(chunk.getSize(), chunk.getBlockSize());

    chunk.release();
  }

  @Test(expected = UnsupportedOperationException.class)
  public void getSlabIdShouldThrowUnSupportedOperationException() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    chunk.getSlabId();

    chunk.release();
  }

  @Test
  public void getFreeListIdShouldReturnMinusOne() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    assertThat(chunk.getFreeListId()).isEqualTo(-1);

    chunk.release();
  }

  @Test
  public void getDataTypeShouldReturnNull() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    assertThat(chunk.getDataType()).isNull();

    chunk.release();
  }

  @Test
  public void getDataDataShouldReturnNull() {
    OffHeapStoredObject chunk = createValueAsUnserializedStoredObject(getValue());
    assertThat(chunk.getDataValue()).isNull();
  }

  @Test(expected = AssertionError.class)
  public void getRawBytesShouldThrowExceptionIfValueIsCompressed() {
    Object regionEntryValue = getValue();
    byte[] regionEntryValueAsBytes = convertValueToByteArray(regionEntryValue);

    boolean isSerialized = true;
    boolean isCompressed = true;

    OffHeapStoredObject chunk = (OffHeapStoredObject) ma
        .allocateAndInitialize(regionEntryValueAsBytes, isSerialized, isCompressed);

    chunk.getRawBytes();

    chunk.release();
  }

  @Test
  public void getSerializedValueShouldSerializeTheValue() {
    Object regionEntryValue = getValue();
    byte[] regionEntryValueAsBytes = convertValueToByteArray(regionEntryValue);

    boolean isSerialized = false;
    boolean isCompressed = false;

    OffHeapStoredObject chunk = (OffHeapStoredObject) ma
        .allocateAndInitialize(regionEntryValueAsBytes, isSerialized, isCompressed);

    byte[] serializedValue = chunk.getSerializedValue();

    assertThat(serializedValue).isEqualTo(EntryEventImpl.serialize(regionEntryValueAsBytes));

    chunk.release();
  }

  @Test
  public void fillShouldFillTheChunk() {
    boolean isSerialized = false;
    boolean isCompressed = false;

    OffHeapStoredObject chunk =
        (OffHeapStoredObject) ma.allocateAndInitialize(new byte[100], isSerialized, isCompressed);

    // first fill the unused part with FILL_PATTERN
    OffHeapStoredObject.fill(chunk.getAddress());

    // Validate that it is filled
    chunk.validateFill();

    chunk.release();
  }
}
