// Copyright 2016 Google Inc. All rights reserved.
//
// Licensed 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 com.google.archivepatcher.generator.bsdiff;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;

@RunWith(JUnit4.class)
public class RandomAccessObjectTest {
  private static final byte[] BLOB = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};

  @Test
  public void fileLengthTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try (RandomAccessObject obj = new RandomAccessObject.RandomAccessFileObject(tmpFile, "r")) {
      Assert.assertEquals(13, obj.length());
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void byteArrayLengthTest() throws IOException {
    try (RandomAccessObject obj = new RandomAccessObject.RandomAccessByteArrayObject(BLOB)) {
      Assert.assertEquals(13, obj.length());
    }
  }

  @Test
  public void mmapLengthTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try (RandomAccessObject obj =
        new RandomAccessObject.RandomAccessMmapObject(new RandomAccessFile(tmpFile, "r"), "r")) {
      Assert.assertEquals(13, obj.length());
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void fileReadByteTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try (RandomAccessObject obj = new RandomAccessObject.RandomAccessFileObject(tmpFile, "r")) {
      for (int x = 0; x < BLOB.length; x++) {
        Assert.assertEquals(x + 1, obj.readByte());
      }

      try {
        obj.readByte();
        Assert.fail("Should've thrown an IOException");
      } catch (IOException expected) {
      }
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void byteArrayReadByteTest() throws IOException {
    // Mix positives and negatives to test sign preservation in readByte()
    byte[] bytes = new byte[] {-128, -127, -126, -1, 0, 1, 125, 126, 127};
    try (RandomAccessObject obj = new RandomAccessObject.RandomAccessByteArrayObject(bytes)) {
      for (int x = 0; x < bytes.length; x++) {
        Assert.assertEquals(bytes[x], obj.readByte());
      }

      try {
        obj.readByte();
        Assert.fail("Should've thrown an IOException");
      } catch (BufferUnderflowException expected) {
      }
    }
  }

  @Test
  public void byteArrayReadUnsignedByteTest() throws IOException {
    // Test values above 127 to test unsigned-ness of readUnsignedByte()
    int[] ints = new int[] {255, 254, 253};
    byte[] bytes = new byte[] {(byte) 0xff, (byte) 0xfe, (byte) 0xfd};
    try (RandomAccessObject obj = new RandomAccessObject.RandomAccessByteArrayObject(bytes)) {
      for (int x = 0; x < bytes.length; x++) {
        Assert.assertEquals(ints[x], obj.readUnsignedByte());
      }

      try {
        obj.readUnsignedByte();
        Assert.fail("Should've thrown an IOException");
      } catch (BufferUnderflowException expected) {
      }
    }
  }

  @Test
  public void mmapReadByteTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try (RandomAccessObject obj =
        new RandomAccessObject.RandomAccessMmapObject(new RandomAccessFile(tmpFile, "r"), "r")) {
      for (int x = 0; x < BLOB.length; x++) {
        Assert.assertEquals(x + 1, obj.readByte());
      }

      try {
        obj.readByte();
        Assert.fail("Should've thrown an BufferUnderflowException");
      } catch (BufferUnderflowException expected) {
      }
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void fileWriteByteTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try (RandomAccessObject obj = new RandomAccessObject.RandomAccessFileObject(tmpFile, "rw")) {
      for (int x = 0; x < BLOB.length; x++) {
        obj.writeByte((byte) (5 - x));
      }

      // Writing a byte past the end of a file should be ok - this just extends the file.
      obj.writeByte((byte) 243);

      // As per RandomAccessFile documentation, the reported length should update after writing off
      // the end of a file.
      Assert.assertEquals(BLOB.length + 1, obj.length());

      obj.seek(0);
      for (int x = 0; x < BLOB.length; x++) {
        Assert.assertEquals(5 - x, obj.readByte());
      }

      // Note that because of signed bytes, if cased to an int, this would actually resolve to -13.
      Assert.assertEquals((byte) 243, obj.readByte());

      try {
        obj.readByte();
        Assert.fail("Should've thrown an IOException");
      } catch (IOException expected) {
      }
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void fileWriteByteToEmptyFileTest() throws IOException {
    File tmpFile = File.createTempFile("RandomAccessObjectTest", "temp");

    try (RandomAccessObject obj = new RandomAccessObject.RandomAccessFileObject(tmpFile, "rw")) {
      for (int x = 0; x < BLOB.length; x++) {
        obj.writeByte((byte) (5 - x));
      }

      obj.seek(0);
      for (int x = 0; x < BLOB.length; x++) {
        Assert.assertEquals(5 - x, obj.readByte());
      }

      Assert.assertEquals(BLOB.length, obj.length());
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void byteArrayWriteByteTest() throws IOException {
    final int len = 13;
    try (RandomAccessObject obj =
        new RandomAccessObject.RandomAccessByteArrayObject(new byte[len])) {
      for (int x = 0; x < len; x++) {
        obj.writeByte((byte) (5 - x));
      }

      try {
        // Writing a byte past the end of an array is not ok.
        obj.writeByte((byte) 243);
        Assert.fail("Should've thrown a BufferOverflowException");
      } catch (BufferOverflowException expected) {
      }

      obj.seek(0);
      for (int x = 0; x < len; x++) {
        Assert.assertEquals(5 - x, obj.readByte());
      }

      try {
        obj.readByte();
        Assert.fail("Should've thrown a BufferUnderflowException");
      } catch (BufferUnderflowException expected) {
      }
    }
  }

  @Test
  public void mmapWriteByteTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try (RandomAccessObject obj =
        new RandomAccessObject.RandomAccessMmapObject(new RandomAccessFile(tmpFile, "rw"), "rw")) {
      for (int x = 0; x < BLOB.length; x++) {
        obj.writeByte((byte) (5 - x));
      }

      try {
        // Writing a byte past the end of an mmap is not ok.
        obj.writeByte((byte) 243);
        Assert.fail("Should've thrown an BufferOverflowException");
      } catch (BufferOverflowException expected) {
      }

      Assert.assertEquals(BLOB.length, obj.length());

      obj.seek(0);
      for (int x = 0; x < BLOB.length; x++) {
        Assert.assertEquals(5 - x, obj.readByte());
      }

      try {
        obj.readByte();
        Assert.fail("Should've thrown an BufferUnderflowException");
      } catch (BufferUnderflowException expected) {
      }
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void mmapWriteByteToEmptyFileTest() throws IOException {
    File tmpFile = File.createTempFile("RandomAccessObjectTest", "temp");

    try (RandomAccessObject obj =
        new RandomAccessObject.RandomAccessMmapObject(new RandomAccessFile(tmpFile, "rw"), "rw")) {
      for (int x = 0; x < BLOB.length; x++) {
        try {
          // Writing a byte past the end of an mmap is not ok.
          obj.writeByte((byte) (5 - x));
          Assert.fail("Should've thrown an BufferOverflowException");
        } catch (BufferOverflowException expected) {
        }
      }

      try {
        obj.seek(BLOB.length);
        Assert.fail("Should've thrown an IllegalArgumentException");
      } catch (IllegalArgumentException expected) {
      }

      for (int x = 0; x < BLOB.length; x++) {
        try {
          obj.readByte();
          Assert.fail("Should've thrown an BufferUnderflowException");
        } catch (BufferUnderflowException expected) {
        }
      }

      Assert.assertEquals(0, obj.length());
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void fileSeekTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try {
      RandomAccessObject obj = new RandomAccessObject.RandomAccessFileObject(tmpFile, "rw");
      seekTest(obj);

      try {
        obj.seek(-1);
        Assert.fail("Should've thrown an IOException");
      } catch (IOException expected) {
      }

      // This should not throw an exception.
      obj.seek(BLOB.length);
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void byteArraySeekTest() throws IOException {
    byte[] data = new byte[BLOB.length];
    System.arraycopy(BLOB, 0, data, 0, BLOB.length);
    RandomAccessObject obj = new RandomAccessObject.RandomAccessByteArrayObject(data);
    seekTest(obj);

    try {
      obj.seek(-1);
      Assert.fail("Should've thrown an IllegalArgumentException");
    } catch (IllegalArgumentException expected) {
    }

    // Should not fail.
    obj.seek(BLOB.length);

    // Only fails once you try to read past the end.
    try {
      obj.readByte();
      Assert.fail("Should've thrown a BufferUnderflowException");
    } catch (BufferUnderflowException expected) {
    }
  }

  @Test
  public void mmapSeekTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try {
      RandomAccessObject obj =
          new RandomAccessObject.RandomAccessMmapObject(new RandomAccessFile(tmpFile, "rw"), "rw");
      seekTest(obj);

      try {
        obj.seek(-1);
        Assert.fail("Should've thrown an IllegalArgumentException");
      } catch (IllegalArgumentException expected) {
      }

      // Should not fail.
      obj.seek(BLOB.length);

      // Only fails once you try to read past the end.
      try {
        obj.readByte();
        Assert.fail("Should've thrown a BufferUnderflowException");
      } catch (BufferUnderflowException expected) {
      }
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void fileReadIntTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try {
      RandomAccessObject obj = new RandomAccessObject.RandomAccessFileObject(tmpFile, "r");
      readIntTest(obj);
      try {
        obj.readInt();
        Assert.fail("Should've thrown a EOFException");
      } catch (EOFException expected) {
      }
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void byteArrayReadIntTest() throws IOException {
    RandomAccessObject obj = new RandomAccessObject.RandomAccessByteArrayObject(BLOB);
    readIntTest(obj);
    try {
      obj.readInt();
      Assert.fail("Should've thrown a BufferUnderflowException");
    } catch (BufferUnderflowException expected) {
    }
  }

  @Test
  public void mmapReadIntTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try {
      RandomAccessObject obj =
          new RandomAccessObject.RandomAccessMmapObject(new RandomAccessFile(tmpFile, "r"), "r");
      readIntTest(obj);

      try {
        obj.readInt();
        Assert.fail("Should've thrown an BufferUnderflowException");
      } catch (BufferUnderflowException expected) {
      }
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void fileWriteIntTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try (RandomAccessObject obj = new RandomAccessObject.RandomAccessFileObject(tmpFile, "rw")) {
      for (int x = 0; x < BLOB.length / 4; x++) {
        obj.writeInt(500 + x);
      }

      obj.seekToIntAligned(0);
      for (int x = 0; x < BLOB.length / 4; x++) {
        Assert.assertEquals(500 + x, obj.readInt());
      }
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void byteArrayWriteIntTest() throws IOException {
    final int len = 13;
    try (RandomAccessObject obj =
        new RandomAccessObject.RandomAccessByteArrayObject(new byte[len])) {
      for (int x = 0; x < len / 4; x++) {
        obj.writeInt(500 + x);
      }

      obj.seek(0);
      for (int x = 0; x < len / 4; x++) {
        Assert.assertEquals(500 + x, obj.readInt());
      }
    }
  }

  @Test
  public void mmapWriteIntTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try (RandomAccessObject obj =
        new RandomAccessObject.RandomAccessMmapObject(new RandomAccessFile(tmpFile, "rw"), "rw")) {
      for (int x = 0; x < BLOB.length / 4; x++) {
        obj.writeInt(500 + x);
      }

      obj.seekToIntAligned(0);
      for (int x = 0; x < BLOB.length / 4; x++) {
        Assert.assertEquals(500 + x, obj.readInt());
      }
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void fileSeekToIntAlignedTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try {
      RandomAccessObject obj = new RandomAccessObject.RandomAccessFileObject(tmpFile, "rw");
      seekToIntAlignedTest(obj);
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void byteArraySeekToIntAlignedTest() throws IOException {
    byte[] data = new byte[BLOB.length];
    System.arraycopy(BLOB, 0, data, 0, BLOB.length);
    RandomAccessObject obj = new RandomAccessObject.RandomAccessByteArrayObject(data);
    seekToIntAlignedTest(obj);
  }

  @Test
  public void mmapSeekToIntAlignedTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try {
      RandomAccessObject obj =
          new RandomAccessObject.RandomAccessMmapObject(new RandomAccessFile(tmpFile, "rw"), "rw");
      seekToIntAlignedTest(obj);
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void fileCloseTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try {
      RandomAccessObject obj = new RandomAccessObject.RandomAccessFileObject(tmpFile, "r", true);
      obj.close();
      Assert.assertFalse(tmpFile.exists());
      tmpFile = null;
    } finally {
      if (tmpFile != null) {
        tmpFile.delete();
      }
    }

    tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try {
      RandomAccessObject obj = new RandomAccessObject.RandomAccessFileObject(tmpFile, "r");
      obj.close();
      Assert.assertTrue(tmpFile.exists());
    } finally {
      tmpFile.delete();
    }
  }

  @Test
  public void mmapCloseTest() throws IOException {
    File tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try {
      try (RandomAccessObject obj = new RandomAccessObject.RandomAccessMmapObject(tmpFile, "r")) {}
      Assert.assertFalse(tmpFile.exists());
      tmpFile = null;
    } finally {
      if (tmpFile != null) {
        tmpFile.delete();
      }
    }

    tmpFile = storeInTempFile(new ByteArrayInputStream(BLOB));

    try {
      try (RandomAccessObject obj =
          new RandomAccessObject.RandomAccessMmapObject(new RandomAccessFile(tmpFile, "r"), "r")) {}
      Assert.assertTrue(tmpFile.exists());
    } finally {
      tmpFile.delete();
    }
  }

  private void seekTest(final RandomAccessObject obj) throws IOException {
    obj.seek(7);
    Assert.assertEquals(8, obj.readByte());
    obj.seek(3);
    Assert.assertEquals(4, obj.readByte());
    obj.seek(9);
    Assert.assertEquals(10, obj.readByte());
    obj.seek(5);
    obj.writeByte((byte) 23);
    obj.seek(5);
    Assert.assertEquals(23, obj.readByte());
    obj.seek(4);
    Assert.assertEquals(5, obj.readByte());

    obj.seek(0);
    for (int x = 0; x < BLOB.length; x++) {
      if (x == 5) {
        Assert.assertEquals(23, obj.readByte());
      } else {
        Assert.assertEquals(x + 1, obj.readByte());
      }
    }
  }

  private void readIntTest(final RandomAccessObject obj) throws IOException {
    Assert.assertEquals(0x01020304, obj.readInt());
    Assert.assertEquals(0x05060708, obj.readInt());
    Assert.assertEquals(0x090A0B0C, obj.readInt());
  }

  private void seekToIntAlignedTest(final RandomAccessObject obj) throws IOException {
    obj.seekToIntAligned(3);
    Assert.assertEquals(3 * 4 + 1, obj.readByte());

    obj.seekToIntAligned(2);
    Assert.assertEquals(2 * 4 + 1, obj.readByte());
    Assert.assertEquals(0x0A0B0C0D, obj.readInt());

    obj.seekToIntAligned(0);
    Assert.assertEquals(1, obj.readByte());

    obj.seekToIntAligned(1);
    Assert.assertEquals(5, obj.readByte());
    Assert.assertEquals(0x06070809, obj.readInt());

    obj.seekToIntAligned(2);
    obj.writeInt(0x26391bd2);

    obj.seekToIntAligned(0);
    Assert.assertEquals(0x01020304, obj.readInt());
    Assert.assertEquals(0x05060708, obj.readInt());
    Assert.assertEquals(0x26391bd2, obj.readInt());
  }

  private File storeInTempFile(InputStream content) throws IOException {
    File tmpFile = null;
    try {
      tmpFile = File.createTempFile("RandomAccessObjectTest", "temp");
      tmpFile.deleteOnExit();
      FileOutputStream out = new FileOutputStream(tmpFile);
      byte[] buffer = new byte[32768];
      int numRead = 0;
      while ((numRead = content.read(buffer)) >= 0) {
        out.write(buffer, 0, numRead);
      }
      out.flush();
      out.close();
      return tmpFile;
    } catch (IOException e) {
      if (tmpFile != null) {
        // Attempt immediate cleanup.
        tmpFile.delete();
      }
      throw e;
    }
  }
}