#include "nwnx_sql"
#include "nwnx_object"
#include "nwnx_tests"

void cleanup()
{
    NWNX_Tests_Report("NWNX_SQL", "Cleanup sql_test",    NWNX_SQL_ExecuteQuery("DROP TABLE sql_test"));
    NWNX_Tests_Report("NWNX_SQL", "Cleanup stress_test", NWNX_SQL_ExecuteQuery("DROP TABLE stress_test"));
    NWNX_Tests_Report("NWNX_SQL", "Cleanup error_test",  NWNX_SQL_ExecuteQuery("DROP TABLE error_test"));
}

void main()
{
    WriteTimestampedLogEntry("NWNX_SQL unit test begin..");

    string db_type = GetStringUpperCase(NWNX_SQL_GetDatabaseType());
    WriteTimestampedLogEntry("Testing database " + db_type);

    string sCreate = "";
    string sInsert = "";

    /* MySQL and SQLite version */
    if (db_type == "MYSQL" || db_type == "SQLITE")
    {
        sCreate = "CREATE TABLE sql_test (" +
                  "colInt INT, colFloat FLOAT, colStr VARCHAR(256)," +
                  "colObjId INT, colObj TEXT(1000000) );";

        sInsert = "INSERT INTO sql_test(colInt, colFloat, colStr, colObjId, colObj) VALUES(?, ?, ?, ?, ?)";
    }

    /* PostgreSQL version */
    if (db_type == "POSTGRESQL")
    {
        sCreate = "CREATE TABLE sql_test (" +
                  "colInt INT, colFloat FLOAT, colStr VARCHAR(256)," +
                  "colObjId INT, colObj TEXT );";

        // Even though we're using 0 based parameter numbers, PostgreSQL requires the parameter
        // numbers in the actual SQL string to be 1 based (e.g. $1, $2...  not $0, $1... )
         sInsert = "INSERT INTO sql_test(colInt, colFloat, colStr, colObjId, colObj) VALUES($1, $2, $3, $4, $5)";
    }

    int b = NWNX_SQL_ExecuteQuery(sCreate);
    NWNX_Tests_Report("NWNX_SQL", "Create Table", b);

    object o = CreateObject(OBJECT_TYPE_CREATURE, "nw_chicken", GetStartingLocation());
    if (!GetIsObjectValid(o))
    {
        WriteTimestampedLogEntry("NWNX_SQL test: Failed to create creature");
        cleanup();
        return;
    }

    vector v = Vector(5.0, 5.0, 0.0); // slightly different location.

    b = NWNX_SQL_PrepareQuery(sInsert);
    NWNX_Tests_Report("NWNX_SQL", "Complex PrepareQuery", b);
    NWNX_Tests_Report("NWNX_SQL", "GetPreparedQueryParamCount", NWNX_SQL_GetPreparedQueryParamCount() == 5);

    NWNX_SQL_PreparedInt(0, 42);
    NWNX_SQL_PreparedFloat(1, 0.42);
    NWNX_SQL_PreparedString(2, "FourtyTwooo");
    NWNX_SQL_PreparedObjectId(3, o);
    NWNX_SQL_PreparedObjectFull(4, o);

    b = NWNX_SQL_ExecutePreparedQuery();
    NWNX_Tests_Report("NWNX_SQL", "Complex ExecutePreparedQuery", b);

    b = NWNX_SQL_ExecuteQuery("SELECT * FROM sql_test;");
    NWNX_Tests_Report("NWNX_SQL", "Select ExecuteQuery", b);

    if (b)
    {
        while (NWNX_SQL_ReadyToReadNextRow())
        {
            NWNX_SQL_ReadNextRow();
            int n = StringToInt(NWNX_SQL_ReadDataInActiveRow(0));
            NWNX_Tests_Report("NWNX_SQL", "ReadInt", n == 42);
            float f = StringToFloat(NWNX_SQL_ReadDataInActiveRow(1));
            NWNX_Tests_Report("NWNX_SQL", "ReadFloat", fabs(f - 0.42) < 0.01);
            string s = NWNX_SQL_ReadDataInActiveRow(2);
            NWNX_Tests_Report("NWNX_SQL", "ReadString", s == "FourtyTwooo");

            string sObjId = NWNX_SQL_ReadDataInActiveRow(3); // In base 10
            object o2 = NWNX_Object_StringToObject(IntToHexString(StringToInt(sObjId)));
            NWNX_Tests_Report("NWNX_SQL", "ReadObjectId", o == o2);

            object o3 = NWNX_SQL_ReadFullObjectInActiveRow(4, GetArea(o), v.x, v.y, v.z);
            NWNX_Tests_Report("NWNX_SQL", "ReadFullObject", GetIsObjectValid(o3));
            // Alternatively:
            // object o3 = NWNX_Object_Deserialize(NWNX_SQL_ReadDataInActiveRow(4));
        }
    }


    object oPlc = CreateObject(OBJECT_TYPE_PLACEABLE, "nw_plc_chestburd", GetStartingLocation());
    object oItem = CreateObject(OBJECT_TYPE_ITEM, "x0_it_mring013", GetStartingLocation());
    if (!GetIsObjectValid(oPlc) || !GetIsObjectValid(oItem))
    {
        WriteTimestampedLogEntry("NWNX_SQL test: Failed to create objects..");
    }
    else
    {
        object oTmp = GetFirstItemInInventory(oPlc);
        while (GetIsObjectValid(oTmp))
        {
            WriteTimestampedLogEntry("NWNX_SQL Destroying auto created object " + GetTag(oTmp));
            DestroyObject(oTmp);
            oTmp = GetNextItemInInventory(oPlc);
        }

        string test2 = "";
        if (db_type == "MYSQL" || db_type == "SQLITE")
        {
            test2="INSERT INTO sql_test(colInt, colFloat, colStr, colObjId, colObj) VALUES(1337,0.0,'xxx',1337,?)";
        }
        if (db_type == "POSTGRESQL")
        {
            test2="INSERT INTO sql_test(colInt, colFloat, colStr, colObjId, colObj) VALUES(1337,0.0,'xxx',1337,$1)";
        }

        NWNX_SQL_PrepareQuery(test2);

        NWNX_SQL_PreparedObjectFull(0, oItem);
        b = NWNX_SQL_ExecutePreparedQuery();
        NWNX_Tests_Report("NWNX_SQL", "Insert item full", b);

        b = NWNX_SQL_ExecuteQuery("SELECT colObj FROM sql_test WHERE colInt=1337");
        NWNX_Tests_Report("NWNX_SQL", "Select item", b);

        if (NWNX_SQL_ReadyToReadNextRow())
        {
            NWNX_SQL_ReadNextRow();

            object oItem2 = NWNX_SQL_ReadFullObjectInActiveRow(0, oPlc);
            NWNX_Tests_Report("NWNX_SQL", "ReadFullObject Item", GetIsObjectValid(oItem2));
            NWNX_Tests_Report("NWNX_SQL", "Deserialized to placeable's inventory", oItem2 == GetFirstItemInInventory(oPlc));
            NWNX_Tests_Report("NWNX_SQL", "Deserialized to placeable's inventory - possessor", GetItemPossessor(oItem2) == oPlc);

            object oItem3 = NWNX_SQL_ReadFullObjectInActiveRow(0, GetArea(oPlc), v.x, v.y, v.z);
            NWNX_Tests_Report("NWNX_SQL", "Deserialized to area", GetArea(oItem3) == GetArea(oPlc));

            object oItem4 = NWNX_SQL_ReadFullObjectInActiveRow(0, o);
            NWNX_Tests_Report("NWNX_SQL", "Deserialized to creature's inventory - possessor", GetItemPossessor(oItem4) == o);
        }
        else
        {
            WriteTimestampedLogEntry("NWNX_SQL not ready to read item");
        }

    }

    int STRESS_CNT = 10;

    WriteTimestampedLogEntry("NWNX_SQL stress test.");
    NWNX_SQL_ExecuteQuery("create table stress_test ( i_key int, i_int int, s_text varchar(8))");
    int i;
    // Brute force some inserts
    for ( i=1 ; i<=STRESS_CNT ; i++ )  // Generate 1000 rows.
    {
        // Simulates the existing NWNX2 ODBC way of generating SQL.
        NWNX_SQL_ExecuteQuery("insert into stress_test values ( " + IntToString(i) + ", " + IntToString(i*2) + ", '" + IntToString(i*100) + "')");
    }
    NWNX_SQL_ExecuteQuery("delete from stress_test where i_key > 0");
    int res = NWNX_SQL_GetAffectedRows();
    WriteTimestampedLogEntry("Deleted " + IntToString(res) + " rows.");
    report ("Delete rows", res == STRESS_CNT);

    // now do some elegant inserts
    string test3 = "";
    if (db_type == "MYSQL" || db_type == "SQLITE")
    {
        test3 = "insert into stress_test values ( ?, ?, ? )";
    }
    if (db_type == "POSTGRESQL")
    {
        test3 = "insert into stress_test values ( $1, $2, $3 )";
    }

    NWNX_SQL_PrepareQuery(test3);
    for ( i = 1 ; i <= STRESS_CNT ; i++ )
    {
        NWNX_SQL_PreparedInt(0, i);
        NWNX_SQL_PreparedInt(1, i*2);
        NWNX_SQL_PreparedString(2, IntToString(i*100));
        b = NWNX_SQL_ExecutePreparedQuery();
        NWNX_Tests_Report("NWNX_SQL", "Elegant Looping ExecutePreparedQuery", b);
    }
    NWNX_SQL_ExecuteQuery("delete from stress_test where i_key > 0");
    res = NWNX_SQL_GetAffectedRows();
    WriteTimestampedLogEntry("Deleted " + IntToString(res) + " rows.");
    NWNX_Tests_Report("Delete rows", res == STRESS_CNT);

    // Test some error output.
    b = NWNX_SQL_ExecuteQuery("create table error_test (col varchar(10))");
    NWNX_Tests_Report("Test Table Create", b);

    b = NWNX_SQL_ExecuteQuery("insert into error_test values('abcdefghij')");
    NWNX_Tests_Report("good insert", b);

    if (db_type != "SQLITE")
    {// SQLite doesn't care about size constraints of columns
        b = NWNX_SQL_ExecuteQuery("insert into error_test values('abcde000fghij')");
        report ("bad insert", !b);
        if (!b) {
        WriteTimestampedLogEntry("There should be an error a couple rows up.");
        }
    }

    string test4 = "";
    if (db_type == "MYSQL" || db_type == "SQLITE")
    {
        test3 = "insert into error_test values ( ? )";
    }
    if (db_type == "POSTGRESQL")
    {
        test3 = "insert into error_test values ( $1 )";
    }
    NWNX_SQL_PrepareQuery(test3);
    NWNX_SQL_PreparedString(100, "lala"); // out of bounds, must not crash.

    NWNX_Tests_Report("NWNX_SQL", "Negative prepare query", NWNX_SQL_PrepareQuery("not a valid query!") == 0);
    NWNX_Tests_Report("NWNX_SQL", "GetLastError", NWNX_SQL_GetLastError() != "");

    // Test with null values
    NWNX_SQL_ExecuteQuery("INSERT INTO sql_test(colInt, colFloat, colStr, colObjId, colObj) VALUES(5121, null, null, null, null)");
    NWNX_Tests_Report("NWNX_SQL", "Select null", NWNX_SQL_ExecuteQuery("SELECT * FROM sql_test WHERE colInt=5121"));
    if (NWNX_SQL_ReadyToReadNextRow())
    {
        NWNX_SQL_ReadNextRow();
        int n = StringToInt(NWNX_SQL_ReadDataInActiveRow(0));
        NWNX_Tests_Report("NWNX_SQL", "ReadInt", n == 5121);
        float f = StringToFloat(NWNX_SQL_ReadDataInActiveRow(1));
        NWNX_Tests_Report("NWNX_SQL", "ReadFloat", f == 0.0);
        string s = NWNX_SQL_ReadDataInActiveRow(2);
        NWNX_Tests_Report("NWNX_SQL", "ReadString", s == "");

        string sObjId = NWNX_SQL_ReadDataInActiveRow(3); // In base 10
        NWNX_Tests_Report("NWNX_SQL", "ReadObjectId", sObjId == "");

        object obj = NWNX_SQL_ReadFullObjectInActiveRow(4);
        NWNX_Tests_Report("NWNX_SQL", "ReadFullObject", obj == OBJECT_INVALID);
    }

    cleanup();
    WriteTimestampedLogEntry("Testing database " + db_type + " complete.");
    WriteTimestampedLogEntry("NWNX_SQL unit tests end.");
}