/*
 * Copyright 2020 The Apache Software Foundation.
 *
 * 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 org.apache.pdfbox.io;

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 java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Random;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
 * Unittest for {@link org.apache.pdfbox.io.NonSeekableRandomAccessReadInputStream}
 */
class NonSeekableRandomAccessReadInputStreamTest
{
    @Test
    void testPositionSkip() throws IOException
    {
        byte[] inputValues = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        ByteArrayInputStream bais = new ByteArrayInputStream(inputValues);

        try (NonSeekableRandomAccessReadInputStream randomAccessSource = new NonSeekableRandomAccessReadInputStream(
                bais))
        {
            assertEquals(0, randomAccessSource.getPosition());
            randomAccessSource.skip(5);
            assertEquals(5, randomAccessSource.read());
            assertEquals(6, randomAccessSource.getPosition());
        }
    }

    @Test
    void testPositionRead() throws IOException
    {
        byte[] inputValues = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        ByteArrayInputStream bais = new ByteArrayInputStream(inputValues);

        NonSeekableRandomAccessReadInputStream randomAccessSource = new NonSeekableRandomAccessReadInputStream(
                bais);

        assertEquals(0, randomAccessSource.getPosition());
        assertEquals(0, randomAccessSource.read());
        assertEquals(1, randomAccessSource.read());
        assertEquals(2, randomAccessSource.read());
        assertEquals(3, randomAccessSource.getPosition());

        assertFalse(randomAccessSource.isClosed());
        randomAccessSource.close();
        assertTrue(randomAccessSource.isClosed());
    }

    @Test
    void testSeekEOF() throws IOException
    {
        byte[] inputValues = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        ByteArrayInputStream bais = new ByteArrayInputStream(inputValues);

        try (NonSeekableRandomAccessReadInputStream randomAccessSource = new NonSeekableRandomAccessReadInputStream(
                bais))
        {
            Assertions.assertThrows(IOException.class, () -> randomAccessSource.seek(3),
                    "seek should have thrown an IOException");
        }
    }

    @Test
    void testPositionReadBytes() throws IOException
    {
        byte[] inputValues = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        ByteArrayInputStream bais = new ByteArrayInputStream(inputValues);

        try (NonSeekableRandomAccessReadInputStream randomAccessSource = new NonSeekableRandomAccessReadInputStream(
                bais))
        {
            assertEquals(0, randomAccessSource.getPosition());
            byte[] buffer = new byte[4];
            randomAccessSource.read(buffer);
            assertEquals(0, buffer[0]);
            assertEquals(3, buffer[3]);
            assertEquals(4, randomAccessSource.getPosition());
            
            randomAccessSource.read(buffer, 1, 2);
            assertEquals(0, buffer[0]);
            assertEquals(4, buffer[1]);
            assertEquals(5, buffer[2]);
            assertEquals(3, buffer[3]);
            assertEquals(6, randomAccessSource.getPosition());
        }
    }

    @Test
    void testPositionPeek() throws IOException
    {
        byte[] inputValues = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        ByteArrayInputStream bais = new ByteArrayInputStream(inputValues);

        try (NonSeekableRandomAccessReadInputStream randomAccessSource = new NonSeekableRandomAccessReadInputStream(
                bais))
        {
            assertEquals(0, randomAccessSource.getPosition());
            randomAccessSource.skip(6);
            assertEquals(6, randomAccessSource.getPosition());
            
            assertEquals(6, randomAccessSource.peek());
            assertEquals(6, randomAccessSource.getPosition());
        }
    }

    @Test
    void testPositionUnreadBytes() throws IOException
    {
        byte[] inputValues = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        ByteArrayInputStream bais = new ByteArrayInputStream(inputValues);

        try (NonSeekableRandomAccessReadInputStream randomAccessSource = new NonSeekableRandomAccessReadInputStream(
                bais))
        {
            assertEquals(0, randomAccessSource.getPosition());
            randomAccessSource.read();
            randomAccessSource.read();
            byte[] readBytes = new byte[6];
            assertEquals(readBytes.length, randomAccessSource.read(readBytes));
            assertEquals(8, randomAccessSource.getPosition());
            randomAccessSource.rewind(readBytes.length);
            assertEquals(2, randomAccessSource.getPosition());
            assertEquals(2, randomAccessSource.read());
            assertEquals(3, randomAccessSource.getPosition());
            randomAccessSource.read(readBytes, 2, 4);
            assertEquals(7, randomAccessSource.getPosition());
            randomAccessSource.rewind(4);
            assertEquals(3, randomAccessSource.getPosition());

            // PDFBOX-5965: check that it also works near EOF
            assertEquals(3, randomAccessSource.read());
            assertEquals(4, randomAccessSource.read());
            assertEquals(5, randomAccessSource.read());
            assertEquals(6, randomAccessSource.read());
            assertEquals(7, randomAccessSource.read());
            assertEquals(8, randomAccessSource.read());
            assertEquals(9, randomAccessSource.read());
            assertEquals(10, randomAccessSource.read());
            assertEquals(-1, randomAccessSource.read());
            assertTrue(randomAccessSource.isEOF());
            randomAccessSource.rewind(4);
            assertFalse(randomAccessSource.isEOF());
            assertEquals(7, randomAccessSource.read());
            assertEquals(8, randomAccessSource.read());
            assertEquals(9, randomAccessSource.read());
            assertEquals(10, randomAccessSource.read());
            assertEquals(-1, randomAccessSource.read());
        }
    }

    @Test
    void testView() throws IOException
    {
        byte[] inputValues = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        ByteArrayInputStream bais = new ByteArrayInputStream(inputValues);

        try (NonSeekableRandomAccessReadInputStream randomAccessSource = new NonSeekableRandomAccessReadInputStream(
                bais))
        {
            Assertions.assertThrows(IOException.class, () -> randomAccessSource.createView(3, 5),
                    "createView should have thrown an IOException");
        }
    }

    @Test
    void testBufferSwitch() throws IOException
    {
        byte[] original = createRandomData();

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(original);
        try (RandomAccessRead rar = new NonSeekableRandomAccessReadInputStream(
                byteArrayInputStream))
        {
            rar.skip(4098);
            assertEquals(4098, rar.getPosition());
            rar.rewind(4);
            assertEquals(4094, rar.getPosition());
            assertEquals(original[4094] & 0xFF, rar.read());
        }
    }

    @Test
    void testRewindException() throws IOException
    {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(createRandomData());
        try (RandomAccessRead rar = new NonSeekableRandomAccessReadInputStream(
                byteArrayInputStream))
        {
            rar.skip(10000);
            assertEquals(10000, rar.getPosition());
            rar.rewind(4096);
            assertEquals(5904, rar.getPosition());
            Assertions.assertThrows(IOException.class, () -> rar.rewind(4096),
                    "createView should have thrown an IOException");
        }
    }

    @Test
    void testRewindAcrossBuffers() throws IOException
    {
        byte[] ba = new byte[4096 + 5];
        int rewSize = 7;
        byte testVal = 123;
        ba[ba.length - rewSize] = testVal;
        ByteArrayInputStream bais = new ByteArrayInputStream(ba);
        try (RandomAccessRead rar = new NonSeekableRandomAccessReadInputStream(bais))
        {
            int len = rar.read(new byte[ba.length - rewSize]);
            assertEquals(ba.length - rewSize, len);
            len = rar.read(new byte[rewSize]);
            assertEquals(rewSize, len);
            int by = rar.read();
            assertEquals(-1, by);
            assertTrue(rar.isEOF());
            rar.rewind(len);
            by = rar.read(); // went ArrayIndexOutOfBoundsException here
            assertEquals(testVal, by);
        }
    }

    @Test
    void testRewindAcrossBuffers2() throws IOException
    {
        byte[] ba = new byte[4096 * 2];
        ba[4095] = 1;
        ba[4096] = 2;
        ba[4097] = 3;
        ba[4096 * 2 - 1] = 4;
        ByteArrayInputStream bais = new ByteArrayInputStream(ba);
        try (RandomAccessRead rar = new NonSeekableRandomAccessReadInputStream(bais))
        {
            assertEquals(0, rar.length()); // not really what I'd expect...
            int len = rar.read(new byte[4096 + 1]);
            assertEquals(4096 * 2, rar.length());
            assertEquals(4096 + 1, len);
            rar.rewind(2);
            assertEquals(1, rar.read());
            assertEquals(2, rar.read());
            assertEquals(3, rar.read());
            assertEquals(4096 * 2, rar.length());

            byte[] buf = new byte[4096];
            len = rar.read(buf);
            assertEquals(4096 - 2, len);
            assertEquals(4, buf[len-1]);
            assertEquals(-1, rar.read());
            assertEquals(-1, rar.read(new byte[1]));
        }
    }

    @Test
    void testAccessClosed() throws IOException
    {
        byte[] ba = new byte[1];
        ba[0] = 1;
        ByteArrayInputStream bais = new ByteArrayInputStream(ba);
        RandomAccessRead rar = new NonSeekableRandomAccessReadInputStream(bais);
        assertEquals(1, rar.read());
        assertEquals(-1, rar.read());
        rar.close();
        Assertions.assertThrows(IOException.class, rar::read,
                    "read() should have thrown an IOException");
    }

    private byte[] createRandomData()
    {
        final long seed = new Random().nextLong();
        final Random random = new Random(seed);
        final int numBytes = 10000 + random.nextInt(20000);
        byte[] original = new byte[numBytes];

        int upto = 0;
        while (upto < numBytes)
        {
            final int left = numBytes - upto;
            if (random.nextBoolean() || left < 2)
            {
                // Fill w/ pseudo-random bytes:
                final int end = upto + Math.min(left, 10 + random.nextInt(100));
                while (upto < end)
                {
                    original[upto++] = (byte) random.nextInt();
                }
            }
            else
            {
                // Fill w/ very predictable bytes:
                final int end = upto + Math.min(left, 2 + random.nextInt(10));
                final byte value = (byte) random.nextInt(4);
                while (upto < end)
                {
                    original[upto++] = value;
                }
            }
        }
        return original;
    }

    /**
     * PDFBOX-5158: endless loop reading a stream of a multiple of 4096 bytes from a FileInputStream. Test does not fail
     * with a ByteArrayInputStream, so we need to create a temp file.
     *
     * @throws IOException
     */
    @Test
    void testPDFBOX5158() throws IOException
    {
        Path path = Files.createTempFile("len4096", ".pdf");
        try (OutputStream os = Files.newOutputStream(path))
        {
            os.write(new byte[4096]);
        }
        assertEquals(4096, path.toFile().length());
        try (RandomAccessRead rar = new NonSeekableRandomAccessReadInputStream(
                Files.newInputStream(path)))
        {
            assertEquals(0, rar.read());
        }
        Files.delete(path);
    }

    /**
     * PDFBOX-5161: failure to read bytes after reading a multiple of 4096. Construction source must be an InputStream.
     *
     * @throws IOException
     */
    @Test
    void testPDFBOX5161() throws IOException
    {
        try (RandomAccessRead rar = new NonSeekableRandomAccessReadInputStream(
                new ByteArrayInputStream(new byte[4099])))
        {
            byte[] buf = new byte[4096];
            int bytesRead = rar.read(buf);
            assertEquals(4096, bytesRead);
            bytesRead = rar.read(buf, 0, 3);
            assertEquals(3, bytesRead);
        }
    }

}
