// Copyright 2022, FOSS-VG Developers and Contributers
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// version 3 along with this program.
// If not, see https://www.gnu.org/licenses/agpl-3.0.en.html

#include <iostream>
#include <cstdint>
#include <vector>
#include <fstream>

#include "assert.hpp"

#include "../lib/nbt.hpp"
#include "../lib/error.hpp"
#include "../lib/javacompat.hpp"

int main(){
    std::cout << "################################################################################" << std::endl;
    std::cout << "NBT read/write helper tests" << std::endl;
    std::cout << "################################################################################" << std::endl;

    uint8_t validNBTDataBlob[] = {0x0a, 0x00, 0x00, 0x08, 0x00, 0x3e, 0x53, 0x70, 0x61, 0x63, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x61, 0x6c, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x73, 0x20, 0x61, 0x72, 0x65, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x20, 0x69, 0x6e, 0x20, 0x74, 0x61, 0x67, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x2c, 0x20, 0x72, 0x69, 0x67, 0x68, 0x74, 0x3f, 0x00, 0x16, 0x49, 0x64, 0x6b, 0x2e, 0x20, 0x4c, 0x65, 0x74, 0xe2, 0x80, 0x99, 0x73, 0x20, 0x66, 0x69, 0x6e, 0x64, 0x20, 0x6f, 0x75, 0x74, 0x2e, 0x0a, 0x00, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x75, 0x6e, 0x64, 0x03, 0x00, 0x0b, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0xd3, 0x07, 0x23, 0x41, 0x08, 0x00, 0x09, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x74, 0x65, 0x78, 0x74, 0x00, 0x0c, 0x65, 0x61, 0x74, 0x20, 0x61, 0x20, 0x63, 0x6f, 0x6f, 0x6b, 0x69, 0x65, 0x00, 0x06, 0x00, 0x06, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x41, 0x23, 0x07, 0xd3, 0x4e, 0xfd, 0x07, 0xf1, 0x05, 0x00, 0x05, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x42, 0x0d, 0x12, 0x43, 0x02, 0x00, 0x05, 0x69, 0x6e, 0x74, 0x31, 0x36, 0x07, 0xd0, 0x03, 0x00, 0x05, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x00, 0x9a, 0x21, 0x12, 0x0b, 0x00, 0x0b, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x00, 0x00, 0x00, 0x04, 0x17, 0xbf, 0xe8, 0x3c, 0x00, 0x00, 0xa8, 0xfb, 0x7f, 0xff, 0xff, 0xff, 0x61, 0x72, 0x72, 0x61, 0x04, 0x00, 0x05, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x00, 0x00, 0x00, 0xbc, 0x97, 0xde, 0x9e, 0x3e, 0x0c, 0x00, 0x0b, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xa8, 0xf9, 0x00, 0x00, 0x00, 0x00, 0x01, 0x67, 0xe7, 0xbd, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xab, 0xd8, 0x00, 0x00, 0xab, 0xd8, 0x00, 0x0b, 0x12, 0x21, 0x00, 0x79, 0x61, 0x72, 0x01, 0x00, 0x04, 0x69, 0x6e, 0x74, 0x38, 0x64, 0x07, 0x00, 0x0a, 0x69, 0x6e, 0x74, 0x38, 0x5f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x00, 0x00, 0x00, 0x08, 0x71, 0x35, 0x77, 0x62, 0x54, 0x64, 0xf5, 0x32, 0x09, 0x00, 0x09, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x38, 0x01, 0x00, 0x00, 0x00, 0x05, 0x41, 0x60, 0x4e, 0x7f, 0xfa, 0x09, 0x00, 0x0c, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x73, 0x08, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x50, 0x61, 0x63, 0x6d, 0x61, 0x6e, 0x00, 0x03, 0x61, 0x74, 0x65, 0x00, 0x03, 0x61, 0x6c, 0x6c, 0x00, 0x03, 0x74, 0x68, 0x65, 0x00, 0x04, 0x64, 0x6f, 0x74, 0x73, 0x00, 0x02, 0x73, 0x6f, 0x00, 0x03, 0x6e, 0x6f, 0x77, 0x00, 0x06, 0x68, 0x65, 0xe2, 0x80, 0x99, 0x73, 0x00, 0x06, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x00, 0x03, 0x66, 0x6f, 0x72, 0x00, 0x03, 0x74, 0x68, 0x65, 0x00, 0x06, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x2e, 0x08, 0x00, 0x06, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x00, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x00};
    uint64_t validNBTDataBlobSize = 478;
    uint64_t currentPositionInBlob;

    // used for all integer tests
    uint8_t dataForIntTest[] = {30, 31, 32, 33, 34, 35, 36, 37, 38, 39};
    uint64_t dataSize = 10;
    uint64_t currentPosition;

    // int8 ############################################################
    // read successfully
    currentPosition = 5;
    ASSERT(NBT::Helper::readInt8(dataForIntTest, dataSize, currentPosition).value == 35);
    ASSERT(NBT::Helper::readInt8(dataForIntTest, dataSize, currentPosition).isError == false);
    // begin of data
    currentPosition = 0;
    ASSERT(NBT::Helper::readInt8(dataForIntTest, dataSize, currentPosition).value == 30);
    ASSERT(NBT::Helper::readInt8(dataForIntTest, dataSize, currentPosition).isError == false);
    // end of data
    currentPosition = 9;
    ASSERT(NBT::Helper::readInt8(dataForIntTest, dataSize, currentPosition).value == 39);
    ASSERT(NBT::Helper::readInt8(dataForIntTest, dataSize, currentPosition).isError == false);
    // out of bounds
    currentPosition = 10;
    ASSERT(NBT::Helper::readInt8(dataForIntTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt8(dataForIntTest, dataSize, currentPosition).errorCode == ErrorCodes::OUT_OF_RANGE);
    // read from a bigger blob
    currentPositionInBlob = 0x13A;
    ASSERT(NBT::Helper::readInt8(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).isError == false);
    ASSERT(NBT::Helper::readInt8(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).value == 100);

    std::cout << "Passed readInt8 NBT helper test" << std::endl;

    std::vector<uint8_t>* writeInt8TestResult = new std::vector<uint8_t>();
    NBT::Helper::writeInt8(writeInt8TestResult, (int8_t) 8);
    std::vector<uint8_t> dereferencedWriteInt8TestResult = *writeInt8TestResult;
    delete writeInt8TestResult;
    ASSERT(dereferencedWriteInt8TestResult.back() == (uint8_t) 8);

    std::cout << "Passed writeInt8 NBT helper test" << std::endl;

    // int16 ###########################################################
    // read successfully
    currentPosition = 5;
    ASSERT(NBT::Helper::readInt16(dataForIntTest, dataSize, currentPosition).value == 8996);
    ASSERT(NBT::Helper::readInt16(dataForIntTest, dataSize, currentPosition).isError == false);
    // begin of data
    currentPosition = 0;
    ASSERT(NBT::Helper::readInt16(dataForIntTest, dataSize, currentPosition).value == 7711);
    ASSERT(NBT::Helper::readInt16(dataForIntTest, dataSize, currentPosition).isError == false);
    // end of data
    currentPosition = 8;
    ASSERT(NBT::Helper::readInt16(dataForIntTest, dataSize, currentPosition).value == 9767);
    ASSERT(NBT::Helper::readInt16(dataForIntTest, dataSize, currentPosition).isError == false);
    // partially out of bounds
    currentPosition = 9;
    ASSERT(NBT::Helper::readInt16(dataForIntTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt16(dataForIntTest, dataSize, currentPosition).errorCode == ErrorCodes::OVERRUN);
    // fully out of bounds
    currentPosition = 10;
    ASSERT(NBT::Helper::readInt16(dataForIntTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt16(dataForIntTest, dataSize, currentPosition).errorCode == ErrorCodes::OUT_OF_RANGE);
    // read from a bigger blob
    currentPositionInBlob = 0xb9;
    ASSERT(NBT::Helper::readInt16(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).isError == false);
    ASSERT(NBT::Helper::readInt16(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).value == 2000);

    std::cout << "Passed readInt16 NBT helper test" << std::endl;

    std::vector<uint8_t>* writeInt16TestResult = new std::vector<uint8_t>();
    NBT::Helper::writeInt16(writeInt16TestResult, (int16_t) 0xABCD);
    std::vector<uint8_t> dereferencedWriteInt16TestResult = *writeInt16TestResult;
    delete writeInt16TestResult;
    ASSERT(dereferencedWriteInt16TestResult[0] == (uint8_t) 0xAB && dereferencedWriteInt16TestResult[1] == (uint8_t) 0xCD);

    std::cout << "Passed writeInt16 NBT helper test" << std::endl;

    // int32 ###########################################################
    // read successfully
    currentPosition = 5;
    ASSERT(NBT::Helper::readInt32(dataForIntTest, dataSize, currentPosition).value == 589571366);
    ASSERT(NBT::Helper::readInt32(dataForIntTest, dataSize, currentPosition).isError == false);
    // begin of data
    currentPosition = 0;
    ASSERT(NBT::Helper::readInt32(dataForIntTest, dataSize, currentPosition).value == 505356321);
    ASSERT(NBT::Helper::readInt32(dataForIntTest, dataSize, currentPosition).isError == false);
    // end of data
    currentPosition = 6;
    ASSERT(NBT::Helper::readInt32(dataForIntTest, dataSize, currentPosition).value == 606414375);
    ASSERT(NBT::Helper::readInt32(dataForIntTest, dataSize, currentPosition).isError == false);
    // partially out of bounds
    currentPosition = 7;
    ASSERT(NBT::Helper::readInt32(dataForIntTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt32(dataForIntTest, dataSize, currentPosition).errorCode == ErrorCodes::OVERRUN);
    // fully out of bounds
    currentPosition = 10;
    ASSERT(NBT::Helper::readInt32(dataForIntTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt32(dataForIntTest, dataSize, currentPosition).errorCode == ErrorCodes::OUT_OF_RANGE);
    // read from a bigger blob
    currentPositionInBlob = 0xc3;
    ASSERT(NBT::Helper::readInt32(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).isError == false);
    ASSERT(NBT::Helper::readInt32(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).value == 10101010);

    std::cout << "Passed readInt32 NBT helper test" << std::endl;

    std::vector<uint8_t>* writeInt32TestResult = new std::vector<uint8_t>();
    NBT::Helper::writeInt32(writeInt32TestResult, (int32_t) 0x12345678);
    // delete before checking assertions so we don't leak
    std::vector<uint8_t> dereferencedWriteInt32TestResult = *writeInt32TestResult;
    delete writeInt32TestResult;
    ASSERT(
        dereferencedWriteInt32TestResult[0] == (uint8_t) 0x12 &&
        dereferencedWriteInt32TestResult[1] == (uint8_t) 0x34 &&
        dereferencedWriteInt32TestResult[2] == (uint8_t) 0x56 &&
        dereferencedWriteInt32TestResult[3] == (uint8_t) 0x78
    );

    std::cout << "Passed writeInt32 NBT helper test" << std::endl;

    // int64 ###########################################################
    // read successfully
    currentPosition = 1;
    ASSERT(NBT::Helper::readInt64(dataForIntTest, dataSize, currentPosition).value == 2242829044932683046);
    ASSERT(NBT::Helper::readInt64(dataForIntTest, dataSize, currentPosition).isError == false);
    // begin of data
    currentPosition = 0;
    ASSERT(NBT::Helper::readInt64(dataForIntTest, dataSize, currentPosition).value == 2170488872094606373);
    ASSERT(NBT::Helper::readInt64(dataForIntTest, dataSize, currentPosition).isError == false);
    // end of data
    currentPosition = 2;
    ASSERT(NBT::Helper::readInt64(dataForIntTest, dataSize, currentPosition).value == 2315169217770759719);
    ASSERT(NBT::Helper::readInt64(dataForIntTest, dataSize, currentPosition).isError == false);
    // partially out of bounds
    currentPosition = 3;
    ASSERT(NBT::Helper::readInt64(dataForIntTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt64(dataForIntTest, dataSize, currentPosition).errorCode == ErrorCodes::OVERRUN);
    // fully out of bounds
    currentPosition = 10;
    ASSERT(NBT::Helper::readInt64(dataForIntTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt64(dataForIntTest, dataSize, currentPosition).errorCode == ErrorCodes::OUT_OF_RANGE);
    // read from a bigger blob
    currentPositionInBlob = 0xf1;
    ASSERT(NBT::Helper::readInt64(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).isError == false);
    ASSERT(NBT::Helper::readInt64(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).value == 810001800766);

    std::cout << "Passed readInt64 NBT helper test" << std::endl;

    std::vector<uint8_t>* writeInt64TestResult = new std::vector<uint8_t>();
    NBT::Helper::writeInt64(writeInt64TestResult, (int64_t) 0x0123456789ABCDEF);
    // delete before checking assertions so we don't leak
    std::vector<uint8_t> dereferencedWriteInt64TestResult = *writeInt64TestResult;
    delete writeInt64TestResult;
    ASSERT(
        dereferencedWriteInt64TestResult[0] == (uint8_t) 0x01 &&
        dereferencedWriteInt64TestResult[1] == (uint8_t) 0x23 &&
        dereferencedWriteInt64TestResult[2] == (uint8_t) 0x45 &&
        dereferencedWriteInt64TestResult[3] == (uint8_t) 0x67 &&
        dereferencedWriteInt64TestResult[4] == (uint8_t) 0x89 &&
        dereferencedWriteInt64TestResult[5] == (uint8_t) 0xAB &&
        dereferencedWriteInt64TestResult[6] == (uint8_t) 0xCD &&
        dereferencedWriteInt64TestResult[7] == (uint8_t) 0xEF
    );

    std::cout << "Passed writeInt64 NBT helper test" << std::endl;

    //##################################################################

    // used for integer "array" tests
    uint8_t dataForIntArrayTest[] = {
         0,  0,  0, 20,
         1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
        21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
         0,  0,  0,  5,
        41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
         0,  0,  0, 10,
        61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80,
        81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99,100,
         0,  0,  0,  0
    };
    dataSize = 116;

    // int8 "array" ####################################################
    // read successfully
    currentPosition = 0;
    ASSERT(NBT::Helper::readInt8Array(dataForIntArrayTest, dataSize, currentPosition).value == std::vector<int8_t>({1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}));
    ASSERT(NBT::Helper::readInt8Array(dataForIntArrayTest, dataSize, currentPosition).isError == false);
    // read empty
    currentPosition = 112;
    ASSERT(NBT::Helper::readInt8Array(dataForIntArrayTest, dataSize, currentPosition).value == std::vector<int8_t>());
    ASSERT(NBT::Helper::readInt8Array(dataForIntArrayTest, dataSize, currentPosition).isError == false);
    // read overrun
    currentPosition = 20;
    ASSERT(NBT::Helper::readInt8Array(dataForIntArrayTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt8Array(dataForIntArrayTest, dataSize, currentPosition).errorCode == ErrorCodes::OVERRUN);
    // read with size partially out of bounds
    currentPosition = 114;
    ASSERT(NBT::Helper::readInt8Array(dataForIntArrayTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt8Array(dataForIntArrayTest, dataSize, currentPosition).errorCode == ErrorCodes::OVERRUN);
    // read out of bounds
    currentPosition = 200;
    ASSERT(NBT::Helper::readInt8Array(dataForIntArrayTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt8Array(dataForIntArrayTest, dataSize, currentPosition).errorCode == ErrorCodes::OUT_OF_RANGE);
    // read from a bigger blob
    currentPositionInBlob = 0x148;
    ASSERT(NBT::Helper::readInt8Array(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).isError == false);
    ASSERT(NBT::Helper::readInt8Array(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).value == std::vector<int8_t>({(int8_t) 0x71, (int8_t) 0x35, (int8_t) 0x77, (int8_t) 0x62, (int8_t) 0x54, (int8_t) 0x64, (int8_t) 0xf5, (int8_t) 0x32}));

    std::cout << "Passed readInt8Array NBT helper test" << std::endl;

    std::vector<uint8_t>* int8ArrayTestOutput = new std::vector<uint8_t>();
    int8_t writeDataTest[] = {1,2,3,4};
    NBT::Helper::writeInt8Array(int8ArrayTestOutput, writeDataTest, (uint32_t)4);
    ASSERT(
        int8ArrayTestOutput->at(0) == 0 &&
        int8ArrayTestOutput->at(1) == 0 &&
        int8ArrayTestOutput->at(2) == 0 &&
        int8ArrayTestOutput->at(3) == 4 &&
        int8ArrayTestOutput->at(4) == 1 &&
        int8ArrayTestOutput->at(5) == 2 &&
        int8ArrayTestOutput->at(6) == 3 &&
        int8ArrayTestOutput->at(7) == 4
    );
    int8ArrayTestOutput->clear();
    NBT::Helper::writeInt8Array(int8ArrayTestOutput, std::vector<int8_t>({1,2,3,4}));
    ASSERT(
        int8ArrayTestOutput->at(0) == 0 &&
        int8ArrayTestOutput->at(1) == 0 &&
        int8ArrayTestOutput->at(2) == 0 &&
        int8ArrayTestOutput->at(3) == 4 &&
        int8ArrayTestOutput->at(4) == 1 &&
        int8ArrayTestOutput->at(5) == 2 &&
        int8ArrayTestOutput->at(6) == 3 &&
        int8ArrayTestOutput->at(7) == 4
    );
    delete int8ArrayTestOutput;
    std::cout << "Passed writeInt8Array NBT helper test" << std::endl;

    // int32 "array" ###################################################
    // read successfully
    currentPosition = 68;
    ASSERT(NBT::Helper::readInt32Array(dataForIntArrayTest, dataSize, currentPosition).value == std::vector<int32_t>({1027489600, 1094861636, 1162233672, 1229605708, 1296977744, 1364349780, 1431721816, 1499093852, 1566465888, 1633837924}));
    ASSERT(NBT::Helper::readInt32Array(dataForIntArrayTest, dataSize, currentPosition).isError == false);
    // read empty
    currentPosition = 112;
    ASSERT(NBT::Helper::readInt32Array(dataForIntArrayTest, dataSize, currentPosition).value == std::vector<int32_t>());
    ASSERT(NBT::Helper::readInt32Array(dataForIntArrayTest, dataSize, currentPosition).isError == false);
    // read overrun
    currentPosition = 20;
    ASSERT(NBT::Helper::readInt32Array(dataForIntArrayTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt32Array(dataForIntArrayTest, dataSize, currentPosition).errorCode == ErrorCodes::OVERRUN);
    // read with size partially out of bounds
    currentPosition = 114;
    ASSERT(NBT::Helper::readInt32Array(dataForIntArrayTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt32Array(dataForIntArrayTest, dataSize, currentPosition).errorCode == ErrorCodes::OVERRUN);
    // read out of bounds
    currentPosition = 200;
    ASSERT(NBT::Helper::readInt32Array(dataForIntArrayTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt32Array(dataForIntArrayTest, dataSize, currentPosition).errorCode == ErrorCodes::OUT_OF_RANGE);
    // read from a bigger blob
    currentPositionInBlob = 0xd5;
    ASSERT(NBT::Helper::readInt32Array(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).isError == false);
    ASSERT(NBT::Helper::readInt32Array(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).value == std::vector<int32_t>({(int32_t) 0x17bfe83c, (int32_t) 0xa8fb, (int32_t) 0x7fffffff, (int32_t) 0x61727261}));

    std::cout << "Passed readInt32Array NBT helper test" << std::endl;

    std::vector<uint8_t>* int32ArrayTestOutput = new std::vector<uint8_t>();
    int32_t input32[] = {0x01234567, 0x01234567};
    NBT::Helper::writeInt32Array(int32ArrayTestOutput, input32, 2);
    ASSERT(
        int32ArrayTestOutput->at(0) == 0x00 &&
        int32ArrayTestOutput->at(1) == 0x00 &&
        int32ArrayTestOutput->at(2) == 0x00 &&
        int32ArrayTestOutput->at(3) == 0x02 &&
        int32ArrayTestOutput->at(4) == 0x01 &&
        int32ArrayTestOutput->at(5) == 0x23 &&
        int32ArrayTestOutput->at(6) == 0x45 &&
        int32ArrayTestOutput->at(7) == 0x67 &&
        int32ArrayTestOutput->at(8) == 0x01 &&
        int32ArrayTestOutput->at(9) == 0x23 &&
        int32ArrayTestOutput->at(10) == 0x45 &&
        int32ArrayTestOutput->at(11) == 0x67
    );
    int32ArrayTestOutput->clear();
    NBT::Helper::writeInt32Array(int32ArrayTestOutput, std::vector<int32_t>({0x01234567, 0x01234567}));
    ASSERT(
        int32ArrayTestOutput->at(0) == 0x00 &&
        int32ArrayTestOutput->at(1) == 0x00 &&
        int32ArrayTestOutput->at(2) == 0x00 &&
        int32ArrayTestOutput->at(3) == 0x02 &&
        int32ArrayTestOutput->at(4) == 0x01 &&
        int32ArrayTestOutput->at(5) == 0x23 &&
        int32ArrayTestOutput->at(6) == 0x45 &&
        int32ArrayTestOutput->at(7) == 0x67 &&
        int32ArrayTestOutput->at(8) == 0x01 &&
        int32ArrayTestOutput->at(9) == 0x23 &&
        int32ArrayTestOutput->at(10) == 0x45 &&
        int32ArrayTestOutput->at(11) == 0x67
    );
    delete int32ArrayTestOutput;

    std::cout << "Passed writeInt32Array NBT helper test" << std::endl;

    // int64 "array" ###################################################
    // read successfully
    currentPosition = 44;
    ASSERT(NBT::Helper::readInt64Array(dataForIntArrayTest, dataSize, currentPosition).value == std::vector<int64_t>({2966230773313449776, 3544952156018063160, 4123673537695186954, 4413034230074983236, 4991755612779596620}));
    ASSERT(NBT::Helper::readInt64Array(dataForIntArrayTest, dataSize, currentPosition).isError == false);
    // read empty
    currentPosition = 112;
    ASSERT(NBT::Helper::readInt64Array(dataForIntArrayTest, dataSize, currentPosition).value == std::vector<int64_t>());
    ASSERT(NBT::Helper::readInt64Array(dataForIntArrayTest, dataSize, currentPosition).isError == false);
    // read overrun
    currentPosition = 20;
    ASSERT(NBT::Helper::readInt64Array(dataForIntArrayTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt64Array(dataForIntArrayTest, dataSize, currentPosition).errorCode == ErrorCodes::OVERRUN);
    // read with size partially out of bounds
    currentPosition = 114;
    ASSERT(NBT::Helper::readInt64Array(dataForIntArrayTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt64Array(dataForIntArrayTest, dataSize, currentPosition).errorCode == ErrorCodes::OVERRUN);
    // read out of bounds
    currentPosition = 200;
    ASSERT(NBT::Helper::readInt64Array(dataForIntArrayTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readInt64Array(dataForIntArrayTest, dataSize, currentPosition).errorCode == ErrorCodes::OUT_OF_RANGE);
    // read from a bigger blob
    currentPositionInBlob = 0x107;
    ASSERT(NBT::Helper::readInt64Array(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).isError == false);
    ASSERT(NBT::Helper::readInt64Array(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).value == std::vector<int64_t>({(int64_t) 239865, (int64_t) 23586749, (int64_t) 9223372036854775807, (int64_t) 188944201329624, (int64_t) 3116157694992754}));

    std::cout << "Passed readInt64Array NBT helper test" << std::endl;

    std::vector<uint8_t>* int64ArrayTestOutput = new std::vector<uint8_t>();
    int64_t input64[] = {0x0123456789ABCDEF, 0x0123456789ABCDEF};
    NBT::Helper::writeInt64Array(int64ArrayTestOutput, input64, 2);
    ASSERT(
        int64ArrayTestOutput->at(0) == 0x00 &&
        int64ArrayTestOutput->at(1) == 0x00 &&
        int64ArrayTestOutput->at(2) == 0x00 &&
        int64ArrayTestOutput->at(3) == 0x02 &&
        int64ArrayTestOutput->at(4) == 0x01 &&
        int64ArrayTestOutput->at(5) == 0x23 &&
        int64ArrayTestOutput->at(6) == 0x45 &&
        int64ArrayTestOutput->at(7) == 0x67 &&
        int64ArrayTestOutput->at(8) == 0x89 &&
        int64ArrayTestOutput->at(9) == 0xAB &&
        int64ArrayTestOutput->at(10) == 0xCD &&
        int64ArrayTestOutput->at(11) == 0xEF &&
        int64ArrayTestOutput->at(12) == 0x01 &&
        int64ArrayTestOutput->at(13) == 0x23 &&
        int64ArrayTestOutput->at(14) == 0x45 &&
        int64ArrayTestOutput->at(15) == 0x67 &&
        int64ArrayTestOutput->at(16) == 0x89 &&
        int64ArrayTestOutput->at(17) == 0xAB &&
        int64ArrayTestOutput->at(18) == 0xCD &&
        int64ArrayTestOutput->at(19) == 0xEF
    );
    int64ArrayTestOutput->clear();
    NBT::Helper::writeInt64Array(int64ArrayTestOutput, std::vector<int64_t>({0x0123456789ABCDEF, 0x0123456789ABCDEF}));
    ASSERT(
        int64ArrayTestOutput->at(0) == 0x00 &&
        int64ArrayTestOutput->at(1) == 0x00 &&
        int64ArrayTestOutput->at(2) == 0x00 &&
        int64ArrayTestOutput->at(3) == 0x02 &&
        int64ArrayTestOutput->at(4) == 0x01 &&
        int64ArrayTestOutput->at(5) == 0x23 &&
        int64ArrayTestOutput->at(6) == 0x45 &&
        int64ArrayTestOutput->at(7) == 0x67 &&
        int64ArrayTestOutput->at(8) == 0x89 &&
        int64ArrayTestOutput->at(9) == 0xAB &&
        int64ArrayTestOutput->at(10) == 0xCD &&
        int64ArrayTestOutput->at(11) == 0xEF &&
        int64ArrayTestOutput->at(12) == 0x01 &&
        int64ArrayTestOutput->at(13) == 0x23 &&
        int64ArrayTestOutput->at(14) == 0x45 &&
        int64ArrayTestOutput->at(15) == 0x67 &&
        int64ArrayTestOutput->at(16) == 0x89 &&
        int64ArrayTestOutput->at(17) == 0xAB &&
        int64ArrayTestOutput->at(18) == 0xCD &&
        int64ArrayTestOutput->at(19) == 0xEF &&
        true
    );
    delete int64ArrayTestOutput;

    std::cout << "Passed writeInt64Array NBT helper test" << std::endl;

    // float ###########################################################
    uint8_t dataForFloatTest[] = {0xC7, 0x77, 0x77, 0x77};
    dataSize = 4;
    currentPosition = 0;
    // read successfully
    ASSERT(NBT::Helper::readFloat(dataForFloatTest, dataSize, currentPosition).value == -63351.46484375f);
    ASSERT(NBT::Helper::readFloat(dataForFloatTest, dataSize, currentPosition).isError == false);
    // read overrun
    currentPosition = 1;
    ASSERT(NBT::Helper::readFloat(dataForFloatTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readFloat(dataForFloatTest, dataSize, currentPosition).errorCode == ErrorCodes::OVERRUN);
    // read out of bounds
    currentPosition = 4;
    ASSERT(NBT::Helper::readFloat(dataForFloatTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readFloat(dataForFloatTest, dataSize, currentPosition).errorCode == ErrorCodes::OUT_OF_RANGE);
    // read from a bigger blob
    currentPositionInBlob = 0xad;
    ASSERT(NBT::Helper::readFloat(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).isError == false);
    ASSERT(NBT::Helper::readFloat(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).value == 35.2678337097168);

    std::cout << "Passed readFloat NBT helper test" << std::endl;

    std::vector<uint8_t>* writeFloatTestResult = new std::vector<uint8_t>();
    NBT::Helper::writeFloat(writeFloatTestResult, (float) -63351.46484375f);
    std::vector<uint8_t> dereferencedWriteFloatTestResult = *writeFloatTestResult;
    delete writeFloatTestResult;
    ASSERT(
        dereferencedWriteFloatTestResult[0] == (uint8_t) 0xC7 &&
        dereferencedWriteFloatTestResult[1] == (uint8_t) 0x77 &&
        dereferencedWriteFloatTestResult[2] == (uint8_t) 0x77 &&
        dereferencedWriteFloatTestResult[3] == (uint8_t) 0x77
    );

    std::cout << "Passed writeFloat NBT helper test" << std::endl;

    // double ##########################################################
    uint8_t dataForDoubleTest[] = {0xC0, 0x34, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00};
    dataSize = 8;
    currentPosition = 0;
    // read successfully
    ASSERT(NBT::Helper::readDouble(dataForDoubleTest, dataSize, currentPosition).value == -20.015625476837158203125);
    ASSERT(NBT::Helper::readDouble(dataForDoubleTest, dataSize, currentPosition).isError == false);
    // read overrun
    currentPosition = 1;
    ASSERT(NBT::Helper::readDouble(dataForDoubleTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readDouble(dataForDoubleTest, dataSize, currentPosition).errorCode == ErrorCodes::OVERRUN);
    // read out of bounds
    currentPosition = 8;
    ASSERT(NBT::Helper::readDouble(dataForDoubleTest, dataSize, currentPosition).isError == true);
    ASSERT(NBT::Helper::readDouble(dataForDoubleTest, dataSize, currentPosition).errorCode == ErrorCodes::OUT_OF_RANGE);
    // read from a bigger blob
    currentPositionInBlob = 0x9d;
    ASSERT(NBT::Helper::readDouble(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).isError == false);
    ASSERT(NBT::Helper::readDouble(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).value == 623593.6542742235);

    std::cout << "Passed readDouble NBT helper test" << std::endl;

    std::vector<uint8_t>* writeDoubleTestResult = new std::vector<uint8_t>();
    NBT::Helper::writeDouble(writeDoubleTestResult, (double) -20.015625476837158203125);
    std::vector<uint8_t> dereferencedWriteDoubleTestResult = *writeDoubleTestResult;
    delete writeDoubleTestResult;
    ASSERT(
        dereferencedWriteDoubleTestResult[0] == (uint8_t) 0xC0 &&
        dereferencedWriteDoubleTestResult[1] == (uint8_t) 0x34 &&
        dereferencedWriteDoubleTestResult[2] == (uint8_t) 0x04 &&
        dereferencedWriteDoubleTestResult[3] == (uint8_t) 0x00 &&
        dereferencedWriteDoubleTestResult[4] == (uint8_t) 0x08 &&
        dereferencedWriteDoubleTestResult[5] == (uint8_t) 0x00 &&
        dereferencedWriteDoubleTestResult[6] == (uint8_t) 0x00 &&
        dereferencedWriteDoubleTestResult[7] == (uint8_t) 0x00
    );

    std::cout << "Passed writeDouble NBT helper test" << std::endl;

    //readString test
    char* nextChar = new char;

    //reading data from the java modified utf8 file
    std::streampos javaSize;
    std::string javaStdString;
    const char* javaFilePath = "./resources/unicode_data/java-style_unicode";
    std::ifstream javaFile(javaFilePath, std::ios::in | std::ios::binary | std::ios::ate);
    if(javaFile.is_open()){
        javaSize = javaFile.tellg();
        javaFile.seekg(0, std::ios::beg);

        for (int i=0; i<javaSize; i++) {
            javaFile.read(nextChar, 1);
            javaStdString.push_back(*nextChar);
        }

        javaFile.close();
    } else {
        std::cerr << "Failed to open file: " << javaFilePath << std::endl;
        return 2;
    }


    //reading data from the normal utf8 file
    std::streampos normalSize;
    std::string normalStdString;
    const char* normalFilePath = "./resources/unicode_data/normal_utf-8";
    std::ifstream normalFile(normalFilePath, std::ios::in | std::ios::binary | std::ios::ate);
    if(normalFile.is_open()){
        normalSize = normalFile.tellg();
        normalFile.seekg(0, std::ios::beg);

        for (int i=0; i<normalSize; i++) {
            normalFile.read(nextChar, 1);
            normalStdString.push_back(*nextChar);
        }

        normalFile.close();
    } else {
        std::cerr << "Failed to open file: " << normalFilePath << std::endl;
        return 2;
    }
    tiny_utf8::string normalString = tiny_utf8::string(normalStdString);
    delete nextChar;

    ASSERT(normalString == NBT::Helper::readString(reinterpret_cast<uint8_t*>(javaStdString.data()), javaStdString.size(), 0).value)

    javaStdString[0] = '1';
    ASSERT(NBT::Helper::readString(reinterpret_cast<uint8_t*>(javaStdString.data()), javaStdString.size(), 0).errorCode == ErrorCodes::OVERRUN);
    ASSERT(NBT::Helper::readString(reinterpret_cast<uint8_t*>(javaStdString.data()), javaStdString.size(), javaStdString.size()).errorCode == ErrorCodes::OUT_OF_RANGE);

    //reading data from the blob at the top of this file
    currentPositionInBlob = 0x1cf;
    ASSERT(NBT::Helper::readString(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).isError == false);
    ASSERT(NBT::Helper::readString(validNBTDataBlob, validNBTDataBlobSize, currentPositionInBlob).value == "Hello World!");

    std::cout << "Passed readString NBT helper test." << std::endl;

    char* nextChar1 = new char;

    //reading data from the java modified utf8 file
    std::streampos javaSize1;
    std::vector<uint8_t> javaStdString1;
    const char* javaFilePath1 = "./resources/unicode_data/java-style_unicode";
    std::ifstream javaFile1(javaFilePath1, std::ios::in | std::ios::binary | std::ios::ate);
    if(javaFile1.is_open()){
        javaSize1 = javaFile1.tellg();
        javaFile1.seekg(0, std::ios::beg);

        for (int i=0; i<javaSize1; i++) {
            javaFile1.read(nextChar1, 1);
            javaStdString1.push_back(*nextChar1);
        }

        javaFile1.close();
    } else {
        std::cerr << "Failed to open file: " << javaFilePath1 << std::endl;
        return 2;
    }
    delete nextChar1;

    std::vector<uint8_t>* exportedString = new std::vector<uint8_t>();
    NBT::Helper::writeString(exportedString, normalString);
    ASSERT(javaStdString1 == *exportedString);

    //check that we get an error when trying to write a string that is too long
    std::string overrunString = std::string(0x10000, '.');
    ASSERT(NBT::Helper::writeString(exportedString, tiny_utf8::string(overrunString)).isError);
    ASSERT(NBT::Helper::writeString(exportedString, tiny_utf8::string(overrunString)).errorCode == ErrorCodes::OVERRUN);

    std::cout << "Passed writeString NBT helper test." << std::endl;

    return 0;
}
