#include "./testutils.h"

using namespace CppUtilities;

#include "../conversion/stringconversion.h"

#ifndef CPP_UTILITIES_NO_ICONV
/*!
 * \brief Allows printing std::wstring using CPPUNIT_ASSERT_EQUAL.
 */
std::ostream &operator<<(std::ostream &out, const std::wstring &s)
{
    const auto utf8 = CppUtilities::
#ifdef CONVERSION_UTILITIES_IS_BYTE_ORDER_LITTLE_ENDIAN
        convertUtf16LEToUtf8
#else
        convertUtf16BEToUtf8
#endif
        (reinterpret_cast<const char *>(s.data()), s.size() * (sizeof(std::wstring::value_type) / sizeof(char)));
    out.write(utf8.first.get(), static_cast<std::streamsize>(utf8.second));
    return out;
}
#endif

#include "../conversion/conversionexception.h"
#include "../conversion/stringbuilder.h"

#include "../io/ansiescapecodes.h"
#include "../io/binaryreader.h"
#include "../io/binarywriter.h"
#include "../io/bitreader.h"
#include "../io/buffersearch.h"
#include "../io/copy.h"
#include "../io/inifile.h"
#include "../io/misc.h"
#include "../io/nativefilestream.h"
#include "../io/path.h"

#ifdef CPP_UTILITIES_USE_LIBARCHIVE
#include "../io/archive.h"
#endif

#include <cppunit/TestFixture.h>
#include <cppunit/extensions/HelperMacros.h>

#include <algorithm>
#include <cstddef>
#include <fstream>
#include <regex>
#include <sstream>

#ifdef PLATFORM_WINDOWS
#include <cstdio>
#endif

#ifdef PLATFORM_UNIX
#include <sys/fcntl.h>
#include <sys/types.h>
#endif

using namespace std;
using namespace CppUtilities::Literals;
using namespace CPPUNIT_NS;

/*!
 * \brief The IoTests class tests classes and functions provided by the files inside the io directory.
 */
class IoTests : public TestFixture {
    CPPUNIT_TEST_SUITE(IoTests);
    CPPUNIT_TEST(testBinaryReader);
    CPPUNIT_TEST(testBinaryWriter);
    CPPUNIT_TEST(testBitReader);
    CPPUNIT_TEST(testBufferSearch);
    CPPUNIT_TEST(testPathUtilities);
    CPPUNIT_TEST(testIniFile);
    CPPUNIT_TEST(testAdvancedIniFile);
    CPPUNIT_TEST(testCopy);
    CPPUNIT_TEST(testCopyWithNativeFileStream);
    CPPUNIT_TEST(testReadFile);
    CPPUNIT_TEST(testWriteFile);
    CPPUNIT_TEST(testAnsiEscapeCodes);
#ifdef CPP_UTILITIES_USE_NATIVE_FILE_BUFFER
    CPPUNIT_TEST(testNativeFileStream);
#endif
#ifdef CPP_UTILITIES_USE_LIBARCHIVE
    CPPUNIT_TEST(testExtractingArchive);
#endif
    CPPUNIT_TEST_SUITE_END();

public:
    void setUp() override;
    void tearDown() override;

    void testBinaryReader();
    void testBinaryWriter();
    void testBitReader();
    void testBufferSearch();
    void testPathUtilities();
    void testIniFile();
    void testAdvancedIniFile();
    void testCopy();
    void testCopyWithNativeFileStream();
    void testReadFile();
    void testWriteFile();
    void testAnsiEscapeCodes();
#ifdef CPP_UTILITIES_USE_NATIVE_FILE_BUFFER
    void testNativeFileStream();
#endif
#ifdef CPP_UTILITIES_USE_LIBARCHIVE
    void testExtractingArchive();
#endif
};

CPPUNIT_TEST_SUITE_REGISTRATION(IoTests);

void IoTests::setUp()
{
}

void IoTests::tearDown()
{
}

/*!
 * \brief Tests the most important methods of the BinaryReader.
 */
void IoTests::testBinaryReader()
{
    // read test file
    fstream testFile;
    testFile.exceptions(ios_base::failbit | ios_base::badbit);
    testFile.open(testFilePath("some_data"), ios_base::in | ios_base::binary);
    BinaryReader reader(&testFile);
    CPPUNIT_ASSERT_EQUAL(static_cast<istream::pos_type>(398), reader.readStreamsize());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::uint16_t>(0x0102u), reader.readUInt16LE());
    CPPUNIT_ASSERT_EQUAL(static_cast<istream::pos_type>(396), reader.readRemainingBytes());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::uint16_t>(0x0102u), reader.readUInt16BE());
    CPPUNIT_ASSERT_EQUAL(0x010203u, reader.readUInt24LE());
    CPPUNIT_ASSERT_EQUAL(0x010203u, reader.readUInt24BE());
    CPPUNIT_ASSERT_EQUAL(0x01020304u, reader.readUInt32LE());
    CPPUNIT_ASSERT_EQUAL(0x01020304u, reader.readUInt32BE());
    CPPUNIT_ASSERT_EQUAL(0x0102030405u, reader.readUInt40LE());
    CPPUNIT_ASSERT_EQUAL(0x0102030405u, reader.readUInt40BE());
    CPPUNIT_ASSERT_EQUAL(0x01020304050607u, reader.readUInt56LE());
    CPPUNIT_ASSERT_EQUAL(0x01020304050607u, reader.readUInt56BE());
    CPPUNIT_ASSERT_EQUAL(0x0102030405060708u, reader.readUInt64LE());
    CPPUNIT_ASSERT_EQUAL(0x0102030405060708u, reader.readUInt64BE());
    testFile.seekg(0);
    CPPUNIT_ASSERT_EQUAL(reader.readInt16LE(), static_cast<std::int16_t>(0x0102));
    CPPUNIT_ASSERT_EQUAL(reader.readInt16BE(), static_cast<std::int16_t>(0x0102));
    CPPUNIT_ASSERT_EQUAL(0x010203, reader.readInt24LE());
    CPPUNIT_ASSERT_EQUAL(0x010203, reader.readInt24BE());
    CPPUNIT_ASSERT_EQUAL(0x01020304, reader.readInt32LE());
    CPPUNIT_ASSERT_EQUAL(0x01020304, reader.readInt32BE());
    CPPUNIT_ASSERT_EQUAL(0x0102030405, reader.readInt40LE());
    CPPUNIT_ASSERT_EQUAL(0x0102030405, reader.readInt40BE());
    CPPUNIT_ASSERT_EQUAL(0x01020304050607, reader.readInt56LE());
    CPPUNIT_ASSERT_EQUAL(0x01020304050607, reader.readInt56BE());
    CPPUNIT_ASSERT_EQUAL(0x0102030405060708, reader.readInt64LE());
    CPPUNIT_ASSERT_EQUAL(0x0102030405060708, reader.readInt64BE());
    CPPUNIT_ASSERT_EQUAL(1.125f, reader.readFloat32LE());
    CPPUNIT_ASSERT_EQUAL(1.625, reader.readFloat64LE());
    CPPUNIT_ASSERT_EQUAL(1.125f, reader.readFloat32BE());
    CPPUNIT_ASSERT_EQUAL(reader.readFloat64BE(), 1.625);
    CPPUNIT_ASSERT_EQUAL(false, reader.readBool());
    CPPUNIT_ASSERT_EQUAL(true, reader.readBool());
    CPPUNIT_ASSERT_EQUAL("abc"s, reader.readString(3));
    CPPUNIT_ASSERT_EQUAL(reader.readLengthPrefixedString(), "ABC"s);
    CPPUNIT_ASSERT_EQUAL("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901"
                         "23456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123"
                         "45678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345"
                         "678901234567890123456789"s,
        reader.readLengthPrefixedString());
    CPPUNIT_ASSERT_EQUAL("def"s, reader.readTerminatedString());
    testFile.seekg(-4, ios_base::cur);
    CPPUNIT_ASSERT_EQUAL("def"s, reader.readTerminatedString(5, 0));
    CPPUNIT_ASSERT_THROW(reader.readLengthPrefixedString(), ConversionException);
    CPPUNIT_ASSERT_MESSAGE("pos in stream not advanced on conversion error", reader.readByte() == 0);

    // test ownership
    reader.setStream(nullptr, true);
    reader.setStream(new fstream(), true);
    BinaryReader reader2(reader);
    CPPUNIT_ASSERT(reader2.stream() == reader.stream());
    CPPUNIT_ASSERT(!reader2.hasOwnership());
    reader.setStream(&testFile, false);
    reader.setStream(new fstream(), true);
}

/*!
 * \brief Tests the most important methods of the BinaryWriter.
 */
void IoTests::testBinaryWriter()
{
    // prepare reading expected data
    fstream testFile;
    testFile.exceptions(ios_base::failbit | ios_base::badbit);
    testFile.open(testFilePath("some_data"), ios_base::in | ios_base::binary);

    // prepare output stream
    stringstream outputStream(ios_base::in | ios_base::out | ios_base::binary);
    outputStream.exceptions(ios_base::failbit | ios_base::badbit);
    char testData[397];
#if defined(__GLIBCXX__) && !defined(_LIBCPP_VERSION)
#define USE_RDBUF_DIRECTLY
    outputStream.rdbuf()->pubsetbuf(testData, sizeof(testData));
#endif

    // write test data
    BinaryWriter writer(&outputStream);
    writer.writeUInt16LE(0x0102u);
    writer.writeUInt16BE(0x0102u);
    writer.writeUInt24LE(0x010203u);
    writer.writeUInt24BE(0x010203u);
    writer.writeUInt32LE(0x01020304u);
    writer.writeUInt32BE(0x01020304u);
    writer.writeUInt40LE(0x0102030405u);
    writer.writeUInt40BE(0x0102030405u);
    writer.writeUInt56LE(0x01020304050607u);
    writer.writeUInt56BE(0x01020304050607u);
    writer.writeUInt64LE(0x0102030405060708u);
    writer.writeUInt64BE(0x0102030405060708u);

#ifndef USE_RDBUF_DIRECTLY
    outputStream.seekg(0);
    outputStream.read(testData, 58);
#endif

    // test written values
    for (char c : testData) {
        const auto pos = static_cast<std::size_t>(testFile.tellg());
        if (pos >= 58) {
            break;
        }
        char expected;
        testFile.read(&expected, 1);
        CPPUNIT_ASSERT_EQUAL_MESSAGE(argsToString("offset ", pos), asHexNumber(expected), asHexNumber(c));
    }
    testFile.seekg(0);
    outputStream.seekp(0);

    // write more test data
    writer.writeInt16LE(0x0102);
    writer.writeInt16BE(0x0102);
    writer.writeInt24LE(0x010203);
    writer.writeInt24BE(0x010203);
    writer.writeInt32LE(0x01020304);
    writer.writeInt32BE(0x01020304);
    writer.writeInt40LE(0x0102030405);
    writer.writeInt40BE(0x0102030405);
    writer.writeInt56LE(0x01020304050607);
    writer.writeInt56BE(0x01020304050607);
    writer.writeInt64LE(0x0102030405060708);
    writer.writeInt64BE(0x0102030405060708);
    writer.writeFloat32LE(1.125);
    writer.writeFloat64LE(1.625);
    writer.writeFloat32BE(1.125);
    writer.writeFloat64BE(1.625);
    writer.writeBool(false);
    writer.writeBool(true);
    writer.writeString("abc");
    writer.writeLengthPrefixedString("ABC");
    writer.writeLengthPrefixedString("012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
                                     "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901"
                                     "234567890123456789012345678901234567890123456789012345678901234567890123456789");
    writer.writeTerminatedString("def");

#ifndef USE_RDBUF_DIRECTLY
    outputStream.seekg(0);
    outputStream.read(testData, 58);
#endif

    // test written values
    for (char c : testData) {
        const auto pos = static_cast<std::size_t>(testFile.tellg());
        if (pos >= 58) {
            break;
        }
        char expected;
        testFile.read(&expected, 1);
        CPPUNIT_ASSERT_EQUAL_MESSAGE(argsToString("offset ", pos), asHexNumber(expected), asHexNumber(c));
    }

    // test ownership
    writer.setStream(nullptr, true);
    writer.setStream(new fstream(), true);
    BinaryWriter writer2(writer);
    CPPUNIT_ASSERT(writer2.stream() == writer.stream());
    CPPUNIT_ASSERT(!writer2.hasOwnership());
    writer.setStream(&testFile, false);
    writer.setStream(new fstream(), true);
}

/*!
 * \brief Tests the BitReader class.
 */
void IoTests::testBitReader()
{
    const std::uint8_t testData[] = { 0x81, 0x90, 0x3C, 0x44, 0x28, 0x00, 0x44, 0x10, 0x20, 0xFF, 0xFA };
    BitReader reader(reinterpret_cast<const char *>(testData), sizeof(testData));
    CPPUNIT_ASSERT(reader.readBit() == 1);
    reader.skipBits(6);
    CPPUNIT_ASSERT_EQUAL(static_cast<std::uint8_t>(3), reader.showBits<std::uint8_t>(2));
    CPPUNIT_ASSERT_EQUAL(static_cast<std::uint8_t>(3), reader.readBits<std::uint8_t>(2));
    CPPUNIT_ASSERT_EQUAL(static_cast<std::uint32_t>(0x103C4428 << 1), reader.readBits<std::uint32_t>(32));
    reader.align();
    CPPUNIT_ASSERT_EQUAL(static_cast<std::uint8_t>(0x44), reader.readBits<std::uint8_t>(8));
    CPPUNIT_ASSERT_EQUAL(static_cast<std::uint8_t>(7), reader.readUnsignedExpGolombCodedBits<std::uint8_t>());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::int8_t>(4), reader.readSignedExpGolombCodedBits<std::int8_t>());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::uint8_t>(0), reader.readBit());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::uint8_t>(0), reader.readBit());
    reader.skipBits(8 + 4);
    CPPUNIT_ASSERT_EQUAL(4_st, reader.bitsAvailable());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::uint8_t>(0xA), reader.readBits<std::uint8_t>(4));
    CPPUNIT_ASSERT_THROW(reader.readBit(), std::ios_base::failure);
    CPPUNIT_ASSERT_THROW(reader.skipBits(1), std::ios_base::failure);
    reader.reset(reinterpret_cast<const char *>(testData), sizeof(testData));
    CPPUNIT_ASSERT_EQUAL(static_cast<std::size_t>(8 * sizeof(testData)), reader.bitsAvailable());
}

/*!
 * \brief Tests the BufferSearch class.
 */
void IoTests::testBufferSearch()
{
    // setup search to test
    auto expectedResult = std::string();
    auto hasResult = false;
    auto bs = BufferSearch("Updated version: ", "\t\n", "Starting build", [&](BufferSearch &, std::string &&result) {
        CPPUNIT_ASSERT_EQUAL(expectedResult, result);
        CPPUNIT_ASSERT_MESSAGE("callback only invoked once", !hasResult);
        hasResult = true;
    });

    // feed data into the search
    char buffer[30] = { 0 };
    bs(buffer, 0);
    CPPUNIT_ASSERT(!hasResult);
    std::strcpy(buffer, "Starting Updated");
    CPPUNIT_ASSERT(bs.process(std::string_view(buffer, 16)) == nullptr);
    CPPUNIT_ASSERT(!hasResult);
    std::strcpy(buffer, " version: some ");
    bs(buffer, 15);
    CPPUNIT_ASSERT(!hasResult);
    expectedResult = "some version number";
    std::strcpy(buffer, "version number\tmore chars");
    CPPUNIT_ASSERT_EQUAL(static_cast<std::ptrdiff_t>(14), bs.process(buffer, 25) - buffer);
    CPPUNIT_ASSERT(hasResult);
    hasResult = false;
    std::strcpy(buffer, "... Starting build ...");
    bs(buffer, 22);
    CPPUNIT_ASSERT(!hasResult);
}

/*!
 * \brief Tests fileName() and removeInvalidChars().
 */
void IoTests::testPathUtilities()
{
    CPPUNIT_ASSERT_EQUAL("libc++utilities.so"s, fileName("C:\\libs\\libc++utilities.so"));
    CPPUNIT_ASSERT_EQUAL("libc++utilities.so"s, fileName("C:\\libs/libc++utilities.so"));
    CPPUNIT_ASSERT_EQUAL("libc++utilities.so"s, fileName("/usr/lib/libc++utilities.so"));
    CPPUNIT_ASSERT_EQUAL("libc++utilities.so"s, fileName("libc++utilities.so"));
    CPPUNIT_ASSERT_EQUAL("/usr/lib/"s, directory("/usr/lib/libc++utilities.so"));
    CPPUNIT_ASSERT_EQUAL(string(), directory("libc++utilities.so"));
    CPPUNIT_ASSERT_EQUAL("C:\\libs\\"s, directory("C:\\libs\\libc++utilities.so"));
    CPPUNIT_ASSERT_EQUAL("C:\\libs/"s, directory("C:\\libs/libc++utilities.so"));
    string invalidPath("lib/c++uti*lities.so?");
    removeInvalidChars(invalidPath);
    CPPUNIT_ASSERT(invalidPath == "libc++utilities.so");

    const auto input = std::string_view("some/path/täst");
    const auto expected = input;
    const auto output = makeNativePath(input);
#ifdef PLATFORM_WINDOWS
#ifndef CPP_UTILITIES_NO_ICONV
    const auto outputAsUtf8 = convertUtf16LEToUtf8(reinterpret_cast<const char *>(output.data()), output.size() * 2);
    const auto outputView = std::string_view(outputAsUtf8.first.get(), outputAsUtf8.second);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("makeNativePath()", expected, outputView);
#else
    CPP_UTILITIES_UNUSED(expected)
    CPP_UTILITIES_UNUSED(output)
#endif
#else
    CPPUNIT_ASSERT_EQUAL_MESSAGE("makeNativePath()", expected, output);
#endif
}

/*!
 * \brief Tests IniFile.
 */
void IoTests::testIniFile()
{
    // prepare reading test file
    fstream inputFile;
    inputFile.exceptions(ios_base::failbit | ios_base::badbit);
    inputFile.open(testFilePath("test.ini"), ios_base::in);

    IniFile ini;
    ini.parse(inputFile);
    const auto globalScope = ini.data().at(0);
    const auto scope1 = ini.data().at(1);
    const auto scope2 = ini.data().at(2);
    CPPUNIT_ASSERT(globalScope.first.empty());
    CPPUNIT_ASSERT(globalScope.second.find("key0") != globalScope.second.cend());
    CPPUNIT_ASSERT(globalScope.second.find("key0")->second == "value 0");
    CPPUNIT_ASSERT(globalScope.second.find("key1") == globalScope.second.cend());
    CPPUNIT_ASSERT(scope1.first == "scope 1");
    CPPUNIT_ASSERT(scope1.second.find("key1") != scope1.second.cend());
    CPPUNIT_ASSERT(scope1.second.find("key1")->second == "value 1");
    CPPUNIT_ASSERT(scope1.second.find("key2") != scope1.second.cend());
    CPPUNIT_ASSERT(scope1.second.find("key2")->second == "value=2");
    CPPUNIT_ASSERT(scope2.first == "scope 2");
    CPPUNIT_ASSERT(scope2.second.find("key5") == scope2.second.cend());

    // write values to another file
    fstream outputFile;
    outputFile.exceptions(ios_base::failbit | ios_base::badbit);
    outputFile.open(workingCopyPath("output.ini", WorkingCopyMode::NoCopy), ios_base::out | ios_base::trunc);
    ini.make(outputFile);

    // parse written values (again)
    outputFile.close();
    outputFile.open(workingCopyPath("output.ini", WorkingCopyMode::NoCopy), ios_base::in);
    IniFile ini2;
    ini2.parse(outputFile);
    CPPUNIT_ASSERT(ini.data() == ini2.data());
}

/*!
 * \brief Tests AdvancedIniFile.
 */
void IoTests::testAdvancedIniFile()
{
    // prepare reading test file
    fstream inputFile;
    inputFile.exceptions(ios_base::failbit | ios_base::badbit);
    inputFile.open(testFilePath("pacman.conf"), ios_base::in);

    // parse the test file
    AdvancedIniFile ini;
    ini.parse(inputFile);

    // check whether scope data is as expected
    CPPUNIT_ASSERT_EQUAL_MESSAGE("5 scopes (taking implicit empty section at the end into account)", 5_st, ini.sections.size());
    auto options = ini.findSection("options");
    CPPUNIT_ASSERT(options != ini.sectionEnd());
#if !defined(PLATFORM_WINDOWS) || defined(PLATFORM_MINGW) || defined(PLATFORM_CYGWIN)
#define STD_REGEX_WORKS // the parsed data looks good, apparently std::regex behaves differently on MSVC
    TESTUTILS_ASSERT_LIKE_FLAGS(
        "comment block before section", "# Based on.*\n.*# GENERAL OPTIONS\n#\n"s, std::regex::extended, options->precedingCommentBlock);
#endif
    CPPUNIT_ASSERT_EQUAL(7_st, options->fields.size());
    CPPUNIT_ASSERT_EQUAL("HoldPkg"s, options->fields[0].key);
    CPPUNIT_ASSERT_EQUAL("pacman glibc"s, options->fields[0].value);
    CPPUNIT_ASSERT_MESSAGE("value present", options->fields[0].flags && IniFileFieldFlags::HasValue);
#ifdef STD_REGEX_WORKS
    TESTUTILS_ASSERT_LIKE_FLAGS("comment block between section header and first field",
        "# The following paths are.*\n.*#HookDir     = /etc/pacman\\.d/hooks/\n"s, std::regex::extended, options->fields[0].precedingCommentBlock);
#endif
    CPPUNIT_ASSERT_EQUAL(""s, options->fields[0].followingInlineComment);
    CPPUNIT_ASSERT_EQUAL("Foo"s, options->fields[1].key);
    CPPUNIT_ASSERT_EQUAL("bar"s, options->fields[1].value);
    CPPUNIT_ASSERT_MESSAGE("value present", options->fields[1].flags && IniFileFieldFlags::HasValue);
#ifdef STD_REGEX_WORKS
    TESTUTILS_ASSERT_LIKE_FLAGS("comment block between fields", "#XferCommand.*\n.*#CleanMethod = KeepInstalled\n"s, std::regex::extended,
        options->fields[1].precedingCommentBlock);
#endif
    CPPUNIT_ASSERT_EQUAL("# inline comment"s, options->fields[1].followingInlineComment);
    CPPUNIT_ASSERT_EQUAL("CheckSpace"s, options->fields[3].key);
    CPPUNIT_ASSERT_EQUAL(""s, options->fields[3].value);
    CPPUNIT_ASSERT_MESSAGE("no value present", !(options->fields[3].flags && IniFileFieldFlags::HasValue));
#ifdef STD_REGEX_WORKS
    TESTUTILS_ASSERT_LIKE_FLAGS("empty lines in comments preserved", "\n# Pacman.*\n.*\n\n#NoUpgrade   =\n.*#TotalDownload\n"s, std::regex::extended,
        options->fields[3].precedingCommentBlock);
#endif
    CPPUNIT_ASSERT_EQUAL(""s, options->fields[3].followingInlineComment);
    auto extraScope = ini.findSection(options, "extra");
    CPPUNIT_ASSERT(extraScope != ini.sectionEnd());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("comment block which is only an empty line", "\n"s, extraScope->precedingCommentBlock);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("inline comment after scope", "# an inline comment after a scope name"s, extraScope->followingInlineComment);
    CPPUNIT_ASSERT_EQUAL(1_st, extraScope->fields.size());
    CPPUNIT_ASSERT(ini.sections.back().flags && IniFileSectionFlags::Implicit);
#ifdef STD_REGEX_WORKS
    TESTUTILS_ASSERT_LIKE_FLAGS("comment block after last field present in implicitly added last scope", "\n# If you.*\n.*custompkgs\n"s,
        std::regex::extended, ini.sections.back().precedingCommentBlock);
#endif

    // test finding a field from file level and const access
    const auto *const constIniFile = &ini;
    auto includeField = constIniFile->findField("extra", "Include");
    CPPUNIT_ASSERT(includeField.has_value());
    CPPUNIT_ASSERT_EQUAL("Include"s, includeField.value()->key);
    CPPUNIT_ASSERT_EQUAL("/etc/pacman.d/mirrorlist"s, includeField.value()->value);
    CPPUNIT_ASSERT_MESSAGE("field not present", !constIniFile->findField("extra", "Includ").has_value());
    CPPUNIT_ASSERT_MESSAGE("scope not present", !constIniFile->findField("extr", "Includ").has_value());

    // write values again; there shouldn't be a difference as the parser and the writer are supposed to
    // preserve the order of all elements and comments
    std::stringstream newFile;
    ini.make(newFile);
    std::string originalContents;
    inputFile.clear();
    inputFile.seekg(std::ios_base::beg);
    // ignore warning about null pointer dereference from GCC 12 for now (which is *likely* not correct)
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnull-dereference"
#endif
    originalContents.assign((istreambuf_iterator<char>(inputFile)), istreambuf_iterator<char>());
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
    CPPUNIT_ASSERT_EQUAL(originalContents, newFile.str());
}

/*!
 * \brief Tests CopyHelper.
 */
void IoTests::testCopy()
{
    // prepare streams
    auto testFile = std::fstream();
    testFile.exceptions(ios_base::failbit | ios_base::badbit);
    testFile.open(testFilePath("some_data"), ios_base::in | ios_base::binary);
    auto outputStream = std::stringstream(ios_base::in | ios_base::out | ios_base::binary);
    outputStream.exceptions(ios_base::failbit | ios_base::badbit);

    // copy
    auto copyHelper = CopyHelper<13>();
    copyHelper.copy(testFile, outputStream, 50);
    CPPUNIT_ASSERT_EQUAL(static_cast<std::fstream::pos_type>(0), outputStream.tellg());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::fstream::pos_type>(50), outputStream.tellp());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::fstream::pos_type>(50), testFile.tellg());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::fstream::pos_type>(50), testFile.tellp());

    // verify
    testFile.seekg(0);
    for (auto i = 0; i < 50; ++i) {
        CPPUNIT_ASSERT_EQUAL(testFile.get(), outputStream.get());
    }
}

/*!
 * \brief Tests CopyHelper in combination with NativeFileStream.
 */
void IoTests::testCopyWithNativeFileStream()
{
    // prepare streams
    auto testFile = NativeFileStream();
    testFile.exceptions(ios_base::failbit | ios_base::badbit);
    testFile.open(testFilePath("some_data"), ios_base::in | ios_base::binary);
    auto outputPath = workingCopyPath("copied_data", WorkingCopyMode::Cleanup);
    auto outputStream = NativeFileStream();
    outputStream.exceptions(ios_base::failbit | ios_base::badbit);
    outputStream.open(outputPath, ios_base::out | ios_base::binary);

    // copy
    auto copyHelper = CopyHelper<13>();
    copyHelper.copy(testFile, outputStream, 50);
    CPPUNIT_ASSERT(outputStream.is_open());
    CPPUNIT_ASSERT(testFile.is_open());

    // test seek positions (the expected values are from a run with normal std::fstream)
    CPPUNIT_ASSERT_EQUAL(static_cast<std::fstream::pos_type>(50), outputStream.tellg());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::fstream::pos_type>(50), outputStream.tellp());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::fstream::pos_type>(50), testFile.tellg());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::fstream::pos_type>(50), testFile.tellp());

    // add a few more bytes to output stream (to be sure it is still usable) and re-open for reading
    const auto aFewMoreBytes = "a few more bytes"sv;
    outputStream << aFewMoreBytes;
    outputStream.close();
    outputStream.open(outputPath, ios_base::in | ios_base::binary);

    // verify
    testFile.seekg(0);
    for (auto i = 0; i < 50; ++i) {
        CPPUNIT_ASSERT_EQUAL(testFile.get(), outputStream.get());
    }
    auto tail = std::string(aFewMoreBytes.size(), '0');
    outputStream.read(tail.data(), static_cast<std::streamsize>(tail.size()));
    CPPUNIT_ASSERT_EQUAL(aFewMoreBytes, std::string_view(tail.data(), tail.size()));

    // repeat with callback version
    auto percentage = 0.0;
    const auto isAborted = [] { return false; };
    const auto callback = [&percentage](double p) { percentage = p; };
    testFile.seekg(0);
    outputStream.close();
    outputStream.open(outputPath, ios_base::out | ios_base::trunc | ios_base::binary);
    copyHelper.callbackCopy(testFile, outputStream, 50, isAborted, callback);
    CPPUNIT_ASSERT_EQUAL(1.0, percentage);

    // verify again
    CPPUNIT_ASSERT(outputStream.is_open());
    CPPUNIT_ASSERT(testFile.is_open());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::fstream::pos_type>(50), outputStream.tellg());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::fstream::pos_type>(50), outputStream.tellp());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::fstream::pos_type>(50), testFile.tellg());
    CPPUNIT_ASSERT_EQUAL(static_cast<std::fstream::pos_type>(50), testFile.tellp());
    outputStream << aFewMoreBytes;
    outputStream.close();
    outputStream.open(outputPath, ios_base::in | ios_base::binary);
    testFile.seekg(0);
    for (auto i = 0; i < 50; ++i) {
        CPPUNIT_ASSERT_EQUAL(testFile.get(), outputStream.get());
    }
    tail.assign(aFewMoreBytes.size(), '0');
    outputStream.read(tail.data(), static_cast<std::streamsize>(tail.size()));
    CPPUNIT_ASSERT_EQUAL(aFewMoreBytes, std::string_view(tail.data(), tail.size()));
}

/*!
 * \brief Tests readFile().
 */
void IoTests::testReadFile()
{
    // read a file successfully
    const string iniFilePath(testFilePath("test.ini"));
    CPPUNIT_ASSERT_EQUAL("# file for testing INI parser\n"
                         "key0=value 0\n"
                         "\n"
                         "[scope 1]\n"
                         "key1=value 1 # comment\n"
                         "key2=value=2\n"
                         "key3=value 3\n"
                         "\n"
                         "[scope 2]\n"
                         "key4=value 4\n"
                         "#key5=value 5\n"
                         "key6=value 6\n"s,
        readFile(iniFilePath));

    // fail by exceeding max size
    CPPUNIT_ASSERT_THROW(readFile(iniFilePath, 10), std::ios_base::failure);

    // handle UTF-8 in path and file contents correctly via NativeFileStream
#if !defined(PLATFORM_WINDOWS) || defined(CPP_UTILITIES_USE_NATIVE_FILE_BUFFER)
    CPPUNIT_ASSERT_EQUAL("file with non-ASCII character 'ä' in its name\n"s, readFile(testFilePath("täst.txt")));
#endif
}

/*!
 * \brief Tests writeFile().
 */
void IoTests::testWriteFile()
{
    const string path(workingCopyPath("test.ini", WorkingCopyMode::NoCopy));
    writeFile(path, "some contents");
    CPPUNIT_ASSERT_EQUAL("some contents"s, readFile(path));
}

/*!
 * \brief Tests formatting functions of CppUtilities::EscapeCodes namespace.
 */
void IoTests::testAnsiEscapeCodes()
{
    stringstream ss1;
    EscapeCodes::enabled = true;
    ss1 << EscapeCodes::Phrases::Error << "some error" << EscapeCodes::Phrases::End;
    ss1 << EscapeCodes::Phrases::Warning << "some warning" << EscapeCodes::Phrases::End;
    ss1 << EscapeCodes::Phrases::Info << "some info" << EscapeCodes::Phrases::End;
    ss1 << EscapeCodes::Phrases::ErrorMessage << "Arch-style error" << EscapeCodes::Phrases::End;
    ss1 << EscapeCodes::Phrases::WarningMessage << "Arch-style warning" << EscapeCodes::Phrases::End;
    ss1 << EscapeCodes::Phrases::PlainMessage << "Arch-style message" << EscapeCodes::Phrases::End;
    ss1 << EscapeCodes::Phrases::SuccessMessage << "Arch-style success" << EscapeCodes::Phrases::End;
    ss1 << EscapeCodes::Phrases::SubMessage << "Arch-style sub-message" << EscapeCodes::Phrases::End;
    ss1 << EscapeCodes::color(EscapeCodes::Color::Blue, EscapeCodes::Color::Red, EscapeCodes::TextAttribute::Blink)
        << "blue, blinking text on red background" << EscapeCodes::TextAttribute::Reset << '\n';
    cout << "\noutput for formatting with ANSI escape codes:\n" << ss1.str() << "---------------------------------------------\n";
    CPPUNIT_ASSERT_EQUAL("\033[1;31mError: \033[0m\033[1msome error\033[0m\n"
                         "\033[1;33mWarning: \033[0m\033[1msome warning\033[0m\n"
                         "\033[1;34mInfo: \033[0m\033[1msome info\033[0m\n"
                         "\033[1;31m==> ERROR: \033[0m\033[1mArch-style error\033[0m\n"
                         "\033[1;33m==> WARNING: \033[0m\033[1mArch-style warning\033[0m\n"
                         "    \033[0m\033[1mArch-style message\033[0m\n"
                         "\033[1;32m==> \033[0m\033[1mArch-style success\033[0m\n"
                         "\033[1;32m  -> \033[0m\033[1mArch-style sub-message\033[0m\n"
                         "\033[5;34;41mblue, blinking text on red background\033[0m\n"s,
        ss1.str());

    stringstream ss2;
    EscapeCodes::enabled = false;
    ss2 << EscapeCodes::Phrases::Info << "some info" << EscapeCodes::Phrases::End;
    CPPUNIT_ASSERT_EQUAL("Info: some info\n"s, ss2.str());
}

#ifdef CPP_UTILITIES_USE_NATIVE_FILE_BUFFER
/*!
 * \brief Tests the NativeFileStream class.
 */
void IoTests::testNativeFileStream()
{
    return;
    // open file by path
    const auto txtFilePath(workingCopyPath("täst.txt"));
    NativeFileStream fileStream;
    fileStream.exceptions(ios_base::badbit | ios_base::failbit);
    CPPUNIT_ASSERT(!fileStream.is_open());
    fileStream.open(txtFilePath, ios_base::in);
    CPPUNIT_ASSERT(fileStream.is_open());
#if defined(PLATFORM_WINDOWS) && defined(CPP_UTILITIES_USE_BOOST_IOSTREAMS)
    CPPUNIT_ASSERT(fileStream.fileHandle() != nullptr);
#else
    CPPUNIT_ASSERT(fileStream.fileDescriptor() != -1);
#endif
    CPPUNIT_ASSERT_EQUAL(static_cast<char>(fileStream.get()), 'f');
    fileStream.seekg(0, ios_base::end);
    CPPUNIT_ASSERT_EQUAL(fileStream.tellg(), static_cast<NativeFileStream::pos_type>(47));
    fileStream.close();
    CPPUNIT_ASSERT(!fileStream.is_open());
    try {
        fileStream.open("non existing file", ios_base::in | ios_base::out | ios_base::binary);
        CPPUNIT_FAIL("expected exception");
    } catch (const std::ios_base::failure &failure) {
#ifdef PLATFORM_WINDOWS
#ifdef CPP_UTILITIES_USE_BOOST_IOSTREAMS
        TESTUTILS_ASSERT_LIKE("expected error with some message", "CreateFileW failed: .+", failure.what());
#else
        TESTUTILS_ASSERT_LIKE("expected error with some message", "_wopen failed: .+", failure.what());
#endif
#else
        TESTUTILS_ASSERT_LIKE("expected error with some message", "open failed: .+", failure.what());
#endif
    }
    fileStream.clear();

    // open file from file descriptor
#ifndef PLATFORM_WINDOWS
    auto readWriteFileDescriptor = open(txtFilePath.data(), O_RDWR);
    CPPUNIT_ASSERT(readWriteFileDescriptor);
    fileStream.open(readWriteFileDescriptor, ios_base::in | ios_base::out | ios_base::binary);
    CPPUNIT_ASSERT(fileStream.is_open());
    CPPUNIT_ASSERT_EQUAL(static_cast<char>(fileStream.get()), 'f');
    fileStream.seekg(0, ios_base::end);
    CPPUNIT_ASSERT_EQUAL(fileStream.tellg(), static_cast<NativeFileStream::pos_type>(47));
    fileStream.flush();
    fileStream.close();
    CPPUNIT_ASSERT(!fileStream.is_open());
#endif
    try {
        fileStream.open(-1, ios_base::in | ios_base::out | ios_base::binary);
        fileStream.get();
        CPPUNIT_FAIL("expected exception");
    } catch (const std::ios_base::failure &failure) {
#ifndef PLATFORM_WINDOWS
        TESTUTILS_ASSERT_LIKE(
            "expected error message", "(basic_ios::clear|failed reading: Bad file descriptor): iostream error"s, string(failure.what()));
#else
        CPP_UTILITIES_UNUSED(failure)
#endif
    }
    fileStream.clear();

    // append + write file via path
    NativeFileStream fileStream2;
    fileStream2.exceptions(ios_base::failbit | ios_base::badbit);
    fileStream2.open(txtFilePath, ios_base::in | ios_base::out | ios_base::app);
    CPPUNIT_ASSERT(fileStream2.is_open());
    fileStream2 << "foo";
    fileStream2.flush();
    fileStream2.close();
    CPPUNIT_ASSERT(!fileStream2.is_open());
    CPPUNIT_ASSERT_EQUAL("file with non-ASCII character 'ä' in its name\nfoo"s, readFile(txtFilePath, 50));

    // truncate + write file via path
    fileStream2.open(txtFilePath, ios_base::out | ios_base::trunc);
    CPPUNIT_ASSERT(fileStream2.is_open());
    fileStream2 << "bar";
    fileStream2.close();
    CPPUNIT_ASSERT(!fileStream2.is_open());
    CPPUNIT_ASSERT_EQUAL("bar"s, readFile(txtFilePath, 4));

    // append + write via file descriptor from file handle
#ifdef PLATFORM_WINDOWS
    const auto wideTxtFilePath = NativeFileStream::makeWidePath(txtFilePath);
    const auto appendFileHandle = _wfopen(wideTxtFilePath.get(), L"a+");
#else
    const auto appendFileHandle = fopen(txtFilePath.data(), "a");
#endif
    CPPUNIT_ASSERT(appendFileHandle);
    fileStream2.open(fileno(appendFileHandle), ios_base::out | ios_base::app);
    CPPUNIT_ASSERT(fileStream2.is_open());
    fileStream2 << "foo";
    fileStream2.close();
    CPPUNIT_ASSERT(!fileStream2.is_open());
    CPPUNIT_ASSERT_EQUAL("barfoo"s, readFile(txtFilePath, 7));
}
#endif

#ifdef CPP_UTILITIES_USE_LIBARCHIVE
void IoTests::testExtractingArchive()
{
    const auto archivePath = testFilePath("archive-test.zip");
    const auto archiveContents = extractFiles(archivePath);
    const auto &root = archiveContents.at(std::string());
    const auto &subdir = archiveContents.at("subdir");
    const auto &subsubdir = archiveContents.at("subdir/foo");

    CPPUNIT_ASSERT_EQUAL(1_st, root.size());
    CPPUNIT_ASSERT_EQUAL("test.txt"s, root.at(0).name);
    CPPUNIT_ASSERT_EQUAL(ArchiveFileType::Regular, root.at(0).type);
    CPPUNIT_ASSERT_EQUAL(DateTime::fromDate(1970, 1, 1), root.at(0).creationTime);
    // check only year and month of modification time as timezone is ambiguous, see remark in `archive.h` for details
    // not guaranteed: CPPUNIT_ASSERT_EQUAL(DateTime::fromDateAndTime(2024, 3, 3, 20, 46, 42), DateTime::fromTimeStamp(root.at(0).modificationTime.toTimeStamp()));
    CPPUNIT_ASSERT_EQUAL(2024, root.at(0).modificationTime.year());
    CPPUNIT_ASSERT_EQUAL(3, root.at(0).modificationTime.month());
    CPPUNIT_ASSERT_EQUAL("testfile\n"s, root.at(0).content);

    CPPUNIT_ASSERT_EQUAL(1_st, subdir.size());
    CPPUNIT_ASSERT_EQUAL("nested-testfile.txt"s, subdir.at(0).name);
    CPPUNIT_ASSERT_EQUAL(ArchiveFileType::Regular, subdir.at(0).type);
    CPPUNIT_ASSERT_EQUAL("some file\n"s, subdir.at(0).content);

    CPPUNIT_ASSERT_EQUAL(1_st, subsubdir.size());
    CPPUNIT_ASSERT_EQUAL("bar"s, subsubdir.at(0).name);
    CPPUNIT_ASSERT_EQUAL(ArchiveFileType::Regular, subsubdir.at(0).type);
    CPPUNIT_ASSERT_EQUAL(std::string(), subsubdir.at(0).content);
}
#endif
