/**
* @file       Tests.cpp
*
* @brief      Test routines for Zerocoin.
*
* @author     Ian Miers, Christina Garman and Matthew Green
* @date       June 2013
*
* @copyright  Copyright 2013 Ian Miers, Christina Garman and Matthew Green
* @license    This project is released under the MIT license.
**/
// Copyright (c) 2017-2018 The PIVX developers

#include <boost/test/unit_test.hpp>
#include <string>
#include <iostream>
#include <fstream>
#include <cmath>
// #include <curses.h>
#include <exception>
#include "streams.h"
#include "libzerocoin/ParamGeneration.h"
#include "libzerocoin/Denominations.h"
#include "libzerocoin/Coin.h"
#include "libzerocoin/CoinSpend.h"
#include "libzerocoin/Accumulator.h"
#include "test_agrarian.h"

using namespace std;
using namespace libzerocoin;

#define COLOR_STR_GREEN   "\033[32m"
#define COLOR_STR_NORMAL  "\033[0m"
#define COLOR_STR_RED     "\033[31m"

#define TESTS_COINS_TO_ACCUMULATE   10
#define NON_PRIME_TESTS                100

// Global test counters
uint32_t    gNumTests        = 0;
uint32_t    gSuccessfulTests = 0;

// Proof size
uint32_t    gProofSize            = 0;
uint32_t    gCoinSize            = 0;
uint32_t    gSerialNumberSize    = 0;

// Global coin array
PrivateCoin    *gCoins[TESTS_COINS_TO_ACCUMULATE];

// Global params
ZerocoinParams *g_Params;

//////////
// Utility routines
//////////

void
LogTestResult(string testName, bool (*testPtr)())
{
    string colorGreen(COLOR_STR_GREEN);
    string colorNormal(COLOR_STR_NORMAL);
    string colorRed(COLOR_STR_RED);

    cout << "Testing if " << testName << "..." << endl;

    bool testResult = testPtr();

    if (testResult == true) {
        cout << "\t" << colorGreen << "[PASS]"  << colorNormal << endl;
        gSuccessfulTests++;
    } else {
        cout << colorRed << "\t[FAIL]" << colorNormal << endl;
    }

    gNumTests++;
}

CBigNum
GetTestModulus()
{
    static CBigNum testModulus(0);

    // TODO: should use a hard-coded RSA modulus for testing
    if (!testModulus) {
        CBigNum p, q;

        // Note: we are NOT using safe primes for testing because
        // they take too long to generate. Don't do this in real
        // usage. See the paramgen utility for better code.
        p = CBigNum::generatePrime(1024, false);
        q = CBigNum::generatePrime(1024, false);
        testModulus = p * q;
    }

    return testModulus;
}

//////////
// Test routines
//////////

bool
Test_GenRSAModulus()
{
    CBigNum result = GetTestModulus();

    if (!result) {
        return false;
    }
    else {
        return true;
    }
}

bool
Test_CalcParamSizes()
{
    bool result = true;
#if 0

    uint32_t pLen, qLen;

    try {
        calculateGroupParamLengths(4000, 80, &pLen, &qLen);
        if (pLen < 1024 || qLen < 256) {
            result = false;
        }
        calculateGroupParamLengths(4000, 96, &pLen, &qLen);
        if (pLen < 2048 || qLen < 256) {
            result = false;
        }
        calculateGroupParamLengths(4000, 112, &pLen, &qLen);
        if (pLen < 3072 || qLen < 320) {
            result = false;
        }
        calculateGroupParamLengths(4000, 120, &pLen, &qLen);
        if (pLen < 3072 || qLen < 320) {
            result = false;
        }
        calculateGroupParamLengths(4000, 128, &pLen, &qLen);
        if (pLen < 3072 || qLen < 320) {
            result = false;
        }
    } catch (exception &e) {
        result = false;
    }
#endif

    return result;
}

bool
Test_GenerateGroupParams()
{
    uint32_t pLen = 1024, qLen = 256, count;
    IntegerGroupParams group;

    for (count = 0; count < 1; count++) {

        try {
            group = deriveIntegerGroupParams(calculateSeed(GetTestModulus(), "test", ZEROCOIN_DEFAULT_SECURITYLEVEL, "TEST GROUP"), pLen, qLen);
        } catch (std::runtime_error e) {
            cout << "Caught exception " << e.what() << endl;
            return false;
        }

        // Now perform some simple tests on the resulting parameters
        if ((uint32_t)group.groupOrder.bitSize() < qLen || (uint32_t)group.modulus.bitSize() < pLen) {
            return false;
        }

        CBigNum c = group.g.pow_mod(group.groupOrder, group.modulus);
        //cout << "g^q mod p = " << c << endl;
        if (!(c.isOne())) return false;

        // Try at multiple parameter sizes
        pLen = pLen * 1.5;
        qLen = qLen * 1.5;
    }

    return true;
}

bool
Test_ParamGen()
{
    bool result = true;

    try {
        // Instantiating testParams runs the parameter generation code
        ZerocoinParams testParams(GetTestModulus(),ZEROCOIN_DEFAULT_SECURITYLEVEL);
    } catch (runtime_error e) {
        cout << e.what() << endl;
        result = false;
    }

    return result;
}

bool
Test_Accumulator()
{
    // This test assumes a list of coins were generated during
    // the Test_MintCoin() test.
    if (gCoins[0] == NULL) {
        return false;
    }
    try {
        // Accumulate the coin list from first to last into one accumulator
            Accumulator accOne(&g_Params->accumulatorParams, CoinDenomination::ZQ_ONE);
            Accumulator accTwo(&g_Params->accumulatorParams,CoinDenomination::ZQ_ONE);
            Accumulator accThree(&g_Params->accumulatorParams,CoinDenomination::ZQ_ONE);
            Accumulator accFour(&g_Params->accumulatorParams,CoinDenomination::ZQ_ONE);
        AccumulatorWitness wThree(g_Params, accThree, gCoins[0]->getPublicCoin());

        for (uint32_t i = 0; i < TESTS_COINS_TO_ACCUMULATE; i++) {
            accOne += gCoins[i]->getPublicCoin();
            accTwo += gCoins[TESTS_COINS_TO_ACCUMULATE - (i+1)]->getPublicCoin();
            accThree += gCoins[i]->getPublicCoin();
            wThree += gCoins[i]->getPublicCoin();
            if(i != 0) {
                accFour += gCoins[i]->getPublicCoin();
            }
        }

        // Compare the accumulated results
        if (accOne.getValue() != accTwo.getValue() || accOne.getValue() != accThree.getValue()) {
            cout << "Accumulators don't match" << endl;
            return false;
        }

        if(accFour.getValue() != wThree.getValue()) {
            cout << "Witness math not working," << endl;
            return false;
        }

        // Verify that the witness is correct
        if (!wThree.VerifyWitness(accThree, gCoins[0]->getPublicCoin()) ) {
            cout << "Witness not valid" << endl;
            return false;
        }

        // Serialization test: see if we can serialize the accumulator
        CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
        ss << accOne;

        // Deserialize it into a new object
        Accumulator newAcc(g_Params, ss);

        // Compare the results
        if (accOne.getValue() != newAcc.getValue()) {
            return false;
        }

    } catch (runtime_error e) {
        return false;
    }

    return true;
}

bool
Test_EqualityPoK()
{
    // Run this test 10 times
    for (uint32_t i = 0; i < 10; i++) {
        try {
            // Generate a random integer "val"
            CBigNum val = CBigNum::randBignum(g_Params->coinCommitmentGroup.groupOrder);

            // Manufacture two commitments to "val", both
            // under different sets of parameters
            Commitment one(&g_Params->accumulatorParams.accumulatorPoKCommitmentGroup, val);

            Commitment two(&g_Params->serialNumberSoKCommitmentGroup, val);

            // Now generate a proof of knowledge that "one" and "two" are
            // both commitments to the same value
            CommitmentProofOfKnowledge pok(&g_Params->accumulatorParams.accumulatorPoKCommitmentGroup,
                                           &g_Params->serialNumberSoKCommitmentGroup,
                                           one, two);

            // Serialize the proof into a stream
            CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
            ss << pok;

            // Deserialize back into a PoK object
            CommitmentProofOfKnowledge newPok(&g_Params->accumulatorParams.accumulatorPoKCommitmentGroup,
                                              &g_Params->serialNumberSoKCommitmentGroup,
                                              ss);

            if (newPok.Verify(one.getCommitmentValue(), two.getCommitmentValue()) != true) {
                return false;
            }

            // Just for fun, deserialize the proof a second time
            CDataStream ss2(SER_NETWORK, PROTOCOL_VERSION);
            ss2 << pok;

            // This time tamper with it, then deserialize it back into a PoK
            ss2[15] = 0;
            CommitmentProofOfKnowledge newPok2(&g_Params->accumulatorParams.accumulatorPoKCommitmentGroup,
                                               &g_Params->serialNumberSoKCommitmentGroup,
                                               ss2);

            // If the tampered proof verifies, that's a failure!
            if (newPok2.Verify(one.getCommitmentValue(), two.getCommitmentValue()) == true) {
                return false;
            }

        } catch (runtime_error &e) {
            return false;
        }
    }

    return true;
}

bool
Test_MintCoin()
{
    gCoinSize = 0;

    try {
        // Generate a list of coins
        for (uint32_t i = 0; i < TESTS_COINS_TO_ACCUMULATE; i++) {
            gCoins[i] = new PrivateCoin(g_Params,libzerocoin::CoinDenomination::ZQ_ONE);

            PublicCoin pc = gCoins[i]->getPublicCoin();
            CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
            ss << pc;
            gCoinSize += ss.size();
        }

        gCoinSize /= TESTS_COINS_TO_ACCUMULATE;

    } catch (exception &e) {
        return false;
    }

    return true;
}

bool Test_InvalidCoin()
{
    CBigNum coinValue;

    try {
        // Pick a random non-prime CBigNum
        for (uint32_t i = 0; i < NON_PRIME_TESTS; i++) {
            coinValue = CBigNum::randBignum(g_Params->coinCommitmentGroup.modulus);
            coinValue = coinValue * 2;
            if (!coinValue.isPrime()) break;
        }

        PublicCoin pubCoin(g_Params);
        if (pubCoin.validate()) {
            // A blank coin should not be valid!
            return false;
        }

        PublicCoin pubCoin2(g_Params, coinValue, ZQ_ONE);
        if (pubCoin2.validate()) {
            // A non-prime coin should not be valid!
            return false;
        }

        PublicCoin pubCoin3 = pubCoin2;
        if (pubCoin2.validate()) {
            // A copy of a non-prime coin should not be valid!
            return false;
        }

        // Serialize and deserialize the coin
        CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
        ss << pubCoin;
        PublicCoin pubCoin4(g_Params, ss);
        if (pubCoin4.validate()) {
            // A deserialized copy of a non-prime coin should not be valid!
            return false;
        }

    } catch (runtime_error &e) {
        cout << "Caught exception: " << e.what() << endl;
        return false;
    }

    return true;
}

bool
Test_MintAndSpend()
{
    try {
        // This test assumes a list of coins were generated in Test_MintCoin()
        if (gCoins[0] == NULL)
        {
            // No coins: mint some.
            Test_MintCoin();
            if (gCoins[0] == NULL) {
                return false;
            }
        }

        // Accumulate the list of generated coins into a fresh accumulator.
        // The first one gets marked as accumulated for a witness, the
        // others just get accumulated normally.
        Accumulator acc(&g_Params->accumulatorParams,CoinDenomination::ZQ_ONE);
        AccumulatorWitness wAcc(g_Params, acc, gCoins[0]->getPublicCoin());

        for (uint32_t i = 0; i < TESTS_COINS_TO_ACCUMULATE; i++) {
            acc += gCoins[i]->getPublicCoin();
            wAcc +=gCoins[i]->getPublicCoin();
        }

        // Now spend the coin
        //SpendMetaData m(1,1);
        CDataStream cc(SER_NETWORK, PROTOCOL_VERSION);
        cc << *gCoins[0];
        PrivateCoin myCoin(g_Params,cc);

        CoinSpend spend(g_Params, g_Params, myCoin, acc, 0, wAcc, 0, SpendType::SPEND);
        spend.Verify(acc);

        // Serialize the proof and deserialize into newSpend
        CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
        ss << spend;
        gProofSize = ss.size();
        CoinSpend newSpend(g_Params, g_Params, ss);

        // See if we can verify the deserialized proof (return our result)
        bool ret =  newSpend.Verify(acc);

        // Extract the serial number
        CBigNum serialNumber = newSpend.getCoinSerialNumber();
        gSerialNumberSize = ceil((double)serialNumber.bitSize() / 8.0);

        return ret;
    } catch (runtime_error &e) {
        cout << e.what() << endl;
        return false;
    }

    return false;
}

void
Test_RunAllTests()
{
    // Make a new set of parameters from a random RSA modulus
    g_Params = new ZerocoinParams(GetTestModulus());

    gNumTests = gSuccessfulTests = gProofSize = 0;
    for (uint32_t i = 0; i < TESTS_COINS_TO_ACCUMULATE; i++) {
        gCoins[i] = NULL;
    }

    // Run through all of the Zerocoin tests
    LogTestResult("an RSA modulus can be generated", Test_GenRSAModulus);
    LogTestResult("parameter sizes are correct", Test_CalcParamSizes);
    LogTestResult("group/field parameters can be generated", Test_GenerateGroupParams);
    LogTestResult("parameter generation is correct", Test_ParamGen);
    LogTestResult("coins can be minted", Test_MintCoin);
    LogTestResult("invalid coins will be rejected", Test_InvalidCoin);
    LogTestResult("the accumulator works", Test_Accumulator);
    LogTestResult("the commitment equality PoK works", Test_EqualityPoK);
    LogTestResult("a minted coin can be spent", Test_MintAndSpend);

    cout << endl << "Average coin size is " << gCoinSize << " bytes." << endl;
    cout << "Serial number size is " << gSerialNumberSize << " bytes." << endl;
    cout << "Spend proof size is " << gProofSize << " bytes." << endl;

    // Summarize test results
    if (gSuccessfulTests < gNumTests) {
        cout << endl << "ERROR: SOME TESTS FAILED" << endl;
    }

    // Clear any generated coins
    for (uint32_t i = 0; i < TESTS_COINS_TO_ACCUMULATE; i++) {
        delete gCoins[i];
    }

    cout << endl << gSuccessfulTests << " out of " << gNumTests << " tests passed." << endl << endl;
    delete g_Params;
}

BOOST_FIXTURE_TEST_SUITE(libzerocoin, TestingSetup)
BOOST_AUTO_TEST_CASE(libzerocoin_tests)
{
    cout << "libzerocoin v" << ZEROCOIN_VERSION_STRING << " test utility." << endl << endl;

    Test_RunAllTests();
}
BOOST_AUTO_TEST_SUITE_END()
