/**
 *  Set of tests for `DBConnection` and `DBCache` classes.
 *      Copyright 2023 Anton Tarasenko
 *------------------------------------------------------------------------------
 * This file is part of Acedia.
 *
 * Acedia is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3 of the License, or
 * (at your option) any later version.
 *
 * Acedia 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Acedia.  If not, see <https://www.gnu.org/licenses/>.
 */
class TEST_DBConnection extends TestCase
    abstract;

var DBCache cache;

protected static function TESTS()
{
    Context("Testing how `DBCache` handles data writes and reads when"
        @ "the actual database's data is yet to be downloaded.");
    Test_DBCache_BasicWriteRead_DuringLoading();
    Test_DBCache_AllOperations_DuringLoading();
    Context("Testing how `DBCache` handles applying pending writes after real"
        @ "data was set.");
    Test_DBCache_ApplyingPendingEdits();
    Test_DBCache_ApplyingPendingEdit_AllOperations();
    Context("Testing how `DBCache` handles data writes and reads when"
        @ "the actual database's data is already setup.");
    Test_DBCache_BasicWriteRead_AfterLoading();
    Test_DBCache_AllOperations_AfterLoading();
}

/*  Creates following object:
{
    "A": "simpleValue",
    "B": {
        "A": [true, {
            "A": "simpleValue",
            "B": 11.12,
            "": [true, null, "huh"]
        }, "huh"],
        "B": -13.95
    },
    "C": -5,
    "D": []
}   */
protected static function HashTable MakeTestJSONObject()
{
    local ArrayList outerArray, innerArray;
    local HashTable result, outerObject, innerObject;

    innerArray = __().collections.EmptyArrayList();
    innerArray.AddBool(true).AddItem(none).AddString("huh");
    innerObject = __().collections.EmptyHashTable();
    innerObject.SetString(P("A"), "simpleValue");
    innerObject.SetFloat(P("B"), 11.12).SetItem(P(""), innerArray);
    outerArray = __().collections.EmptyArrayList();
    outerArray.AddBool(true).AddItem(innerObject).AddString("huh");
    outerObject = __().collections.EmptyHashTable();
    outerObject.SetItem(P("A"), outerArray).SetFloat(P("B"), -13.95);
    result = __().collections.EmptyHashTable();
    result.SetString(P("A"), "simpleValue");
    result.SetItem(P("B"), outerObject);
    result.SetInt(P("C"), -5);
    result.SetItem(P("D"), __().collections.EmptyArrayList());
    return result;
}

protected static function CheckLocation(string pointer, string expectedResult)
{
    local AcediaObject value;

    value = default.cache
        .Read(__().json.Pointer(__().text.FromString(pointer)));
    if (BaseText(value) != none)
    {
        TEST_ExpectTrue(
                __().json.Print(value).ToString()
            ==  ("\"" $ expectedResult $ "\""));
    }
    else {
        TEST_ExpectTrue(__().json.Print(value).ToString() == expectedResult);
    }
}

//      This is a rather extensive and long test.
//  Despite its length, I've chosen to not break it up into smaller parts,
//  since the same value `default.cache` is changed and built up all the way
//  through this method.
//      Trying to break it up into smaller and simpler tests would only mean
//  putting less stress of `DBCache` than we currently do.
protected static function Test_DBCache_BasicWriteRead_DuringLoading()
{
    local AcediaObject complexData;

    Issue("Simple write/read sequence not working correctly.");
    default.cache = DBCache(__().memory.Allocate(class'DBCache'));
    complexData = MakeTestJSONObject();
    default.cache.Write(
        __().json.Pointer(P("/just/some/place/far/away")),
        complexData);
    default.cache.Write(
        __().json.Pointer(P("/just/another/place/not/near")),
        complexData);
    CheckLocation("/just/another/place/not/near/A", "simpleValue");
    CheckLocation("/just/some/place/far/away/B/A/2", "huh");
    CheckLocation("/just/another/place/not/near/B/A/1//2", "huh");
    CheckLocation("/just/some/place/far/away/B/A/1/", "[true,null,\"huh\"]");

    Issue("Data is read incorrectly after being overridden.");
    default.cache.Write(
        __().json.Pointer(P("/just/another/place/not/near/B/A/1//1")),
        __().box.bool(false));
    CheckLocation(
        "/just/another/place/not/near/B/A/1/",
        "[true,false,\"huh\"]");
    default.cache.Write(
        __().json.Pointer(P("/just/another/place/not/near/B/A")),
        __().box.int(121));
    CheckLocation("/just/another/place/not/near/B/A", "121");
    default.cache.Write(
        __().json.Pointer(P("/just/some/place/far/away/B/A/1/B")),
        complexData);
    CheckLocation("/just/some/place/far/away/B/A/1/B/B/A/0", "true");
    default.cache.Write(
        __().json.Pointer(
            P("/just/some/place/far/away/C/inside_the_number!/hey")),
         __().box.float(1.1));
    CheckLocation("/just/some/place/far/away/C/inside_the_number!/hey", "null");
    default.cache.Write(
        __().json.Pointer(P("/just/some/place/far/away/D/7/hey")),
        __().box.int(-345));
    CheckLocation("/just/some/place/far/away/D", "[]");
    CheckLocation("/just/some/place/far/away/D/7/hey", "-345");
    default.cache.Write(
        __().json.Pointer(
            P("/just/some/place/far/away/D/inside_the_array!/hey")),
        __().box.float(1.1));
    CheckLocation("/just/some/place/far/away/D/inside_the_array!/hey", "null");
    CheckLocation("/just/some/place/far/away/D", "[]");
    default.cache.Write(
        __().json.Pointer(P("/just/some/place/far/away/D/-")),
        __().box.bool(true));
    CheckLocation("/just/some/place/far/away/D", "[true]");
    default.cache.Write(
        __().json.Pointer(P("/just/some/place/far/away/D/7")),
        __().box.bool(true));
    CheckLocation(
        "/just/some/place/far/away/D",
        "[true,null,null,null,null,null,null,true]");
    Issue("Writing at the end of a JSON array several times doesn't correctly"
        @ "keep all values.");
    default.cache.Write(
        __().json.Pointer(P("/just/some/place/far/away/D/-")),
        __().box.int(13524));
    default.cache.Write(
        __().json.Pointer(P("/just/some/place/far/away/D/-")), none);
    default.cache.Write(
        __().json.Pointer(P("/just/some/place/far/away/D/-")),
        __().box.int(121));
    CheckLocation(
        "/just/some/place/far/away/D",
        "[true,null,null,null,null,null,null,true,13524,null,121]");
}

protected static function Test_DBCache_AllOperations_DuringLoading()
{
    local AcediaObject  complexData;
    local ArrayList     newArrayData;

    newArrayData = __().collections.EmptyArrayList();
    newArrayData = newArrayData.AddInt(45);
    newArrayData = newArrayData.AddItem(none);
    newArrayData = newArrayData.AddString("lol");

    Issue("Increment/remove/read sequence not working correctly.");
    default.cache = DBCache(__().memory.Allocate(class'DBCache'));
    complexData = MakeTestJSONObject();
    default.cache.Increment(__().json.Pointer(P("")), complexData);
    default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
        __().text.FromString("oi"));
    default.cache.Remove(__().json.Pointer(P("/B/A/1/B")));
    default.cache.Remove(__().json.Pointer(P("/B/A/1/")));
    default.cache.Increment(__().json.Pointer(P("/B/A")), newArrayData);
    default.cache.Increment(__().json.Pointer(P("/C")), __().box.float(34.5));
    default.cache.Increment(__().json.Pointer(P("/C")), __().box.bool(true));
    default.cache.Increment(__().json.Pointer(P("/D")), newArrayData);
    default.cache.Increment(__().json.Pointer(P("/D")), newArrayData);
    default.cache.Increment(__().json.Pointer(P("/D")), newArrayData);
    default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
        __().text.FromString("! Yeah!"));
    //  Override all increments!
    default.cache.Write(__().json.Pointer(P("/D")), newArrayData);
    CheckLocation("/B/A/1/A", "simpleValueoi! Yeah!");
    CheckLocation("/B/A",
        "[true,{\"A\":\"simpleValueoi! Yeah!\"},\"huh\",45,null,\"lol\"]");
    CheckLocation("/C", "29.5");
    CheckLocation("/D", "[45,null,\"lol\"]");
}

protected static function Test_DBCache_ApplyingPendingEdits()
{
    SubTest_DBCache_ApplyingPendingEdits_Simple();
    SubTest_DBCache_ApplyingPendingEdits_Complex();
}

protected static function SubTest_DBCache_ApplyingPendingEdits_Simple()
{
    local int                           i;
    local array<DBCache.PendingEdit>   result;

    Issue("Pending writes successfully apply in simple JSON object case.");
    default.cache = DBCache(__().memory.Allocate(class'DBCache'));
    default.cache.Write(
        __().json.Pointer(P("")),
        __().collections.EmptyHashTable());
    default.cache.Write(__().json.Pointer(P("/hey")), __().box.int(476));
    default.cache.Write(__().json.Pointer(P("/hope")), none);
    default.cache.Write(__().json.Pointer(P("/-")), __().ref.float(324.3));
    result = default.cache.SetRealData(__().text.FromString("woah"));
    for (i = 0; i < result.length; i += 1) {
        TEST_ExpectTrue(result[i].successful);
    }
    CheckLocation("/hey", "476");
    CheckLocation("/hope", "null");
    CheckLocation("/-", "324.3");

    Issue("Pending don't fail in a simple case of writing into JSON string.");
    default.cache = DBCache(__().memory.Allocate(class'DBCache'));
    default.cache.Write(__().json.Pointer(P("/hey")), __().box.int(476));
    default.cache.Write(__().json.Pointer(P("/hope")), none);
    default.cache.Write(__().json.Pointer(P("/-")), __().ref.float(324.3));
    result = default.cache.SetRealData(__().text.FromString("woah"));
    for (i = 0; i < result.length; i += 1) {
        TEST_ExpectFalse(result[i].successful);
    }
    CheckLocation("/hey", "null");
    CheckLocation("/hope", "null");
    CheckLocation("/-", "null");
}

protected static function SubTest_DBCache_ApplyingPendingEdits_Complex()
{
    local array<DBCache.PendingEdit> result;

    Issue("Pending writes incorrectly apply in complex case.");
    default.cache = DBCache(__().memory.Allocate(class'DBCache'));
    default.cache.Write(__().json.Pointer(P("/B/A/1/B")), __().box.int(777));
    default.cache.Write(__().json.Pointer(P("/B/A/-")), __().box.bool(true));
    default.cache.Write(__().json.Pointer(P("/D/5")), __().box.float(1.1));
    default.cache.Write(
        __().json.Pointer(P("/new")),
        __().collections.EmptyHashTable());
    default.cache.Write(
        __().json.Pointer(P("/new/sub")),
        __().text.FromString("!SubString!"));
    default.cache.Write(
        __().json.Pointer(P("/D/impossiburu")),
        __().text.FromString("!SubString!"));
    result = default.cache.SetRealData(MakeTestJSONObject());
    CheckLocation("/B/A/1/B", "777");
    CheckLocation("/B/A/3", "true");
    CheckLocation("/D/5", "1.1");
    CheckLocation("/D/2", "null");
    CheckLocation("/new", "{\"sub\":\"!SubString!\"}");
    TEST_ExpectTrue(result[0].successful);
    TEST_ExpectTrue(result[1].successful);
    TEST_ExpectTrue(result[2].successful);
    TEST_ExpectTrue(result[3].successful);
    TEST_ExpectTrue(result[4].successful);
    TEST_ExpectFalse(result[5].successful);
}

protected static function Test_DBCache_ApplyingPendingEdit_AllOperations()
{
    local ArrayList                     newArrayData;
    local array<DBCache.PendingEdit>    result;

    newArrayData = __().collections.EmptyArrayList();
    newArrayData = newArrayData.AddInt(45).AddItem(none).AddString("lol");

    Issue("Pending increments and removals incorrectly apply in complex case.");
    default.cache = DBCache(__().memory.Allocate(class'DBCache'));
    default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
        __().text.FromString("oi"));
    default.cache.Remove(__().json.Pointer(P("/B/A/1/B")));
    default.cache.Remove(__().json.Pointer(P("/B/A/1/")));
    default.cache.Increment(__().json.Pointer(P("/B/A")), newArrayData);
    default.cache.Increment(__().json.Pointer(P("/C")), __().box.float(34.5));
    default.cache.Increment(__().json.Pointer(P("/C")), __().box.bool(true));
    default.cache.Increment(__().json.Pointer(P("/D")), newArrayData);
    default.cache.Increment(__().json.Pointer(P("/D")), newArrayData);
    default.cache.Remove(__().json.Pointer(P("/B/A/Y")));
    default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
        __().text.FromString("! Yeah!"));
    default.cache.Write(__().json.Pointer(P("/D")), newArrayData);
    result = default.cache.SetRealData(MakeTestJSONObject());
    TEST_ExpectTrue(result.length == 9);
    TEST_ExpectTrue(result[0].successful);
    TEST_ExpectTrue(result[1].successful);
    TEST_ExpectTrue(result[2].successful);
    TEST_ExpectTrue(result[3].successful);
    TEST_ExpectTrue(result[4].successful);
    TEST_ExpectFalse(result[5].successful);
    TEST_ExpectFalse(result[6].successful);
    TEST_ExpectTrue(result[7].successful);
    TEST_ExpectTrue(result[8].successful);
}

protected static function Test_DBCache_BasicWriteRead_AfterLoading()
{
    local AcediaObject complexData;

    Issue("Simple write/read sequence not working.");
    default.cache = DBCache(__().memory.Allocate(class'DBCache'));
    complexData = MakeTestJSONObject();
    TEST_ExpectTrue(default.cache.SetRealData(complexData).length == 0);
    default.cache.Write(__().json.Pointer(P("/B/A/1/B")), __().box.int(777));
    default.cache.Write(__().json.Pointer(P("/B/A/-")), __().box.bool(true));
    default.cache.Write(__().json.Pointer(P("/D/5")), __().box.float(1.1));
    default.cache.Write(
        __().json.Pointer(P("/new")),
        __().collections.EmptyHashTable());
    default.cache.Write(
        __().json.Pointer(P("/new/sub")),
        __().text.FromString("!SubString!"));
    default.cache.Write(
        __().json.Pointer(P("/D/impossiburu")),
        __().text.FromString("!SubString!"));
    CheckLocation("/B/A/1/B", "777");
    CheckLocation("/B/A/3", "true");
    CheckLocation("/D/5", "1.1");
    CheckLocation("/new", "{\"sub\":\"!SubString!\"}");

    Issue("Simple write/read are affecting data when they shouldn't.");
    CheckLocation("/D/2", "null");
    default.cache.Write(__().json.Pointer(P("")), __().box.float(1.1));
    default.cache.Write(__().json.Pointer(P("/hm?")), __().box.float(2.2));
    CheckLocation("", "1.1");
}

protected static function Test_DBCache_AllOperations_AfterLoading()
{
    local ArrayList newArrayData;

    newArrayData = __().collections.EmptyArrayList();
    newArrayData = newArrayData.AddInt(45).AddItem(none).AddString("lol");

    Issue("Increment/remove/read sequence not working correctly.");
    default.cache = DBCache(__().memory.Allocate(class'DBCache'));
    default.cache.SetRealData(MakeTestJSONObject());
    TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
        __().text.FromString("oi")));
    TEST_ExpectTrue(default.cache.Remove(__().json.Pointer(P("/B/A/1/B"))));
    TEST_ExpectTrue(default.cache.Remove(__().json.Pointer(P("/B/A/1/"))));
    TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/B/A")),
        newArrayData));
    TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/C")),
        __().box.float(34.5)));
    TEST_ExpectFalse(default.cache.Increment(__().json.Pointer(P("/C")),
        __().box.bool(true)));
    TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/D")),
        newArrayData));
    TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/D")),
        newArrayData));
    TEST_ExpectFalse(default.cache.Remove(__().json.Pointer(P("/B/A/Y"))));
    TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
        __().text.FromString("! Yeah!")));
    default.cache.Write(__().json.Pointer(P("/D")), newArrayData);
    CheckLocation("/B/A/1/A", "simpleValueoi! Yeah!");
    CheckLocation("/B/A",
        "[true,{\"A\":\"simpleValueoi! Yeah!\"},\"huh\",45,null,\"lol\"]");
    CheckLocation("/C", "29.5");
    CheckLocation("/D", "[45,null,\"lol\"]");
}

defaultproperties
{
    caseGroup   = "Database"
    caseName    = "DBConnection related tests"
}