/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <boost/test/unit_test.hpp>

#include <ignite/ignite_error.h>
#include <ignite/ignition.h>

#include <ignite/thin/ignite_client_configuration.h>
#include <ignite/thin/ignite_client.h>

#include <ignite/test_type.h>
#include <test_utils.h>

using namespace ignite::thin;
using namespace ignite::thin::cache::query;
using namespace boost::unit_test;

class SqlFieldsQueryTestSuiteFixture
{
public:
    static ignite::Ignite StartNode(const char* name)
    {
        return ignite_test::StartCrossPlatformServerNode("sql-query-fields.xml", name);
    }

    SqlFieldsQueryTestSuiteFixture()
    {
        serverNode = StartNode("ServerNode");

        IgniteClientConfiguration cfg;

        cfg.SetEndPoints("127.0.0.1:11110");

        client = IgniteClient::Start(cfg);

        cacheAllFields = client.GetCache<int64_t, ignite::TestType>("cacheAllFields");
    }

    ~SqlFieldsQueryTestSuiteFixture()
    {
        ignite::Ignition::StopAll(false);
    }

    /**
     * Perform an invalid SQL query and check whether returned SQLSTATE is expected.
     */
    void CheckSqlStateForQuery(const std::string& sql, const std::string& expectedSqlState)
    {
        SqlFieldsQuery testQry(sql);
        testQry.SetSchema("PUBLIC");

        try
        {
            cacheAllFields.Query(testQry);

            BOOST_FAIL("Expected to get SQL error here");
        }
        catch (ignite::IgniteError& err)
        {
            std::string msg(err.GetText());

            std::string sqlState(msg.substr(0, msg.find(':')));

            BOOST_CHECK_EQUAL(expectedSqlState, sqlState);
        }
    }

protected:
    /** Server node. */
    ignite::Ignite serverNode;

    /** Client. */
    IgniteClient client;

    /** Cache with TestType. */
    cache::CacheClient<int64_t, ignite::TestType> cacheAllFields;
};

BOOST_AUTO_TEST_SUITE(SqlFieldsQueryBasicTestSuite)

BOOST_AUTO_TEST_CASE(SqlFieldsQueryDefaults)
{
    std::string sql("select * from TestType");

    SqlFieldsQuery qry(sql);

    BOOST_CHECK_EQUAL(qry.GetSql(), sql);
    BOOST_CHECK_EQUAL(qry.GetTimeout(), 0);
    BOOST_CHECK_EQUAL(qry.GetMaxRows(), 0);
    BOOST_CHECK_EQUAL(qry.GetPageSize(), 1024);
    BOOST_CHECK_EQUAL(qry.GetSchema(), std::string());

    BOOST_CHECK(!qry.IsLocal());
    BOOST_CHECK(!qry.IsCollocated());
    BOOST_CHECK(!qry.IsDistributedJoins());
    BOOST_CHECK(!qry.IsEnforceJoinOrder());
    BOOST_CHECK(!qry.IsLazy());
}

BOOST_AUTO_TEST_CASE(SqlFieldsQuerySetGet)
{
    std::string sql("select * from TestType");

    SqlFieldsQuery qry(sql);

    qry.SetTimeout(1000);
    qry.SetMaxRows(100);
    qry.SetPageSize(4096);
    qry.SetSchema("PUBLIC");

    qry.SetLocal(true);
    qry.SetCollocated(true);
    qry.SetDistributedJoins(true);
    qry.SetEnforceJoinOrder(true);
    qry.SetLazy(true);

    qry.SetUpdateBatchSize(42);

    std::vector<int32_t> parts;
    parts.push_back(1);
    parts.push_back(4);
    parts.push_back(90);
    parts.push_back(12432);

    qry.SetPartitions(parts);

    BOOST_CHECK_EQUAL(qry.GetSql(), sql);
    BOOST_CHECK_EQUAL(qry.GetTimeout(), 1000);
    BOOST_CHECK_EQUAL(qry.GetMaxRows(), 100);
    BOOST_CHECK_EQUAL(qry.GetPageSize(), 4096);
    BOOST_CHECK_EQUAL(qry.GetSchema(), std::string("PUBLIC"));

    BOOST_CHECK(qry.IsLocal());
    BOOST_CHECK(qry.IsCollocated());
    BOOST_CHECK(qry.IsDistributedJoins());
    BOOST_CHECK(qry.IsEnforceJoinOrder());
    BOOST_CHECK(qry.IsLazy());

    BOOST_CHECK_EQUAL(qry.GetUpdateBatchSize(), 42);

    std::vector<int32_t> partsOut = qry.GetPartitions();

    BOOST_CHECK_EQUAL_COLLECTIONS(parts.begin(), parts.end(), partsOut.begin(), partsOut.end());
}

BOOST_AUTO_TEST_SUITE_END()

/**
 * Check that error empty cursor error.
 *
 * @param err Error.
 */
bool IsCursorEmptyError(const ignite::IgniteError& err)
{
    return err.GetCode() == ignite::IgniteError::IGNITE_ERR_GENERIC &&
        std::string(err.GetText()) == "The cursor is empty";
}

/**
 * Check that cursor is empty.
 *
 * @param cursor Cursor.
 */
void CheckCursorEmpty(QueryFieldsCursor& cursor)
{
    BOOST_CHECK(!cursor.HasNext());
    BOOST_CHECK_EXCEPTION(cursor.GetNext(), ignite::IgniteError, IsCursorEmptyError);
}

/**
 * Check that row is empty.
 *
 * @param row Row.
 */
void CheckRowCursorEmpty(QueryFieldsRow& row)
{
    BOOST_CHECK(!row.HasNext());
    BOOST_CHECK_EXCEPTION(row.GetNext<int8_t>(), ignite::IgniteError, IsCursorEmptyError);
}

/**
 * Check that row columns equal value fields.
 *
 * @param row Row.
 * @param val Value.
 */
void CheckRowEqualsValue(QueryFieldsRow& row, const ignite::TestType& val)
{
    BOOST_CHECK_EQUAL(row.GetNext<int8_t>(), val.i8Field);
    BOOST_CHECK_EQUAL(row.GetNext<int16_t>(), val.i16Field);
    BOOST_CHECK_EQUAL(row.GetNext<int32_t>(), val.i32Field);
    BOOST_CHECK_EQUAL(row.GetNext<int64_t>(), val.i64Field);
    BOOST_CHECK_EQUAL(row.GetNext<std::string>(), val.strField);
    BOOST_CHECK_CLOSE(row.GetNext<float>(), val.floatField, 0.0001f);
    BOOST_CHECK_CLOSE(row.GetNext<double>(), val.doubleField, 0.0001);
    BOOST_CHECK_EQUAL(row.GetNext<bool>(), val.boolField);
    BOOST_CHECK_EQUAL(row.GetNext<ignite::Guid>(), val.guidField);
    BOOST_CHECK(row.GetNext<ignite::Date>() == val.dateField);
    BOOST_CHECK_EQUAL(row.GetNext<ignite::Time>().GetMilliseconds(), val.timeField.GetMilliseconds());
    BOOST_CHECK(row.GetNext<ignite::Timestamp>() == val.timestampField);

    std::vector<int8_t> resArray = row.GetNext< std::vector<int8_t> >();

    BOOST_CHECK_EQUAL_COLLECTIONS(
            resArray.begin(), resArray.end(),
            val.i8ArrayField.begin(), val.i8ArrayField.end());
}

/**
 * Make custom test value.
 *
 * @param seed Seed to generate value.
 */
IGNORE_SIGNED_OVERFLOW
ignite::TestType MakeCustomTestValue(int32_t seed)
{
    ignite::TestType val;

    val.i8Field = seed;
    val.i16Field = 2 * seed;
    val.i32Field = 4 * seed;
    val.i64Field = 8 * seed;
    val.strField = "Lorem ipsum";
    val.floatField = 16.0f * seed;
    val.doubleField = 32.0 * seed;
    val.boolField = ((seed % 2) == 0);
    val.guidField = ignite::Guid(0x1020304050607080 * seed, 0x9000A0B0C0D0E0F0 * seed);
    val.dateField = ignite::Date(235682736 * seed);
    val.timeField = ignite::Time((124523 * seed) % (24 * 60 * 60 * 1000));
    val.timestampField = ignite::Timestamp(128341594123 * seed);

    val.i8ArrayField.push_back(9 * seed);
    val.i8ArrayField.push_back(6 * seed);
    val.i8ArrayField.push_back(3 * seed);
    val.i8ArrayField.push_back(42 * seed);

    return val;
}


BOOST_FIXTURE_TEST_SUITE(SqlFieldsQueryTestSuite, SqlFieldsQueryTestSuiteFixture)

BOOST_AUTO_TEST_CASE(SelectEmpty)
{
    SqlFieldsQuery qry("select * from TestType");

    QueryFieldsCursor cursor = cacheAllFields.Query(qry);

    CheckCursorEmpty(cursor);

    const std::vector<std::string>& columns = cursor.GetColumnNames();

    BOOST_CHECK_EQUAL(columns.size(), 13);

    BOOST_CHECK_EQUAL(columns.at(0), "I8FIELD");
    BOOST_CHECK_EQUAL(columns.at(1), "I16FIELD");
    BOOST_CHECK_EQUAL(columns.at(2), "I32FIELD");
    BOOST_CHECK_EQUAL(columns.at(3), "I64FIELD");
    BOOST_CHECK_EQUAL(columns.at(4), "STRFIELD");
    BOOST_CHECK_EQUAL(columns.at(5), "FLOATFIELD");
    BOOST_CHECK_EQUAL(columns.at(6), "DOUBLEFIELD");
    BOOST_CHECK_EQUAL(columns.at(7), "BOOLFIELD");
    BOOST_CHECK_EQUAL(columns.at(8), "GUIDFIELD");
    BOOST_CHECK_EQUAL(columns.at(9), "DATEFIELD");
    BOOST_CHECK_EQUAL(columns.at(10), "TIMEFIELD");
    BOOST_CHECK_EQUAL(columns.at(11), "TIMESTAMPFIELD");
    BOOST_CHECK_EQUAL(columns.at(12), "I8ARRAYFIELD");
}

BOOST_AUTO_TEST_CASE(SelectSingleValue)
{
    ignite::TestType val = MakeCustomTestValue(1);

    cacheAllFields.Put(42, val);

    SqlFieldsQuery qry("select i8Field, i16Field, i32Field, i64Field, strField, floatField, "
        "doubleField, boolField, guidField, dateField, timeField, timestampField, i8ArrayField FROM TestType");

    QueryFieldsCursor cursor = cacheAllFields.Query(qry);

    BOOST_CHECK(cursor.HasNext());

    QueryFieldsRow row = cursor.GetNext();

    BOOST_CHECK(row.HasNext());

    CheckRowEqualsValue(row, val);

    CheckRowCursorEmpty(row);
    CheckCursorEmpty(cursor);
}

BOOST_AUTO_TEST_CASE(SelectTwoValues)
{
    ignite::TestType val1 = MakeCustomTestValue(1);
    ignite::TestType val2 = MakeCustomTestValue(2);

    cacheAllFields.Put(1, val1);
    cacheAllFields.Put(2, val2);

    SqlFieldsQuery qry("select i8Field, i16Field, i32Field, i64Field, strField, floatField, "
        "doubleField, boolField, guidField, dateField, timeField, timestampField, i8ArrayField FROM TestType "
        "ORDER BY _key");

    QueryFieldsCursor cursor = cacheAllFields.Query(qry);

    BOOST_CHECK(cursor.HasNext());
    QueryFieldsRow row = cursor.GetNext();
    BOOST_CHECK(row.HasNext());

    CheckRowEqualsValue(row, val1);
    CheckRowCursorEmpty(row);

    BOOST_CHECK(cursor.HasNext());
    row = cursor.GetNext();
    BOOST_CHECK(row.HasNext());

    CheckRowEqualsValue(row, val2);

    CheckRowCursorEmpty(row);
    CheckCursorEmpty(cursor);
}

BOOST_AUTO_TEST_CASE(Select10000Values)
{
    const int32_t num = 10000;

    std::map<int64_t, ignite::TestType> values;

    for (int32_t i = 0; i < num; ++i)
        values[i] = MakeCustomTestValue(i);

    BOOST_CHECK_EQUAL(values.size(), static_cast<size_t>(num));

    cacheAllFields.PutAll(values);

    SqlFieldsQuery qry("select i8Field, i16Field, i32Field, i64Field, strField, floatField, "
        "doubleField, boolField, guidField, dateField, timeField, timestampField, i8ArrayField FROM TestType "
        "ORDER BY _key");

    QueryFieldsCursor cursor = cacheAllFields.Query(qry);

    for (int64_t i = 0; i < num; ++i)
    {
        BOOST_CHECK(cursor.HasNext());
        QueryFieldsRow row = cursor.GetNext();
        BOOST_CHECK(row.HasNext());

        CheckRowEqualsValue(row, values[i]);
        CheckRowCursorEmpty(row);
    }

    CheckCursorEmpty(cursor);
}

BOOST_AUTO_TEST_CASE(Select10ValuesPageSize1)
{
    const int32_t num = 10;

    std::map<int64_t, ignite::TestType> values;

    for (int32_t i = 0; i < num; ++i)
        values[i] = MakeCustomTestValue(i);

    BOOST_CHECK_EQUAL(values.size(), static_cast<size_t>(num));

    cacheAllFields.PutAll(values);

    SqlFieldsQuery qry("select i8Field, i16Field, i32Field, i64Field, strField, floatField, "
                       "doubleField, boolField, guidField, dateField, timeField, timestampField, i8ArrayField FROM TestType "
                       "ORDER BY _key");

    qry.SetPageSize(1);

    QueryFieldsCursor cursor = cacheAllFields.Query(qry);

    for (int64_t i = 0; i < num; ++i)
    {
        BOOST_CHECK(cursor.HasNext());
        QueryFieldsRow row = cursor.GetNext();
        BOOST_CHECK(row.HasNext());

        CheckRowEqualsValue(row, values[i]);
        CheckRowCursorEmpty(row);
    }

    CheckCursorEmpty(cursor);
}


BOOST_AUTO_TEST_CASE(SelectKeyValue)
{
    const int64_t key = 123;
    const ignite::TestType val = MakeCustomTestValue(1);

    cacheAllFields.Put(key, val);

    SqlFieldsQuery qry("select _key, _val FROM TestType");

    QueryFieldsCursor cursor = cacheAllFields.Query(qry);

    BOOST_CHECK(cursor.HasNext());

    QueryFieldsRow row = cursor.GetNext();

    BOOST_CHECK(row.HasNext());

    const int64_t keyActual = row.GetNext<int64_t>();
    const ignite::TestType valActual = row.GetNext<ignite::TestType>();

    BOOST_CHECK_EQUAL(key, keyActual);
    BOOST_CHECK(val == valActual);

    CheckRowCursorEmpty(row);
    CheckCursorEmpty(cursor);
}

BOOST_AUTO_TEST_CASE(SelectTwoValuesInDifferentOrder)
{
    typedef ignite::common::concurrent::SharedPointer<void> SP_Void;

    ignite::TestType val1 = MakeCustomTestValue(1);
    ignite::TestType val2 = MakeCustomTestValue(2);

    cacheAllFields.Put(1, val1);
    cacheAllFields.Put(2, val2);

    SqlFieldsQuery qry("select i8Field, i16Field, i32Field, i64Field, strField, floatField, "
        "doubleField, boolField, guidField, dateField, timeField, timestampField, i8ArrayField FROM TestType "
        "ORDER BY _key");

    // Checking if everything going to be OK if we destroy cursor before fetching rows.
    QueryFieldsRow row1 = QueryFieldsRow(SP_Void());
    QueryFieldsRow row2 = QueryFieldsRow(SP_Void());

    {
        QueryFieldsCursor cursor = cacheAllFields.Query(qry);

        BOOST_CHECK(cursor.HasNext());
        row1 = cursor.GetNext();

        BOOST_CHECK(cursor.HasNext());
        row2 = cursor.GetNext();

        CheckCursorEmpty(cursor);
    }

    BOOST_CHECK(row2.HasNext());
    CheckRowEqualsValue(row2, val2);
    CheckRowCursorEmpty(row2);

    BOOST_CHECK(row1.HasNext());
    CheckRowEqualsValue(row1, val1);
    CheckRowCursorEmpty(row1);
}

BOOST_AUTO_TEST_CASE(CreateTableInsertSelect)
{
    SqlFieldsQuery qry("create table TestTable(id int primary key, val int)");
    qry.SetSchema("PUBLIC");

    QueryFieldsCursor cursor = cacheAllFields.Query(qry);

    QueryFieldsRow row = cursor.GetNext();
    BOOST_CHECK(row.HasNext());
    BOOST_CHECK_EQUAL(row.GetNext<int64_t>(), 0);
    CheckRowCursorEmpty(row);

    qry.SetSql("insert into TestTable(id, val) values(1, 2)");

    cursor = cacheAllFields.Query(qry);

    row = cursor.GetNext();
    BOOST_CHECK(row.HasNext());
    BOOST_CHECK_EQUAL(row.GetNext<int64_t>(), 1);
    CheckRowCursorEmpty(row);

    qry.SetSql("select id, val from TestTable");

    cursor = cacheAllFields.Query(qry);

    BOOST_CHECK(cursor.HasNext());

    row = cursor.GetNext();

    BOOST_CHECK(row.HasNext());
    BOOST_CHECK_EQUAL(row.GetNext<int32_t>(), 1);
    BOOST_CHECK_EQUAL(row.GetNext<int32_t>(), 2);
    CheckRowCursorEmpty(row);

    CheckCursorEmpty(cursor);
}


/**
 * Test that null value can be inserted using SQL query.
 */
BOOST_AUTO_TEST_CASE(TestInsertNull)
{
    const int64_t testKey(111);
    SqlFieldsQuery insertPersonQry("INSERT INTO TestType(_key, strField, i32Field) VALUES(?, ?, ?)");

    insertPersonQry.AddArgument<int64_t>(testKey);
    insertPersonQry.AddArgument<std::string*>(0);
    insertPersonQry.AddArgument<int32_t*>(0);

    cacheAllFields.Query(insertPersonQry);

    SqlFieldsQuery select("Select _key from TestType WHERE strField is NULL AND i32Field is NULL");

    QueryFieldsCursor cursor = cacheAllFields.Query(select);

    BOOST_CHECK(cursor.HasNext());

    QueryFieldsRow row = cursor.GetNext();

    BOOST_CHECK(row.HasNext());
    BOOST_CHECK_EQUAL(row.GetNext<int64_t>(), testKey);
    CheckRowCursorEmpty(row);

    CheckCursorEmpty(cursor);
}


/**
 * Test that SQL errors contain SQLSTATE with cause.
 */
BOOST_AUTO_TEST_CASE(TestSqlStateOnErrors)
{
    CheckSqlStateForQuery("select * from \"UnknownCache\".UNKNOWN_TABLE", "42000");
    CheckSqlStateForQuery("select * from UNKNOWN_TABLE", "42000");
    CheckSqlStateForQuery("insert into \"cacheAllFields\".TestType(_key) values(null)", "22004");
    CheckSqlStateForQuery("insert into \"cacheAllFields\".TestType(_key) values('abc')", "0700B");
    CheckSqlStateForQuery("insert into \"cacheAllFields\".TestType(_key, _val) values(1, null)", "0A000");

    SqlFieldsQuery qry("CREATE TABLE PUBLIC.varchar_table(id INT PRIMARY KEY, str VARCHAR(5))");
    cacheAllFields.Query(qry);

    CheckSqlStateForQuery("insert into varchar_table(id, str) values(1, 'too_long')", "23000");
}

BOOST_AUTO_TEST_SUITE_END()
