// 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 <algorithm>
#include <queue>
#include <system_error>

#include <IWallet.h>

#include "CryptoNoteCore/Currency.h"
#include "Logging/LoggerGroup.h"
#include "Logging/ConsoleLogger.h"
#include <System/Event.h>
#include "PaymentGate/WalletService.h"
#include "PaymentGate/WalletServiceErrorCategory.h"
#include "INodeStubs.h"
#include "Wallet/WalletErrors.h"

using namespace CryptoNote;
using namespace PaymentService;

namespace CryptoNote {

bool operator== (const WalletOrder& lhs, const WalletOrder& rhs) {
  return std::make_tuple(lhs.address, lhs.amount) == std::make_tuple(rhs.address, rhs.amount);
}

bool operator== (const DonationSettings& lhs, const DonationSettings& rhs) {
  return std::make_tuple(lhs.address, lhs.threshold) == std::make_tuple(rhs.address, rhs.threshold);
}

} //namespace CryptoNote

struct IWalletBaseStub : public CryptoNote::IWallet {
  IWalletBaseStub(System::Dispatcher& dispatcher) : m_eventOccurred(dispatcher) {}
  virtual ~IWalletBaseStub() {}

  virtual void initialize(const std::string& password) override { }
  virtual void initializeWithViewKey(const Crypto::SecretKey& viewSecretKey, const std::string& password) override { }
  virtual void load(std::istream& source, const std::string& password) override { }
  virtual void shutdown() override { }

  virtual void changePassword(const std::string& oldPassword, const std::string& newPassword) override { }
  virtual void save(std::ostream& destination, bool saveDetails = true, bool saveCache = true) override { }

  virtual size_t getAddressCount() const override { return 0; }
  virtual std::string getAddress(size_t index) const override { return ""; }
  virtual KeyPair getAddressSpendKey(size_t index) const override { return KeyPair(); }
  virtual KeyPair getAddressSpendKey(const std::string& address) const override { return KeyPair(); }
  virtual KeyPair getViewKey() const override { return KeyPair(); }
  virtual std::string createAddress() override { return ""; }
  virtual std::string createAddress(const Crypto::SecretKey& spendSecretKey) override { return ""; }
  virtual std::string createAddress(const Crypto::PublicKey& spendPublicKey) override { return ""; }
  virtual void deleteAddress(const std::string& address) override { }

  virtual uint64_t getActualBalance() const override { return 0; }
  virtual uint64_t getActualBalance(const std::string& address) const override { return 0; }
  virtual uint64_t getPendingBalance() const override { return 0; }
  virtual uint64_t getPendingBalance(const std::string& address) const override { return 0; }

  virtual size_t getTransactionCount() const override { return 0; }
  virtual WalletTransaction getTransaction(size_t transactionIndex) const override { return WalletTransaction(); }
  virtual size_t getTransactionTransferCount(size_t transactionIndex) const override { return 0; }
  virtual WalletTransfer getTransactionTransfer(size_t transactionIndex, size_t transferIndex) const override { return WalletTransfer(); }

  virtual WalletTransactionWithTransfers getTransaction(const Crypto::Hash& transactionHash) const override { return WalletTransactionWithTransfers(); }
  virtual std::vector<TransactionsInBlockInfo> getTransactions(const Crypto::Hash& blockHash, size_t count) const override { return {}; }
  virtual std::vector<TransactionsInBlockInfo> getTransactions(uint32_t blockIndex, size_t count) const override { return {}; }
  virtual std::vector<Crypto::Hash> getBlockHashes(uint32_t blockIndex, size_t count) const override { return {}; }
  virtual uint32_t getBlockCount() const override { return 0; }
  virtual std::vector<WalletTransactionWithTransfers> getUnconfirmedTransactions() const override { return {}; }
  virtual std::vector<size_t> getDelayedTransactionIds() const override { return {}; }

  virtual size_t transfer(const TransactionParameters& sendingTransaction) override { return 0; }

  virtual size_t makeTransaction(const TransactionParameters& sendingTransaction) override { return 0; }
  virtual void commitTransaction(size_t transactionId) override { }
  virtual void rollbackUncommitedTransaction(size_t transactionId) override { }

  virtual void start() override { m_stopped = false; }
  virtual void stop() override { m_stopped = true; m_eventOccurred.set(); }

  //blocks until an event occurred
  virtual WalletEvent getEvent() override {
    throwIfStopped();

    while(m_events.empty()) {
      m_eventOccurred.wait();
      m_eventOccurred.clear();
      throwIfStopped();
    }

    WalletEvent event = std::move(m_events.front());
    m_events.pop();

    return event;
  }

  void pushEvent(const WalletEvent& event) {
    m_events.push(event);
    m_eventOccurred.set();
  }

protected:
  void throwIfStopped() {
    if (m_stopped) {
      throw std::system_error(make_error_code(std::errc::operation_canceled));
    }
  }

  bool m_stopped = false;
  System::Event m_eventOccurred;
  std::queue<WalletEvent> m_events;
};

class WalletServiceTest: public ::testing::Test {
public:
  WalletServiceTest() :
    currency(CryptoNote::CurrencyBuilder(logger).currency()),
    generator(currency),
    nodeStub(generator),
    walletBase(dispatcher)
  {}

  virtual void SetUp() override;
protected:
  Logging::ConsoleLogger logger;
  Currency currency;
  TestBlockchainGenerator generator;
  INodeTrivialRefreshStub nodeStub;
  WalletConfiguration walletConfig;
  System::Dispatcher dispatcher;
  IWalletBaseStub walletBase;

  std::unique_ptr<WalletService> createWalletService(CryptoNote::IWallet& wallet);
  std::unique_ptr<WalletService> createWalletService();
  Crypto::Hash generateRandomHash();
};

void WalletServiceTest::SetUp() {
  logger.setMaxLevel(Logging::DEBUGGING);

  walletConfig.walletFile = "test";
  walletConfig.walletPassword = "test";
}

std::unique_ptr<WalletService> WalletServiceTest::createWalletService(CryptoNote::IWallet& wallet) {
  return std::unique_ptr<WalletService> (new WalletService(currency, dispatcher, nodeStub, wallet, walletConfig, logger));
}

std::unique_ptr<WalletService> WalletServiceTest::createWalletService() {
  return createWalletService(walletBase);
}

Crypto::Hash WalletServiceTest::generateRandomHash() {
  Crypto::Hash hash;
  std::generate(std::begin(hash.data), std::end(hash.data), std::rand);
  return hash;
}

class WalletServiceTest_createAddress : public WalletServiceTest {
};

struct WalletCreateAddressStub: public IWalletBaseStub {
  WalletCreateAddressStub(System::Dispatcher& d) : IWalletBaseStub(d) {}

  virtual std::string createAddress() override { return address; }
  virtual std::string createAddress(const Crypto::SecretKey& spendSecretKey) override { return address; }
  virtual std::string createAddress(const Crypto::PublicKey& spendPublicKey) override { return address; }

  std::string address = "correctAddress";
};

TEST_F(WalletServiceTest_createAddress, returnsCorrectAddress) {
  WalletCreateAddressStub wallet(dispatcher);

  std::unique_ptr<WalletService> service = createWalletService(wallet);
  std::string address;
  std::error_code ec = service->createAddress(address);

  ASSERT_FALSE(ec);
  ASSERT_EQ(wallet.address, address);
}

TEST_F(WalletServiceTest_createAddress, invalidSecretKey) {
  std::unique_ptr<WalletService> service = createWalletService();

  std::string address;
  std::error_code ec = service->createAddress("wrong key", address);
  ASSERT_EQ(make_error_code(CryptoNote::error::WalletServiceErrorCode::WRONG_KEY_FORMAT), ec);
}

TEST_F(WalletServiceTest_createAddress, invalidPublicKey) {
  std::unique_ptr<WalletService> service = createWalletService();

  std::string address;
  std::error_code ec = service->createTrackingAddress("wrong key", address);
  ASSERT_EQ(make_error_code(CryptoNote::error::WalletServiceErrorCode::WRONG_KEY_FORMAT), ec);
}

TEST_F(WalletServiceTest_createAddress, correctSecretKey) {
  Crypto::PublicKey pub;
  Crypto::SecretKey sec;
  Crypto::generate_keys(pub, sec);

  WalletCreateAddressStub wallet(dispatcher);
  std::unique_ptr<WalletService> service = createWalletService(wallet);

  std::string address;
  std::error_code ec = service->createAddress(Common::podToHex(sec), address);

  ASSERT_FALSE(ec);
  ASSERT_EQ(wallet.address, address);
}

TEST_F(WalletServiceTest_createAddress, correctPublicKey) {
  Crypto::PublicKey pub;
  Crypto::SecretKey sec;
  Crypto::generate_keys(pub, sec);

  WalletCreateAddressStub wallet(dispatcher);
  std::unique_ptr<WalletService> service = createWalletService(wallet);

  std::string address;
  std::error_code ec = service->createTrackingAddress(Common::podToHex(pub), address);

  ASSERT_FALSE(ec);
  ASSERT_EQ(wallet.address, address);
}

class WalletServiceTest_getSpendKeys : public WalletServiceTest {
};

struct WalletgetSpendKeysStub: public IWalletBaseStub {
  WalletgetSpendKeysStub(System::Dispatcher& d) : IWalletBaseStub(d) {
    Crypto::generate_keys(keyPair.publicKey, keyPair.secretKey);
  }

  virtual KeyPair getAddressSpendKey(const std::string& address) const override {
    return keyPair;
  }

  KeyPair keyPair;
};

TEST_F(WalletServiceTest_getSpendKeys, returnsKeysCorrectly) {
  WalletgetSpendKeysStub wallet(dispatcher);
  std::unique_ptr<WalletService> service = createWalletService(wallet);

  std::string publicSpendKey;
  std::string secretSpendKey;
  auto ec = service->getSpendkeys("address", publicSpendKey, secretSpendKey);
  ASSERT_FALSE(ec);
  ASSERT_EQ(Common::podToHex(wallet.keyPair.publicKey), publicSpendKey);
  ASSERT_EQ(Common::podToHex(wallet.keyPair.secretKey), secretSpendKey);
}

class WalletServiceTest_getBalance : public WalletServiceTest {
};

struct WalletGetBalanceStub: public IWalletBaseStub {
  WalletGetBalanceStub(System::Dispatcher& d, bool byAddress) : IWalletBaseStub(d), byAddress(byAddress) {
  }

  virtual uint64_t getActualBalance() const override {
    if (byAddress) {
      throw std::runtime_error("wrong overload");
    }

    return actualBalance;
  }

  virtual uint64_t getPendingBalance() const override {
    if (byAddress) {
      throw std::runtime_error("wrong overload");
    }

    return pendingBalance;
  }

  virtual uint64_t getActualBalance(const std::string& address) const override {
    if (!byAddress) {
      throw std::runtime_error("wrong overload");
    }

    return actualBalance;
  }

  virtual uint64_t getPendingBalance(const std::string& address) const override {
    if (!byAddress) {
      throw std::runtime_error("wrong overload");
    }

    return pendingBalance;
  }

  bool byAddress;
  uint64_t actualBalance = 345466;
  uint64_t pendingBalance = 12121;
};

TEST_F(WalletServiceTest_getBalance, returnsCorrectBalance) {
  WalletGetBalanceStub wallet(dispatcher, false);
  std::unique_ptr<WalletService> service = createWalletService(wallet);

  uint64_t actual;
  uint64_t pending;
  auto ec = service->getBalance(actual, pending);

  ASSERT_FALSE(ec);
  ASSERT_EQ(wallet.actualBalance, actual);
  ASSERT_EQ(wallet.pendingBalance, pending);
}

TEST_F(WalletServiceTest_getBalance, returnsCorrectBalanceByAddress) {
  WalletGetBalanceStub wallet(dispatcher, true);
  std::unique_ptr<WalletService> service = createWalletService(wallet);

  uint64_t actual;
  uint64_t pending;
  auto ec = service->getBalance("address", actual, pending);

  ASSERT_FALSE(ec);
  ASSERT_EQ(wallet.actualBalance, actual);
  ASSERT_EQ(wallet.pendingBalance, pending);
}

class WalletServiceTest_getBlockHashes : public WalletServiceTest {
protected:
  std::vector<Crypto::Hash> convertBlockHashes(const std::vector<std::string>& hashes) {
    std::vector<Crypto::Hash> result;
    result.reserve(hashes.size());

    std::for_each(hashes.begin(), hashes.end(), [&result] (const std::string& str) {
      Crypto::Hash hash;
      Common::podFromHex(str, hash);
      result.push_back(hash);
    });

    return result;
  }
};

struct WalletGetBlockHashesStub: public IWalletBaseStub {
  WalletGetBlockHashesStub(System::Dispatcher& d) : IWalletBaseStub(d) {
  }

  virtual std::vector<Crypto::Hash> getBlockHashes(uint32_t blockIndex, size_t count) const override {
    return blockHashes;
  }

  std::vector<Crypto::Hash> blockHashes;
};

TEST_F(WalletServiceTest_getBlockHashes, returnsEmptyBlockHashes) {
  WalletGetBlockHashesStub wallet(dispatcher);
  auto service = createWalletService(wallet);

  std::vector<std::string> blockHashes;
  ASSERT_FALSE(service->getBlockHashes(0, 1, blockHashes));
  ASSERT_EQ(wallet.blockHashes, convertBlockHashes(blockHashes));
}

TEST_F(WalletServiceTest_getBlockHashes, returnsBlockHashes) {
  WalletGetBlockHashesStub wallet(dispatcher);
  std::generate_n(std::back_inserter(wallet.blockHashes), 10, [this] () { return generateRandomHash(); });
  auto service = createWalletService(wallet);

  std::vector<std::string> blockHashes;
  ASSERT_FALSE(service->getBlockHashes(0, 10, blockHashes));
  ASSERT_EQ(wallet.blockHashes, convertBlockHashes(blockHashes));
}

class WalletServiceTest_getViewKey : public WalletServiceTest {
};

struct WalletGetViewKeyStub: public IWalletBaseStub {
  WalletGetViewKeyStub(System::Dispatcher& d) : IWalletBaseStub(d) {
    Crypto::generate_keys(keyPair.publicKey, keyPair.secretKey);
  }

  virtual KeyPair getViewKey() const override {
    return keyPair;
  }

  KeyPair keyPair;
};

TEST_F(WalletServiceTest_getViewKey, returnsCorrectValue) {
  WalletGetViewKeyStub wallet(dispatcher);
  auto service = createWalletService(wallet);

  std::string viewSecretKey;
  ASSERT_FALSE(service->getViewKey(viewSecretKey));
  ASSERT_EQ(Common::podToHex(wallet.keyPair.secretKey), viewSecretKey);
}

class WalletTransactionBuilder {
public:
  WalletTransactionBuilder& hash(const Crypto::Hash& hash) {
    transaction.hash = hash;
    return *this;
  }

  WalletTransactionBuilder& extra(const std::string& extra) {
    transaction.extra = Common::asString(Common::fromHex(extra));
    return *this;
  }

  WalletTransactionBuilder& state(WalletTransactionState state) {
    transaction.state = state;
    return *this;
  }

  WalletTransaction build() const {
    return transaction;
  }

  WalletTransactionBuilder& timestamp(uint64_t t) {
    transaction.timestamp = t;
    return *this;
  }

  WalletTransactionBuilder& blockHeight(uint32_t height) {
    transaction.blockHeight = height;
    return *this;
  }

  WalletTransactionBuilder& totalAmount(int64_t amount) {
    transaction.totalAmount = amount;
    return *this;
  }

  WalletTransactionBuilder& fee(uint64_t fee) {
    transaction.fee = fee;
    return *this;
  }

  WalletTransactionBuilder& creationTime(uint64_t t) {
    transaction.creationTime = t;
    return *this;
  }

  WalletTransactionBuilder& unlockTime(uint64_t unlock) {
    transaction.unlockTime = unlock;
    return *this;
  }

  WalletTransactionBuilder& isBase(bool base) {
    transaction.isBase = base;
    return *this;
  }

private:
  WalletTransaction transaction;
};

class WalletTransactionWithTransfersBuilder {
public:
  WalletTransactionWithTransfersBuilder& transaction(const WalletTransaction& transaction) {
    tx.transaction = transaction;
    return *this;
  }

  WalletTransactionWithTransfersBuilder& addTransfer(const std::string& address, int64_t amount) {
    tx.transfers.push_back(WalletTransfer{WalletTransferType::USUAL, address, amount});
    return *this;
  }

  WalletTransactionWithTransfers build() const {
    return tx;
  }

private:
  WalletTransactionWithTransfers tx;
};

class WalletServiceTest_getTransactions : public WalletServiceTest {
  virtual void SetUp() override;
protected:
  std::vector<TransactionsInBlockInfo> testTransactions;
  const std::string RANDOM_ADDRESS1 = "288DiQfYSxDNQoWpR6cy94i2AWyGnxo1L1MF2ZiXg58h9P52o576CSDcJp7ZceSXSUQ7u8aTF1MigQXzAtqRZ3Uq58Sne8x";
  const std::string RANDOM_ADDRESS2 = "29PQ8VbzPi163kG59w5V8PR9A6watydfYAvwFcDS74KhDEyU9CGgqsDH719oeLbpAa4xtPsgfQ6Bv9RmKs1XZWudV6q6cmU";
  const std::string RANDOM_ADDRESS3 = "23E4CVgzJok9zXnrKzvHgbKvMXZnAgsB9FA1pkAppR6d42dWMEuJjsfcJp7ZceSXSUQ7u8aTF1MigQXzAtqRZ3Uq5AHHbzZ";
  const std::string TRANSACTION_EXTRA = "022100dededededededededededededededededededededededededededededededede";
  const std::string PAYMENT_ID = "dededededededededededededededededededededededededededededededede";
};

void WalletServiceTest_getTransactions::SetUp() {
  TransactionsInBlockInfo block;
  block.blockHash = generateRandomHash();
  block.transactions.push_back(
    WalletTransactionWithTransfersBuilder().addTransfer(RANDOM_ADDRESS1, 222).addTransfer(RANDOM_ADDRESS2, 33333).transaction(
          WalletTransactionBuilder().hash(generateRandomHash()).extra(TRANSACTION_EXTRA).build()
    ).build()
  );

  testTransactions.push_back(block);
}

class WalletGetTransactionsStub : public IWalletBaseStub {
public:
  WalletGetTransactionsStub(System::Dispatcher& d) : IWalletBaseStub(d) {}
  virtual std::vector<TransactionsInBlockInfo> getTransactions(const Crypto::Hash& blockHash, size_t count) const override {
    return transactions;
  }

  virtual std::vector<TransactionsInBlockInfo> getTransactions(uint32_t blockIndex, size_t count) const override {
    return transactions;
  }

  std::vector<TransactionsInBlockInfo> transactions;
};

TEST_F(WalletServiceTest_getTransactions, addressesFilter_emptyReturnsTransaction) {
  WalletGetTransactionsStub wallet(dispatcher);
  wallet.transactions = testTransactions;

  auto service = createWalletService(wallet);

  std::vector<TransactionsInBlockRpcInfo> transactions;
  auto ec = service->getTransactions({}, 0, 1, "", transactions);

  ASSERT_FALSE(ec);

  ASSERT_EQ(1, transactions.size());
  ASSERT_EQ(Common::podToHex(testTransactions[0].transactions[0].transaction.hash), transactions[0].transactions[0].transactionHash);
}

TEST_F(WalletServiceTest_getTransactions, addressesFilter_existentReturnsTransaction) {
  WalletGetTransactionsStub wallet(dispatcher);
  wallet.transactions = testTransactions;

  auto service = createWalletService(wallet);

  std::vector<TransactionsInBlockRpcInfo> transactions;
  auto ec = service->getTransactions({RANDOM_ADDRESS1}, 0, 1, "", transactions);

  ASSERT_FALSE(ec);

  ASSERT_EQ(1, transactions.size());
  ASSERT_EQ(Common::podToHex(testTransactions[0].transactions[0].transaction.hash), transactions[0].transactions[0].transactionHash);
}

TEST_F(WalletServiceTest_getTransactions, addressesFilter_nonExistentReturnsNoTransactions) {
  WalletGetTransactionsStub wallet(dispatcher);
  wallet.transactions = testTransactions;

  auto service = createWalletService(wallet);

  std::vector<TransactionsInBlockRpcInfo> transactions;
  auto ec = service->getTransactions({RANDOM_ADDRESS3}, 0, 1, "", transactions);

  ASSERT_FALSE(ec);

  ASSERT_EQ(1, transactions.size());
  ASSERT_TRUE(transactions[0].transactions.empty());
}

TEST_F(WalletServiceTest_getTransactions, addressesFilter_existentAndNonExistentReturnsTransaction) {
  WalletGetTransactionsStub wallet(dispatcher);
  wallet.transactions = testTransactions;

  auto service = createWalletService(wallet);

  std::vector<TransactionsInBlockRpcInfo> transactions;
  auto ec = service->getTransactions({RANDOM_ADDRESS1, RANDOM_ADDRESS3}, 0, 1, "", transactions);

  ASSERT_FALSE(ec);

  ASSERT_EQ(1, transactions.size());
  ASSERT_EQ(Common::podToHex(testTransactions[0].transactions[0].transaction.hash), transactions[0].transactions[0].transactionHash);
}

TEST_F(WalletServiceTest_getTransactions, paymentIdFilter_existentReturnsTransaction) {
  WalletGetTransactionsStub wallet(dispatcher);
  wallet.transactions = testTransactions;

  auto service = createWalletService(wallet);

  std::vector<TransactionsInBlockRpcInfo> transactions;
  auto ec = service->getTransactions({}, 0, 1, PAYMENT_ID, transactions);

  ASSERT_FALSE(ec);

  ASSERT_EQ(1, transactions.size());
  ASSERT_EQ(Common::podToHex(testTransactions[0].transactions[0].transaction.hash), transactions[0].transactions[0].transactionHash);
  ASSERT_EQ(PAYMENT_ID, transactions[0].transactions[0].paymentId);
}

TEST_F(WalletServiceTest_getTransactions, paymentIdFilter_nonExistentReturnsNoTransaction) {
  WalletGetTransactionsStub wallet(dispatcher);
  wallet.transactions = testTransactions;

  auto service = createWalletService(wallet);

  std::vector<TransactionsInBlockRpcInfo> transactions;
  auto ec = service->getTransactions({}, 0, 1, "dfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdf", transactions);

  ASSERT_FALSE(ec);

  ASSERT_EQ(1, transactions.size());
  ASSERT_TRUE(transactions[0].transactions.empty());
}

TEST_F(WalletServiceTest_getTransactions, invalidAddress) {
  WalletGetTransactionsStub wallet(dispatcher);
  wallet.transactions = testTransactions;

  auto service = createWalletService(wallet);

  std::vector<TransactionsInBlockRpcInfo> transactions;
  auto ec = service->getTransactions({"invalid address"}, 0, 1, "", transactions);
  ASSERT_EQ(make_error_code(CryptoNote::error::BAD_ADDRESS), ec);
}

TEST_F(WalletServiceTest_getTransactions, invalidPaymentId) {
  WalletGetTransactionsStub wallet(dispatcher);
  wallet.transactions = testTransactions;

  auto service = createWalletService(wallet);

  std::vector<TransactionsInBlockRpcInfo> transactions;
  auto ec = service->getTransactions({}, 0, 1, "invalid payment id", transactions);
  ASSERT_EQ(make_error_code(CryptoNote::error::WalletServiceErrorCode::WRONG_PAYMENT_ID_FORMAT), ec);
}

TEST_F(WalletServiceTest_getTransactions, blockNotFound) {
  WalletGetTransactionsStub wallet(dispatcher);
  auto service = createWalletService(wallet);
  std::vector<TransactionsInBlockRpcInfo> transactions;

  auto ec = service->getTransactions({}, 0, 1, "", transactions);
  ASSERT_EQ(make_error_code(CryptoNote::error::WalletServiceErrorCode::OBJECT_NOT_FOUND), ec);
}

class WalletServiceTest_getTransaction : public WalletServiceTest_getTransactions {
};

struct WalletGetTransactionStub : public IWalletBaseStub {
  WalletGetTransactionStub (System::Dispatcher& dispatcher) : IWalletBaseStub(dispatcher) {
  }

  virtual WalletTransactionWithTransfers getTransaction(const Crypto::Hash& transactionHash) const override {
    return transaction;
  }

  WalletTransactionWithTransfers transaction;
};

TEST_F(WalletServiceTest_getTransaction, wrongHash) {
  auto service = createWalletService();

  TransactionRpcInfo transaction;
  auto ec = service->getTransaction("wrong hash", transaction);
  ASSERT_EQ(make_error_code(CryptoNote::error::WalletServiceErrorCode::WRONG_HASH_FORMAT), ec);
}

TEST_F(WalletServiceTest_getTransaction, returnsCorrectFields) {
  WalletGetTransactionStub wallet(dispatcher);
  wallet.transaction = WalletTransactionWithTransfersBuilder().transaction(
        WalletTransactionBuilder()
        .state(WalletTransactionState::FAILED)
        .hash(generateRandomHash())
        .creationTime(789123)
        .extra(TRANSACTION_EXTRA)
        .fee(293945)
        .isBase(false)
        .timestamp(929293847)
        .totalAmount(-200000)
        .unlockTime(23456)
        .build()
    ).addTransfer("address1", 231).addTransfer("address2", 883).build();

  auto service = createWalletService(wallet);

  TransactionRpcInfo transaction;
  auto ec = service->getTransaction(Common::podToHex(Crypto::Hash()), transaction);

  ASSERT_FALSE(ec);
  ASSERT_EQ(static_cast<uint8_t>(wallet.transaction.transaction.state), transaction.state);
  ASSERT_EQ(wallet.transaction.transaction.blockHeight, transaction.blockIndex);
  ASSERT_EQ(Common::toHex(Common::asBinaryArray(wallet.transaction.transaction.extra)), transaction.extra);
  ASSERT_EQ(PAYMENT_ID, transaction.paymentId);
  ASSERT_EQ(wallet.transaction.transaction.fee, transaction.fee);
  ASSERT_EQ(wallet.transaction.transaction.isBase, transaction.isBase);
  ASSERT_EQ(wallet.transaction.transaction.timestamp, transaction.timestamp);
  ASSERT_EQ(Common::podToHex(wallet.transaction.transaction.hash), transaction.transactionHash);
  ASSERT_EQ(wallet.transaction.transaction.unlockTime, transaction.unlockTime);

  ASSERT_EQ(wallet.transaction.transfers.size(), transaction.transfers.size());

  ASSERT_EQ(wallet.transaction.transfers[0].address, transaction.transfers[0].address);
  ASSERT_EQ(wallet.transaction.transfers[0].amount, transaction.transfers[0].amount);

  ASSERT_EQ(wallet.transaction.transfers[1].address, transaction.transfers[1].address);
  ASSERT_EQ(wallet.transaction.transfers[1].amount, transaction.transfers[1].amount);

}

struct WalletGetTransactionThrowStub : public IWalletBaseStub {
  WalletGetTransactionThrowStub (System::Dispatcher& dispatcher) : IWalletBaseStub(dispatcher) {
  }

  virtual WalletTransactionWithTransfers getTransaction(const Crypto::Hash& transactionHash) const override {
    throw std::system_error(make_error_code(error::OBJECT_NOT_FOUND));
  }
};

TEST_F(WalletServiceTest_getTransaction, transactionNotFound) {
  WalletGetTransactionThrowStub wallet(dispatcher);
  auto service = createWalletService(wallet);

  TransactionRpcInfo transaction;
  auto ec = service->getTransaction(Common::podToHex(Crypto::Hash()), transaction);

  ASSERT_EQ(make_error_code(error::OBJECT_NOT_FOUND), ec);
}

class WalletServiceTest_sendTransaction : public WalletServiceTest_getTransactions {
  virtual void SetUp() override;
protected:
  SendTransaction::Request request;
};

void WalletServiceTest_sendTransaction::SetUp() {
  request.sourceAddresses.insert(request.sourceAddresses.end(), {RANDOM_ADDRESS1, RANDOM_ADDRESS2});
  request.transfers.push_back(WalletRpcOrder {RANDOM_ADDRESS3, 11111});
  request.fee = 2021;
  request.anonymity = 4;
  request.unlockTime = 848309;
}

struct WalletTransferStub : public IWalletBaseStub {
  WalletTransferStub(System::Dispatcher& dispatcher, const Crypto::Hash& hash) : IWalletBaseStub(dispatcher), hash(hash) {
  }

  virtual size_t transfer(const TransactionParameters& sendingTransaction) override {
    params = sendingTransaction;
    return 0;
  }

  virtual WalletTransaction getTransaction(size_t transactionIndex) const override {
    return WalletTransactionBuilder().hash(hash).build();
  }

  Crypto::Hash hash;
  TransactionParameters params;
};

bool isEquivalent(const SendTransaction::Request& request, const TransactionParameters& params) {
  std::string extra;
  if (!request.paymentId.empty()) {
    extra = "022100" + request.paymentId;
  } else {
    extra = request.extra;
  }

  std::vector<WalletOrder> orders;
  std::for_each(request.transfers.begin(), request.transfers.end(), [&orders] (const WalletRpcOrder& order) {
    orders.push_back( WalletOrder{order.address, order.amount});
  });

  return std::make_tuple(request.sourceAddresses, orders, request.fee, request.anonymity, extra, request.unlockTime)
      ==
      std::make_tuple(params.sourceAddresses, params.destinations, params.fee, params.mixIn, Common::toHex(Common::asBinaryArray(params.extra)), params.unlockTimestamp);
}

TEST_F(WalletServiceTest_sendTransaction, passesCorrectParameters) {
  WalletTransferStub wallet(dispatcher, generateRandomHash());
  auto service = createWalletService(wallet);

  std::string hash;
  auto ec = service->sendTransaction(request, hash);

  ASSERT_FALSE(ec);
  ASSERT_EQ(Common::podToHex(wallet.hash), hash);
  ASSERT_TRUE(isEquivalent(request, wallet.params));
}

TEST_F(WalletServiceTest_sendTransaction, incorrectSourceAddress) {
  auto service = createWalletService();
  request.sourceAddresses.push_back("wrong address");

  std::string hash;
  auto ec = service->sendTransaction(request, hash);
  ASSERT_EQ(make_error_code(CryptoNote::error::BAD_ADDRESS), ec);
}

TEST_F(WalletServiceTest_sendTransaction, incorrectTransferAddress) {
  auto service = createWalletService();
  request.transfers.push_back(WalletRpcOrder{"wrong address", 12131});

  std::string hash;
  auto ec = service->sendTransaction(request, hash);
  ASSERT_EQ(make_error_code(CryptoNote::error::BAD_ADDRESS), ec);
}

class WalletServiceTest_createDelayedTransaction : public WalletServiceTest_getTransactions {
  virtual void SetUp() override;
protected:
  CreateDelayedTransaction::Request request;
};

void WalletServiceTest_createDelayedTransaction::SetUp() {
  request.addresses.insert(request.addresses.end(), {RANDOM_ADDRESS1, RANDOM_ADDRESS2});
  request.transfers.push_back(WalletRpcOrder {RANDOM_ADDRESS3, 11111});
  request.fee = 2021;
  request.anonymity = 4;
  request.unlockTime = 848309;
}

struct WalletMakeTransactionStub : public IWalletBaseStub {
  WalletMakeTransactionStub(System::Dispatcher& dispatcher, const Crypto::Hash& hash) : IWalletBaseStub(dispatcher), hash(hash) {
  }

  virtual size_t makeTransaction(const TransactionParameters& sendingTransaction) override {
    params = sendingTransaction;
    return 0;
  }

  virtual WalletTransaction getTransaction(size_t transactionIndex) const override {
    return WalletTransactionBuilder().hash(hash).build();
  }

  Crypto::Hash hash;
  TransactionParameters params;
};

bool isEquivalent(const CreateDelayedTransaction::Request& request, const TransactionParameters& params) {
  std::string extra;
  if (!request.paymentId.empty()) {
    extra = "022100" + request.paymentId;
  } else {
    extra = request.extra;
  }

  std::vector<WalletOrder> orders;
  std::for_each(request.transfers.begin(), request.transfers.end(), [&orders] (const WalletRpcOrder& order) {
    orders.push_back( WalletOrder{order.address, order.amount});
  });

  return std::make_tuple(request.addresses, orders, request.fee, request.anonymity, extra, request.unlockTime)
      ==
      std::make_tuple(params.sourceAddresses, params.destinations, params.fee, params.mixIn, Common::toHex(Common::asBinaryArray(params.extra)), params.unlockTimestamp);
}

TEST_F(WalletServiceTest_createDelayedTransaction, passesCorrectParameters) {
  WalletMakeTransactionStub wallet(dispatcher, generateRandomHash());
  auto service = createWalletService(wallet);

  std::string hash;
  auto ec = service->createDelayedTransaction(request, hash);

  ASSERT_FALSE(ec);
  ASSERT_EQ(Common::podToHex(wallet.hash), hash);
  ASSERT_TRUE(isEquivalent(request, wallet.params));
}

TEST_F(WalletServiceTest_createDelayedTransaction, incorrectSourceAddress) {
  auto service = createWalletService();
  request.addresses.push_back("wrong address");

  std::string hash;
  auto ec = service->createDelayedTransaction(request, hash);
  ASSERT_EQ(make_error_code(CryptoNote::error::BAD_ADDRESS), ec);
}

TEST_F(WalletServiceTest_createDelayedTransaction, incorrectTransferAddress) {
  auto service = createWalletService();
  request.transfers.push_back(WalletRpcOrder{"wrong address", 12131});

  std::string hash;
  auto ec = service->createDelayedTransaction(request, hash);
  ASSERT_EQ(make_error_code(CryptoNote::error::BAD_ADDRESS), ec);
}

class WalletServiceTest_getDelayedTransactionHashes: public WalletServiceTest {
};

struct WalletGetDelayedTransactionIdsStub : public IWalletBaseStub {
  WalletGetDelayedTransactionIdsStub(System::Dispatcher& dispatcher, const Crypto::Hash& hash) : IWalletBaseStub(dispatcher), hash(hash) {
  }

  virtual std::vector<size_t> getDelayedTransactionIds() const override {
    return {0};
  }

  virtual WalletTransaction getTransaction(size_t transactionIndex) const override {
    return WalletTransactionBuilder().hash(hash).build();
  }

  const Crypto::Hash hash;
};

TEST_F(WalletServiceTest_getDelayedTransactionHashes, returnsCorrectResult) {
  WalletGetDelayedTransactionIdsStub wallet(dispatcher, generateRandomHash());
  auto service = createWalletService(wallet);

  std::vector<std::string> hashes;
  auto ec = service->getDelayedTransactionHashes(hashes);

  ASSERT_FALSE(ec);
  ASSERT_EQ(1, hashes.size());
  ASSERT_EQ(Common::podToHex(wallet.hash), hashes[0]);
}

class WalletServiceTest_getUnconfirmedTransactionHashes: public WalletServiceTest_getTransactions {
public:
  virtual void SetUp() override;
protected:
  std::vector<CryptoNote::WalletTransactionWithTransfers> transactions;
};

void WalletServiceTest_getUnconfirmedTransactionHashes::SetUp() {
  transactions = { WalletTransactionWithTransfersBuilder().transaction(
                    WalletTransactionBuilder().hash(generateRandomHash()).build()
                   ).addTransfer(RANDOM_ADDRESS1, 100).addTransfer(RANDOM_ADDRESS2, 333).build()
                  ,
                   WalletTransactionWithTransfersBuilder().transaction(
                     WalletTransactionBuilder().hash(generateRandomHash()).build()
                   ).addTransfer(RANDOM_ADDRESS3, 123).addTransfer(RANDOM_ADDRESS2, 4252).build()
  };
}

struct WalletGetUnconfirmedTransactionsStub : public IWalletBaseStub {
  WalletGetUnconfirmedTransactionsStub(System::Dispatcher& dispatcher) : IWalletBaseStub(dispatcher) {
  }

  virtual std::vector<WalletTransactionWithTransfers> getUnconfirmedTransactions() const override {
    return transactions;
  }

  std::vector<CryptoNote::WalletTransactionWithTransfers> transactions;
};

TEST_F(WalletServiceTest_getUnconfirmedTransactionHashes, returnsAllHashesWithoutAddresses) {
  WalletGetUnconfirmedTransactionsStub wallet(dispatcher);
  wallet.transactions = transactions;
  auto service = createWalletService(wallet);

  std::vector<std::string> hashes;
  auto ec = service->getUnconfirmedTransactionHashes({}, hashes);

  ASSERT_FALSE(ec);
  ASSERT_EQ(2, hashes.size());
  ASSERT_EQ(hashes[0], Common::podToHex(transactions[0].transaction.hash));
  ASSERT_EQ(hashes[1], Common::podToHex(transactions[1].transaction.hash));
}

TEST_F(WalletServiceTest_getUnconfirmedTransactionHashes, returnsOneTransactionWithAddressFilter) {
  WalletGetUnconfirmedTransactionsStub wallet(dispatcher);
  wallet.transactions = transactions;
  auto service = createWalletService(wallet);

  std::vector<std::string> hashes;
  auto ec = service->getUnconfirmedTransactionHashes({RANDOM_ADDRESS1}, hashes);

  ASSERT_FALSE(ec);
  ASSERT_EQ(1, hashes.size());
  ASSERT_EQ(hashes[0], Common::podToHex(transactions[0].transaction.hash));
}

TEST_F(WalletServiceTest_getUnconfirmedTransactionHashes, returnsTwoTransactionsWithAddressFilter) {
  WalletGetUnconfirmedTransactionsStub wallet(dispatcher);
  wallet.transactions = transactions;
  auto service = createWalletService(wallet);

  std::vector<std::string> hashes;
  auto ec = service->getUnconfirmedTransactionHashes({RANDOM_ADDRESS2}, hashes);

  ASSERT_FALSE(ec);
  ASSERT_EQ(2, hashes.size());
  ASSERT_EQ(hashes[0], Common::podToHex(transactions[0].transaction.hash));
  ASSERT_EQ(hashes[1], Common::podToHex(transactions[1].transaction.hash));
}

TEST_F(WalletServiceTest_getUnconfirmedTransactionHashes, wrongAddressFilter) {
  auto service = createWalletService();

  std::vector<std::string> hashes;
  auto ec = service->getUnconfirmedTransactionHashes({"wrong address"}, hashes);

  ASSERT_EQ(make_error_code(CryptoNote::error::BAD_ADDRESS), ec);
}
