/**
 * 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 org.apache.hadoop.fs.ChecksumException;
import org.junit.Test;

import static org.junit.Assert.*;

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 int CHECKSUM_TYPES[] = new int[] {
    DataChecksum.CHECKSUM_CRC32, DataChecksum.CHECKSUM_CRC32C
  };
  
  @Test
  public void testBulkOps() throws Exception {
    for (int 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 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");
    int numSums = (dataLength - 1)/checksum.getBytesPerChecksum() + 1;
    int sumsLength = numSums * checksum.getChecksumSize();
    
    byte data[] = new byte[dataLength +
                           DATA_OFFSET_IN_BUFFER +
                           DATA_TRAILER_IN_BUFFER];
    new Random().nextBytes(data);
    ByteBuffer dataBuf = ByteBuffer.wrap(
        data, DATA_OFFSET_IN_BUFFER, dataLength);

    byte checksums[] = new byte[SUMS_OFFSET_IN_BUFFER + sumsLength];
    ByteBuffer checksumBuf = ByteBuffer.wrap(
        checksums, SUMS_OFFSET_IN_BUFFER, sumsLength);
    
    // Swap out for direct buffers if requested.
    if (useDirect) {
      dataBuf = directify(dataBuf);
      checksumBuf = directify(checksumBuf);
    }
    
    // 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"));
    }
  }
  
  @Test
  public void testEquality() {
    assertEquals(
        DataChecksum.newDataChecksum(DataChecksum.CHECKSUM_CRC32, 512),
        DataChecksum.newDataChecksum(DataChecksum.CHECKSUM_CRC32, 512));
    assertFalse(
        DataChecksum.newDataChecksum(DataChecksum.CHECKSUM_CRC32, 512).equals(
        DataChecksum.newDataChecksum(DataChecksum.CHECKSUM_CRC32, 1024)));
    assertFalse(
        DataChecksum.newDataChecksum(DataChecksum.CHECKSUM_CRC32, 512).equals(
        DataChecksum.newDataChecksum(DataChecksum.CHECKSUM_CRC32C, 512)));        
  }
  
  @Test
  public void testToString() {
    assertEquals("DataChecksum(type=CRC32, chunkSize=512)",
        DataChecksum.newDataChecksum(DataChecksum.CHECKSUM_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;
  }
}
