// 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 "EventWaiter.h"
#include "Logging/ConsoleLogger.h"
#include "Transfers/BlockchainSynchronizer.h"
#include "Transfers/TransfersSynchronizer.h"

#include "INodeStubs.h"
#include "TestBlockchainGenerator.h"
#include "TransactionApiHelpers.h"
#include "CryptoNoteCore/TransactionApi.h"

#include "WalletLegacy/WalletLegacy.h"

#include <boost/scoped_array.hpp>

#include <future>
#include <algorithm>

using namespace CryptoNote;

/*
class TransfersObserver : public ITransfersObserver {
public:

  virtual void onTransactionUpdated(ITransfersSubscription* object, const Hash& transactionHash,
    uint64_t amountIn, uint64_t amountOut) override {
    std::lock_guard<std::mutex> lk(m_mutex);
    m_transfers.push_back(std::make_pair(transactionHash, amountIn - amountOut));
  }

  std::vector<std::pair<Hash, int64_t>> m_transfers;
  std::mutex m_mutex;
}; */


class INodeStubWithPoolTx : public INodeTrivialRefreshStub {
public:
  INodeStubWithPoolTx(TestBlockchainGenerator& generator) : INodeTrivialRefreshStub(generator), detached(false) {}

  void relayTransaction(const CryptoNote::Transaction& transaction, const Callback& callback) override {
    std::unique_lock<std::mutex> lk(mutex);
    relayedTxs.push_back(std::make_pair(this->getLastLocalBlockHeight(), transaction));
    lk.unlock();
    INodeTrivialRefreshStub::relayTransaction(transaction, callback);
  }

  void startAlternativeChain(uint32_t height) override {
    std::unique_lock<std::mutex> lk(mutex);
    INodeTrivialRefreshStub::startAlternativeChain(height);
    detachHeight = height;
    detached = true;
  }


  void getPoolSymmetricDifference(std::vector<Crypto::Hash>&& known_pool_tx_ids, Crypto::Hash known_block_id, bool& is_bc_actual, std::vector<std::unique_ptr<CryptoNote::ITransactionReader>>& new_txs, std::vector<Crypto::Hash>& deleted_tx_ids, const Callback& callback) override
  {
    std::unique_lock<std::mutex> lk(mutex);
    std::sort(relayedTxs.begin(), relayedTxs.end(), [](const std::pair<uint32_t, CryptoNote::Transaction>& val1, const std::pair<uint32_t, CryptoNote::Transaction>& val2)->bool {return val1.first < val2.first; });
    is_bc_actual = true;
    
    if (detached) {
      size_t i = 0;
      for (; i < relayedTxs.size(); ++i) {
        if (relayedTxs[i].first >= detachHeight) {
          break;
        }
      }

      for (; i < relayedTxs.size(); ++i) {
        new_txs.push_back(CryptoNote::createTransactionPrefix(relayedTxs[i].second));
      }
    }

    lk.unlock();
    callback(std::error_code()); 
  };

  
  std::vector<std::pair<uint32_t, CryptoNote::Transaction>> relayedTxs;
  uint32_t detachHeight;
  bool detached;
  std::mutex mutex;
};

class WalletSendObserver : public CryptoNote::IWalletLegacyObserver
{
public:
  WalletSendObserver() {}

  bool waitForSendEnd(std::error_code& ec) {
    if (!sent.wait_for(std::chrono::milliseconds(5000))) return false;
    ec = sendResult;
    return true;
  }

  virtual void sendTransactionCompleted(CryptoNote::TransactionId transactionId, std::error_code result) override {
    sendResult = result;
    sent.notify();
  }

  std::error_code sendResult;
  EventWaiter sent;
};

class DetachTest : public ::testing::Test, public IBlockchainSynchronizerObserver {
public:

  DetachTest() :
    m_currency(CryptoNote::CurrencyBuilder(m_logger).currency()),
    generator(m_currency),
    m_node(generator),
    m_sync(m_node, m_currency.genesisBlockHash()),
    m_transfersSync(m_currency, m_sync, m_node) {
  }

  void addAccounts(size_t count) {
    while (count--) {
      m_accounts.push_back(generateAccountKeys());
    }
  }

  void addMinerAccount() {
    m_accounts.push_back(reinterpret_cast<const AccountKeys&>(generator.getMinerAccount()));
  }

  AccountSubscription createSubscription(size_t acc, uint64_t timestamp = 0) {
    const auto& keys = m_accounts[acc];
    AccountSubscription sub;
    sub.keys = keys;
    sub.syncStart.timestamp = timestamp;
    sub.syncStart.height = 0;
    sub.transactionSpendableAge = 5;
    return sub;
  }

  void subscribeAccounts() {

   // m_transferObservers.reset(new TransfersObserver[m_accounts.size()]);

    for (size_t i = 0; i < m_accounts.size(); ++i) {
      m_subscriptions.push_back(&m_transfersSync.addSubscription(createSubscription(i)));
      //m_subscriptions.back()->addObserver(&m_transferObservers[i]);
    }
  }

  void generateMoneyForAccount(size_t idx) {
    generator.getBlockRewardForAddress(
      reinterpret_cast<const CryptoNote::AccountPublicAddress&>(m_accounts[idx].address));
  }

  std::error_code submitTransaction(ITransactionReader& tx) {
    auto data = tx.getTransactionData();

    CryptoNote::BinaryArray txblob(data.data(), data.data() + data.size());
    CryptoNote::Transaction outTx;
    fromBinaryArray(outTx, data);
    std::promise<std::error_code> result;
    std::future<std::error_code> future = result.get_future();

    m_node.relayTransaction(outTx, [&result](std::error_code ec) {
      std::promise<std::error_code> promise = std::move(result);
      promise.set_value(ec);
    });

    return future.get();
  }

  void synchronizationCompleted(std::error_code result) override {
    decltype(syncCompleted) detachedPromise = std::move(syncCompleted);
    detachedPromise.set_value(result);
  }


protected:
  std::vector<AccountKeys> m_accounts;
  std::vector<ITransfersSubscription*> m_subscriptions;

  Logging::ConsoleLogger m_logger;
  CryptoNote::Currency m_currency;
  TestBlockchainGenerator generator;
  INodeStubWithPoolTx m_node;
  BlockchainSynchronizer m_sync;
  TransfersSyncronizer m_transfersSync;

  std::promise<std::error_code> syncCompleted;
  std::future<std::error_code> syncCompletedFuture;

};


namespace CryptoNote {
inline bool operator == (const TransactionOutputInformation& t1, const TransactionOutputInformation& t2) {
  return
    t1.type == t2.type &&
    t1.amount == t2.amount &&
    t1.outputInTransaction == t2.outputInTransaction &&
    t1.transactionPublicKey == t2.transactionPublicKey;
}
}

namespace {
  std::unique_ptr<ITransaction> createMoneyTransfer(
    uint64_t amount,
    uint64_t fee,
    const AccountKeys& senderKeys,
    const AccountPublicAddress& reciever,
    ITransfersContainer& tc) {

    std::vector<TransactionOutputInformation> transfers;
    tc.getOutputs(transfers, ITransfersContainer::IncludeAllUnlocked);

    auto tx = createTransaction();

    std::vector<std::pair<TransactionTypes::InputKeyInfo, KeyPair>> inputs;

    uint64_t foundMoney = 0;

    for (const auto& t : transfers) {
      TransactionTypes::InputKeyInfo info;

      info.amount = t.amount;

      TransactionTypes::GlobalOutput globalOut;
      globalOut.outputIndex = t.globalOutputIndex;
      globalOut.targetKey = t.outputKey;
      info.outputs.push_back(globalOut);

      info.realOutput.outputInTransaction = t.outputInTransaction;
      info.realOutput.transactionIndex = 0;
      info.realOutput.transactionPublicKey = t.transactionPublicKey;

      KeyPair kp;
      tx->addInput(senderKeys, info, kp);

      inputs.push_back(std::make_pair(info, kp));

      foundMoney += info.amount;

      if (foundMoney >= amount + fee) {
        break;
      }
    }

    // output to reciever
    tx->addOutput(amount, reciever);
    // change
    uint64_t change = foundMoney - amount - fee;
    if (change) {
      tx->addOutput(change, senderKeys.address);
    }

    for (size_t inputIdx = 0; inputIdx < inputs.size(); ++inputIdx) {
      tx->signInputKey(inputIdx, inputs[inputIdx].first, inputs[inputIdx].second);
    }

    return tx;
  }
}


TEST_F(DetachTest, testBlockchainDetach) {
  uint64_t sendAmount = 70000000000000;
  auto fee = m_currency.minimumFee();

  addMinerAccount();
  addAccounts(2);
  subscribeAccounts();

  generator.generateEmptyBlocks(20);
  
  syncCompleted = std::promise<std::error_code>();
  syncCompletedFuture = syncCompleted.get_future();
  m_sync.addObserver(this);
  m_sync.start();
  syncCompletedFuture.get();
  m_sync.removeObserver(this);

  auto& tc = m_subscriptions[0]->getContainer();
  tc.balance();

  ASSERT_LE(sendAmount, tc.balance(ITransfersContainer::IncludeAllUnlocked));

  auto tx = createMoneyTransfer(sendAmount, fee, m_accounts[0], m_accounts[1].address, tc);
  submitTransaction(*tx);

  syncCompleted = std::promise<std::error_code>();
  syncCompletedFuture = syncCompleted.get_future();
  m_sync.addObserver(this);
  m_node.updateObservers();
  syncCompletedFuture.get();
  m_sync.removeObserver(this);
  auto& tc2 = m_subscriptions[1]->getContainer();

  ASSERT_EQ(sendAmount, tc2.balance(ITransfersContainer::IncludeAll));
  ASSERT_EQ(0, tc2.balance(ITransfersContainer::IncludeAllUnlocked));
  ASSERT_EQ(1, tc2.transactionsCount());

  std::vector<Crypto::Hash> unconfirmed;
  tc2.getUnconfirmedTransactions(unconfirmed);  
  ASSERT_EQ(0, unconfirmed.size());

  m_node.startAlternativeChain(m_node.getLastLocalBlockHeight() - 1);
  generator.generateEmptyBlocks(2);

  syncCompleted = std::promise<std::error_code>();
  syncCompletedFuture = syncCompleted.get_future();
  m_sync.addObserver(this);
  m_node.updateObservers();
  syncCompletedFuture.get();
  m_sync.removeObserver(this);
  auto& tc3 = m_subscriptions[1]->getContainer();

  ASSERT_EQ(sendAmount, tc3.balance(ITransfersContainer::IncludeAll));
  ASSERT_EQ(0, tc3.balance(ITransfersContainer::IncludeAllUnlocked));
  ASSERT_EQ(1, tc3.transactionsCount());

  tc3.getUnconfirmedTransactions(unconfirmed);
  ASSERT_EQ(1, unconfirmed.size());
  ASSERT_EQ(reinterpret_cast<const Hash&>(unconfirmed[0]), tx->getTransactionHash());
  m_sync.stop();
}


struct CompletionWalletObserver : public IWalletLegacyObserver {
  virtual void synchronizationCompleted(std::error_code result) override {
    decltype(syncCompleted) detachedPromise = std::move(syncCompleted);
    detachedPromise.set_value(result);
  }

  std::promise<std::error_code> syncCompleted;
  std::future<std::error_code> syncCompletedFuture;
};


struct WaitForExternalTransactionObserver : public CryptoNote::IWalletLegacyObserver {
public:
  WaitForExternalTransactionObserver() {}
  std::promise<CryptoNote::TransactionId> promise;

  virtual void externalTransactionCreated(CryptoNote::TransactionId transactionId) override {
    decltype(promise) detachedPromise = std::move(promise);
    detachedPromise.set_value(transactionId);
  }

};

TEST_F(DetachTest, testDetachWithWallet) {
  auto fee = m_currency.minimumFee();

  generator.generateEmptyBlocks(5);
  WalletLegacy Alice(m_currency, m_node);
  WalletLegacy Bob(m_currency, m_node);

  CompletionWalletObserver AliceCompleted, BobCompleted;
  AliceCompleted.syncCompleted = std::promise<std::error_code>();
  AliceCompleted.syncCompletedFuture = AliceCompleted.syncCompleted.get_future();
  BobCompleted.syncCompleted = std::promise<std::error_code>();
  BobCompleted.syncCompletedFuture = BobCompleted.syncCompleted.get_future();
  Alice.addObserver(&AliceCompleted);
  Bob.addObserver(&BobCompleted);
  Alice.initAndGenerate("pass");
  Bob.initAndGenerate("pass");
  AliceCompleted.syncCompletedFuture.get();
  BobCompleted.syncCompletedFuture.get();
  Alice.removeObserver(&AliceCompleted);
  Bob.removeObserver(&BobCompleted);


  AccountKeys AliceKeys;
  Alice.getAccountKeys(AliceKeys);

  generator.getBlockRewardForAddress(AliceKeys.address);


  generator.generateEmptyBlocks(10);

  AliceCompleted.syncCompleted = std::promise<std::error_code>();
  AliceCompleted.syncCompletedFuture = AliceCompleted.syncCompleted.get_future();
  BobCompleted.syncCompleted = std::promise<std::error_code>();
  BobCompleted.syncCompletedFuture = BobCompleted.syncCompleted.get_future();
  Alice.addObserver(&AliceCompleted);
  Bob.addObserver(&BobCompleted);

  m_node.updateObservers();

  AliceCompleted.syncCompletedFuture.get();
  BobCompleted.syncCompletedFuture.get();
  Alice.removeObserver(&AliceCompleted);
  Bob.removeObserver(&BobCompleted);


  ASSERT_EQ(0, Alice.pendingBalance());
  ASSERT_NE(0, Alice.actualBalance());

  CryptoNote::WalletLegacyTransfer tr;

  tr.amount = Alice.actualBalance() / 2;
  tr.address = Bob.getAddress();

  WalletSendObserver wso;
  Alice.addObserver(&wso);
  Alice.sendTransaction(tr, fee);
  std::error_code sendError;
  wso.waitForSendEnd(sendError);
  Alice.removeObserver(&wso);
  ASSERT_FALSE(sendError);

  WaitForExternalTransactionObserver etxo;
  auto externalTxFuture = etxo.promise.get_future();
  Bob.addObserver(&etxo);
  AliceCompleted.syncCompleted = std::promise<std::error_code>();
  AliceCompleted.syncCompletedFuture = AliceCompleted.syncCompleted.get_future();
  BobCompleted.syncCompleted = std::promise<std::error_code>();
  BobCompleted.syncCompletedFuture = BobCompleted.syncCompleted.get_future();
  Alice.addObserver(&AliceCompleted);
  Bob.addObserver(&BobCompleted);

  auto expectedTransactionBlockHeight = m_node.getLastLocalBlockHeight();
  generator.generateEmptyBlocks(1); //unlock bob's pending money

  m_node.updateObservers();

  AliceCompleted.syncCompletedFuture.get();
  BobCompleted.syncCompletedFuture.get();
  Alice.removeObserver(&AliceCompleted);
  Bob.removeObserver(&BobCompleted);

  auto txId = externalTxFuture.get();
  Bob.removeObserver(&etxo);

  WalletLegacyTransaction txInfo;

  Bob.getTransaction(txId, txInfo);

  ASSERT_EQ(txInfo.blockHeight, expectedTransactionBlockHeight);
  ASSERT_EQ(txInfo.totalAmount, tr.amount);

  ASSERT_EQ(Bob.pendingBalance(), 0);
  ASSERT_EQ(Bob.actualBalance(), tr.amount);

  m_node.startAlternativeChain(txInfo.blockHeight - 1);
  generator.generateEmptyBlocks(2);

  //sync Bob
  AliceCompleted.syncCompleted = std::promise<std::error_code>();
  AliceCompleted.syncCompletedFuture = AliceCompleted.syncCompleted.get_future();
  BobCompleted.syncCompleted = std::promise<std::error_code>();
  BobCompleted.syncCompletedFuture = BobCompleted.syncCompleted.get_future();
  Alice.addObserver(&AliceCompleted);
  Bob.addObserver(&BobCompleted);

  m_node.updateObservers();

  AliceCompleted.syncCompletedFuture.get();
  BobCompleted.syncCompletedFuture.get();
  Alice.removeObserver(&AliceCompleted);
  Bob.removeObserver(&BobCompleted);

  Bob.getTransaction(txId, txInfo);
  ASSERT_EQ(txInfo.blockHeight, WALLET_LEGACY_UNCONFIRMED_TRANSACTION_HEIGHT);
  ASSERT_EQ(txInfo.totalAmount, tr.amount);

  ASSERT_EQ(Bob.pendingBalance(), tr.amount);
  ASSERT_EQ(Bob.actualBalance(), 0);
}
