/*
 * 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.bookkeeper.common.util;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

/**
 * Test Case for {@link VarInt}.
 */
public class TestVarInt {
    @Rule
    public final ExpectedException thrown = ExpectedException.none();

    // Long values to check for boundary cases.
    private static final long[] LONG_VALUES = {
        0,
        1,
        127,
        128,
        16383,
        16384,
        2097151,
        2097152,
        268435455,
        268435456,
        34359738367L,
        34359738368L,
        9223372036854775807L,
        -9223372036854775808L,
        -1,
    };

    // VarInt encoding of the above VALUES.
    private static final byte[][] LONG_ENCODED = {
        // 0
        {0x00},
        // 1
        {0x01},
        // 127
        {0x7f},
        // 128
        {(byte) 0x80, 0x01},
        // 16383
        {(byte) 0xff, 0x7f},
        // 16834
        {(byte) 0x80, (byte) 0x80, 0x01},
        // 2097151
        {(byte) 0xff, (byte) 0xff, 0x7f},
        // 2097152
        {(byte) 0x80, (byte) 0x80, (byte) 0x80, 0x01},
        // 268435455
        {(byte) 0xff, (byte) 0xff, (byte) 0xff, 0x7f},
        // 268435456
        {(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, 0x01},
        // 34359738367
        {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x7f},
        // 34359738368
        {(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
            0x01},
        // 9223372036854775807
        {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f},
        // -9223372036854775808L
        {(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
            (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, 0x01},
        // -1
        {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x01}
    };

    // Integer values to check for boundary cases.
    private static final int[] INT_VALUES = {
        0,
        1,
        127,
        128,
        16383,
        16384,
        2097151,
        2097152,
        268435455,
        268435456,
        2147483647,
        -2147483648,
        -1,
    };

    // VarInt encoding of the above VALUES.
    private static final byte[][] INT_ENCODED = {
        // 0
        {(byte) 0x00},
        // 1
        {(byte) 0x01},
        // 127
        {(byte) 0x7f},
        // 128
        {(byte) 0x80, (byte) 0x01},
        // 16383
        {(byte) 0xff, (byte) 0x7f},
        // 16834
        {(byte) 0x80, (byte) 0x80, (byte) 0x01},
        // 2097151
        {(byte) 0xff, (byte) 0xff, (byte) 0x7f},
        // 2097152
        {(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x01},
        // 268435455
        {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f},
        // 268435456
        {(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x01},
        // 2147483647
        {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07},
        // -2147483648
        {(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x08},
        // -1
        {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f}
    };

    private static byte[] encodeInt(int v) throws IOException {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        VarInt.encode(v, stream);
        return stream.toByteArray();
    }

    private static byte[] encodeLong(long v) throws IOException {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        VarInt.encode(v, stream);
        return stream.toByteArray();
    }

    private static int decodeInt(byte[] encoded) throws IOException {
        ByteArrayInputStream stream = new ByteArrayInputStream(encoded);
        return VarInt.decodeInt(stream);
    }

    private static long decodeLong(byte[] encoded) throws IOException {
        ByteArrayInputStream stream = new ByteArrayInputStream(encoded);
        return VarInt.decodeLong(stream);
    }

    @Test
    public void decodeValues() throws IOException {
        assertEquals(LONG_VALUES.length, LONG_ENCODED.length);
        for (int i = 0; i < LONG_ENCODED.length; ++i) {
            ByteArrayInputStream stream = new ByteArrayInputStream(LONG_ENCODED[i]);
            long parsed = VarInt.decodeLong(stream);
            assertEquals(LONG_VALUES[i], parsed);
            assertEquals(-1, stream.read());
        }

        assertEquals(INT_VALUES.length, INT_ENCODED.length);
        for (int i = 0; i < INT_ENCODED.length; ++i) {
            ByteArrayInputStream stream = new ByteArrayInputStream(INT_ENCODED[i]);
            int parsed = VarInt.decodeInt(stream);
            assertEquals(INT_VALUES[i], parsed);
            assertEquals(-1, stream.read());
        }
    }

    @Test
    public void encodeValuesAndGetLength() throws IOException {
        assertEquals(LONG_VALUES.length, LONG_ENCODED.length);
        for (int i = 0; i < LONG_VALUES.length; ++i) {
            byte[] encoded = encodeLong(LONG_VALUES[i]);
            assertThat(encoded, equalTo(LONG_ENCODED[i]));
            assertEquals(LONG_ENCODED[i].length, VarInt.getLength(LONG_VALUES[i]));
        }

        assertEquals(INT_VALUES.length, INT_ENCODED.length);
        for (int i = 0; i < INT_VALUES.length; ++i) {
            byte[] encoded = encodeInt(INT_VALUES[i]);
            assertThat(encoded, equalTo(INT_ENCODED[i]));
            assertEquals(INT_ENCODED[i].length, VarInt.getLength(INT_VALUES[i]));
        }
    }

    @Test
    public void decodeThrowsExceptionForOverflow() throws IOException {
        final byte[] tooLargeNumber =
            {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
                (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x02};

        thrown.expect(IOException.class);
        decodeLong(tooLargeNumber);
    }

    @Test
    public void decodeThrowsExceptionForIntOverflow() throws IOException {
        byte[] encoded = encodeLong(1L << 32);

        thrown.expect(IOException.class);
        decodeInt(encoded);
    }

    @Test
    public void decodeThrowsExceptionForIntUnderflow() throws IOException {
        byte[] encoded = encodeLong(-1);

        thrown.expect(IOException.class);
        decodeInt(encoded);
    }

    @Test
    public void decodeThrowsExceptionForNonterminated() throws IOException {
        final byte[] nonTerminatedNumber =
            {(byte) 0xff, (byte) 0xff};

        thrown.expect(IOException.class);
        decodeLong(nonTerminatedNumber);
    }

    @Test
    public void decodeParsesEncodedValues() throws IOException {
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        for (int i = 10; i < Integer.MAX_VALUE; i = (int) (i * 1.1)) {
            VarInt.encode(i, outStream);
            VarInt.encode(-i, outStream);
        }
        for (long i = 10; i < Long.MAX_VALUE; i = (long) (i * 1.1)) {
            VarInt.encode(i, outStream);
            VarInt.encode(-i, outStream);
        }

        ByteArrayInputStream inStream =
            new ByteArrayInputStream(outStream.toByteArray());
        for (int i = 10; i < Integer.MAX_VALUE; i = (int) (i * 1.1)) {
            assertEquals(i, VarInt.decodeInt(inStream));
            assertEquals(-i, VarInt.decodeInt(inStream));
        }
        for (long i = 10; i < Long.MAX_VALUE; i = (long) (i * 1.1)) {
            assertEquals(i, VarInt.decodeLong(inStream));
            assertEquals(-i, VarInt.decodeLong(inStream));
        }
    }

    @Test
    public void endOfFileThrowsException() throws Exception {
        ByteArrayInputStream inStream =
            new ByteArrayInputStream(new byte[0]);
        thrown.expect(EOFException.class);
        VarInt.decodeInt(inStream);
    }

    @Test
    public void unterminatedThrowsException() throws Exception {
        byte[] e = encodeLong(Long.MAX_VALUE);
        byte[] s = new byte[1];
        s[0] = e[0];
        ByteArrayInputStream inStream = new ByteArrayInputStream(s);
        thrown.expect(IOException.class);
        VarInt.decodeInt(inStream);
    }
}
