/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * 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 com.android.net.module.util;

import static com.android.testutils.MiscAsserts.assertThrows;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;

import android.annotation.SuppressLint;
import android.net.InetAddresses;
import android.net.IpPrefix;
import android.net.MacAddress;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.net.module.util.Struct.Field;
import com.android.net.module.util.Struct.Type;

import org.junit.Test;
import org.junit.runner.RunWith;

import java.math.BigInteger;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class StructTest {

    // IPv6, 0 bytes of options, ifindex 15715755: 0x00efcdab, type 134 (RA), code 0, padding.
    private static final String HDR_EMPTY = "0a00" + "0000" + "abcdef00" + "8600000000000000";

    // UBE16: 0xfeff, UBE32: 0xfeffffff, UBE64: 0xfeffffffffffffff, UBE63: 0x7effffffffffffff
    private static final String NETWORK_ORDER_MSG = "feff" + "feffffff" + "feffffffffffffff"
            + "7effffffffffffff";

    // S8: 0x7f, S16: 0x7fff, S32: 0x7fffffff, S64: 0x7fffffffffffffff
    private static final String SIGNED_DATA = "7f" + "ff7f" + "ffffff7f" + "ffffffffffffff7f";

    // nS8: 0x81, nS16: 0x8001, nS32: 0x80000001, nS64: 800000000000000001
    private static final String SIGNED_NEGATIVE_DATA = "81" + "0180" + "01000080"
            + "0100000000000080";

    // U8: 0xff, U16: 0xffff, U32: 0xffffffff, U64: 0xffffffffffffffff, U63: 0x7fffffffffffffff,
    // U63: 0xffffffffffffffff(-1L)
    private static final String UNSIGNED_DATA = "ff" + "ffff" + "ffffffff" + "ffffffffffffffff"
            + "ffffffffffffff7f" + "ffffffffffffffff";

    // PREF64 option, 2001:db8:3:4:5:6::/96, lifetime: 10064
    private static final String OPT_PREF64 = "2750" + "20010db80003000400050006";
    private static final byte[] TEST_PREFIX64 = new byte[]{
            (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8, (byte) 0x00, (byte) 0x03,
            (byte) 0x00, (byte) 0x04, (byte) 0x00, (byte) 0x05, (byte) 0x00, (byte) 0x06,
    };

    private static final Inet4Address TEST_IPV4_ADDRESS =
            (Inet4Address) InetAddresses.parseNumericAddress("192.168.100.1");
    private static final Inet6Address TEST_IPV6_ADDRESS =
            (Inet6Address) InetAddresses.parseNumericAddress("2001:db8:3:4:5:6:7:8");

    private <T> T doParsingMessageTest(final String hexString, final Class<T> clazz,
            final ByteOrder order) {
        final ByteBuffer buf = toByteBuffer(hexString);
        buf.order(order);
        return Struct.parse(clazz, buf);
    }

    public static class HeaderMsgWithConstructor extends Struct {
        static int sType;
        static int sLength;

        @Field(order = 0, type = Type.U8, padding = 1)
        public final short mFamily;
        @Field(order = 1, type = Type.U16)
        public final int mLen;
        @Field(order = 2, type = Type.S32)
        public final int mIfindex;
        @Field(order = 3, type = Type.U8)
        public final short mIcmpType;
        @Field(order = 4, type = Type.U8, padding = 6)
        public final short mIcmpCode;

        HeaderMsgWithConstructor(final short family, final int len, final int ifindex,
                final short type, final short code) {
            mFamily = family;
            mLen = len;
            mIfindex = ifindex;
            mIcmpType = type;
            mIcmpCode = code;
        }
    }

    private void verifyHeaderParsing(final HeaderMsgWithConstructor msg) {
        assertEquals(10, msg.mFamily);
        assertEquals(0, msg.mLen);
        assertEquals(15715755, msg.mIfindex);
        assertEquals(134, msg.mIcmpType);
        assertEquals(0, msg.mIcmpCode);

        assertEquals(16, Struct.getSize(HeaderMsgWithConstructor.class));
        assertArrayEquals(toByteBuffer(HDR_EMPTY).array(),
                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
    }

    @Test
    public void testClassWithExplicitConstructor() {
        final HeaderMsgWithConstructor msg = doParsingMessageTest(HDR_EMPTY,
                HeaderMsgWithConstructor.class, ByteOrder.LITTLE_ENDIAN);
        verifyHeaderParsing(msg);
    }

    public static class HeaderMsgWithoutConstructor extends Struct {
        static int sType;
        static int sLength;

        @Field(order = 0, type = Type.U8, padding = 1)
        public short mFamily;
        @Field(order = 1, type = Type.U16)
        public int mLen;
        @Field(order = 2, type = Type.S32)
        public int mIfindex;
        @Field(order = 3, type = Type.U8)
        public short mIcmpType;
        @Field(order = 4, type = Type.U8, padding = 6)
        public short mIcmpCode;
    }

    @Test
    public void testClassWithDefaultConstructor() {
        final HeaderMsgWithoutConstructor msg = doParsingMessageTest(HDR_EMPTY,
                HeaderMsgWithoutConstructor.class, ByteOrder.LITTLE_ENDIAN);
        assertEquals(10, msg.mFamily);
        assertEquals(0, msg.mLen);
        assertEquals(15715755, msg.mIfindex);
        assertEquals(134, msg.mIcmpType);
        assertEquals(0, msg.mIcmpCode);

        assertEquals(16, Struct.getSize(HeaderMsgWithoutConstructor.class));
        assertArrayEquals(toByteBuffer(HDR_EMPTY).array(),
                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
    }

    public static class HeaderMessage {
        @Field(order = 0, type = Type.U8, padding = 1)
        short mFamily;
        @Field(order = 1, type = Type.U16)
        int mLen;
        @Field(order = 2, type = Type.S32)
        int mIfindex;
        @Field(order = 3, type = Type.U8)
        short mIcmpType;
        @Field(order = 4, type = Type.U8, padding = 6)
        short mIcmpCode;
    }

    @Test
    public void testInvalidClass_NotSubClass() {
        final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
        assertThrows(IllegalArgumentException.class, () -> Struct.parse(HeaderMessage.class, buf));
    }

    public static class HeaderMessageMissingAnnotation extends Struct {
        @Field(order = 0, type = Type.U8, padding = 1)
        short mFamily;
        @Field(order = 1, type = Type.U16)
        int mLen;
        int mIfindex;
        @Field(order = 2, type = Type.U8)
        short mIcmpType;
        @Field(order = 3, type = Type.U8, padding = 6)
        short mIcmpCode;
    }

    @Test
    public void testInvalidClass_MissingAnnotationField() {
        final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
        assertThrows(IllegalArgumentException.class,
                () -> Struct.parse(HeaderMessageMissingAnnotation.class, buf));
    }

    public static class NetworkOrderMessage extends Struct {
        @Field(order = 0, type = Type.UBE16)
        public final int mUBE16;
        @Field(order = 1, type = Type.UBE32)
        public final long mUBE32;
        @Field(order = 2, type = Type.UBE64)
        public final BigInteger mUBE64;
        @Field(order = 3, type = Type.UBE63)
        public final long mUBE63;

        NetworkOrderMessage(final int be16, final long be32, final BigInteger be64,
                final long be63) {
            mUBE16 = be16;
            mUBE32 = be32;
            mUBE64 = be64;
            mUBE63 = be63;
        }
    }

    @Test
    public void testNetworkOrder() {
        final NetworkOrderMessage msg = doParsingMessageTest(NETWORK_ORDER_MSG,
                NetworkOrderMessage.class, ByteOrder.LITTLE_ENDIAN);
        assertEquals(65279, msg.mUBE16);
        assertEquals(4278190079L, msg.mUBE32);
        assertEquals(new BigInteger("18374686479671623679"), msg.mUBE64);
        assertEquals(9151314442816847871L, msg.mUBE63);

        assertEquals(22, Struct.getSize(NetworkOrderMessage.class));
        assertArrayEquals(toByteBuffer(NETWORK_ORDER_MSG).array(),
                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
    }

    public static class UnsignedDataMessage extends Struct {
        @Field(order = 0, type = Type.U8)
        public final short mU8;
        @Field(order = 1, type = Type.U16)
        public final int mU16;
        @Field(order = 2, type = Type.U32)
        public final long mU32;
        @Field(order = 3, type = Type.U64)
        public final BigInteger mU64;
        @Field(order = 4, type = Type.U63)
        public final long mU63;
        @Field(order = 5, type = Type.U63)
        public final long mLU64; // represent U64 data with U63 type

        UnsignedDataMessage(final short u8, final int u16, final long u32, final BigInteger u64,
                final long u63, final long lu64) {
            mU8 = u8;
            mU16 = u16;
            mU32 = u32;
            mU64 = u64;
            mU63 = u63;
            mLU64 = lu64;
        }
    }

    @Test
    public void testUnsignedData() {
        final UnsignedDataMessage msg = doParsingMessageTest(UNSIGNED_DATA,
                UnsignedDataMessage.class, ByteOrder.LITTLE_ENDIAN);
        assertEquals(255, msg.mU8);
        assertEquals(65535, msg.mU16);
        assertEquals(4294967295L, msg.mU32);
        assertEquals(new BigInteger("18446744073709551615"), msg.mU64);
        assertEquals(9223372036854775807L, msg.mU63);
        assertEquals(-1L, msg.mLU64);

        assertEquals(31, Struct.getSize(UnsignedDataMessage.class));
        assertArrayEquals(toByteBuffer(UNSIGNED_DATA).array(),
                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
    }

    public static class U64DataMessage extends Struct {
        @Field(order = 0, type = Type.U64) long mU64;
    }

    @Test
    public void testInvalidType_U64WithLongPrimitive() {
        assertThrows(IllegalArgumentException.class,
                () -> Struct.parse(U64DataMessage.class, toByteBuffer("ffffffffffffffff")));
    }

    // BigInteger U64: 0x0000000000001234, BigInteger UBE64: 0x0000000000001234, BigInteger U64: 0
    private static final String SMALL_VALUE_BIGINTEGER = "3412000000000000" + "0000000000001234"
            + "0000000000000000";

    public static class SmallValueBigInteger extends Struct {
        @Field(order = 0, type = Type.U64) public final BigInteger mSmallValue;
        @Field(order = 1, type = Type.UBE64) public final BigInteger mBSmallValue;
        @Field(order = 2, type = Type.U64) public final BigInteger mZero;

        SmallValueBigInteger(final BigInteger smallValue, final BigInteger bSmallValue,
                final BigInteger zero) {
            mSmallValue = smallValue;
            mBSmallValue = bSmallValue;
            mZero = zero;
        }
    }

    @Test
    public void testBigIntegerSmallValueOrZero() {
        final SmallValueBigInteger msg = doParsingMessageTest(SMALL_VALUE_BIGINTEGER,
                SmallValueBigInteger.class, ByteOrder.LITTLE_ENDIAN);
        assertEquals(new BigInteger("4660"), msg.mSmallValue);
        assertEquals(new BigInteger("4660"), msg.mBSmallValue);
        assertEquals(new BigInteger("0"), msg.mZero);

        assertEquals(24, Struct.getSize(SmallValueBigInteger.class));
        assertArrayEquals(toByteBuffer(SMALL_VALUE_BIGINTEGER).array(),
                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
    }

    public static class SignedDataMessage extends Struct {
        @Field(order = 0, type = Type.S8)
        public final byte mS8;
        @Field(order = 1, type = Type.S16)
        public final short mS16;
        @Field(order = 2, type = Type.S32)
        public final int mS32;
        @Field(order = 3, type = Type.S64)
        public final long mS64;

        SignedDataMessage(final byte s8, final short s16, final int s32, final long s64) {
            mS8 = s8;
            mS16 = s16;
            mS32 = s32;
            mS64 = s64;
        }
    }

    @Test
    public void testSignedPositiveData() {
        final SignedDataMessage msg = doParsingMessageTest(SIGNED_DATA, SignedDataMessage.class,
                ByteOrder.LITTLE_ENDIAN);
        assertEquals(127, msg.mS8);
        assertEquals(32767, msg.mS16);
        assertEquals(2147483647, msg.mS32);
        assertEquals(9223372036854775807L, msg.mS64);

        assertEquals(15, Struct.getSize(SignedDataMessage.class));
        assertArrayEquals(toByteBuffer(SIGNED_DATA).array(),
                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
    }

    @Test
    public void testSignedNegativeData() {
        final SignedDataMessage msg = doParsingMessageTest(SIGNED_NEGATIVE_DATA,
                SignedDataMessage.class, ByteOrder.LITTLE_ENDIAN);
        assertEquals(-127, msg.mS8);
        assertEquals(-32767, msg.mS16);
        assertEquals(-2147483647, msg.mS32);
        assertEquals(-9223372036854775807L, msg.mS64);

        assertEquals(15, Struct.getSize(SignedDataMessage.class));
        assertArrayEquals(toByteBuffer(SIGNED_NEGATIVE_DATA).array(),
                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
    }

    public static class HeaderMessageWithDuplicateOrder extends Struct {
        @Field(order = 0, type = Type.U8, padding = 1)
        short mFamily;
        @Field(order = 1, type = Type.U16)
        int mLen;
        @Field(order = 2, type = Type.S32)
        int mIfindex;
        @Field(order = 2, type = Type.U8)
        short mIcmpType;
        @Field(order = 3, type = Type.U8, padding = 6)
        short mIcmpCode;
    }

    @Test
    public void testInvalidClass_DuplicateFieldOrder() {
        final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
        assertThrows(IllegalArgumentException.class,
                () -> Struct.parse(HeaderMessageWithDuplicateOrder.class, buf));
    }

    public static class HeaderMessageWithNegativeOrder extends Struct {
        @Field(order = 0, type = Type.U8, padding = 1)
        short mFamily;
        @Field(order = 1, type = Type.U16)
        int mLen;
        @Field(order = 2, type = Type.S32)
        int mIfindex;
        @Field(order = 3, type = Type.U8)
        short mIcmpType;
        @Field(order = -4, type = Type.U8, padding = 6)
        short mIcmpCode;
    }

    @Test
    public void testInvalidClass_NegativeFieldOrder() {
        final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
        assertThrows(IllegalArgumentException.class,
                () -> Struct.parse(HeaderMessageWithNegativeOrder.class, buf));
    }

    public static class HeaderMessageOutOfIndexBounds extends Struct {
        @Field(order = 0, type = Type.U8, padding = 1)
        short mFamily;
        @Field(order = 1, type = Type.U16)
        int mLen;
        @Field(order = 2, type = Type.S32)
        int mIfindex;
        @Field(order = 3, type = Type.U8)
        short mIcmpType;
        @Field(order = 5, type = Type.U8, padding = 6)
        short mIcmpCode;
    }

    @Test
    public void testInvalidClass_OutOfIndexBounds() {
        final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
        assertThrows(IllegalArgumentException.class,
                () -> Struct.parse(HeaderMessageOutOfIndexBounds.class, buf));
    }

    public static class HeaderMessageMismatchedPrimitiveType extends Struct {
        @Field(order = 0, type = Type.U8, padding = 1)
        short mFamily;
        @Field(order = 1, type = Type.U16)
        short mLen; // should be integer
        @Field(order = 2, type = Type.S32)
        int mIfindex;
        @Field(order = 3, type = Type.U8)
        short mIcmpType;
        @Field(order = 4, type = Type.U8, padding = 6)
        short mIcmpCode;
    }

    @Test
    public void testInvalidClass_MismatchedPrimitiveDataType() {
        final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
        assertThrows(IllegalArgumentException.class,
                () -> Struct.parse(HeaderMessageMismatchedPrimitiveType.class, buf));
    }

    public static class PrefixMessage extends Struct {
        @Field(order = 0, type = Type.UBE16)
        public final int mLifetime;
        @Field(order = 1, type = Type.ByteArray, arraysize = 12)
        public final byte[] mPrefix;

        PrefixMessage(final int lifetime, final byte[] prefix) {
            mLifetime = lifetime;
            mPrefix = prefix;
        }
    }

    @SuppressLint("NewApi")
    private void verifyPrefixByteArrayParsing(final PrefixMessage msg) throws Exception {
        // The original PREF64 option message has just 12 bytes for prefix byte array
        // (Highest 96 bits of the Prefix), copyOf pads the 128-bits IPv6 address with
        // prefix and 4-bytes zeros.
        final InetAddress addr = InetAddress.getByAddress(Arrays.copyOf(msg.mPrefix, 16));
        final IpPrefix prefix = new IpPrefix(addr, 96);
        assertEquals(10064, msg.mLifetime);
        assertTrue(prefix.equals(new IpPrefix("2001:db8:3:4:5:6::/96")));

        assertEquals(14, Struct.getSize(PrefixMessage.class));
        assertArrayEquals(toByteBuffer(OPT_PREF64).array(),
                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
    }

    public static class PrefixMessageWithZeroLengthArray extends Struct {
        @Field(order = 0, type = Type.UBE16)
        final int mLifetime;
        @Field(order = 1, type = Type.ByteArray, arraysize = 0)
        final byte[] mPrefix;

        PrefixMessageWithZeroLengthArray(final int lifetime, final byte[] prefix) {
            mLifetime = lifetime;
            mPrefix = prefix;
        }
    }

    @Test
    public void testInvalidClass_ZeroLengthByteArray() {
        final ByteBuffer buf = toByteBuffer(OPT_PREF64);
        assertThrows(IllegalArgumentException.class,
                () -> Struct.parse(PrefixMessageWithZeroLengthArray.class, buf));
    }

    @Test
    public void testPrefixArrayField() throws Exception {
        final PrefixMessage msg = doParsingMessageTest(OPT_PREF64, PrefixMessage.class,
                ByteOrder.LITTLE_ENDIAN);
        verifyPrefixByteArrayParsing(msg);
    }

    public static class HeaderMessageWithMutableField extends Struct {
        @Field(order = 0, type = Type.U8, padding = 1)
        final short mFamily;
        @Field(order = 1, type = Type.U16)
        final int mLen;
        @Field(order = 2, type = Type.S32)
        int mIfindex;
        @Field(order = 3, type = Type.U8)
        short mIcmpType;
        @Field(order = 4, type = Type.U8, padding = 6)
        final short mIcmpCode;

        HeaderMessageWithMutableField(final short family, final int len, final short code) {
            mFamily = family;
            mLen = len;
            mIcmpCode = code;
        }
    }

    @Test
    public void testMixMutableAndImmutableFields() {
        final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
        assertThrows(IllegalArgumentException.class,
                () -> Struct.parse(HeaderMessageWithMutableField.class, buf));
    }

    public static class HeaderMsgWithStaticConstant extends Struct {
        private static final String TAG = "HeaderMessage";
        private static final int FIELD_COUNT = 5;

        @Field(order = 0, type = Type.U8, padding = 1)
        public final short mFamily;
        @Field(order = 1, type = Type.U16)
        public final int mLen;
        @Field(order = 2, type = Type.S32)
        public final int mIfindex;
        @Field(order = 3, type = Type.U8)
        public final short mIcmpType;
        @Field(order = 4, type = Type.U8, padding = 6)
        public final short mIcmpCode;

        HeaderMsgWithStaticConstant(final short family, final int len, final int ifindex,
                final short type, final short code) {
            mFamily = family;
            mLen = len;
            mIfindex = ifindex;
            mIcmpType = type;
            mIcmpCode = code;
        }
    }

    @Test
    public void testStaticConstantField() {
        final HeaderMsgWithStaticConstant msg = doParsingMessageTest(HDR_EMPTY,
                HeaderMsgWithStaticConstant.class, ByteOrder.LITTLE_ENDIAN);
        assertEquals(10, msg.mFamily);
        assertEquals(0, msg.mLen);
        assertEquals(15715755, msg.mIfindex);
        assertEquals(134, msg.mIcmpType);
        assertEquals(0, msg.mIcmpCode);

        assertEquals(16, Struct.getSize(HeaderMsgWithStaticConstant.class));
        assertArrayEquals(toByteBuffer(HDR_EMPTY).array(),
                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
    }

    public static class MismatchedConstructor extends Struct {
        @Field(order = 0, type = Type.U16) final int mInt1;
        @Field(order = 1, type = Type.U16) final int mInt2;
        MismatchedConstructor(String int1, String int2) {
            mInt1 = Integer.valueOf(int1);
            mInt2 = Integer.valueOf(int2);
        }
    }

    @Test
    public void testMisMatchedConstructor() {
        final ByteBuffer buf = toByteBuffer("1234" + "5678");
        assertThrows(IllegalArgumentException.class,
                () -> Struct.parse(MismatchedConstructor.class, buf));
    }

    public static class ClassWithTwoConstructors extends Struct {
        @Field(order = 0, type = Type.U16) public final int mInt1;
        @Field(order = 1, type = Type.U16) public final int mInt2;
        ClassWithTwoConstructors(String int1, String int2) {
            mInt1 = Integer.valueOf(int1);
            mInt2 = Integer.valueOf(int2);
        }
        ClassWithTwoConstructors(int int1, int int2) {
            mInt1 = int1;
            mInt2 = int2;
        }
    }

    @Test
    public void testClassWithTwoConstructors() {
        final ClassWithTwoConstructors msg = doParsingMessageTest("1234" + "5678",
                ClassWithTwoConstructors.class, ByteOrder.LITTLE_ENDIAN);
        assertEquals(13330 /* 0x3412 */, msg.mInt1);
        assertEquals(30806 /* 0x7856 */, msg.mInt2);

        assertEquals(4, Struct.getSize(ClassWithTwoConstructors.class));
        assertArrayEquals(toByteBuffer("1234" + "5678").array(),
                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
    }

    @Test
    public void testInvalidOutputByteBuffer_ZeroCapacity() {
        final ByteBuffer output = ByteBuffer.allocate(0);
        output.order(ByteOrder.LITTLE_ENDIAN);
        final HeaderMsgWithConstructor msg = doParsingMessageTest(HDR_EMPTY,
                HeaderMsgWithConstructor.class, ByteOrder.LITTLE_ENDIAN);
        assertThrows(BufferOverflowException.class, () -> msg.writeToByteBuffer(output));
    }

    @Test
    public void testConsecutiveWrites() {
        final HeaderMsgWithConstructor msg1 = doParsingMessageTest(HDR_EMPTY,
                HeaderMsgWithConstructor.class, ByteOrder.LITTLE_ENDIAN);
        final PrefixMessage msg2 = doParsingMessageTest(OPT_PREF64, PrefixMessage.class,
                ByteOrder.LITTLE_ENDIAN);

        int size = Struct.getSize(HeaderMsgWithConstructor.class)
                + Struct.getSize(PrefixMessage.class);
        final ByteBuffer output = ByteBuffer.allocate(size);
        output.order(ByteOrder.LITTLE_ENDIAN);

        msg1.writeToByteBuffer(output);
        msg2.writeToByteBuffer(output);
        output.flip();

        final ByteBuffer concat = ByteBuffer.allocate(size).put(toByteBuffer(HDR_EMPTY))
                .put(toByteBuffer(OPT_PREF64));
        assertArrayEquals(output.array(), concat.array());
    }

    @Test
    public void testClassesParsedFromCache() throws Exception {
        for (int i = 0; i < 100; i++) {
            final HeaderMsgWithConstructor msg1 = doParsingMessageTest(HDR_EMPTY,
                    HeaderMsgWithConstructor.class, ByteOrder.LITTLE_ENDIAN);
            verifyHeaderParsing(msg1);

            final PrefixMessage msg2 = doParsingMessageTest(OPT_PREF64, PrefixMessage.class,
                    ByteOrder.LITTLE_ENDIAN);
            verifyPrefixByteArrayParsing(msg2);
        }
    }

    public static class BigEndianDataMessage extends Struct {
        @Field(order = 0, type = Type.S32) public int mInt1;
        @Field(order = 1, type = Type.S32) public int mInt2;
        @Field(order = 2, type = Type.UBE16) public int mInt3;
        @Field(order = 3, type = Type.U16) public int mInt4;
        @Field(order = 4, type = Type.U64) public BigInteger mBigInteger1;
        @Field(order = 5, type = Type.UBE64) public BigInteger mBigInteger2;
        @Field(order = 6, type = Type.S64) public long mLong;
    }

    private static final String BIG_ENDIAN_DATA = "00000001" + "fffffffe" + "fffe" + "fffe"
            + "ff00004500002301" + "ff00004500002301" + "ff00004500002301";

    @Test
    public void testBigEndianByteBuffer() {
        final BigEndianDataMessage msg = doParsingMessageTest(BIG_ENDIAN_DATA,
                BigEndianDataMessage.class, ByteOrder.BIG_ENDIAN);

        assertEquals(1, msg.mInt1);
        assertEquals(-2, msg.mInt2);
        assertEquals(65534, msg.mInt3);
        assertEquals(65534, msg.mInt4);
        assertEquals(new BigInteger("18374686776024376065"), msg.mBigInteger1);
        assertEquals(new BigInteger("18374686776024376065"), msg.mBigInteger2);
        assertEquals(0xff00004500002301L, msg.mLong);

        assertEquals(36, Struct.getSize(BigEndianDataMessage.class));
        assertArrayEquals(toByteBuffer(BIG_ENDIAN_DATA).array(),
                msg.writeToBytes(ByteOrder.BIG_ENDIAN));
    }

    private ByteBuffer toByteBuffer(final String hexString) {
        return ByteBuffer.wrap(HexDump.hexStringToByteArray(hexString));
    }

    public static class MacAddressMessage extends Struct {
        public @Field(order = 0, type = Type.EUI48) final MacAddress mMac1;
        public @Field(order = 1, type = Type.EUI48) final MacAddress mMac2;

        MacAddressMessage(final MacAddress mac1, final MacAddress mac2) {
            this.mMac1 = mac1;
            this.mMac2 = mac2;
        }
    }

    @Test
    public void testMacAddressType() {
        final MacAddressMessage msg = doParsingMessageTest("001122334455" + "ffffffffffff",
                MacAddressMessage.class, ByteOrder.BIG_ENDIAN);

        assertEquals(MacAddress.fromString("00:11:22:33:44:55"), msg.mMac1);
        assertEquals(MacAddress.fromString("ff:ff:ff:ff:ff:ff"), msg.mMac2);

        assertEquals(12, Struct.getSize(MacAddressMessage.class));
        assertArrayEquals(toByteBuffer("001122334455" + "ffffffffffff").array(),
                msg.writeToBytes(ByteOrder.BIG_ENDIAN));
    }

    public static class BadMacAddressType extends Struct {
        @Field(order = 0, type = Type.EUI48) byte[] mMac;
    }

    @Test
    public void testIncorrectType_EUI48WithByteArray() {
        assertThrows(IllegalArgumentException.class,
                () -> Struct.parse(BadMacAddressType.class, toByteBuffer("ffffffffffff")));
    }

    @Test
    public void testStructToByteArrayRoundTrip() {
        final SignedDataMessage littleEndianMsg = doParsingMessageTest(SIGNED_DATA,
                SignedDataMessage.class, ByteOrder.LITTLE_ENDIAN);
        assertArrayEquals(toByteBuffer(SIGNED_DATA).array(),
                littleEndianMsg.writeToBytes(ByteOrder.LITTLE_ENDIAN));

        final SignedDataMessage bigEndianMsg = doParsingMessageTest(SIGNED_DATA,
                SignedDataMessage.class, ByteOrder.BIG_ENDIAN);
        assertArrayEquals(toByteBuffer(SIGNED_DATA).array(),
                bigEndianMsg.writeToBytes(ByteOrder.BIG_ENDIAN));

        final SignedDataMessage nativeOrderMsg = ByteOrder.nativeOrder().equals(
                ByteOrder.LITTLE_ENDIAN) ? littleEndianMsg : bigEndianMsg;
        assertArrayEquals(toByteBuffer(SIGNED_DATA).array(),
                nativeOrderMsg.writeToBytes());
    }

    @Test
    public void testStructToByteArray() {
        final SignedDataMessage msg = new SignedDataMessage((byte) -5, (short) 42, (int) 0xff000004,
                (long) 0xff000004ff000005L);
        final String leHexString = "fb" + "2a00" + "040000ff" + "050000ff040000ff";
        final String beHexString = "fb" + "002a" + "ff000004" + "ff000004ff000005";
        final String hexString = ByteOrder.nativeOrder().equals(ByteOrder.LITTLE_ENDIAN)
                ? leHexString : beHexString;
        assertArrayEquals(toByteBuffer(hexString).array(), msg.writeToBytes());
    }

    public static class IpAddressMessage extends Struct {
        @Field(order = 0, type = Type.Ipv4Address) public final Inet4Address ipv4Address;
        @Field(order = 1, type = Type.Ipv6Address) public final Inet6Address ipv6Address;

        IpAddressMessage(final Inet4Address ipv4Address, final Inet6Address ipv6Address) {
            this.ipv4Address = ipv4Address;
            this.ipv6Address = ipv6Address;
        }
    }

    @Test
    public void testIpAddressType() {
        final IpAddressMessage msg = doParsingMessageTest("c0a86401"
                + "20010db8000300040005000600070008", IpAddressMessage.class, ByteOrder.BIG_ENDIAN);

        assertEquals(TEST_IPV4_ADDRESS, msg.ipv4Address);
        assertEquals(TEST_IPV6_ADDRESS, msg.ipv6Address);

        assertEquals(20, Struct.getSize(IpAddressMessage.class));
        assertArrayEquals(toByteBuffer("c0a86401" + "20010db8000300040005000600070008").array(),
                msg.writeToBytes(ByteOrder.BIG_ENDIAN));
    }

    public static class WrongIpAddressType extends Struct {
        @Field(order = 0, type = Type.Ipv4Address) public byte[] ipv4Address;
        @Field(order = 1, type = Type.Ipv6Address) public byte[] ipv6Address;
    }

    @Test
    public void testIncorrectType_IpAddressWithByteArray() {
        assertThrows(IllegalArgumentException.class,
                () -> Struct.parse(WrongIpAddressType.class,
                                   toByteBuffer("c0a86401" + "20010db8000300040005000600070008")));
    }

    public static class FullTypeMessage extends Struct {
        @Field(order = 0, type = Type.U8) public final short u8;
        @Field(order = 1, type = Type.U16) public final int u16;
        @Field(order = 2, type = Type.U32) public final long u32;
        @Field(order = 3, type = Type.U63) public final long u63;
        @Field(order = 4, type = Type.U64) public final BigInteger u64;
        @Field(order = 5, type = Type.S8) public final byte s8;
        @Field(order = 6, type = Type.S16) public final short s16;
        @Field(order = 7, type = Type.S32) public final int s32;
        @Field(order = 8, type = Type.S64) public final long s64;
        @Field(order = 9, type = Type.UBE16) public final int ube16;
        @Field(order = 10, type = Type.UBE32) public final long ube32;
        @Field(order = 11, type = Type.UBE63) public final long ube63;
        @Field(order = 12, type = Type.UBE64) public final BigInteger ube64;
        @Field(order = 13, type = Type.ByteArray, arraysize = 12) public final byte[] bytes;
        @Field(order = 14, type = Type.EUI48) public final MacAddress eui48;
        @Field(order = 15, type = Type.Ipv4Address) public final Inet4Address ipv4Address;
        @Field(order = 16, type = Type.Ipv6Address) public final Inet6Address ipv6Address;

        FullTypeMessage(final short u8, final int u16, final long u32, final long u63,
                final BigInteger u64, final byte s8, final short s16, final int s32, final long s64,
                final int ube16, final long ube32, final long ube63, final BigInteger ube64,
                final byte[] bytes, final MacAddress eui48, final Inet4Address ipv4Address,
                final Inet6Address ipv6Address) {
            this.u8 = u8;
            this.u16 = u16;
            this.u32 = u32;
            this.u63 = u63;
            this.u64 = u64;
            this.s8 = s8;
            this.s16 = s16;
            this.s32 = s32;
            this.s64 = s64;
            this.ube16 = ube16;
            this.ube32 = ube32;
            this.ube63 = ube63;
            this.ube64 = ube64;
            this.bytes = bytes;
            this.eui48 = eui48;
            this.ipv4Address = ipv4Address;
            this.ipv6Address = ipv6Address;
        }
    }

    private static final String FULL_TYPE_DATA = "ff" + "ffff" + "ffffffff" + "7fffffffffffffff"
            + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff" + "7fffffffffffffff" + "7fff"
            + "7fffffff" + "7fffffffffffffff" + "ffffffffffffffff" + "20010db80003000400050006"
            + "001122334455" + "c0a86401" + "20010db8000300040005000600070008";
    private static final String FULL_TYPE_DATA_DIFF_MAC = "ff" + "ffff" + "ffffffff"
            + "7fffffffffffffff" + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff"
            + "7fffffffffffffff" + "7fff" + "7fffffff" + "7fffffffffffffff" + "ffffffffffffffff"
            + "20010db80003000400050006" + "112233445566"
            + "c0a86401" + "20010db8000300040005000600070008";
    private static final String FULL_TYPE_DATA_DIFF_LONG = "ff" + "ffff" + "ffffffff"
            + "7ffffffffffffffe" + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff"
            + "7fffffffffffffff" + "7fff" + "7fffffff" + "7fffffffffffffff" + "ffffffffffffffff"
            + "20010db80003000400050006" + "001122334455"
            + "c0a86401" + "20010db8000300040005000600070008";
    private static final String FULL_TYPE_DATA_DIFF_INTEGER = "ff" + "ffff" + "ffffffff"
            + "7fffffffffffffff" + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff"
            + "7fffffffffffffff" + "7fff" + "ffffff7f" + "7fffffffffffffff" + "ffffffffffffffff"
            + "20010db80003000400050006" + "001122334455"
            + "c0a86401" + "20010db8000300040005000600070008";
    private static final String FULL_TYPE_DATA_DIFF_IPV4 = "ff" + "ffff" + "ffffffff"
            + "7fffffffffffffff" + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff"
            + "7fffffffffffffff" + "7fff" + "ffffff7f" + "7fffffffffffffff" + "ffffffffffffffff"
            + "20010db80003000400050006" + "001122334455"
            + "c0a81010" + "20010db8000300040005000600070008";
    private static final String FULL_TYPE_DATA_DIFF_IPV6 = "ff" + "ffff" + "ffffffff"
            + "7fffffffffffffff" + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff"
            + "7fffffffffffffff" + "7fff" + "ffffff7f" + "7fffffffffffffff" + "ffffffffffffffff"
            + "20010db80003000400050006" + "001122334455"
            + "c0a86401" + "20010db800030004000500060007000a";
    @Test
    public void testStructClass_equals() {
        final FullTypeMessage msg = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
                ByteOrder.BIG_ENDIAN);

        assertEquals(255, msg.u8);
        assertEquals(65535, msg.u16);
        assertEquals(4294967295L, msg.u32);
        assertEquals(9223372036854775807L, msg.u63);
        assertEquals(new BigInteger("18446744073709551615"), msg.u64);
        assertEquals(127, msg.s8);
        assertEquals(32767, msg.s16);
        assertEquals(2147483647, msg.s32);
        assertEquals(9223372036854775807L, msg.s64);
        assertEquals(32767, msg.ube16);
        assertEquals(2147483647, msg.ube32);
        assertEquals(9223372036854775807L, msg.ube63);
        assertEquals(new BigInteger("18446744073709551615"), msg.ube64);
        assertArrayEquals(TEST_PREFIX64, msg.bytes);
        assertEquals(MacAddress.fromString("00:11:22:33:44:55"), msg.eui48);
        assertEquals(TEST_IPV4_ADDRESS, msg.ipv4Address);
        assertEquals(TEST_IPV6_ADDRESS, msg.ipv6Address);

        assertEquals(98, msg.getSize(FullTypeMessage.class));
        assertArrayEquals(toByteBuffer(FULL_TYPE_DATA).array(),
                msg.writeToBytes(ByteOrder.BIG_ENDIAN));

        final FullTypeMessage msg1 = new FullTypeMessage((short) 0xff, (int) 0xffff,
                (long) 0xffffffffL, (long) 0x7fffffffffffffffL,
                new BigInteger("18446744073709551615"), (byte) 0x7f, (short) 0x7fff,
                (int) 0x7fffffff, (long) 0x7fffffffffffffffL, (int) 0x7fff, (long) 0x7fffffffL,
                (long) 0x7fffffffffffffffL, new BigInteger("18446744073709551615"), TEST_PREFIX64,
                MacAddress.fromString("00:11:22:33:44:55"), TEST_IPV4_ADDRESS, TEST_IPV6_ADDRESS);
        assertTrue(msg.equals(msg1));
    }

    public static class FullTypeMessageWithDupType extends Struct {
        @Field(order = 0, type = Type.U8) public final short u8;
        @Field(order = 1, type = Type.U16) public final int u16;
        @Field(order = 2, type = Type.U32) public final long u32;
        @Field(order = 3, type = Type.S64) public final long u63; // old: U63, new: S64
        @Field(order = 4, type = Type.UBE64) public final BigInteger u64; // old: U64, new: UBE64
        @Field(order = 5, type = Type.S8) public final byte s8;
        @Field(order = 6, type = Type.S16) public final short s16;
        @Field(order = 7, type = Type.S32) public final int s32;
        @Field(order = 8, type = Type.S64) public final long s64;
        @Field(order = 9, type = Type.U16) public final int ube16; // old:UBE16, new: U16
        @Field(order = 10, type = Type.UBE32) public final long ube32;
        @Field(order = 11, type = Type.UBE63) public final long ube63;
        @Field(order = 12, type = Type.UBE64) public final BigInteger ube64;
        @Field(order = 13, type = Type.ByteArray, arraysize = 12) public final byte[] bytes;
        @Field(order = 14, type = Type.EUI48) public final MacAddress eui48;
        @Field(order = 15, type = Type.Ipv4Address) public final Inet4Address ipv4Address;
        @Field(order = 16, type = Type.Ipv6Address) public final Inet6Address ipv6Address;

        FullTypeMessageWithDupType(final short u8, final int u16, final long u32, final long u63,
                final BigInteger u64, final byte s8, final short s16, final int s32, final long s64,
                final int ube16, final long ube32, final long ube63, final BigInteger ube64,
                final byte[] bytes, final MacAddress eui48, final Inet4Address ipv4Address,
                final Inet6Address ipv6Address) {
            this.u8 = u8;
            this.u16 = u16;
            this.u32 = u32;
            this.u63 = u63;
            this.u64 = u64;
            this.s8 = s8;
            this.s16 = s16;
            this.s32 = s32;
            this.s64 = s64;
            this.ube16 = ube16;
            this.ube32 = ube32;
            this.ube63 = ube63;
            this.ube64 = ube64;
            this.bytes = bytes;
            this.eui48 = eui48;
            this.ipv4Address = ipv4Address;
            this.ipv6Address = ipv6Address;
        }
    }

    @Test
    public void testStructClass_notEqualWithDifferentClass() {
        final FullTypeMessage msg = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
                ByteOrder.BIG_ENDIAN);
        final FullTypeMessageWithDupType msg1 = doParsingMessageTest(FULL_TYPE_DATA,
                FullTypeMessageWithDupType.class, ByteOrder.BIG_ENDIAN);

        assertFalse(msg.equals(msg1));
    }

    @Test
    public void testStructClass_notEqualWithDifferentValue() {
        final FullTypeMessage msg = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
                ByteOrder.BIG_ENDIAN);

        // With different MAC address.
        final FullTypeMessage msg1 = doParsingMessageTest(FULL_TYPE_DATA_DIFF_MAC,
                FullTypeMessage.class, ByteOrder.BIG_ENDIAN);
        assertNotEquals(msg.eui48, msg1.eui48);
        assertFalse(msg.equals(msg1));

        // With different byte array.
        final FullTypeMessage msg2 = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
                ByteOrder.BIG_ENDIAN);
        msg2.bytes[5] = (byte) 42; // change one byte in the array.
        assertFalse(msg.equals(msg2));

        // With different Long primitive.
        final FullTypeMessage msg3 = doParsingMessageTest(FULL_TYPE_DATA_DIFF_LONG,
                FullTypeMessage.class, ByteOrder.BIG_ENDIAN);
        assertNotEquals(msg.u63, msg3.u63);
        assertFalse(msg.equals(msg3));

        // With different Integer primitive.
        final FullTypeMessage msg4 = doParsingMessageTest(FULL_TYPE_DATA_DIFF_INTEGER,
                FullTypeMessage.class, ByteOrder.BIG_ENDIAN);
        assertNotEquals(msg.ube32, msg4.ube32);
        assertFalse(msg.equals(msg4));

        // With different IPv4 address.
        final FullTypeMessage msg5 = doParsingMessageTest(FULL_TYPE_DATA_DIFF_IPV4,
                FullTypeMessage.class, ByteOrder.BIG_ENDIAN);
        assertNotEquals(msg.ipv4Address, msg5.ipv4Address);
        assertFalse(msg.equals(msg5));

        // With different IPv6 address.
        final FullTypeMessage msg6 = doParsingMessageTest(FULL_TYPE_DATA_DIFF_IPV6,
                FullTypeMessage.class, ByteOrder.BIG_ENDIAN);
        assertNotEquals(msg.ipv6Address, msg6.ipv6Address);
        assertFalse(msg.equals(msg6));
    }

    @Test
    public void testStructClass_toString() {
        final String expected = "u8: 255, u16: 65535, u32: 4294967295,"
                + " u63: 9223372036854775807, u64: 18446744073709551615, s8: 127, s16: 32767,"
                + " s32: 2147483647, s64: 9223372036854775807, ube16: 32767, ube32: 2147483647,"
                + " ube63: 9223372036854775807, ube64: 18446744073709551615,"
                + " bytes: 0x20010DB80003000400050006,"
                + " eui48: 00:11:22:33:44:55,"
                + " ipv4Address: 192.168.100.1,"
                + " ipv6Address: 2001:db8:3:4:5:6:7:8";

        final FullTypeMessage msg = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
                ByteOrder.BIG_ENDIAN);
        assertEquals(expected, msg.toString());
    }

    @Test
    public void testStructClass_toStringWithNullMember() {
        final String expected = "u8: 255, u16: 65535, u32: 4294967295,"
                + " u63: 9223372036854775807, u64: null, s8: 127, s16: 32767,"
                + " s32: 2147483647, s64: 9223372036854775807, ube16: 32767, ube32: 2147483647,"
                + " ube63: 9223372036854775807, ube64: 18446744073709551615,"
                + " bytes: null, eui48: null, ipv4Address: 192.168.100.1,"
                + " ipv6Address: null";

        final FullTypeMessage msg = new FullTypeMessage((short) 0xff, (int) 0xffff,
                (long) 0xffffffffL, (long) 0x7fffffffffffffffL,
                null /* u64 */, (byte) 0x7f, (short) 0x7fff,
                (int) 0x7fffffff, (long) 0x7fffffffffffffffL, (int) 0x7fff, (long) 0x7fffffffL,
                (long) 0x7fffffffffffffffL, new BigInteger("18446744073709551615"),
                null /* bytes */, null /* eui48 */, TEST_IPV4_ADDRESS, null /* ipv6Address */);
        assertEquals(expected, msg.toString());
    }

    @Test
    public void testStructClass_hashcode() {
        final FullTypeMessage msg = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
                ByteOrder.BIG_ENDIAN);
        final FullTypeMessage msg1 = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
                ByteOrder.BIG_ENDIAN);

        assertNotEquals(0, msg.hashCode());
        assertNotEquals(0, msg1.hashCode());
        assertTrue(msg.equals(msg1));
        assertEquals(msg.hashCode(), msg1.hashCode());
    }

    public static class InvalidByteArray extends Struct {
        @Field(order = 0, type = Type.ByteArray, arraysize = 12) public byte[] bytes;
    }

    @Test
    public void testStructClass_WrongByteArraySize() {
        final InvalidByteArray msg = doParsingMessageTest("20010db80003000400050006",
                InvalidByteArray.class, ByteOrder.BIG_ENDIAN);

        // Actual byte array size doesn't match the size declared in the annotation.
        msg.bytes = new byte[16];
        assertThrows(IllegalStateException.class, () -> msg.writeToBytes());

        final ByteBuffer output = ByteBuffer.allocate(Struct.getSize(InvalidByteArray.class));
        output.order(ByteOrder.LITTLE_ENDIAN);
        assertThrows(IllegalStateException.class, () -> msg.writeToByteBuffer(output));
    }

    @Test
    public void testStructClass_NullByteArray() {
        final InvalidByteArray msg = doParsingMessageTest("20010db80003000400050006",
                InvalidByteArray.class, ByteOrder.BIG_ENDIAN);

        msg.bytes = null;
        assertThrows(NullPointerException.class, () -> msg.writeToBytes());

        final ByteBuffer output = ByteBuffer.allocate(Struct.getSize(InvalidByteArray.class));
        output.order(ByteOrder.LITTLE_ENDIAN);
        assertThrows(NullPointerException.class, () -> msg.writeToByteBuffer(output));
    }

    @Test
    public void testStructClass_ParsingByteArrayAfterInitialization() {
        InvalidByteArray msg = new InvalidByteArray();
        msg.bytes = new byte[]{(byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03};

        // Although bytes member has been initialized with the length different with
        // annotation size, parsing from ByteBuffer will get bytes member have the
        // reference to byte array with correct size.
        msg = doParsingMessageTest("20010db80003000400050006", InvalidByteArray.class,
                ByteOrder.BIG_ENDIAN);
        assertArrayEquals(TEST_PREFIX64, msg.bytes);
    }
}
