#include <gtest/gtest.h>

#include "base.h"
#include "storage.h"
#include "cursor.h"
#include "session.h"

class StorageCursorTest : public ::testing::Test {
protected:
    StorageCursorTest():
        ::testing::Test(),
        table (db->getStorage<uint64_t, std::string>("table1")),
        emptyTable (db->getStorage<uint64_t, std::string>("empty")) {}

    ~StorageCursorTest() {}

    static void SetUpTestSuite() {
        if (db == nullptr) {
            db = new LMDBAL::Base("testBase");
            db->addStorage<uint64_t, std::string>("table1");
            db->addStorage<uint64_t, std::string>("empty");
            session = db->open();
        }
    }

    int getTableCursorsSize() const {
        return table->cursors.size();
    }

    static void TearDownTestSuite() {
        cursor.drop();
        transaction.terminate();
        session.close();
        db->removeDirectory();
        delete db;
        db = nullptr;
    }

    static LMDBAL::Base* db;
    static LMDBAL::Cursor<uint64_t, std::string> cursor;
    static LMDBAL::Transaction transaction;
    static LMDBAL::Session session;

    LMDBAL::Storage<uint64_t, std::string>* table;
    LMDBAL::Storage<uint64_t, std::string>* emptyTable;
};

LMDBAL::Base* StorageCursorTest::db = nullptr;
LMDBAL::Cursor<uint64_t, std::string> StorageCursorTest::cursor;
LMDBAL::Transaction StorageCursorTest::transaction = LMDBAL::Transaction();
LMDBAL::Session StorageCursorTest::session = LMDBAL::Session();

static const std::map<uint64_t, std::string> data({
    {245665783, "bothering nerds"},
    {3458, "resilent pick forefront"},
    {105190, "apportunity legal bat"},
    {6510, "outside"},
    {7438537, "damocles plush apparently rusty"},
    {19373572, "local guidence"},
    {138842, "forgetting tusks prepare"},
    {981874, "butchered soaking pawn"},
    {19302, "tanned inmate"},
    {178239, "custody speaks neurotic"},
});

TEST_F(StorageCursorTest, PopulatingTheTable) {
    uint32_t amount = table->addRecords(data);
    EXPECT_EQ(amount, data.size());
}

TEST_F(StorageCursorTest, Creation) {
    EXPECT_EQ(getTableCursorsSize(), 0);
    cursor = table->createCursor();
    EXPECT_EQ(getTableCursorsSize(), 1);

    EXPECT_THROW(cursor.first(), LMDBAL::CursorNotReady);
    EXPECT_THROW(cursor.last(), LMDBAL::CursorNotReady);
    EXPECT_THROW(cursor.next(), LMDBAL::CursorNotReady);
    EXPECT_THROW(cursor.prev(), LMDBAL::CursorNotReady);
    EXPECT_THROW(cursor.current(), LMDBAL::CursorNotReady);

    cursor.open();
}

TEST_F(StorageCursorTest, FirstPrivate) {
    EXPECT_EQ(getTableCursorsSize(), 1);
    std::pair<uint64_t, std::string> element = cursor.first();
    std::map<uint64_t, std::string>::const_iterator reference = data.begin();

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);
}

TEST_F(StorageCursorTest, NextPrivate) {
    EXPECT_EQ(getTableCursorsSize(), 1);
    std::map<uint64_t, std::string>::const_iterator reference = data.begin();

    reference++;
    for (; reference != data.end(); ++reference) {
        std::pair<uint64_t, std::string> element = cursor.next();
        EXPECT_EQ(element.first, reference->first);
        EXPECT_EQ(element.second, reference->second);
    }

    EXPECT_THROW(cursor.next(), LMDBAL::NotFound);

    std::pair<uint64_t, std::string> element = cursor.first();
    reference = data.begin();

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);
}

TEST_F(StorageCursorTest, LastPrivate) {
    EXPECT_EQ(getTableCursorsSize(), 1);
    std::pair<uint64_t, std::string> element = cursor.last();
    std::map<uint64_t, std::string>::const_reverse_iterator reference = data.rbegin();

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);
}

TEST_F(StorageCursorTest, PrevPrivate) {
    EXPECT_EQ(getTableCursorsSize(), 1);
    std::map<uint64_t, std::string>::const_reverse_iterator reference = data.rbegin();

    reference++;
    for (; reference != data.rend(); ++reference) {
        std::pair<uint64_t, std::string> element = cursor.prev();
        EXPECT_EQ(element.first, reference->first);
        EXPECT_EQ(element.second, reference->second);
    }

    EXPECT_THROW(cursor.prev(), LMDBAL::NotFound);

    std::pair<uint64_t, std::string> element = cursor.last();
    reference = data.rbegin();

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);
}

TEST_F(StorageCursorTest, CurrentPrivate) {
    EXPECT_EQ(getTableCursorsSize(), 1);
    std::pair<uint64_t, std::string> element = cursor.first();
    std::map<uint64_t, std::string>::const_iterator reference = data.begin();

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);

    element = cursor.current();

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);

    cursor.next();
    element = cursor.current();
    ++reference;

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);

    cursor.next();
    cursor.next();
    cursor.prev();
    element = cursor.current();
    ++reference;
    ++reference;
    --reference;

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);
}

TEST_F(StorageCursorTest, SettingPrivate) {
    EXPECT_EQ(getTableCursorsSize(), 1);

    EXPECT_FALSE(cursor.set(6684));
    EXPECT_EQ(cursor.current().second, "tanned inmate");

    std::map<uint64_t, std::string>::const_iterator reference = data.begin();
    std::advance(reference, 5);
    EXPECT_TRUE(cursor.set(reference->first));
    EXPECT_EQ(cursor.current().second, reference->second);

    ++reference;
    cursor.next();
    EXPECT_EQ(cursor.current().second, reference->second);

    ++reference;
    cursor.next();
    EXPECT_EQ(cursor.current().second, reference->second);

    --reference;
    cursor.prev();
    EXPECT_EQ(cursor.current().second, reference->second);

    --reference;
    cursor.prev();
    EXPECT_EQ(cursor.current().second, reference->second);

    --reference;
    cursor.prev();
    EXPECT_EQ(cursor.current().second, reference->second);
}

TEST_F(StorageCursorTest, Destruction) {
    EXPECT_EQ(getTableCursorsSize(), 1);
    cursor.close();
    EXPECT_EQ(getTableCursorsSize(), 1);

    EXPECT_THROW(cursor.first(), LMDBAL::CursorNotReady);
    EXPECT_THROW(cursor.last(), LMDBAL::CursorNotReady);
    EXPECT_THROW(cursor.next(), LMDBAL::CursorNotReady);
    EXPECT_THROW(cursor.prev(), LMDBAL::CursorNotReady);
    EXPECT_THROW(cursor.current(), LMDBAL::CursorNotReady);

    cursor = LMDBAL::Cursor<uint64_t, std::string>();
    EXPECT_EQ(getTableCursorsSize(), 0);

    cursor = table->createCursor();
    EXPECT_EQ(getTableCursorsSize(), 1);
}

TEST_F(StorageCursorTest, FirstPublic) {
    EXPECT_EQ(getTableCursorsSize(), 1);
    transaction = db->beginReadOnlyTransaction();

    cursor.open(transaction);
    std::pair<uint64_t, std::string> element = cursor.first();
    std::map<uint64_t, std::string>::const_iterator reference = data.begin();

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);
}

TEST_F(StorageCursorTest, NextPublic) {
    EXPECT_EQ(getTableCursorsSize(), 1);
    std::map<uint64_t, std::string>::const_iterator reference = data.begin();

    reference++;
    for (; reference != data.end(); ++reference) {
        std::pair<uint64_t, std::string> element = cursor.next();
        EXPECT_EQ(element.first, reference->first);
        EXPECT_EQ(element.second, reference->second);
    }

    EXPECT_THROW(cursor.next(), LMDBAL::NotFound);

    std::pair<uint64_t, std::string> element = cursor.first();
    reference = data.begin();

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);
}

TEST_F(StorageCursorTest, LastPublic) {
    EXPECT_EQ(getTableCursorsSize(), 1);
    std::pair<uint64_t, std::string> element = cursor.last();
    std::map<uint64_t, std::string>::const_reverse_iterator reference = data.rbegin();

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);
}

TEST_F(StorageCursorTest, PrevPublic) {
    EXPECT_EQ(getTableCursorsSize(), 1);
    std::map<uint64_t, std::string>::const_reverse_iterator reference = data.rbegin();

    reference++;
    for (; reference != data.rend(); ++reference) {
        std::pair<uint64_t, std::string> element = cursor.prev();
        EXPECT_EQ(element.first, reference->first);
        EXPECT_EQ(element.second, reference->second);
    }

    EXPECT_THROW(cursor.prev(), LMDBAL::NotFound);

    std::pair<uint64_t, std::string> element = cursor.last();
    reference = data.rbegin();

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);
}

TEST_F(StorageCursorTest, CurrentPublic) {
    EXPECT_EQ(getTableCursorsSize(), 1);
    std::pair<uint64_t, std::string> element = cursor.first();
    std::map<uint64_t, std::string>::const_iterator reference = data.begin();

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);

    element = cursor.current();

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);

    cursor.next();
    element = cursor.current();
    ++reference;

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);

    cursor.next();
    cursor.next();
    cursor.prev();
    element = cursor.current();
    ++reference;
    ++reference;
    --reference;

    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);
}

TEST_F(StorageCursorTest, SettingPublic) {
    EXPECT_EQ(getTableCursorsSize(), 1);

    EXPECT_FALSE(cursor.set(557));
    EXPECT_EQ(cursor.current().second, "resilent pick forefront");

    std::map<uint64_t, std::string>::const_iterator reference = data.begin();
    std::advance(reference, 3);
    EXPECT_TRUE(cursor.set(reference->first));
    EXPECT_EQ(cursor.current().second, reference->second);

    ++reference;
    cursor.next();
    EXPECT_EQ(cursor.current().second, reference->second);

    ++reference;
    cursor.next();
    EXPECT_EQ(cursor.current().second, reference->second);

    --reference;
    cursor.prev();
    EXPECT_EQ(cursor.current().second, reference->second);

    --reference;
    cursor.prev();
    EXPECT_EQ(cursor.current().second, reference->second);

    --reference;
    cursor.prev();
    EXPECT_EQ(cursor.current().second, reference->second);
}

TEST_F(StorageCursorTest, CursorRAIIBehaviour) {
    int initialiCursorsAmount = getTableCursorsSize();
    {
        LMDBAL::Cursor<uint64_t, std::string> cur = table->createCursor();
        EXPECT_EQ(initialiCursorsAmount + 1, getTableCursorsSize());
        cur.open(transaction);
        EXPECT_NO_THROW(cur.first());
    }

    EXPECT_EQ(initialiCursorsAmount, getTableCursorsSize());
    LMDBAL::Cursor<uint64_t, std::string> cur;
    EXPECT_EQ(initialiCursorsAmount, getTableCursorsSize());
    EXPECT_EQ(cur.empty(), true);
    cur = table->createCursor();
    EXPECT_EQ(cur.empty(), false);
    EXPECT_EQ(initialiCursorsAmount + 1, getTableCursorsSize());
    EXPECT_THROW(emptyTable->destroyCursor(cur), LMDBAL::Unknown);
    table->destroyCursor(cur);
    EXPECT_EQ(cur.empty(), true);
    EXPECT_EQ(initialiCursorsAmount, getTableCursorsSize());

    cur = table->createCursor();
    EXPECT_EQ(initialiCursorsAmount + 1, getTableCursorsSize());
    EXPECT_EQ(cur.empty(), false);

    cur.drop();
    EXPECT_EQ(cur.empty(), true);
    EXPECT_EQ(initialiCursorsAmount, getTableCursorsSize());

    EXPECT_NO_THROW(cur.drop());
    EXPECT_THROW(table->destroyCursor(cur), LMDBAL::Unknown);
}

TEST_F(StorageCursorTest, CornerCases) {
    EXPECT_EQ(getTableCursorsSize(), 1);
    transaction.terminate();
    EXPECT_THROW(cursor.current(), LMDBAL::CursorNotReady);
    cursor.close();

    LMDBAL::Cursor<uint64_t, std::string> emptyCursor = emptyTable->createCursor();
    emptyCursor.open();
    EXPECT_THROW(emptyCursor.first(), LMDBAL::NotFound);
    EXPECT_THROW(emptyCursor.last(), LMDBAL::NotFound);
    EXPECT_THROW(emptyCursor.next(), LMDBAL::NotFound);
    EXPECT_THROW(emptyCursor.prev(), LMDBAL::NotFound);
    EXPECT_THROW(emptyCursor.current(), LMDBAL::Unknown);
    emptyCursor.close();

    cursor.open();
    EXPECT_THROW(cursor.current(), LMDBAL::Unknown);       //yeah, nice thing to write in the doc

    std::map<uint64_t, std::string>::const_reverse_iterator breference = data.rbegin();
    std::pair<uint64_t, std::string> element(cursor.prev());
    EXPECT_EQ(element.first, breference->first);            //nice thing to write in the doc, again!
    EXPECT_EQ(element.second, breference->second);
    element = cursor.current();
    EXPECT_EQ(element.first, breference->first);
    EXPECT_EQ(element.second, breference->second);
    EXPECT_THROW(cursor.next(), LMDBAL::NotFound);
    cursor.close();

    cursor.open();
    element = cursor.next();
    std::map<uint64_t, std::string>::const_iterator reference = data.begin();
    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);
    element = cursor.current();
    EXPECT_EQ(element.first, reference->first);
    EXPECT_EQ(element.second, reference->second);
    EXPECT_THROW(cursor.prev(), LMDBAL::NotFound);

    cursor.close();
}

TEST_F(StorageCursorTest, TerminatedTransaction) {
    LMDBAL::Cursor<uint64_t, std::string> cr = table->createCursor();

    {
        LMDBAL::Transaction txn = db->beginReadOnlyTransaction();
        cr.open(txn);
        EXPECT_NO_THROW(cr.first());
    }

    EXPECT_FALSE(cr.opened());

    LMDBAL::Transaction txn2;
    {
        LMDBAL::Transaction txn = db->beginReadOnlyTransaction();
        EXPECT_TRUE(txn.isActive());
        EXPECT_FALSE(txn2.isActive());

        cr.open(txn);
        EXPECT_TRUE(cr.opened());

        txn2 = std::move(txn);
        EXPECT_FALSE(txn.isActive());
        EXPECT_TRUE(txn2.isActive());
        EXPECT_TRUE(cr.opened());
    }

    EXPECT_TRUE(txn2.isActive());
    EXPECT_TRUE(cr.opened());

    txn2.terminate();
    EXPECT_FALSE(txn2.isActive());
    EXPECT_FALSE(cr.opened());
}