/*
 * 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.tika.io;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;

import org.junit.Test;

/**
 * Test class for {@code TailStream}.
 */
public class TailStreamTest
{
    /** Constant for generating test text. */
    private static final String TEXT =
            "Lorem ipsum dolor sit amet, consetetur "
                    + "sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut "
                    + "labore et dolore magna aliquyam erat, sed diam voluptua. At vero"
                    + " eos et accusam et justo duo dolores et ea rebum. Stet clita "
                    + "kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor "
                    + "sit amet.";

    /**
     * Generates a test text using the specified parameters.
     * 
     * @param from the start index of the text
     * @param length the length of the text
     * @return the generated test text
     */
    private static String generateText(int from, int length)
    {
        int count = from + length;
        StringBuilder buf = new StringBuilder(count);
        while (buf.length() < count)
        {
            buf.append(TEXT);
        }
        return buf.substring(from, from + length);
    }

    /**
     * Generates a stream which contains a test text.
     * 
     * @param from the start index of the text
     * @param length the length of the generated stream
     * @return the stream with the test text
     */
    private static InputStream generateStream(int from, int length)
    {
        return new ByteArrayInputStream(generateText(from, length).getBytes(UTF_8));
    }

    /**
     * Helper method for reading the content of an input stream.
     * 
     * @param in the stream to be read
     * @return an array with the content of the stream
     * @throws IOException if an error occurs
     */
    private static byte[] readStream(InputStream in) throws IOException
    {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        int c;
        while ((c = in.read()) != -1)
        {
            bos.write(c);
        }
        return bos.toByteArray();
    }

    /**
     * Tests whether the tail buffer can be obtained before data was read.
     */
    @Test
    public void testTailBeforeRead() throws IOException
    {
        TailStream stream = new TailStream(generateStream(0, 100), 50);
        assertEquals("Wrong buffer length", 0, stream.getTail().length);
        stream.close();
    }

    /**
     * Tests the content of the tail buffer if it is only partly filled.
     */
    @Test
    public void testTailBufferPartlyRead() throws IOException
    {
        final int count = 64;
        TailStream stream = new TailStream(generateStream(0, count), 2 * count);
        byte[] data = readStream(stream);
        assertTrue("Wrong content", Arrays.equals(data, stream.getTail()));
        stream.close();
    }

    /**
     * Tests the content of the tail buffer if only single bytes were read.
     */
    @Test
    public void testTailSingleByteReads() throws IOException
    {
        final int count = 128;
        TailStream stream = new TailStream(generateStream(0, 2 * count), count);
        readStream(stream);
        assertEquals("Wrong buffer", generateText(count, count), new String(
                stream.getTail(), UTF_8));
    }

    /**
     * Tests the content of the tail buffer if larger chunks are read.
     */
    @Test
    public void testTailChunkReads() throws IOException
    {
        final int count = 16384;
        final int tailSize = 61;
        final int bufSize = 100;
        TailStream stream = new TailStream(generateStream(0, count), tailSize);
        byte[] buf = new byte[bufSize];
        int read = stream.read(buf, 10, 8);
        assertEquals("Wrong number of bytes read", 8, read);
        while (read != -1)
        {
            read = stream.read(buf);
        }
        assertEquals("Wrong buffer", generateText(count - tailSize, tailSize),
                new String(stream.getTail(), UTF_8));
        stream.close();
    }

    /**
     * Tests whether mark() and reset() work as expected.
     */
    @Test
    public void testReadWithMarkAndReset() throws IOException
    {
        final int tailSize = 64;
        TailStream stream =
                new TailStream(generateStream(0, 2 * tailSize), tailSize);
        byte[] buf = new byte[tailSize / 2];
        stream.read(buf);
        stream.mark(tailSize);
        stream.read(buf);
        stream.reset();
        readStream(stream);
        assertEquals("Wrong buffer", generateText(tailSize, tailSize),
                new String(stream.getTail(), UTF_8));
    }

    /**
     * Tests whether a reset() operation without a mark is simply ignored.
     */
    @Test
    public void testResetWithoutMark() throws IOException
    {
        final int tailSize = 75;
        final int count = 128;
        TailStream stream = new TailStream(generateStream(0, count), tailSize);
        stream.reset();
        byte[] buf = new byte[count];
        stream.read(buf);
        assertEquals("Wrong buffer", generateText(count - tailSize, tailSize),
                new String(stream.getTail(), UTF_8));
        stream.close();
    }

    /**
     * Tests whether skip() also fills the tail buffer.
     */
    @Test
    public void testSkip() throws IOException
    {
        final int tailSize = 128;
        final int count = 1024;
        final int skipCount = 512;
        TailStream stream = new TailStream(generateStream(0, count), tailSize);
        assertEquals("Wrong skip result", skipCount, stream.skip(skipCount));
        assertEquals("Wrong buffer",
                generateText(skipCount - tailSize, tailSize),
                new String(stream.getTail(), UTF_8));
        stream.close();
    }

    /**
     * Tests a skip operation at the end of the stream.
     */
    @Test
    public void testSkipEOS() throws IOException
    {
        final int count = 128;
        TailStream stream = new TailStream(generateStream(0, count), 2 * count);
        assertEquals("Wrong skip result", count, stream.skip(2 * count));
        assertEquals("Wrong buffer", generateText(0, count),
                new String(stream.getTail(), UTF_8));
        stream.close();
    }

    /**
     * Tests skip() if read reaches the end of the stream and returns -1.
     */
    @Test
    public void testSkipReadEnd() throws IOException
    {
        final int count = 128;
        TailStream stream = new TailStream(generateStream(0, count), 2 * count);
        readStream(stream);
        assertEquals("Wrong result", -1, stream.skip(1));
    }
}
