// vim: set ts=4 sw=4 tw=99 noet:
//
// AMX Mod X, based on AMX Mod by Aleksander Naszko ("OLO").
// Copyright (C) The AMX Mod X Development Team.
//
// This software is licensed under the GNU General Public License, version 3 or higher.
// Additional exceptions apply. For full license details, see LICENSE.txt or visit:
//     https://alliedmods.net/amxmodx-license

#include <amxmodx>

/**
 * Warning: To get expected result, file encoding must be UTF-8 without BOM.
 */

public plugin_init()
{
    register_plugin("UTF-8 Test", AMXX_VERSION_STR, "AMXX Dev Team");
    register_srvcmd("utf8test", "OnServerCommand");
}

new GlobalTestNumber;
new GlobalErrorCount;

new ErrorCount;
new TestNumber;

enum TestType
{
    TT_Equal = 0,
    TT_LessThan,
    TT_GreaterThan,
    TT_LessThanEqual,
    TT_GreaterThanEqual,
    TT_NotEqual
};

new const TestWords[TestType][] =
{
    "==",
    "<",
    ">",
    "<=",
    ">=",
    "!="
};

test(any:a, any:b = true, TestType:type = TT_Equal, const description[] = "")
{
    ++TestNumber;
    ++GlobalTestNumber;

    new passed = 0;

    switch (type)
    {
        case TT_Equal:              passed = a == b;
        case TT_LessThan:           passed = a < b;
        case TT_GreaterThan:        passed = a > b;
        case TT_LessThanEqual:      passed = a <= b;
        case TT_GreaterThanEqual:   passed = a >= b;
        case TT_NotEqual:           passed = a != b;
    }

    if (!passed)
    {
        log_amx("  [FAIL] #%d %s (%d %s %d)", TestNumber, description, a, bool:TestWords[type], b);
        ErrorCount++;
        GlobalErrorCount++;
    }
}

showGlobalResult()
{
    log_amx("Finished globally %d tests, %d failed.", GlobalTestNumber,  GlobalErrorCount);

    GlobalTestNumber = 0;
    GlobalErrorCount = 0;
}

showResult()
{
    log_amx("  Finished %d tests, %d failed.", TestNumber,  ErrorCount);
    log_amx("-");

    TestNumber = 0;
    ErrorCount = 0;
}

public OnServerCommand()
{
    /**
     * Initiliaze some data.
     */
    new reference[] = "𤭢hi AMXX® Hello㋡ crab?ൠ";

    new Array:a = ArrayCreate(sizeof reference);
    ArrayPushString(a, reference);

    new Trie:t = TrieCreate();
    TrieSetString(t, "reference", reference);

    new DataPack:d = CreateDataPack();
    WritePackString(d, reference);
    ResetPack(d);

    set_localinfo("reference", reference);

    log_amx("Checking get_char_bytes()...");
    {
        test(get_char_bytes(""), 1, TT_Equal, "Empty string");
        test(get_char_bytes("a") , 1, TT_Equal, "1 byte character");
        test(get_char_bytes("®") , 2, TT_Equal, "2 bytes character");
        test(get_char_bytes("㋡"), 3, TT_Equal, "3 bytes character");
        test(get_char_bytes("𤭢"), 4, TT_Equal, "4 bytes character");
        test(get_char_bytes("^xE1^xB9"), 3, TT_Equal, "Truncated character");

        showResult();
    }

    log_amx("Checking is_char_mb()...");
    {
        /**
         * is_char_mb() returns also number of bytes if not 0.
         */
        test(is_char_mb(reference[0]), 0, TT_NotEqual, "1 byte character");  // 𤭢
        test(is_char_mb(reference[11]), 0, TT_NotEqual, "2 bytes character"); // ®
        test(is_char_mb(reference[19]), 0, TT_NotEqual, "3 bytes character"); // ㋡
        test(is_char_mb(reference[29]), 0, TT_NotEqual, "4 bytes character"); // ൠ
        test(is_char_mb('^xE1'), 3, TT_Equal, "Truncated character");

        showResult();
    }

    log_amx("Checking truncated character bytes (formatted output)...");
    {
        /**
         * Truncating '𤭢' at different index. '𤭢' = 4 bytes
         * A buffer of 4 = 3 bytes + EOS.
         * Expected result: empty buffer.
         */
        new buffer1[4];
        for(new i = charsmax(buffer1), length1; i >= 0; --i)
        {
            length1 = formatex(buffer1, i, "%s", reference);
            test(buffer1[0], EOS, .description = fmt("Truncating multi-bytes character #%d (buffer)", charsmax(buffer1) - i + 1));
            test(length1, 0, .description = fmt("Truncating multi-bytes character #%d (length)", charsmax(buffer1) - i + 1));
        }

        /**
         * Truncating inside 'ൠ'.
         * Buffer of 14: Enough to hold "㋡ crab?ൠ"
         * Retrieve 11 characters using precision format from '㋡' to inside 'ൠ'..
         * Expected result: 'ൠ'. should be skipped.
         */
        new buffer3[14];
        new length3 = formatex(buffer3, charsmax(buffer3), "%.11s", reference[19]);
        test(strcmp(buffer3, "㋡ crab?"), 0, .description = "Truncating moar multi-bytes character (buffer)");
        test(length3, get_char_bytes("㋡") + strlen(" crab?"), .description = "Truncating moar multi-bytes character (length)")

        showResult();
    }

    log_amx("Checking truncated character bytes (output)...");
    {
        /**
         * Splits string at '㋡'.
         * Buffer can hold only 16 characters.
         * Expected result: '㋡' should not be included and returned position should be after '㋡'.
         */
        new buffer1[16];
        new index1 = split_string(reference, "㋡", buffer1, charsmax(buffer1));
        test(strcmp(buffer1, "𤭢hi AMXX® H"), 0, .description = "Splitting string #1 (buffer)");
        test(index1, strlen("𤭢hi AMXX® Hello") + get_char_bytes("㋡"), .description = "Splitting string #1 (length)");

        /**
         * Splits string at '𤭢'.
         * Expected result: Empty string and returned position should be after '𤭢'.
         */
        new buffer2[5];
        new index2 = split_string(reference, "𤭢", buffer2, charsmax(buffer2));
        test(buffer2[0], EOS, .description = "Splitting string #2 (buffer)");
        test(index2, get_char_bytes("𤭢"), .description = "Splitting string #2 (length)");

        /**
         * Splits string at '\ൠ'.
         * Expected result: Empty string and returned position should -1 (not found).
         */
        new buffer3[12];
        new index3 = split_string(reference, "\ൠ", buffer3, charsmax(buffer3));
        test(buffer3[0], EOS, .description = "Splitting string #3 (buffer)");
        test(index3, -1, .description = "Splitting string #3 (length)");

        /**
         * Truncating '𤭢' at different index. '𤭢' = 4 bytes
         * A buffer of 4 = 3 bytes + EOS.
         * Expected result: empty buffer.
         */
        new buffer4[4];
        for(new i = charsmax(buffer4), length4; i >= 0; --i)
        {
            length4 = get_localinfo("reference", buffer4, i);
            test(buffer4[0], EOS, .description = "Truncating string (buffer)");
            test(length4, 0, .description = "Truncating string (length)");
        }

        showResult();
    }

    log_amx("Checking truncated character bytes (direct copy)...");
    {
        /**
         * Replaces '®' by '𤭢'.
         * Expected result: '𤭢' should eat '® He" which counts 4 bytes.
         */
        new count1 = replace_string(reference, charsmax(reference), "®", "𤭢");
        test(strcmp(reference, "𤭢hi AMXX𤭢ello㋡ crab?ൠ"), 0, .description = "Replacing character (buffer)");
        test(count1, 1, .description = "Replacing character (count)");

        /**
         * Replaces '®' by '𤭢'.
         * Expected result: not found.
         */
        new count2 = replace_string(reference, charsmax(reference), "®", "𤭢");
        test(strcmp(reference, "𤭢hi AMXX𤭢ello㋡ crab?ൠ"), 0, .description = "Replacing inexistent character (buffer)");
        test(count2, 0, .description = "Replacing inexistent character (count)");

        /**
         * Replaces 'ൠ' by '𤭢'.
         * Expected result: 'ൠ' = 3 bytes, '𤭢' = 4 bytes. Not enough spaces to hold '𤭢', skipping it.
         */
        new count3 = replace_string(reference, charsmax(reference), "ൠ", "𤭢");
        test(strcmp(reference, "𤭢hi AMXX𤭢ello㋡ crab?"), 0, .description = "Replacing character / not enough space (buffer)");
        test(count3, 1, .description = "Replacing character / not enough space (count)");

        /**
         * Gets reference string with limited buffer.
         * Expected result: '㋡' should be ignored as no spaces.
         */
        new buffer[charsmax(reference) - 9];
        ArrayGetString(a, 0, buffer, charsmax(buffer));
        test(strcmp(buffer, "𤭢hi AMXX® Hello") == 0, .description  = "Truncating string #1");

        /**
         * Gets reference string with limited buffer.
         * Expected result: '㋡' should be ignored as no spaces.
         */
        TrieGetString(t, "reference", buffer, charsmax(buffer));
        test(strcmp(buffer, "𤭢hi AMXX® Hello") == 0, .description  = "Truncating string #2");

        /**
         * Gets reference string with limited buffer.
         * Expected result: '㋡' should be ignored as no room.
         */
        new length = ReadPackString(d, buffer, charsmax(buffer));
        test(strcmp(buffer, "𤭢hi AMXX® Hello") == 0 && length == strlen("𤭢hi AMXX® Hello"), .description  = "Truncating string #3");

        showResult();
    }

    log_amx("Checking containi() and strfind() with ignorecase...");
    {
        new haystack[64];
        new needle[64];

        /**
         * Empty haystack and needle.
         */
        haystack = ""
        needle = "";
        test(containi(haystack, needle), -1, .description = "Empty haystack and needle (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Empty haystack and needle (strfind)");

        /**
         * Empty haystack.
         */
        haystack = ""
        needle = "crab";
        test(containi(haystack, needle), -1, .description = "Empty haystack (containi)");
        test(strfind(haystack, needle, .ignorecase = true), -1, .description = "Empty haystack (strfind)");

        /**
         * Empty needle.
         */
        haystack = "crab"
        needle = "";
        test(containi(haystack, needle), -1, .description = "Empty needle (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Empty needle (strfind)");

        /**
         * Latin / Single / Both lowercase
         */
        haystack = "a"
        needle = "a";
        test(containi(haystack, needle), 0, .description = "Basic latin lowercase character (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Basic latin lowercase character (strfind)");

        /**
         * Latin / Multiple / Both lowercase
         */
        haystack = "aaa"
        needle = "aaa";
        test(containi(haystack, needle), 0, .description = "Basic latin lowercase characters (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Basic latin lowercase characters (strfind)");

        /**
         * Latin / Single / Both uppercase
         */
        haystack = "A"
        needle = "A";
        test(containi(haystack, needle), 0, .description = "Basic latin uppercase character (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Basic latin uppercase character (strfind)");

        /**
         * Latin / Multiple / Both uppercase
         */
        haystack = "AAA"
        needle = "AAA";
        test(containi(haystack, needle), 0, .description = "Basic latin uppercase characters (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Basic latin uppercase characters (containi)");

        /**
         * Latin / Single unaffected
         */
        haystack = "@"
        needle = "@";
        test(containi(haystack, needle), 0, .description = "Basic latin unaffected character (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Basic latin unaffected character (strfind)");

        /**
         * Latin / Multiple unaffected
         */
        haystack = "@#%"
        needle = "@#%";
        test(containi(haystack, needle), 0, .description = "Basic latin unaffected characters (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Basic latin unaffected characters (strfind)");

        /**
         * Latin / Single / haystack lowercase, needle uppercase
         */
        haystack = "a"
        needle = "A";
        test(containi(haystack, needle), 0, .description = "Basic latin character with mixed case #1 (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Basic latin character with mixed case (strfind)");

        /**
         * Latin / Multiple / haystack lowercase, needle uppercase
         */
        haystack = "aaa"
        needle = "AAA";
        test(containi(haystack, needle), 0, .description = "Basic latin characters with mixed case (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Basic latin characters with mixed case (strfind)");

        /**
         * Latin / Single / haystack uppercase, needle uppercase
         */
        haystack = "A"
        needle = "a";
        test(containi(haystack, needle), 0, .description = "Basic latin character with mixed case #2 (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Basic latin character with mixed case #2 (strfind)");

        /**
         * Latin / Multiple / haystack uppercase, needle uppercase
         */
        haystack = "AAA"
        needle = "aaa";
        test(containi(haystack, needle), 0, .description = "Basic latin characters with mixed case #2 (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Basic latin characters with mixed case #2 (strfind)");

        /**
         * Latin / Sentence / Single needle
         */
        haystack = "Hello world!"
        needle = "o";
        test(containi(haystack, needle), 4, .description = "Basic latin sentence / Single needle (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 4, .description = "Basic latin sentence / Single needle (strfind)");

        /**
         * Latin / Sentence / Single needle / End of string
         */
        haystack = "Hello world!"
        needle = "!";
        test(containi(haystack, needle), 11, .description = "Basic latin sentence / Single needle / End of string (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 11, .description = "Basic latin sentence / Single needle / End of string (strfind)");

        /**
         * Latin / Sentence / Multiple needle
         */
        haystack = "Hello world!"
        needle = "WoRlD";
        test(containi(haystack, needle), 6, .description = "Basic latin sentence / Multiple needle (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 6, .description = "Basic latin sentence / Single needle (strfind)");

        /**
         * Latin / Sentence / Not found
         */
        haystack = "Hello world!"
        needle = "?";
        test(containi(haystack, needle), -1, .description = "Basic latin sentence / Not found (containi)");
        test(strfind(haystack, needle, .ignorecase = true), -1, .description = "Basic latin sentence / Not found (strfind)");

        /**
         * Multi bytes / Single / Both lowercase
         */
        haystack = "ȓ"
        needle = "ȓ";
        test(containi(haystack, needle), 0, .description = "Multi bytes lowercase character (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Multi bytes lowercase character (strfind)");

        /**
         * Multi bytes / Multiple / Both lowercase
         */
        haystack = "ȓǯʌό"
        needle = "ȓǯʌό";
        test(containi(haystack, needle), 0, .description = "Multi bytes lowercase characters (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Multi bytes lowercase characters (strfind)");

        /**
         * Multi bytes / Single / Both uppercase
         */
        haystack = "Ȓ"
        needle = "Ȓ";
        test(containi(haystack, needle), 0, .description = "Multi bytes uppercase character (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Multi bytes uppercase character (strfind)");

        /**
         * Multi bytes / Multiple / Both uppercase
         */
        haystack = "ȒǮɅΌ"
        needle = "ȒǮɅΌ";
        test(containi(haystack, needle), 0, .description = "Multi bytes uppercase characters (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Multi bytes uppercase characters (strfind)");

        /**
         * Multi bytes / Single unaffected
         */
        haystack = "ൠ"
        needle = "ൠ";
        test(containi(haystack, needle), 0, .description = "Multi bytes unaffected character (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Multi bytes unaffected character (strfind)");

        /**
         * Multi bytes / Multiple unaffected
         */
        haystack = "ൠ㋡"
        needle = "ൠ㋡";
        test(containi(haystack, needle), 0, .description = "Multi bytes unaffected characters (containi)");
        test(strfind(haystack, needle, .ignorecase = true), 0, .description = "Multi bytes unaffected characters (containi)");

        /**
         * Multi bytes / Sentence / Multiple needle
         */
        haystack = "L'ÎLE ȒǮɅΌ ЦИТРУС"
        needle = "ȓǯʌό";
        test(containi(haystack, needle), strlen("L'ÎLE "), .description = "Multi bytes sentence (containi)");
        test(strfind(haystack, needle, .ignorecase = true), strlen("L'ÎLE "), .description = "Multi bytes sentence (strfind)");

        /**
         * Multi bytes / Sentence / Single needle / End of string
         */
        haystack = "L'ÎLE ȒǮɅΌ ЦИТРУС"
        needle = "с";
        test(containi(haystack, needle), strlen(haystack) - get_char_bytes("С"), .description = "Multi bytes sentence / end of string (containi)");
        test(strfind(haystack, needle, .ignorecase = true), strlen(haystack) - get_char_bytes("С"), .description = "Multi bytes sentence / end of string (strfind)");

        /**
         * Multi bytes / Sentence / Not Found
         */
        haystack = "L'ÎLE ȒǮɅΌ ЦИТРУС"
        needle = "İ";
        test(containi(haystack, needle), -1, .description = "Multi bytes sentence / not found (containi)");
        test(strfind(haystack, needle, .ignorecase = true), -1, .description = "Multi bytes sentence / not found (strfind)");

        /**
         * Multi bytes / Words + invalid
         */
        haystack = "^xB9hello ^xE1^xB9 ^xE1world!";
        needle = "W";
        test(containi(haystack, needle), strlen("^xB9hello ^xE1^xB9 ^xE1"), .description = "Multi bytes words / invalid bytes (containi)");
        test(strfind(haystack, needle, .ignorecase = true), strlen("^xB9hello ^xE1^xB9 ^xE1"), .description = "Multi bytes words / invalid bytes (strfind)");

        showResult();
    }

    log_amx("Checking strfind() pos parameter...");
    {
        new haystack[64];
        new needle[64];

        /**
         * Multi bytes / Sentence / Negative position
         */
        haystack = "L'ÎLE ȒǮɅΌ ЦИТРУС"
        needle = "И";
        test(strfind(haystack, needle, .ignorecase = true, .pos = -1), -1, .description = "Negative position");

        /**
         * Multi bytes / Sentence / Position > length of string
         */
        haystack = "L'ÎLE ȒǮɅΌ ЦИТРУС"
        needle = "И";
        test(strfind(haystack, needle, .ignorecase = true, .pos = strlen(haystack) + 42), -1, .description = "Position > length of string");

        /**
         * Multi bytes / Sentence / Not found
         */
        haystack = "L'ÎLE ȒǮɅΌ ЦИТРУС"
        needle = "ʌό";
        test(strfind(haystack, needle, .ignorecase = true, .pos = strlen("L'ÎLE ȒǮɅΌ")), -1, .description = "Valid position, needle found");

        /**
         * Multi bytes / Sentence
         */
        haystack = "L'ÎLE ȒǮɅΌ ЦИТРУС"
        needle = "ʌό";
        test(strfind(haystack, needle, .ignorecase = true, .pos = strlen("L'ÎLE ")), strlen("L'ÎLE ȒǮ"), .description = "Valid position, needle not found");

        showResult();
    }

    log_amx("Checking equali() and str[n]cmp() with ignorecase...");
    {
        new reference[32];

        /**
         * Basic latin / Single
         */
        reference = "a";
        test(equali(reference, "A"), .description = "Basic latin character (equali)");
        test(strcmp(reference, "A", .ignorecase = true), 0, .description = "Basic latin character (strcmp)");
        test(strncmp(reference, "A", strlen(reference), .ignorecase = true), 0,  .description = "Basic latin character (strncmp)");

        /**
         * Basic latin / Multiple
         */
        reference = "abc";
        test(equali(reference, "aBC"), true, .description = "Basic latin characters (equali)");
        test(strcmp(reference, "aBC", .ignorecase = true), 0, .description = "Basic latin characters (strcmp)");
        test(strncmp(reference, "aBC", strlen(reference), .ignorecase = true), 0,  .description = "Basic latin characters (strncmp)");

        /**
         * Basic latin / Not found
         */
        reference = "world";
        test(equali(reference, "!?"), false, .description = "Basic latin characters, not found (equali)");
        test(strcmp(reference, "!?", .ignorecase = true), 0, TT_NotEqual, .description = "Basic latin characters, not found (strcmp)");
        test(strncmp(reference, "!?", strlen(reference), .ignorecase = true), 0, TT_NotEqual, .description = "Basic latin characters, not found (strncmp)");

        /**
         * Multi bytes / Single
         */
        reference = "Ʌ";
        test(equali(reference, "ʌ"), true, .description = "Multi bytes character (equali)");
        test(strcmp(reference, "ʌ", .ignorecase = true), 0, .description = "Multi bytes character (strcmp)");
        test(strncmp(reference, "ʌ", strlen(reference), .ignorecase = true), 0, .description = "Multi bytes character (strncmp)");

        /**
         * Multi bytes / Multiple
         */
        reference = "ȒǮɅΌ";
        test(equali(reference, "ȓǯʌό"), true, .description = "Multi bytes characters (equali)");
        test(strcmp(reference, "ȓǯʌό", .ignorecase = true), 0, .description = "Multi bytes characters (strcmp)");
        test(strncmp(reference, "ȓǯʌό", strlen(reference), .ignorecase = true), 0, .description = "Multi bytes characters (strncmp)");

        /**
         * Multi bytes / Not found
         */
        reference = "ȒǮɅΌ";
        test(equali(reference, "ЦИТРУС"), false, .description = "Multi bytes characters, not found (equali)");
        test(strcmp(reference, "ЦИТРУС", .ignorecase = true), 0, TT_NotEqual, .description = "Multi bytes characters, not found (strcmp)");
        test(strncmp(reference, "ЦИТРУС", strlen(reference), .ignorecase = true), 0, TT_NotEqual, .description = "Multi bytes characters, not found (strncmp)");

        showResult();
    }

    log_amx("Checking replace_stringex()...");
    {
        new reference[64];

        /**
         * Multi bytes / Smaller replacement
         */
        reference = "L'ÎLE ȒǮɅΌ ЦИТРУС";
        test(replace_stringex(reference, charsmax(reference), "ʌό", "ൠ", .caseSensitive = false), strlen("L'ÎLE ȒǮൠ"), .description = "Multi bytes characters with smaller replacement (position)");
        test(equal(reference, "L'ÎLE ȒǮൠ ЦИТРУС"), .description = "Multi bytes characters with smaller replacement (buffer)");

        /**
         * Multi bytes / Same replacement
         */
        reference = "L'ÎLE ȒǮɅΌ ЦИТРУС";
        test(replace_stringex(reference, charsmax(reference), "l'î", "L'Î", .caseSensitive = false), strlen("L'Î"), .description = "Multi bytes characters with same replacement (position)");
        test(equal(reference, "L'ÎLE ȒǮɅΌ ЦИТРУС"), .description = "Multi bytes characters with smaller replacement (buffer)");

        /**
         * Multi bytes / Larger replacement / Enough space to move old data
         */
        reference = "L'ÎLE ȒǮɅΌ ЦИТРУС"
        test(replace_stringex(reference, charsmax(reference), "L", "𤭢𤭢", .caseSensitive = false), strlen("𤭢𤭢"), .description = "Multi bytes characters with larger replacement #1 (position)");
        test(equal(reference, "𤭢𤭢'ÎLE ȒǮɅΌ ЦИТРУС"), .description = "Multi bytes characters with larger replacement #1 (buffer)");

        /**
         * Multi bytes / Larger replacement / Not enough space to move old data
         */
        reference = "L'ÎLE ȒǮɅΌ ЦИТРУС"
        test(replace_stringex(reference, strlen(reference), "E", "𤭢", .caseSensitive = false), strlen("L'ÎL𤭢"), .description = "Multi bytes characters with larger replacement #2 (position)");
        test(equal(reference, "L'ÎL𤭢ǮɅΌ ЦИТРУС"), .description = "Multi bytes characters with larger replacement #2 (buffer)");

        showResult();
    }

    log_amx("Checking replace_string()...");
    {
        new reference[64];

        /**
         * Multi bytes / Smaller replacements
         */
        reference = "[ȒǮɅΌ ȒǮɅΌ ȒǮɅΌ]";
        test(replace_string(reference, charsmax(reference), "ȒǮʌό", "ൠ", .caseSensitive = false), 3, .description = "Multi bytes characters with smaller replacement (count)");
        test(equal(reference, "[ൠ ൠ ൠ]"), .description = "Multi bytes characters with smaller replacement (buffer)");

        /**
         * Multi bytes / Larger replacements / Enough space to move data
         */
        reference = "[ȒǮɅΌ ȒǮɅΌ ȒǮɅΌ]";
        test(replace_string(reference, charsmax(reference), "ʌ", "𤭢", .caseSensitive = false), 3, .description = "Multi bytes characters with larger replacement #1 (count)");
        test(equal(reference, "[ȒǮ𤭢Ό ȒǮ𤭢Ό ȒǮ𤭢Ό]"), .description = "Multi bytes characters with larger replacement #1 (buffer)");

        /**
         * Multi bytes / Larger replacements / Not enough space to move data
         */
        reference = "[ȒǮɅΌ ȒǮɅΌ ȒǮɅΌ]";
        test(replace_string(reference, strlen(reference), "ʌ", "𤭢", .caseSensitive = false), 3, .description = "Multi bytes characters with larger replacement #2 (count) #1");
        test(equal(reference, "[ȒǮ𤭢 ȒǮ𤭢 ȒǮ𤭢]"), .description = "Multi bytes characters with larger replacement #2 (buffer)");

        showResult();
    }

    log_amx("Checking mb_ucfirst()...");
    {
        new reference[64];

        /**
         * Empty string.
         */
        reference = "";
        test(mb_ucfirst(reference, charsmax(reference)), 0, .description = "Empty string (length)");
        test(reference[0], EOS, .description = "Empty string (buffer)");

        /**
         * Basic latin / Single / Lowercase
         */
        reference = "c";
        test(mb_ucfirst(reference, charsmax(reference)), 1, .description = "Basic latin character (length)");
        test(reference[0], 'C', .description = "Basic latin character (buffer)");

        /**
         * Basic latin / Single / Uppercase
         */
        reference = "R";
        test(mb_ucfirst(reference, charsmax(reference)), 1, .description = "Basic latin uppercase character (length)");
        test(reference[0], 'R', .description = "Basic latin uppercase character (buffer)");

        /**
         * Basic latin / Single / Uunaffected
         */
        reference = "@";
        test(mb_ucfirst(reference, charsmax(reference)), 1, .description = "Basic latin unaffected character (length)");
        test(equal(reference, "@"), .description = "Basic latin unaffected character (buffer)");

        /**
         * Basic latin / Sentence
         */
        reference = "hello World!";
        test(mb_ucfirst(reference, charsmax(reference)), 12, .description = "Basic latin sentence (length)");
        test(equal(reference, "Hello World!"), .description = "Basic latin sentence (buffer)");

        /**
         * Multi Bytes / Single / Unaffected
         */
        reference = "ൠ";
        test(mb_ucfirst(reference, charsmax(reference)) == get_char_bytes(reference), .description = "Multi bytes character (length)");
        test(equal(reference, "ൠ"), .description = "Multi bytes character (buffer)");

        /**
         * Multi Bytes / Words
         */
        reference = "ȓǯʌό";
        test(mb_ucfirst(reference, charsmax(reference)) == strlen("Ȓǯʌό"), .description = "Multi bytes characters (length)");
        test(equal(reference, "Ȓǯʌό"), .description = "Multi bytes characters (buffer)");

        /**
         * Multi Bytes / Single / Not enough space
         */
        reference = "i̇";
        test(mb_ucfirst(reference, 1), 1, .description = "Multi bytes character / not enough space (length)");
        test(reference[0] == 'I', .description = "Multi bytes character / not enough space (buffer)");

        /**
         * Multi Bytes / Multiple / Not enough space
         */
        reference = "i̇i̇";
        test(mb_ucfirst(reference, 2), 1, .description = "Multi bytes characters / not enough space (length)");
        test(equal(reference, "I"), .description = "Multi bytes characters / not enough space (buffer)");

        showResult();
    }

    log_amx("Checking mb_strtolower/upper()...");
    {
        new reference[64];

        /**
         * Empty string.
         */
        reference = "";
        test(mb_strtolower(reference, charsmax(reference)), 0, .description = "Empty string / return (mb_strtolower)");
        test(reference[0], EOS, .description = "Empty string / buffer (mb_strtolower)");
        test(mb_strtoupper(reference, charsmax(reference)), 0, .description = "Empty string / return (mb_strtoupper)");
        test(reference[0], EOS, .description = "Empty string / buffer (mb_strtoupper)");

        /**
         * Basic latin / Single lowercase
         */
        reference = "c";
        test(mb_strtolower(reference, charsmax(reference)), 1, .description = "Basic latin character / return (mb_strtolower)");
        test(reference[0], 'c', .description = "Basic latin character / buffer (mb_strtolower)");
        test(mb_strtoupper(reference, charsmax(reference)), 1, .description = "Basic latin character / return (mb_strtoupper)");
        test(reference[0], 'C', .description = "Basic latin character / buffer (mb_strtoupper)");

        /**
         * Basic latin / Single uppercase
         */
        reference = "R";
        test(mb_strtolower(reference, charsmax(reference)), 1, .description = "Basic latin uppercase character / return (mb_strtolower)");
        test(reference[0], 'r', .description = "Basic latin uppercase character / buffer (mb_strtolower)");
        test(mb_strtoupper(reference, charsmax(reference)), 1, .description = "Basic latin uppercase character / return (mb_strtoupper)");
        test(reference[0], 'R', .description = "Basic latin uppercase character / buffer (mb_strtoupper)");

        /**
         * Basic latin / Single unaffected
         */
        reference = "@";
        test(mb_strtolower(reference, charsmax(reference)), 1, .description = "Basic latin unaffected character / return (mb_strtolower)");
        test(equal(reference, "@"), .description = "Basic latin unaffected character / buffer (mb_strtolower)");
        test(mb_strtoupper(reference, charsmax(reference)), 1, .description = "Basic latin unaffected character / return (mb_strtoupper)");
        test(equal(reference, "@"), .description = "Basic latin unaffected character / buffer (mb_strtoupper)");

        /**
         * Basic latin / Multiple lowercase
         */
        reference = "ab";
        test(mb_strtolower(reference, charsmax(reference)), 2, .description = "Basic latin lowercase characters / return (mb_strtolower)");
        test(equal(reference, "ab"), .description = "Basic latin lowercase characters / buffer (mb_strtolower)");
        test(mb_strtoupper(reference, charsmax(reference)), 2, .description = "Basic latin lowercase characters / return (mb_strtoupper)");
        test(equal(reference, "AB"), .description = "Basic latin lowercase characters / buffer (mb_strtoupper)");

        /**
         * Basic latin / Multiple uppercase
         */
        reference = "AB";
        test(mb_strtolower(reference, charsmax(reference)), 2, .description = "Basic latin uppercase characters / return (mb_strtolower)");
        test(equal(reference, "ab"), .description = "Basic latin uppercase characters / buffer (mb_strtolower)");
        test(mb_strtoupper(reference, charsmax(reference)), 2, .description = "Basic latin uppercase characters / return (mb_strtoupper)");
        test(equal(reference, "AB"), .description = "Basic latin uppercase characters / buffer (mb_strtoupper)");

        /**
         * Basic latin / Multiple unaffected
         */
        reference = "(-(#)-)";
        test(mb_strtolower(reference, charsmax(reference)), 7, .description = "Basic latin unaffected characters / return (mb_strtolower)");
        test(equal(reference, "(-(#)-)"), .description = "Basic latin unaffected characters / buffer (mb_strtolower)");
        test(mb_strtoupper(reference, charsmax(reference)), 7, .description = "Basic latin unaffected characters / return (mb_strtoupper)");
        test(equal(reference, "(-(#)-)"), .description = "Basic latin unaffected characters / buffer (mb_strtoupper)");

        /**
         * Basic latin / Sentence
         */
        reference = "Hello World!";
        test(mb_strtolower(reference, charsmax(reference)), 12, .description = "Basic latin sentence / return (mb_strtolower)");
        test(equal(reference, "hello world!"), .description = "Basic latin sentence / buffer (mb_strtolower)");
        test(mb_strtoupper(reference, charsmax(reference)), 12, .description = "Basic latin sentence / return (mb_strtoupper)");
        test(equal(reference, "HELLO WORLD!"), .description = "Basic latin sentence / buffer (mb_strtoupper)");

        /**
         * Multi bytes / Single unaffected
         */
        reference = "ൠ";
        test(mb_strtolower(reference, charsmax(reference)) == get_char_bytes(reference), .description = "Multi bytes unaffected character / return (mb_strtolower)");
        test(equal(reference, "ൠ"), .description = "Multi bytes unaffected character / buffer (mb_strtolower)");
        test(mb_strtoupper(reference, charsmax(reference)) == get_char_bytes(reference), .description = "Multi bytes unaffected character / return (mb_strtoupper)");
        test(equal(reference, "ൠ"), .description = "Multi bytes unaffected character / buffer (mb_strtoupper)");

        /**
         * Multi bytes / Words
         */
        reference = "L'ÎLE ȒǮɅΌ ЦИТРУС";
        test(mb_strtolower(reference, charsmax(reference)) == strlen("l'île") + strlen("ȓǯʌό") + strlen("цитрус") + strlen(" ") * 2, .description = "Multi bytes words / buffer (mb_strtolower)");
        test(equal(reference, "l'île ȓǯʌό цитрус"), .description = "Multi bytes words / return (mb_strtolower)");
        test(mb_strtoupper(reference, charsmax(reference)) == strlen("L'ÎLE") + strlen("ȒǮɅΌ") + strlen("ЦИТРУС") + strlen(" ") * 2, .description = "Multi bytes words / buffer (mb_strtoupper)");
        test(equal(reference, "L'ÎLE ȒǮɅΌ ЦИТРУС"), .description = "Multi bytes words / return (mb_strtoupper)");

        /**
         * Multi bytes / Single not enough space
         * Note: UC: "İ" = 2 bytes, LC: "i̇" = 3 bytes
         */
        reference = "İ";
        test(mb_strtolower(reference, 2), 1, .description = "Multi bytes character / not enough space / buffer (mb_strtolower)");
        test(reference[0] == 'i', .description = "Multi bytes character / not enough space / buffer (mb_strtolower)");

        /**
         * Multi bytes / Multiple not enough space
         */
        reference = "İİ";
        test(mb_strtolower(reference, 3), 3, .description = "Multi bytes characters / not enough space / buffer (mb_strtolower)");
        test(equal(reference, "i̇"), .description = "Multi bytes characters / not enough space / buffer (mb_strtolower)");

        /**
         * Multi bytes / Single enough space
         */
        reference = "İ";
        test(mb_strtolower(reference, charsmax(reference)), 3, .description = "Multi bytes character / enough space / buffer (mb_strtolower)");
        test(equal(reference, "i̇"), .description = "Multi bytes character / enough space / buffer (mb_strtolower)");

        /**
         * Multi bytes / Words + invalid 1
         */
        reference = "Hello^xE1^xB9";
        test(mb_strtolower(reference, charsmax(reference)), strlen(reference), .description = "Multi bytes words / invalid / return #1 (mb_strtolower)");
        test(equal(reference, "hello^xE1^xB9"), .description = "Multi bytes words / invalid / buffer #1 (mb_strtolower)");
        test(mb_strtoupper(reference, charsmax(reference)), strlen(reference), .description = "Multi bytes words / invalid / return #1 (mb_strtoupper)");
        test(equal(reference, "HELLO^xE1^xB9"), .description = "Multi bytes words / invalid / buffer #1 (mb_strtoupper)");

        /**
         * Multi bytes / Words + invalid 2
         */
        reference = "Hello^xE1^xB9World!";
        test(mb_strtolower(reference, charsmax(reference)), strlen(reference), .description = "Multi bytes character / enough space / return #2 (mb_strtolower)");
        test(equal(reference, "hello^xE1^xB9world!"), .description = "Multi bytes character / enough space / buffer #2 (mb_strtolower)");
        test(mb_strtoupper(reference, charsmax(reference)), strlen(reference), .description = "Multi bytes character / enough space / return #2 (mb_strtoupper)");
        test(equal(reference, "HELLO^xE1^xB9WORLD!"), .description = "Multi bytes character / enough space / buffer #2 (mb_strtoupper)");

        showResult();
    }

    log_amx("Checking mb_strtotitle()...");
    {
        new reference[64];

        /**
         * Empty string without maxlen.
         */
        reference = "";
        test(mb_strtotitle(reference), 0, .description = "Empty string without maxlen (length)");
        test(reference[0], EOS, .description = "Empty string without maxlen (buffer)");

        /**
         * Empty string with maxlen.
         */
        reference = "";
        test(mb_strtotitle(reference, charsmax(reference)), 0, .description = "Empty string with maxlen (length)");
        test(reference[0], EOS, .description = "Empty string with maxlen (buffer)");

        /**
         * Basic latin / Single
         */
        reference = "a";
        test(mb_strtotitle(reference), 1, .description = "Basic latin character (length)");
        test(reference[0], 'A', .description = "Basic latin character (buffer)");

        /**
         * Basic latin / Single unaffected
         */
        reference = "@";
        test(mb_strtotitle(reference), 1, .description = "Basic latin character unaffected (length)");
        test(reference[0], '@', .description = "Basic latin character unaffected (buffer)");

        /**
         * Basic latin / Single / Maxlen to 0
         */
        reference = "a";
        test(mb_strtotitle(reference, 0), 1, .description = "Basic latin character with maxlen to 0 (length)");
        test(reference[0], 'A', .description = "Basic latin character with maxlen to 0 (buffer)");

        /**
         * Basic latin / Single / Maxlen to 1
         */
        reference = "a";
        test(mb_strtotitle(reference, 1), 1, .description = "Basic latin character with maxlen to 1 (length)");
        test(reference[0], 'A', .description = "Basic latin character with maxlen to 1 (buffer)");

        /**
         * Basic latin / Multiple lowercase
         */
        reference = "aaa";
        test(mb_strtotitle(reference), 3, .description = "Basic latin lowercase characters (length)");
        test(equal(reference, "Aaa"), .description = "Basic latin lowercase characters (buffer)");

        /**
         * Basic latin / Multiple uppercase
         */
        reference = "AAA";
        test(mb_strtotitle(reference), 3, .description = "Basic latin uppercase characters (length)");
        test(equal(reference, "Aaa"), .description = "Basic latin uppercase characters (buffer)");

        /**
         * Basic latin / Multiple unaffected
         */
        reference = "@@@";
        test(mb_strtotitle(reference), 3, .description = "Basic latin characters unaffected (length)");
        test(equal(reference, "@@@"), .description = "Basic latin characters unaffected (buffer)");

        /**
         * Basic latin / Multiple / With maxlen
         */
        reference = "aaa";
        test(mb_strtotitle(reference, 2), 2, .description = "Basic latin characters with maxlen (length)");
        test(equal(reference, "Aa"), .description = "Basic latin characters with maxlen (buffer)");

        /**
         * General / Single
         */
        reference = "ȓ";
        test(mb_strtotitle(reference), 2, .description = "General character (length)");
        test(equal(reference, "Ȓ"), .description = "General character (buffer)");

        /**
         * General / Single truncated / With maxlen
         */
        reference = "ȓ";
        test(mb_strtotitle(reference, 1), 0, .description = "General character truncated (length)");
        test(equal(reference, ""), .description = "General character truncated (buffer)");

        /**
         * General / Word
         */
        reference = "цитрус";
        test(mb_strtotitle(reference), strlen("Цитрус"), .description = "General word (length)");
        test(equal(reference, "Цитрус"), .description = "General word (buffer)");

        /**
         * General / Word truncated / With maxlen
         */
        reference = "цитрус";
        test(mb_strtotitle(reference, 3), strlen("Ц"), .description = "General word truncated (length)");
        test(equal(reference, "Ц"), .description = "General word truncated (buffer)");

        /**
         * General / Sentence / Lowercase
         */
        reference = "l'île ȓǯʌό цитрус";
        test(mb_strtotitle(reference), strlen("L'Île Ȓǯʌό Цитрус"), .description = "General lowercase sentence (length)");
        test(equal(reference, "L'Île Ȓǯʌό Цитрус"), .description = "General lowercase sentence (buffer)");

        /**
         * General / Sentence / Uppercase
         */
        reference = "L'ÎLE ȒǮɅΌ ЦИТРУС";
        test(mb_strtotitle(reference), strlen("L'Île Ȓǯʌό Цитрус"), .description = "General uppercase sentence (length)");
        test(equal(reference, "L'Île Ȓǯʌό Цитрус"), .description = "General uppercase sentence (buffer)");

        showResult();
    }

    log_amx("Checking is_string_category()...");
    {
        new reference[64];
        new outputsize;

        /**
         * Empty string
         */
        reference = "";
        test(is_string_category(reference, charsmax(reference), UTF8C_ALL, outputsize), true, .description = "Empty string (return)");
        test(outputsize, 0, .description = "Empty string (output size)");

        /**
         * Basic latin / Single
         */
        reference = "a";
        test(is_string_category(reference, charsmax(reference), UTF8C_LETTER_LOWERCASE, outputsize), true, .description = "Basic latin character (return)");
        test(outputsize, 1, .description = "Basic latin character (output size)");

        /**
         * Basic latin / Single / Mismatch
         */
        reference = "!";
        test(is_string_category(reference, charsmax(reference), UTF8C_LETTER_UPPERCASE, outputsize), false, .description = "Basic latin mismatched character (return)");
        test(outputsize, 0, .description = "Basic latin mismatched character (output size)");

        /**
         * Basic latin / Multiple
         */
        reference = "123";
        test(is_string_category(reference, charsmax(reference), UTF8C_NUMBER_DECIMAL, outputsize), true, .description = "Basic latin characters (return)");
        test(outputsize, strlen(reference), .description = "Basic latin characters (output size)");

        /**
         * Basic latin / Multiple / Mismatch at start
         */
        reference = "(good)";
        test(is_string_category(reference, charsmax(reference), UTF8C_LETTER_LOWERCASE, outputsize), false, .description = "Basic latin characters / mismatched at start (return)");
        test(outputsize, 0, .description = "Basic latin characters / mismatched at start (output size)");

        /**
         * Basic latin / Multiple / Mismatch before end
         */
        reference = "    %";
        test(is_string_category(reference, charsmax(reference), UTF8C_SEPARATOR_SPACE, outputsize), false, .description = "Basic latin characters / mismatched before end (return)");
        test(outputsize, 4, .description = "Basic latin characters / mismatched before end (output size)");

        /**
         * Multi Byte / Single
         */
        reference = "^xC7^x85";
        test(is_string_category(reference, charsmax(reference), UTF8C_LETTER_TITLECASE, outputsize), true, .description = "Multi bytes single character (return)");
        test(outputsize, 2, .description = "Multi bytes single character (output size)");

        /**
         * Multi Byte / Multiple
         */
        reference = "^xD8^x87^xC2^xAC^xCF^xB6";
        test(is_string_category(reference, charsmax(reference), UTF8C_SYMBOL_MATH, outputsize), true, .description = "Multi bytes characters (return)");
        test(outputsize, 6, .description = "Multi bytes characters (output size)");

        /**
         * Multi Byte / Multiple / Mismatch at start
         */
        reference = "^xDE^x8A^xF0^x9B^xB0^xA2^xC2^xAA^xF0^x96^xAC^xBE";
        test(is_string_category(reference, charsmax(reference), UTF8C_LETTER_OTHER, outputsize), false, .description = "Multi bytes characters / mismatched at start (return)");
        test(outputsize, 8, .description = "Multi bytes characters / mismatched at start (output size)");

        /**
         * Incalid code point / Single
         */
        reference = "\xC5";
        test(is_string_category(reference, charsmax(reference), UTF8C_SYMBOL_OTHER, outputsize), false, .description = "Invalid code point (return)");
        test(outputsize, 0, .description = "Invalid code point (output size)");

        showResult();
    }

    ArrayDestroy(a);
    TrieDestroy(t);
    DestroyDataPack(d);

    showGlobalResult();
}