// Copyright (c) 2011-2016 The Cryptonote developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include "gtest/gtest.h"

#include "CryptoNoteCore/CryptoNoteBasicImpl.h"
#include "CryptoNoteCore/Currency.h"
#include <Logging/LoggerGroup.h>

using namespace CryptoNote;

namespace
{
  const uint64_t TEST_GRANTED_FULL_REWARD_ZONE = 10000;
  const uint64_t TEST_MONEY_SUPPLY = static_cast<uint64_t>(-1);
  const uint64_t TEST_EMISSION_SPEED_FACTOR = 18;

  //--------------------------------------------------------------------------------------------------------------------
  class getBlockReward_and_already_generated_coins : public ::testing::Test {
  public:
    getBlockReward_and_already_generated_coins() :
      ::testing::Test(),
      m_currency(CryptoNote::CurrencyBuilder(m_logger).
        blockGrantedFullRewardZone(TEST_GRANTED_FULL_REWARD_ZONE).
        moneySupply(TEST_MONEY_SUPPLY).
        emissionSpeedFactor(TEST_EMISSION_SPEED_FACTOR).
        currency()) {
    }

  protected:
    static const size_t currentBlockSize = TEST_GRANTED_FULL_REWARD_ZONE / 2;

    Logging::LoggerGroup m_logger;
    CryptoNote::Currency m_currency;
    bool m_blockTooBig;
    int64_t m_emissionChange;
    uint64_t m_blockReward;
  };

  #define TEST_ALREADY_GENERATED_COINS(alreadyGeneratedCoins, expectedReward)                                  \
    m_blockTooBig = !m_currency.getBlockReward(0, currentBlockSize, alreadyGeneratedCoins, 0,                  \
      m_blockReward, m_emissionChange);                                                                        \
    ASSERT_FALSE(m_blockTooBig);                                                                               \
    ASSERT_EQ(UINT64_C(expectedReward), m_blockReward);                                                        \
    ASSERT_EQ(UINT64_C(expectedReward), m_emissionChange);

  TEST_F(getBlockReward_and_already_generated_coins, handles_first_values) {
    TEST_ALREADY_GENERATED_COINS(0, 70368744177663);
    TEST_ALREADY_GENERATED_COINS(m_blockReward, 70368475742208);
    TEST_ALREADY_GENERATED_COINS(UINT64_C(2756434948434199641), 59853779316998);
  }

  TEST_F(getBlockReward_and_already_generated_coins, correctly_steps_from_reward_2_to_1) {
    TEST_ALREADY_GENERATED_COINS(m_currency.moneySupply() - ((UINT64_C(2) << m_currency.emissionSpeedFactor()) + 1), 2);
    TEST_ALREADY_GENERATED_COINS(m_currency.moneySupply() -  (UINT64_C(2) << m_currency.emissionSpeedFactor())     , 2);
    TEST_ALREADY_GENERATED_COINS(m_currency.moneySupply() - ((UINT64_C(2) << m_currency.emissionSpeedFactor()) - 1), 1);
  }

  TEST_F(getBlockReward_and_already_generated_coins, handles_max_already_generaged_coins) {
    TEST_ALREADY_GENERATED_COINS(m_currency.moneySupply() - ((UINT64_C(1) << m_currency.emissionSpeedFactor()) + 1), 1);
    TEST_ALREADY_GENERATED_COINS(m_currency.moneySupply() -  (UINT64_C(1) << m_currency.emissionSpeedFactor())     , 1);
    TEST_ALREADY_GENERATED_COINS(m_currency.moneySupply() - ((UINT64_C(1) << m_currency.emissionSpeedFactor()) - 1), 0);
    TEST_ALREADY_GENERATED_COINS(m_currency.moneySupply() - 1, 0);
    TEST_ALREADY_GENERATED_COINS(m_currency.moneySupply(), 0);
  }

  //--------------------------------------------------------------------------------------------------------------------
  class getBlockReward_and_median_and_blockSize : public ::testing::Test {
  public:
    getBlockReward_and_median_and_blockSize() :
      ::testing::Test(),
      m_currency(CryptoNote::CurrencyBuilder(m_logger).
        blockGrantedFullRewardZone(TEST_GRANTED_FULL_REWARD_ZONE).
        moneySupply(TEST_MONEY_SUPPLY).
        emissionSpeedFactor(TEST_EMISSION_SPEED_FACTOR).
        currency()) {
    }

  protected:
    static const uint64_t alreadyGeneratedCoins = 0;

    virtual void SetUp() override {
      m_blockTooBig = !m_currency.getBlockReward(0, 0, alreadyGeneratedCoins, 0,
        m_standardBlockReward, m_emissionChange);
      ASSERT_FALSE(m_blockTooBig);
      ASSERT_EQ(UINT64_C(70368744177663), m_standardBlockReward);
    }

    void do_test(size_t medianBlockSize, size_t currentBlockSize) {
      m_blockTooBig = !m_currency.getBlockReward(medianBlockSize, currentBlockSize, alreadyGeneratedCoins, 0,
        m_blockReward, m_emissionChange);
    }

    Logging::LoggerGroup m_logger;
    CryptoNote::Currency m_currency;
    bool m_blockTooBig;
    int64_t m_emissionChange;
    uint64_t m_blockReward;
    uint64_t m_standardBlockReward;
  };

  TEST_F(getBlockReward_and_median_and_blockSize, handles_zero_median) {
    do_test(0, TEST_GRANTED_FULL_REWARD_ZONE);
    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(m_standardBlockReward, m_blockReward);
  }

  TEST_F(getBlockReward_and_median_and_blockSize, handles_median_lt_relevance_level) {
    do_test(TEST_GRANTED_FULL_REWARD_ZONE - 1, TEST_GRANTED_FULL_REWARD_ZONE);
    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(m_standardBlockReward, m_blockReward);
  }

  TEST_F(getBlockReward_and_median_and_blockSize, handles_median_eq_relevance_level) {
    do_test(TEST_GRANTED_FULL_REWARD_ZONE, TEST_GRANTED_FULL_REWARD_ZONE - 1);
    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(m_standardBlockReward, m_blockReward);
  }

  TEST_F(getBlockReward_and_median_and_blockSize, handles_median_gt_relevance_level) {
    do_test(TEST_GRANTED_FULL_REWARD_ZONE + 1, TEST_GRANTED_FULL_REWARD_ZONE);
    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(m_standardBlockReward, m_blockReward);
  }

  TEST_F(getBlockReward_and_median_and_blockSize, handles_big_median) {
    size_t blockSize = 1;
    size_t medianSize = std::numeric_limits<uint32_t>::max();

    do_test(medianSize, blockSize);

    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(m_standardBlockReward, m_blockReward);
  }

  TEST_F(getBlockReward_and_median_and_blockSize, handles_big_block_size) {
    size_t blockSize = std::numeric_limits<uint32_t>::max() - 1; // even
    size_t medianSize = blockSize / 2; // 2 * medianSize == blockSize

    do_test(medianSize, blockSize);
    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(0, m_blockReward);
  }

  TEST_F(getBlockReward_and_median_and_blockSize, handles_big_block_size_fail) {
    size_t blockSize = std::numeric_limits<uint32_t>::max();
    size_t medianSize = blockSize / 2 - 1;

    do_test(medianSize, blockSize);
    ASSERT_TRUE(m_blockTooBig);
  }

  TEST_F(getBlockReward_and_median_and_blockSize, handles_big_median_and_block_size) {
    // blockSize should be greater medianSize
    size_t blockSize = std::numeric_limits<uint32_t>::max();
    size_t medianSize = std::numeric_limits<uint32_t>::max() - 1;

    do_test(medianSize, blockSize);
    ASSERT_FALSE(m_blockTooBig);
    ASSERT_LT(m_blockReward, m_standardBlockReward);
  }

  //--------------------------------------------------------------------------------------------------------------------
  class getBlockReward_and_currentBlockSize : public ::testing::Test {
  public:
    getBlockReward_and_currentBlockSize() :
      ::testing::Test(),
      m_currency(CryptoNote::CurrencyBuilder(m_logger).
        blockGrantedFullRewardZone(TEST_GRANTED_FULL_REWARD_ZONE).
        moneySupply(TEST_MONEY_SUPPLY).
        emissionSpeedFactor(TEST_EMISSION_SPEED_FACTOR).
        currency()) {
    }

  protected:
    static const size_t testMedian = 7 * TEST_GRANTED_FULL_REWARD_ZONE;
    static const uint64_t alreadyGeneratedCoins = 0;

    virtual void SetUp() override {
      m_blockTooBig = !m_currency.getBlockReward(testMedian, 0, alreadyGeneratedCoins, 0,
        m_standardBlockReward, m_emissionChange);

      ASSERT_FALSE(m_blockTooBig);
      ASSERT_EQ(UINT64_C(70368744177663), m_standardBlockReward);
    }

    void do_test(size_t currentBlockSize) {
      m_blockTooBig = !m_currency.getBlockReward(testMedian, currentBlockSize, alreadyGeneratedCoins, 0,
        m_blockReward, m_emissionChange);
    }

    Logging::LoggerGroup m_logger;
    CryptoNote::Currency m_currency;
    bool m_blockTooBig;
    int64_t m_emissionChange;
    uint64_t m_blockReward;
    uint64_t m_standardBlockReward;
  };

  TEST_F(getBlockReward_and_currentBlockSize, handles_zero_block_size) {
    do_test(0);
    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(m_standardBlockReward, m_blockReward);
  }

  TEST_F(getBlockReward_and_currentBlockSize, handles_block_size_less_median) {
    do_test(testMedian - 1);
    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(m_standardBlockReward, m_blockReward);
  }

  TEST_F(getBlockReward_and_currentBlockSize, handles_block_size_eq_median) {
    do_test(testMedian);
    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(m_standardBlockReward, m_blockReward);
  }

  TEST_F(getBlockReward_and_currentBlockSize, handles_block_size_gt_median) {
    do_test(testMedian + 1);
    ASSERT_FALSE(m_blockTooBig);
    ASSERT_LT(m_blockReward, m_standardBlockReward);
  }

  TEST_F(getBlockReward_and_currentBlockSize, handles_block_size_less_2_medians) {
    do_test(2 * testMedian - 1);
    ASSERT_FALSE(m_blockTooBig);
    ASSERT_LT(m_blockReward, m_standardBlockReward);
    ASSERT_GT(m_blockReward, 0);
  }

  TEST_F(getBlockReward_and_currentBlockSize, handles_block_size_eq_2_medians) {
    do_test(2 * testMedian);
    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(0, m_blockReward);
  }

  TEST_F(getBlockReward_and_currentBlockSize, handles_block_size_gt_2_medians) {
    do_test(2 * testMedian + 1);
    ASSERT_TRUE(m_blockTooBig);
  }

  TEST_F(getBlockReward_and_currentBlockSize, calculates_correctly) {
    ASSERT_EQ(0, testMedian % 8);

    // reward = 1 - (k - 1)^2
    // k = 9/8 => reward = 63/64
    do_test(testMedian * 9 / 8);
    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(m_standardBlockReward * 63 / 64, m_blockReward);

    // 3/2 = 12/8
    do_test(testMedian * 3 / 2);
    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(m_standardBlockReward * 3 / 4, m_blockReward);

    do_test(testMedian * 15 / 8);
    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(m_standardBlockReward * 15 / 64, m_blockReward);
  }
  //--------------------------------------------------------------------------------------------------------------------
  const unsigned int testEmissionSpeedFactor = 4;
  const size_t testGrantedFullRewardZone = 10000;
  const size_t testMedian = testGrantedFullRewardZone;
  const size_t testBlockSize = testMedian + testMedian * 8 / 10; // expected penalty 0.64 * reward
  const uint64_t testPenalty = 64; // percentage
  const uint64_t testMoneySupply = UINT64_C(1000000000);
  const uint64_t expectedBaseReward = 62500000;  // testMoneySupply >> testEmissionSpeedFactor
  const uint64_t expectedBlockReward = 22500000; // expectedBaseReward - expectedBaseReward * testPenalty / 100
  //--------------------------------------------------------------------------------------------------------------------
  class getBlockReward_fee_and_penalizeFee_test : public ::testing::Test {
  public:
    getBlockReward_fee_and_penalizeFee_test() :
      ::testing::Test(),
      m_currency(CryptoNote::CurrencyBuilder(m_logger).
        blockGrantedFullRewardZone(testGrantedFullRewardZone).
        moneySupply(testMoneySupply).
        emissionSpeedFactor(testEmissionSpeedFactor).
        currency()) {
    }

  protected:
    virtual void SetUp() override {
      uint64_t blockReward;
      int64_t emissionChange;

      m_blockTooBig = !m_currency.getBlockReward(testMedian, testBlockSize, 0, 0, blockReward, emissionChange);

      ASSERT_FALSE(m_blockTooBig);
      ASSERT_EQ(expectedBlockReward, blockReward);
      ASSERT_EQ(expectedBlockReward, emissionChange);
    }

    void do_test(uint64_t alreadyGeneratedCoins, uint64_t fee) {
      m_blockTooBig = !m_currency.getBlockReward(testMedian, testBlockSize, alreadyGeneratedCoins, fee,
        m_blockReward, m_emissionChange);
    }

    Logging::LoggerGroup m_logger;
    CryptoNote::Currency m_currency;
    bool m_blockTooBig;
    int64_t m_emissionChange;
    uint64_t m_blockReward;
  };

  TEST_F(getBlockReward_fee_and_penalizeFee_test, handles_zero_fee_and_penalize_fee) {
    do_test(0, 0);

    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(expectedBlockReward, m_blockReward);
    ASSERT_EQ(expectedBlockReward, m_emissionChange);
    ASSERT_GT(m_emissionChange, 0);
  }

  TEST_F(getBlockReward_fee_and_penalizeFee_test, handles_fee_lt_block_reward_and_penalize_fee) {
    uint64_t fee = expectedBlockReward / 2;
    do_test(0, fee);

    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(expectedBlockReward + fee - fee * testPenalty / 100, m_blockReward);
    ASSERT_EQ(expectedBlockReward - fee * testPenalty / 100, m_emissionChange);
    ASSERT_GT(m_emissionChange, 0);
  }

  TEST_F(getBlockReward_fee_and_penalizeFee_test, handles_fee_eq_block_reward_and_penalize_fee) {
    uint64_t fee = expectedBlockReward;
    do_test(0, fee);

    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(expectedBlockReward + fee - fee * testPenalty / 100, m_blockReward);
    ASSERT_EQ(expectedBlockReward - fee * testPenalty / 100, m_emissionChange);
    ASSERT_GT(m_emissionChange, 0);
  }

  TEST_F(getBlockReward_fee_and_penalizeFee_test, handles_fee_gt_block_reward_and_penalize_fee) {
    uint64_t fee = 2 * expectedBlockReward;
    do_test(0, fee);

    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(expectedBlockReward + fee - fee * testPenalty / 100, m_blockReward);
    ASSERT_EQ(expectedBlockReward - fee * testPenalty / 100, m_emissionChange);
    ASSERT_LT(m_emissionChange, 0);
  }

  TEST_F(getBlockReward_fee_and_penalizeFee_test, handles_emission_change_eq_zero) {
    uint64_t fee = expectedBlockReward * 100 / testPenalty;
    do_test(0, fee);

    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(expectedBlockReward + fee - fee * testPenalty / 100, m_blockReward);
    ASSERT_EQ(0, m_emissionChange);
  }

  TEST_F(getBlockReward_fee_and_penalizeFee_test, handles_fee_if_block_reward_is_zero_and_penalize_fee) {
    uint64_t fee = UINT64_C(100);
    do_test(m_currency.moneySupply(), fee);

    ASSERT_FALSE(m_blockTooBig);
    ASSERT_EQ(fee - fee * testPenalty / 100, m_blockReward);
    ASSERT_EQ(-static_cast<int64_t>(fee * testPenalty / 100), m_emissionChange);
  }
}
