/*
 * 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.catchThrowable;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

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

import org.apache.geode.util.internal.GeodeGlossary;

/**
 * Tests fill pattern validation for the {@link MemoryAllocatorImpl}.
 */
public class MemoryAllocatorFillPatternJUnitTest {

  /** Size of single test slab. */
  private static final int SLAB_SIZE = 1024 * 1024 * 50;

  /** Canned data for write operations. */
  private static final byte[] WRITE_BYTES = "Some string data.".getBytes();

  /** Chunk size for basic huge allocation test. */
  private static final int HUGE_CHUNK_SIZE = 1024 * 200;

  /** The number of chunks to allocate in order to force defragmentation. */
  private static final int DEFRAGMENTATION_CHUNKS = 3;

  /** Our slab size divided in three (with some padding for safety). */
  private static final int DEFRAGMENTATION_CHUNK_SIZE = (SLAB_SIZE / DEFRAGMENTATION_CHUNKS) - 1024;

  /** This should force defragmentation when allocated. */
  private static final int FORCE_DEFRAGMENTATION_CHUNK_SIZE = DEFRAGMENTATION_CHUNK_SIZE * 2;

  /** Our test victim. */
  private MemoryAllocatorImpl allocator = null;

  /** Our test victim's memory slab. */
  private SlabImpl slab = null;

  /**
   * Enables fill validation and creates the test victim.
   */
  @Before
  public void setUp() throws Exception {
    System.setProperty(GeodeGlossary.GEMFIRE_PREFIX + "validateOffHeapWithFill", "true");
    slab = new SlabImpl(SLAB_SIZE);
    allocator = MemoryAllocatorImpl.createForUnitTest(new NullOutOfOffHeapMemoryListener(),
        new NullOffHeapMemoryStats(), new SlabImpl[] {slab});
  }

  /**
   * Frees off heap memory.
   */
  @After
  public void tearDown() throws Exception {
    MemoryAllocatorImpl.freeOffHeapMemory();
    System.clearProperty(GeodeGlossary.GEMFIRE_PREFIX + "validateOffHeapWithFill");
  }

  /**
   * This tests the fill pattern for a single tiny Chunk allocation.
   *
   */
  @Test
  public void testFillPatternBasicForTinyAllocations() {
    doFillPatternBasic(1024);
  }

  /**
   * This tests the fill pattern for a single huge Chunk allocation.
   *
   */
  @Test
  public void testFillPatternBasicForHugeAllocations() {
    doFillPatternBasic(HUGE_CHUNK_SIZE);
  }

  private void doFillPatternBasic(final int chunkSize) {
    /*
     * Pull a chunk off the fragment. This will have no fill because it is a "fresh" chunk.
     */
    OffHeapStoredObject chunk1 = (OffHeapStoredObject) allocator.allocate(chunkSize);

    /*
     * Chunk should have valid fill from initial fragment allocation.
     */
    chunk1.validateFill();

    // "Dirty" the chunk so the release has something to fill over
    chunk1.writeDataBytes(OffHeapStoredObject.MIN_CHUNK_SIZE + 1, WRITE_BYTES);

    // This should free the Chunk (ref count == 1)
    chunk1.release();

    /*
     * This chunk should have a fill because it was reused from the free list (assuming no
     * fragmentation at this point...)
     */
    OffHeapStoredObject chunk = (OffHeapStoredObject) allocator.allocate(chunkSize);

    // Make sure we have a fill this time
    chunk.validateFill();

    // Give the fill code something to write over during the release
    chunk.writeDataBytes(OffHeapStoredObject.MIN_CHUNK_SIZE + 1, WRITE_BYTES);
    chunk.release();

    // Again, make sure the release implemented the fill
    chunk.validateFill();

    // "Dirty up" the free chunk
    chunk.writeDataBytes(OffHeapStoredObject.MIN_CHUNK_SIZE + 1, WRITE_BYTES);

    Throwable thrown = catchThrowable(chunk::validateFill);
    assertTrue(thrown instanceof IllegalStateException);
    assertEquals(
        "Fill pattern violated for chunk " + chunk.getAddress() + " with size " + chunk.getSize(),
        thrown.getMessage());

  }

  /**
   * This tests that fill validation is working properly on newly created fragments after a
   * defragmentation.
   *
   */
  @Test
  public void testFillPatternAfterDefragmentation() {
    /*
     * Stores our allocated memory.
     */
    OffHeapStoredObject[] allocatedChunks = new OffHeapStoredObject[DEFRAGMENTATION_CHUNKS];

    /*
     * Use up most of our memory Our memory looks like [ ][ ][ ]
     */
    for (int i = 0; i < allocatedChunks.length; ++i) {
      allocatedChunks[i] =
          (OffHeapStoredObject) allocator.allocate(DEFRAGMENTATION_CHUNK_SIZE);
      allocatedChunks[i].validateFill();
    }

    /*
     * Release some of our allocated chunks.
     */
    for (int i = 0; i < 2; ++i) {
      allocatedChunks[i].release();
      allocatedChunks[i].validateFill();
    }

    /*
     * Now, allocate another chunk that is slightly larger than one of our initial chunks. This
     * should force a defragmentation causing our memory to look like [ ][ ].
     */
    OffHeapStoredObject slightlyLargerChunk =
        (OffHeapStoredObject) allocator.allocate(FORCE_DEFRAGMENTATION_CHUNK_SIZE);

    /*
     * Make sure the defragmented memory has the fill validation.
     */
    slightlyLargerChunk.validateFill();
  }
}
