/**
 * 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.hadoop.util;

import java.nio.ByteBuffer;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import org.apache.hadoop.fs.ChecksumException;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

public class TestDataChecksum {

  // Set up buffers that have some header and trailer before the
  // actual data or checksums, to make sure the code handles
  // buffer.position(), limit, etc correctly.
  private static final int SUMS_OFFSET_IN_BUFFER = 3;
  private static final int DATA_OFFSET_IN_BUFFER = 3;
  private static final int DATA_TRAILER_IN_BUFFER = 3;
  
  private static final int BYTES_PER_CHUNK = 512;
  private static final DataChecksum.Type CHECKSUM_TYPES[] = {
    DataChecksum.Type.CRC32, DataChecksum.Type.CRC32C
  };
  
  @Test
  public void testBulkOps() throws Exception {
    for (DataChecksum.Type type : CHECKSUM_TYPES) {
      System.err.println(
          "---- beginning tests with checksum type " + type  + "----");
      DataChecksum checksum = DataChecksum.newDataChecksum(
          type, BYTES_PER_CHUNK);
      for (boolean useDirect : new boolean[]{false, true}) {
        doBulkTest(checksum, 1023, useDirect);
        doBulkTest(checksum, 1024, useDirect);
        doBulkTest(checksum, 1025, useDirect);
      }
    }
  }

  private static class Harness {
    final DataChecksum checksum;
    final int dataLength, sumsLength, numSums;
    ByteBuffer dataBuf, checksumBuf;

    Harness(DataChecksum checksum, int dataLength, boolean useDirect) {
      this.checksum = checksum;
      this.dataLength = dataLength;

      numSums = (dataLength - 1)/checksum.getBytesPerChecksum() + 1;
      sumsLength = numSums * checksum.getChecksumSize();

      byte data[] = new byte[dataLength +
                             DATA_OFFSET_IN_BUFFER +
                             DATA_TRAILER_IN_BUFFER];
      new Random().nextBytes(data);
      dataBuf = ByteBuffer.wrap(
        data, DATA_OFFSET_IN_BUFFER, dataLength);

      byte checksums[] = new byte[SUMS_OFFSET_IN_BUFFER + sumsLength];
      checksumBuf = ByteBuffer.wrap(
        checksums, SUMS_OFFSET_IN_BUFFER, sumsLength);

      // Swap out for direct buffers if requested.
      if (useDirect) {
        dataBuf = directify(dataBuf);
        checksumBuf = directify(checksumBuf);
      }
    }

    void testCorrectness() throws ChecksumException {
      // calculate real checksum, make sure it passes
      checksum.calculateChunkedSums(dataBuf, checksumBuf);
      checksum.verifyChunkedSums(dataBuf, checksumBuf, "fake file", 0);

      // Change a byte in the header and in the trailer, make sure
      // it doesn't affect checksum result
      corruptBufferOffset(checksumBuf, 0);
      checksum.verifyChunkedSums(dataBuf, checksumBuf, "fake file", 0);
      corruptBufferOffset(dataBuf, 0);
      dataBuf.limit(dataBuf.limit() + 1);
      corruptBufferOffset(dataBuf, dataLength + DATA_OFFSET_IN_BUFFER);
      dataBuf.limit(dataBuf.limit() - 1);
      checksum.verifyChunkedSums(dataBuf, checksumBuf, "fake file", 0);

      // Make sure bad checksums fail - error at beginning of array
      corruptBufferOffset(checksumBuf, SUMS_OFFSET_IN_BUFFER);
      try {
        checksum.verifyChunkedSums(dataBuf, checksumBuf, "fake file", 0);
        fail("Did not throw on bad checksums");
      } catch (ChecksumException ce) {
        assertEquals(0, ce.getPos());
      }

      // Make sure bad checksums fail - error at end of array
      uncorruptBufferOffset(checksumBuf, SUMS_OFFSET_IN_BUFFER);
      corruptBufferOffset(checksumBuf, SUMS_OFFSET_IN_BUFFER + sumsLength - 1);
      try {
        checksum.verifyChunkedSums(dataBuf, checksumBuf, "fake file", 0);
        fail("Did not throw on bad checksums");
      } catch (ChecksumException ce) {
        int expectedPos = checksum.getBytesPerChecksum() * (numSums - 1);
        assertEquals(expectedPos, ce.getPos());
        assertTrue(ce.getMessage().contains("fake file"));
      }
    }
  }

  private void doBulkTest(DataChecksum checksum, int dataLength,
      boolean useDirect) throws Exception {
    System.err.println("Testing bulk checksums of length " +
        dataLength + " with " +
        (useDirect ? "direct" : "array-backed") + " buffers");

    new Harness(checksum, dataLength, useDirect).testCorrectness();
  }

  /**
   * Simple performance test for the "common case" checksum usage in HDFS:
   * computing and verifying CRC32C with 512 byte chunking on native
   * buffers.
   */
  @Test
  public void commonUsagePerfTest() throws Exception {
    final int NUM_RUNS = 5;
    final DataChecksum checksum = DataChecksum.newDataChecksum(DataChecksum.Type.CRC32C, 512);
    final int dataLength = 512 * 1024 * 1024;
    Harness h = new Harness(checksum, dataLength, true);

    for (int i = 0; i < NUM_RUNS; i++) {
      StopWatch s = new StopWatch().start();
      // calculate real checksum, make sure it passes
      checksum.calculateChunkedSums(h.dataBuf, h.checksumBuf);
      s.stop();
      System.err.println("Calculate run #" + i + ": " +
                         s.now(TimeUnit.MICROSECONDS) + "us");

      s = new StopWatch().start();
      // calculate real checksum, make sure it passes
      checksum.verifyChunkedSums(h.dataBuf, h.checksumBuf, "fake file", 0);
      s.stop();
      System.err.println("Verify run #" + i + ": " +
                         s.now(TimeUnit.MICROSECONDS) + "us");
    }
  }

  @Test
  public void testEquality() {
    assertEquals(
        DataChecksum.newDataChecksum(DataChecksum.Type.CRC32, 512),
        DataChecksum.newDataChecksum(DataChecksum.Type.CRC32, 512));
    assertFalse(
        DataChecksum.newDataChecksum(DataChecksum.Type.CRC32, 512).equals(
        DataChecksum.newDataChecksum(DataChecksum.Type.CRC32, 1024)));
    assertFalse(
        DataChecksum.newDataChecksum(DataChecksum.Type.CRC32, 512).equals(
        DataChecksum.newDataChecksum(DataChecksum.Type.CRC32C, 512)));        
  }
  
  @Test
  public void testToString() {
    assertEquals("DataChecksum(type=CRC32, chunkSize=512)",
        DataChecksum.newDataChecksum(DataChecksum.Type.CRC32, 512).toString());
  }

  private static void corruptBufferOffset(ByteBuffer buf, int offset) {
    buf.put(offset, (byte)(buf.get(offset) + 1));    
  }

  private static void uncorruptBufferOffset(ByteBuffer buf, int offset) {
    buf.put(offset, (byte)(buf.get(offset) - 1));    
  }

  private static ByteBuffer directify(ByteBuffer dataBuf) {
    ByteBuffer newBuf = ByteBuffer.allocateDirect(dataBuf.capacity());
    newBuf.position(dataBuf.position());
    newBuf.mark();
    newBuf.put(dataBuf);
    newBuf.reset();
    newBuf.limit(dataBuf.limit());
    return newBuf;
  }

  @Test
  public void testCrc32() throws Exception {
    new Crc32PerformanceTest(8, 3, true).run();
    new Crc32PerformanceTest(8, 3, false).run();
  }
}
