#include <gtest/gtest.h>

#include <iostream>

#include "base.h"
#include "cache.h"

class CacheTransactionsTest : public testing::Test {
protected:
    CacheTransactionsTest():
        testing::Test(),
        c1(db->getCache<int16_t, int64_t>("cache1")),
        c2(db->getCache<std::string, float>("cache2")) {}

    ~CacheTransactionsTest() {}

    int  waitForChildFork(int pid) {
        int status;
        if (0 > waitpid(pid, &status, 0)) {
            std::cerr << "[----------]  Waitpid error!" << std::endl;
            return (-1);
        }
        if (WIFEXITED(status)) {
            const int exit_status = WEXITSTATUS(status);
            if (exit_status != 0) {
                std::cerr << "[----------]  Non-zero exit status " << exit_status << " from test!" << std::endl;
            }
            return exit_status;
        } else {
            std::cerr << "[----------]  Non-normal exit from child!" << std::endl;
            return (-2);
        }
    }

    static void SetUpTestSuite() {
        if (db == nullptr) {
            db = new LMDBAL::Base("storageTrnansactionsTestBase");
            db->addStorage<int16_t, int64_t>("cache1");
            db->addStorage<std::string, float>("cache2");
        }

        db->open();
        db->drop();
    }

    static void TearDownTestSuite() {
        db->close();
        db->removeDirectory();
        delete db;
        db = nullptr;
    }

    static LMDBAL::Base* db;

    LMDBAL::Cache<int16_t, int64_t>* c1;
    LMDBAL::Cache<std::string, float>* c2;
};


LMDBAL::Base* CacheTransactionsTest::db = nullptr;

TEST_F(CacheTransactionsTest, Adding) {
    EXPECT_EQ(db->ready(), true);
    EXPECT_EQ(c1->count(), 0);
    EXPECT_EQ(c2->count(), 0);

    LMDBAL::WriteTransaction txn = db->beginTransaction();
    c1->addRecord(5, 13, txn);
    c1->addRecord(-53, 782, txn);
    c1->addRecord(5892, -37829, txn);

    c2->addRecord("lorem", 481, txn);
    c2->addRecord("decallence", 8532.48, txn);
    c2->addRecord("prevent recovery", -64.64, txn);

    EXPECT_EQ(c1->count(), 0);
    EXPECT_EQ(c2->count(), 0);

    txn.commit();

    EXPECT_EQ(c1->count(), 3);
    EXPECT_EQ(c1->getRecord(5), 13);
    EXPECT_EQ(c1->getRecord(-53), 782);
    EXPECT_EQ(c1->getRecord(5892), -37829);

    EXPECT_EQ(c2->count(), 3);
    EXPECT_FLOAT_EQ(c2->getRecord("lorem"), 481);
    EXPECT_FLOAT_EQ(c2->getRecord("decallence"), 8532.48);
    EXPECT_FLOAT_EQ(c2->getRecord("prevent recovery"), -64.64);
}

TEST_F(CacheTransactionsTest, Aborting) {
    EXPECT_EQ(db->ready(), true);

    LMDBAL::SizeType s1 = c1->count();
    LMDBAL::SizeType s2 = c2->count();

    LMDBAL::WriteTransaction txn = db->beginTransaction();
    c1->addRecord(18, 40, txn);
    c1->addRecord(85, -4, txn);
    c1->addRecord(-5, -3, txn);

    c2->addRecord("tapestry", .053, txn);
    c2->addRecord("pepper plants are beautifull", -7, txn);
    c2->addRecord("horrots", -23.976, txn);

    EXPECT_EQ(c1->count(), s1);
    EXPECT_EQ(c2->count(), s2);

    txn.abort();

    EXPECT_EQ(c1->count(), s1);
    EXPECT_EQ(c2->count(), s2);
}

TEST_F(CacheTransactionsTest, Reading) {
    EXPECT_EQ(db->ready(), true);

    LMDBAL::Transaction txn = db->beginReadOnlyTransaction();

    EXPECT_EQ(c1->count(txn), 3);
    EXPECT_EQ(c1->getRecord(5, txn), 13);
    EXPECT_EQ(c1->getRecord(-53, txn), 782);
    EXPECT_EQ(c1->getRecord(5892, txn), -37829);

    EXPECT_EQ(c2->count(txn), 3);
    EXPECT_FLOAT_EQ(c2->getRecord("lorem", txn), 481);
    EXPECT_FLOAT_EQ(c2->getRecord("decallence", txn), 8532.48);
    EXPECT_FLOAT_EQ(c2->getRecord("prevent recovery", txn), -64.64);

    txn.terminate();
}

TEST_F(CacheTransactionsTest, ConcurentReading) {
    EXPECT_EQ(db->ready(), true);

    LMDBAL::SizeType size = c1->count();
    LMDBAL::WriteTransaction txn = db->beginTransaction();
    EXPECT_EQ(c1->getRecord(5, txn), 13);
    EXPECT_EQ(c1->getRecord(5), 13);

    c1->removeRecord(5, txn);

    EXPECT_FALSE(c1->checkRecord(5, txn));
    EXPECT_EQ(c1->getRecord(5), 13);

    c1->addRecord(5, 571, txn);
    EXPECT_EQ(c1->getRecord(5, txn), 571);
    EXPECT_EQ(c1->getRecord(5), 13);

    c1->forceRecord(5, -472, txn);
    EXPECT_EQ(c1->getRecord(5, txn), -472);
    EXPECT_EQ(c1->getRecord(5), 13);

    c1->replaceAll({
        {1, 75}
    }, txn);
    EXPECT_FALSE(c1->checkRecord(5, txn));
    EXPECT_EQ(c1->getRecord(5), 13);
    EXPECT_EQ(c1->count(txn), 1);
    EXPECT_EQ(c1->count(), size);

    txn.commit();

    EXPECT_FALSE(c1->checkRecord(5));
    EXPECT_EQ(c1->count(), 1);
}


TEST_F(CacheTransactionsTest, ConcurentModification) {
    EXPECT_EQ(db->ready(), true);

    //if you start one writable transaction after another
    //in a single thread like so:
    //
    //LMDBAL::TransactionID txn1 = db->beginTransaction();
    //LMDBAL::TransactionID txn2 = db->beginTransaction();
    //
    //the execution should block on the second transaction
    //so this test should preform in a sequence
    //first the parent, then the child

    int pid = fork();
    if (pid == 0) {  // I am the child
        usleep(5);
        std::cout << "beggining second transaction" << std::endl;
        LMDBAL::WriteTransaction txn2 = db->beginTransaction();      //<--- this is where the execution should pause
                                                                    //and wait for the first transaction to get finished
        std::cout << "checking result of the first transaction value" << std::endl;
        EXPECT_EQ(c1->getRecord(5, txn2), 812);

        std::cout << "forcing second transaction value" << std::endl;
        c1->forceRecord(5, -46, txn2);

        std::cout << "checking second transaction value" << std::endl;
        EXPECT_EQ(c1->getRecord(5, txn2), -46);

        std::cout << "checking value independently" << std::endl;
        EXPECT_EQ(c1->getRecord(5), 812);

        std::cout << "commiting second transaction" << std::endl;
        txn2.commit();

        std::cout << "quitting child thread" << std::endl;
        exit(testing::Test::HasFailure());
    } else {  // I am the parent
        std::cout << "beggining first transaction" << std::endl;
        LMDBAL::WriteTransaction txn1 = db->beginTransaction();

        std::cout << "putting parent thread to sleep for 5 ms" << std::endl;
        usleep(10);

        std::cout << "adding first transaction value" << std::endl;
        c1->addRecord(5, 812, txn1);

        std::cout << "checking first transaction value" << std::endl;
        EXPECT_EQ(c1->getRecord(5, txn1), 812);

        std::cout << "checking value independently" << std::endl;
        EXPECT_FALSE(c1->checkRecord(5));

        std::cout << "commiting first transaction" << std::endl;
        txn1.commit();

        std::cout << "waiting for the other thread to finish" << std::endl;
        ASSERT_EQ(0, waitForChildFork(pid));    //child process should have no problems
    }

    std::cout << "checking final result" << std::endl;
    EXPECT_EQ(c1->getRecord(5), -46);
}

TEST_F(CacheTransactionsTest, RAIIResourceFree) {
    EXPECT_EQ(db->ready(), true);

    int pid = fork();
    if (pid == 0) {  // I am the child
        usleep(5);
        std::cout << "beggining child transaction" << std::endl;
        LMDBAL::WriteTransaction txn2 = db->beginTransaction();     //<--- this is where the execution should pause
                                                                    //and wait for the first transaction to get finished
        std::cout << "checking result of the parent transaction value" << std::endl;
        EXPECT_FALSE(c1->checkRecord(221, txn2));

        std::cout << "performing modification from the child thread" << std::endl;
        c1->addRecord(221, 14, txn2);

        std::cout << "commiting child transaction" << std::endl;
        txn2.commit();

        std::cout << "quitting child thread, letting child transaction be destroyed after commit" << std::endl;
        exit(testing::Test::HasFailure());
    } else {  // I am the parent
        std::cout << "beggining parent transaction" << std::endl;
        {
            LMDBAL::WriteTransaction txn1 = db->beginTransaction();

            std::cout << "putting parent thread to sleep for 5 ms" << std::endl;
            usleep(10);
            std::cout << "parent thread woke up" << std::endl;

            std::cout << "adding value from parent thread" << std::endl;
            c1->addRecord(221, 320, txn1);

            std::cout << "checking value from parent thread using transaction" << std::endl;
            EXPECT_EQ(c1->getRecord(221, txn1), 320);

            std::cout << "checking value independently from parent thread" << std::endl;
            EXPECT_FALSE(c1->checkRecord(221));

            std::cout << "implicitly aborting transaction by leaving the scope" << std::endl;
        }

        std::cout << "child thread should resume after this line" << std::endl;
        ASSERT_EQ(0, waitForChildFork(pid));    //child process should have no problems
    }

    std::cout << "checking the final result" << std::endl;
    EXPECT_EQ(c1->getRecord(221), 14);
}

TEST_F(CacheTransactionsTest, TransactionTerminationOnClose) {
    LMDBAL::WriteTransaction txn = db->beginTransaction();

    c1->addRecord(578, 4552, txn);

    EXPECT_EQ(c1->getRecord(578, txn), 4552);
    EXPECT_EQ(c1->checkRecord(578), false);

    db->close();
    db->open();

    EXPECT_EQ(txn.isActive(), false);
    EXPECT_THROW(c1->getRecord(578, txn), LMDBAL::TransactionTerminated);
    EXPECT_NO_THROW(txn.commit());

    EXPECT_EQ(c1->checkRecord(578), false);

    txn = db->beginTransaction();
    c1->addRecord(578, 4552, txn);
    txn.commit();

    EXPECT_EQ(c1->getRecord(578), 4552);
}