package com.bumptech.glide.load.resource.bitmap;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = 18)
public class RecyclableBufferedInputStreamTest {

  private static final int DATA_SIZE = 30;
  private static final int BUFFER_SIZE = 10;

  private RecyclableBufferedInputStream stream;
  private byte[] data;
  private ArrayPool byteArrayPool;

  @Before
  public void setUp() {
    data = new byte[DATA_SIZE];
    for (int i = 0; i < DATA_SIZE; i++) {
      data[i] = (byte) i;
    }

    byteArrayPool = new LruArrayPool();
    InputStream wrapped = new ByteArrayInputStream(data);
    stream = new RecyclableBufferedInputStream(wrapped, byteArrayPool, BUFFER_SIZE);
  }

  @Test
  public void testReturnsTrueForMarkSupported() {
    assertTrue(stream.markSupported());
  }

  @Test
  public void testCanReadIndividualBytes() throws IOException {
    for (int i = 0; i < data.length; i++) {
      assertEquals(i, stream.read());
    }
    assertEquals(-1, stream.read());
  }

  @Test
  public void testCanReadBytesInBulkLargerThanBufferSize() throws IOException {
    byte[] buffer = new byte[DATA_SIZE];
    assertEquals(DATA_SIZE, stream.read(buffer, 0, DATA_SIZE));
    for (int i = 0; i < DATA_SIZE; i++) {
      assertEquals(i, buffer[i]);
    }
  }

  @Test
  public void testCanReadBytesInBulkSmallerThanBufferSize() throws IOException {
    int toRead = BUFFER_SIZE / 2;
    byte[] buffer = new byte[toRead];
    assertEquals(toRead, stream.read(buffer, 0, toRead));
    for (int i = 0; i < toRead; i++) {
      assertEquals(i, buffer[i]);
    }
  }

  @Test
  public void testReadingZeroBytesIntoBufferReadsZeroBytes() throws IOException {
    // Make sure the next value is not 0.
    stream.read();
    byte[] buffer = new byte[BUFFER_SIZE];
    assertEquals(0, stream.read(buffer, 0, 0));

    for (int i = 0; i < BUFFER_SIZE; i++) {
      assertEquals(0, buffer[i]);
    }
  }

  @Test
  public void testCanReadIntoBufferLargerThanDataSize() throws IOException {
    int toRead = DATA_SIZE * 2;
    byte[] buffer = new byte[toRead];
    assertEquals(DATA_SIZE, stream.read(buffer, 0, toRead));
    for (int i = 0; i < DATA_SIZE; i++) {
      assertEquals(i, buffer[i]);
    }
    for (int i = DATA_SIZE; i < toRead; i++) {
      assertEquals(0, buffer[i]);
    }
  }

  @Test
  public void testCanReadBytesInBulkWithLimit() throws IOException {
    int toRead = BUFFER_SIZE / 2;
    byte[] buffer = new byte[BUFFER_SIZE];
    assertEquals(toRead, stream.read(buffer, 0, toRead));

    // 0, 1, 2, 3, 4, 0, 0, 0, 0, 0
    for (int i = 0; i < toRead; i++) {
      assertEquals(i, buffer[i]);
    }
    for (int i = toRead; i < BUFFER_SIZE; i++) {
      assertEquals(0, buffer[i]);
    }
  }

  @Test
  public void testCanReadBytesInBulkWithOffset() throws IOException {
    int toRead = BUFFER_SIZE / 2;
    byte[] buffer = new byte[BUFFER_SIZE];
    assertEquals(toRead, stream.read(buffer, BUFFER_SIZE - toRead, toRead));
    // 0, 0, 0, 0, 0, 0, 1, 2, 3, 4
    for (int i = 0; i < toRead; i++) {
      assertEquals(0, buffer[i]);
    }
    for (int i = toRead; i < BUFFER_SIZE; i++) {
      assertEquals(i - toRead, buffer[i]);
    }
  }

  @Test
  public void testCanReadBytesInBulkWhenSomeButNotAllBytesAreInBuffer() throws IOException {
    stream.read();
    byte[] buffer = new byte[BUFFER_SIZE];
    assertEquals(BUFFER_SIZE, stream.read(buffer, 0, BUFFER_SIZE));
    for (int i = 1; i < BUFFER_SIZE + 1; i++) {
      assertEquals(i, buffer[i - 1]);
    }
  }

  @Test
  public void testCanSkipBytes() throws IOException {
    int toSkip = data.length / 2;
    assertEquals(toSkip, stream.skip(toSkip));
    for (int i = toSkip; i < data.length; i++) {
      assertEquals(i, stream.read());
    }
    assertEquals(-1, stream.read());
  }

  @Test
  public void testSkipReturnsZeroIfSkipByteCountIsZero() throws IOException {
    assertEquals(0, stream.skip(0));
    assertEquals(0, stream.read());
  }

  @Test
  public void testSkipReturnsZeroIfSkipByteCountIsNegative() throws IOException {
    assertEquals(0, stream.skip(-13));
    assertEquals(0, stream.read());
  }

  @Test
  public void testCloseClosesWrappedStream() throws IOException {
    InputStream wrapped = mock(InputStream.class);
    stream = new RecyclableBufferedInputStream(wrapped, byteArrayPool);
    stream.close();
    verify(wrapped).close();
  }

  @Test
  public void testCanSafelyBeClosedMultipleTimes() throws IOException {
    InputStream wrapped = mock(InputStream.class);
    stream = new RecyclableBufferedInputStream(wrapped, byteArrayPool);
    stream.close();
    stream.close();
    stream.close();

    verify(wrapped, times(1)).close();
  }

  @Test
  public void testCanMarkAndReset() throws IOException {
    byte[] buffer = new byte[BUFFER_SIZE];
    stream.mark(BUFFER_SIZE);
    assertEquals(BUFFER_SIZE, stream.read(buffer, 0, BUFFER_SIZE));
    for (int i = 0; i < BUFFER_SIZE; i++) {
      assertEquals(i, buffer[i]);
    }
    Arrays.fill(buffer, (byte) 0);
    stream.reset();

    assertEquals(BUFFER_SIZE, stream.read(buffer, 0, BUFFER_SIZE));
    for (int i = 0; i < BUFFER_SIZE; i++) {
      assertEquals(i, buffer[i]);
    }
  }

  @Test
  public void testCanResetRepeatedlyAfterMarking() throws IOException {
    byte[] buffer = new byte[BUFFER_SIZE];
    stream.mark(BUFFER_SIZE);
    for (int repeat = 0; repeat < 10; repeat++) {
      assertEquals(BUFFER_SIZE, stream.read(buffer, 0, BUFFER_SIZE));
      for (int i = 0; i < BUFFER_SIZE; i++) {
        assertEquals(i, buffer[i]);
      }
      stream.reset();
    }
  }

  @Test
  public void testCanMarkInMiddleOfBufferAndStillReadUpToBufferLengthBeforeResetting()
      throws IOException {
    int markPos = BUFFER_SIZE / 2;
    for (int i = 0; i < markPos; i++) {
      stream.read();
    }
    stream.mark(BUFFER_SIZE);

    for (int i = 0; i < BUFFER_SIZE; i++) {
      stream.read();
    }

    stream.reset();
    assertEquals(markPos, stream.read());
  }

  @Test
  public void testAvailableReturnsWrappedAvailableIfNoBytesRead() throws IOException {
    assertEquals(DATA_SIZE, stream.available());
  }

  @Test
  public void testAvailableIncludesDataInBufferAndWrappedAvailableIfBytesRead() throws IOException {
    stream.read();
    assertEquals(DATA_SIZE - 1, stream.available());
  }

  @Test(expected = IOException.class)
  public void testCloseThrowsIfWrappedStreamThrowsOnClose() throws IOException {
    InputStream wrapped = mock(InputStream.class);
    doThrow(new IOException()).when(wrapped).close();
    stream = new RecyclableBufferedInputStream(wrapped, byteArrayPool);
    stream.close();
  }

  @Test(expected = IOException.class)
  public void testAvailableThrowsIfStreamIsClosed() throws IOException {
    stream.close();
    stream.available();
  }

  @Test(expected = IOException.class)
  public void testReadThrowsIfStreamIsClosed() throws IOException {
    stream.close();
    stream.read();
  }

  @Test(expected = IOException.class)
  public void testReadBulkThrowsIfStreamIsClosed() throws IOException {
    stream.close();
    stream.read(new byte[1], 0, 1);
  }

  @Test(expected = IOException.class)
  public void testResetThrowsIfStreamIsClosed() throws IOException {
    stream.close();
    stream.reset();
  }

  @Test(expected = IOException.class)
  public void testSkipThrowsIfStreamIsClosed() throws IOException {
    stream.close();
    stream.skip(10);
  }

  @Test(expected = RecyclableBufferedInputStream.InvalidMarkException.class)
  public void testResetThrowsIfMarkNotSet() throws IOException {
    stream.reset();
  }

  @Test(expected = RecyclableBufferedInputStream.InvalidMarkException.class)
  public void testResetThrowsIfMarkIsInvalid() throws IOException {
    stream.mark(1);
    stream.read(new byte[BUFFER_SIZE], 0, BUFFER_SIZE);
    stream.read();
    stream.reset();
  }
}
