/*
 * 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.jackrabbit.oak.commons;

import junit.framework.TestCase;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Test the utility classes.
 */
public class IOUtilsTest extends TestCase {

    public void testReadFully() throws IOException {
        final Random r = new Random(1);
        byte[] data = new byte[1000];
        final AtomicInteger readCount = new AtomicInteger();
        r.nextBytes(data);
        FilterInputStream in = new FilterInputStream(new ByteArrayInputStream(data)) {
            @Override
            public int read(byte[] buffer, int off, int max) throws IOException {
                readCount.incrementAndGet();
                if (r.nextInt(10) == 0) {
                    return 0;
                }
                return in.read(buffer, off, Math.min(10, max));
            }
        };
        in.mark(10000);
        byte[] test = new byte[1000];

        // readFully is not supposed to call read when reading 0 bytes
        assertEquals(0, IOUtils.readFully(in, test, 0, 0));
        assertEquals(0, readCount.get());

        assertEquals(1000, IOUtils.readFully(in, test, 0, 1000));
        IOUtilsTest.assertEquals(data, test);
        test = new byte[1001];
        in.reset();
        in.mark(10000);
        assertEquals(1000, IOUtils.readFully(in, test, 0, 1001));
        assertEquals(0, IOUtils.readFully(in, test, 0, 0));
    }

    public void testSkipFully() throws IOException {
        final Random r = new Random(1);
        byte[] data = new byte[1000];
        r.nextBytes(data);
        FilterInputStream in = new FilterInputStream(new ByteArrayInputStream(data)) {
            @Override
            public int read(byte[] buffer, int off, int max) throws IOException {
                return in.read(buffer, off, Math.min(10, max));
            }
        };
        in.mark(10000);
        IOUtils.skipFully(in, 1000);
        assertEquals(-1, in.read());
        in.reset();
        try {
            IOUtils.skipFully(in, 1001);
            fail();
        } catch (EOFException e) {
            // expected
        }
    }

    public void testStringReadWrite() throws IOException {
        final Random r = new Random(1);
        for (int i = 0; i < 100000; i += i / 10 + 1) {
            String s = "";
            for (int j = 0; j < 10; j++) {
                String p = new String(new char[i]).replace((char) 0, 'a');
                s += p;
            }
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            IOUtils.writeString(out, s);
            byte[] data = out.toByteArray();
            ByteArrayInputStream in = new ByteArrayInputStream(data) {
                @Override
                public int read(byte[] b, int off, int len) {
                    if (r.nextBoolean()) {
                        len = r.nextInt(len);
                    }
                    return super.read(b, off, len);
                }
            };
            String t = IOUtils.readString(in);
            assertEquals(s, t);
            assertEquals(-1, in.read());
        }
        try {
            InputStream in = new ByteArrayInputStream(new byte[]{1});
            IOUtils.readString(in);
            fail();
        } catch (EOFException e) {
            // expected
        }
    }

    public void testBytesReadWrite() throws IOException {
        final Random r = new Random();
        int iterations = 1000;
        while (iterations-- > 0) {
            int n = Math.abs(r.nextInt()) % 0x40000;
            byte[] buf = new byte[n];
            r.nextBytes(buf);
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            IOUtils.writeBytes(out, buf);
            byte[] buf1 = IOUtils.readBytes(new ByteArrayInputStream(out.toByteArray()));
            assertEquals(buf, buf1);
        }
    }

    public void testVarInt() throws IOException {
        testVarInt(0, 1);
        testVarInt(0x7f, 1);
        testVarInt(0x80, 2);
        testVarInt(0x3fff, 2);
        testVarInt(0x4000, 3);
        testVarInt(0x1fffff, 3);
        testVarInt(0x200000, 4);
        testVarInt(0xfffffff, 4);
        testVarInt(0x10000000, 5);
        testVarInt(-1, 5);
        for (int x = 0; x < 0x20000; x++) {
            testVarInt(x, 0);
            testVarInt(Integer.MIN_VALUE + x, 0);
            testVarInt(Integer.MAX_VALUE - x, 5);
            testVarInt(0x200000 + x - 100, 0);
            testVarInt(0x10000000 + x - 100, 0);
        }
        Random r = new Random(1);
        for (int i = 0; i < 100000; i++) {
            testVarInt(r.nextInt(), 0);
            testVarInt(r.nextInt(10000000), 0);
        }

        // trailing 0s are never written, but are an alternative way to encode a value
        InputStream in = new ByteArrayInputStream(new byte[]{(byte) 0x80, 0});
        assertEquals(0, IOUtils.readVarInt(in));
        assertEquals(-1, in.read());
    }

    public void testVarLong() throws IOException {
        testVarLong(0, 1);
        testVarLong(0x7f, 1);
        testVarLong(0x80, 2);
        testVarLong(0x3fff, 2);
        testVarLong(0x4000, 3);
        testVarLong(0x1fffff, 3);
        testVarLong(0x200000, 4);
        testVarLong(0xfffffff, 4);
        testVarLong(0x10000000, 5);
        testVarLong(0x1fffffffL, 5);
        testVarLong(0x2000000000L, 6);
        testVarLong(0x3ffffffffffL, 6);
        testVarLong(0x40000000000L, 7);
        testVarLong(0x1ffffffffffffL, 7);
        testVarLong(0x2000000000000L, 8);
        testVarLong(0xffffffffffffffL, 8);
        testVarLong(0x100000000000000L, 9);
        testVarLong(-1, 10);
        for (int x = 0; x < 0x20000; x++) {
            testVarLong(x, 0);
            testVarLong(Long.MIN_VALUE + x, 0);
            testVarLong(Long.MAX_VALUE - x, 9);
            testVarLong(0x200000 + x - 100, 0);
            testVarLong(0x10000000 + x - 100, 0);
        }
        Random r = new Random(1);
        for (int i = 0; i < 100000; i++) {
            testVarLong(r.nextLong(), 0);
            testVarLong(r.nextInt(Integer.MAX_VALUE), 0);
        }

        // trailing 0s are never written, but are an alternative way to encode a value
        InputStream in = new ByteArrayInputStream(new byte[]{(byte) 0x80, 0});
        assertEquals(0, IOUtils.readVarLong(in));
        assertEquals(-1, in.read());
    }

    public void testVarEOF() throws IOException {
        try {
            IOUtils.readVarInt(new ByteArrayInputStream(new byte[0]));
            fail();
        } catch (EOFException e) {
            // expected
        }
        try {
            IOUtils.readVarLong(new ByteArrayInputStream(new byte[0]));
            fail();
        } catch (EOFException e) {
            // expected
        }
    }

    private static void testVarInt(int x, int expectedLen) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        IOUtils.writeVarInt(out, x);
        byte[] data = out.toByteArray();
        assertTrue(data.length <= 5);
        if (expectedLen > 0) {
            assertEquals(expectedLen, data.length);
        }
        ByteArrayInputStream in = new ByteArrayInputStream(data);
        int x2 = IOUtils.readVarInt(in);
        assertEquals(x, x2);
        assertEquals(-1, in.read());
    }

    private static void testVarLong(long x, int expectedLen) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        IOUtils.writeVarLong(out, x);
        byte[] data = out.toByteArray();
        assertTrue(data.length <= 10);
        if (expectedLen > 0) {
            assertEquals(expectedLen, data.length);
        }
        ByteArrayInputStream in = new ByteArrayInputStream(data);
        long x2 = IOUtils.readVarLong(in);
        assertEquals(x, x2);
        assertEquals(-1, in.read());
    }

    public static void assertEquals(byte[] expected, byte[] got) {
        assertEquals(expected.length, got.length);
        for (int i = 0; i < got.length; i++) {
            assertEquals(expected[i], got[i]);
        }
    }

}