// Copyright 2019 The Exonum Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! These are tests concerning the business logic of transactions in the cryptocurrency service.
//! See `api.rs` for tests focused on the API of the service.
//!
//! Note how business logic tests use `TestKit::create_block*` methods to send transactions,
//! the service schema to make assertions about the storage state.

#[macro_use]
extern crate exonum_testkit;

use exonum::{
    crypto::{self, PublicKey, SecretKey},
    messages::{RawTransaction, Signed},
};
use exonum_testkit::{TestKit, TestKitBuilder};

// Import data types used in tests from the crate where the service is defined.
use exonum_cryptocurrency::{
    schema::{CurrencySchema, Wallet},
    service::CurrencyService,
    transactions::{TxCreateWallet, TxTransfer},
};

// Imports shared test constants.
use crate::constants::{ALICE_NAME, BOB_NAME};

mod constants;

#[test]
fn test_create_wallet() {
    let mut testkit = init_testkit();
    let (tx, _) = create_wallet(&mut testkit, ALICE_NAME);

    // Check that the user indeed is persisted by the service
    let wallet = get_wallet(&testkit, &tx.author());
    assert_eq!(wallet.pub_key, tx.author());
    assert_eq!(wallet.name, ALICE_NAME);
    assert_eq!(wallet.balance, 100);
}

#[test]
fn test_transfer() {
    let mut testkit = init_testkit();
    let (alice_pubkey, alice_key) = crypto::gen_keypair();
    let (bob_pubkey, bob_key) = crypto::gen_keypair();
    testkit.create_block_with_transactions(txvec![
        TxCreateWallet::sign(ALICE_NAME, &alice_pubkey, &alice_key),
        TxCreateWallet::sign(BOB_NAME, &bob_pubkey, &bob_key),
        TxTransfer::sign(
            &bob_pubkey,
            /* amount */ 10,
            /* seed */ 0,
            &alice_pubkey,
            &alice_key
        ),
    ]);

    let alice_wallet = get_wallet(&testkit, &alice_pubkey);
    assert_eq!(alice_wallet.balance, 90);

    let bob_wallet = get_wallet(&testkit, &bob_pubkey);
    assert_eq!(bob_wallet.balance, 110);
}

#[test]
fn test_transfer_from_nonexisting_wallet() {
    let mut testkit = init_testkit();
    let (alice_pubkey, alice_key) = crypto::gen_keypair();
    let (bob_pubkey, bob_key) = crypto::gen_keypair();
    testkit.create_block_with_transactions(txvec![
        TxCreateWallet::sign(BOB_NAME, &bob_pubkey, &bob_key),
        TxTransfer::sign(
            &bob_pubkey,
            /* amount */ 10,
            /* seed */ 0,
            &alice_pubkey,
            &alice_key
        ),
    ]);

    assert!(try_get_wallet(&testkit, &alice_pubkey).is_none());

    let bob_wallet = get_wallet(&testkit, &bob_pubkey);
    assert_eq!(bob_wallet.balance, 100);
}

#[test]
fn test_transfer_to_nonexisting_wallet() {
    let mut testkit = init_testkit();
    let (alice_pubkey, alice_key) = crypto::gen_keypair();
    let (bob_pubkey, bob_key) = crypto::gen_keypair();
    testkit.create_block_with_transactions(txvec![
        TxCreateWallet::sign(ALICE_NAME, &alice_pubkey, &alice_key),
        TxTransfer::sign(
            &bob_pubkey,
            /* amount */ 10,
            /* seed */ 0,
            &alice_pubkey,
            &alice_key
        ),
        // Although Bob's wallet is created, this occurs after the transfer is executed.
        TxCreateWallet::sign(BOB_NAME, &bob_pubkey, &bob_key),
    ]);

    let alice_wallet = get_wallet(&testkit, &alice_pubkey);
    assert_eq!(alice_wallet.balance, 100);

    let bob_wallet = get_wallet(&testkit, &bob_pubkey);
    assert_eq!(bob_wallet.balance, 100);
}

#[test]
fn test_transfer_overcharge() {
    let mut testkit = init_testkit();
    let (alice_pubkey, alice_key) = crypto::gen_keypair();
    let (bob_pubkey, bob_key) = crypto::gen_keypair();
    testkit.create_block_with_transactions(txvec![
        TxCreateWallet::sign(ALICE_NAME, &alice_pubkey, &alice_key),
        TxCreateWallet::sign(BOB_NAME, &bob_pubkey, &bob_key),
        TxTransfer::sign(
            &bob_pubkey,
            /* amount */ 150,
            /* seed */ 0,
            &alice_pubkey,
            &alice_key
        ),
    ]);

    // The transfer amount is greater than what Alice has at her disposal, so
    // the transfer should fail.
    let alice_wallet = get_wallet(&testkit, &alice_pubkey);
    assert_eq!(alice_wallet.balance, 100);

    let bob_wallet = get_wallet(&testkit, &bob_pubkey);
    assert_eq!(bob_wallet.balance, 100);
}

#[test]
fn test_transfers_in_single_block() {
    let mut testkit = init_testkit();
    let (alice_pubkey, alice_key) = crypto::gen_keypair();
    let (bob_pubkey, bob_key) = crypto::gen_keypair();
    testkit.create_block_with_transactions(txvec![
        TxCreateWallet::sign(ALICE_NAME, &alice_pubkey, &alice_key),
        TxCreateWallet::sign(BOB_NAME, &bob_pubkey, &bob_key),
    ]);

    let tx_a_to_b = TxTransfer::sign(
        &bob_pubkey,
        90, // amount
        0,  // seed
        &alice_pubkey,
        &alice_key,
    );
    let tx_b_to_a = TxTransfer::sign(
        &alice_pubkey,
        120, // amount
        0,   // seed
        &bob_pubkey,
        &bob_key,
    );

    {
        // See what happens if transactions are applied in an "incorrect" order.
        // We use `TestKit::probe_all()` method for this.

        let snapshot = testkit.probe_all(txvec![tx_b_to_a.clone(), tx_a_to_b.clone()]);
        let schema = CurrencySchema::new(&snapshot);
        assert_eq!(schema.wallet(&alice_pubkey).map(|w| w.balance), Some(10));
        assert_eq!(schema.wallet(&bob_pubkey).map(|w| w.balance), Some(190));
    }

    testkit.create_block_with_transactions(txvec![tx_a_to_b, tx_b_to_a]);

    let alice_wallet = get_wallet(&testkit, &alice_pubkey);
    assert_eq!(alice_wallet.balance, 130);

    let bob_wallet = get_wallet(&testkit, &bob_pubkey);
    assert_eq!(bob_wallet.balance, 70);
}

/// Generate random transactions to perform [fuzz testing][fuzz] of the service. The service
/// should maintain invariants under all circumstances; e.g., the total amount of tokens
/// in existence should depend only on the number of registered wallets.
///
/// [fuzz]: https://en.wikipedia.org/wiki/Fuzzing
#[test]
fn test_fuzz_transfers() {
    use rand::{seq::SliceRandom, Rng};
    use std::collections::BTreeSet;
    use std::iter::FromIterator;

    const BLOCKS: usize = 50; // number of blocks to create
    const MAX_TRANSACTIONS: usize = 20; // maximum number of transactions in a block

    let mut rng = rand::thread_rng();

    let mut testkit = init_testkit();
    let alice_keys = crypto::gen_keypair();
    let bob_keys = crypto::gen_keypair();
    let keys = &[alice_keys.clone(), bob_keys.clone()];
    testkit.create_block_with_transactions(txvec![
        TxCreateWallet::sign(ALICE_NAME, &alice_keys.0, &alice_keys.1),
        TxCreateWallet::sign(BOB_NAME, &bob_keys.0, &bob_keys.1),
    ]);

    for _ in 0..BLOCKS {
        let n_txs = rng.gen_range(0, MAX_TRANSACTIONS); // number of transactions in the block

        let txs: Vec<Signed<RawTransaction>> = (0..n_txs)
            .map(|_| {
                let (sender, receiver) = (
                    keys.choose(&mut rng).unwrap(),
                    keys.choose(&mut rng).unwrap(),
                );
                let amount = rng.gen_range(0, 250);
                TxTransfer::sign(&receiver.0, amount, rng.gen::<u64>(), &sender.0, &sender.1)
            })
            .collect();

        testkit.create_block_with_transactions(txs);

        // Test invariants that should be maintained during fuzz testing.
        let schema = CurrencySchema::new(testkit.snapshot());
        let wallets = schema.wallets();
        let wallets: Vec<_> = wallets.values().collect();
        // There must be 2 wallets in the storage.
        assert_eq!(wallets.len(), 2);
        // These wallets should belong to Alice and Bob.
        assert_eq!(
            BTreeSet::from_iter(wallets.iter().map(|w| &w.pub_key)),
            BTreeSet::from_iter(vec![&alice_keys.0, &bob_keys.0])
        );
        // The total amount of funds should equal 200, no matter which transactions were executed.
        assert_eq!(wallets.iter().map(|w| w.balance).sum::<u64>(), 200);
    }
}

/// Initializes testkit with `CurrencyService`.
fn init_testkit() -> TestKit {
    TestKitBuilder::validator()
        .with_service(CurrencyService)
        .create()
}

/// Creates a wallet with the given name and a random key.
fn create_wallet(testkit: &mut TestKit, name: &str) -> (Signed<RawTransaction>, SecretKey) {
    let (pubkey, key) = crypto::gen_keypair();
    let tx = TxCreateWallet::sign(name, &pubkey, &key);
    testkit.create_block_with_transaction(tx.clone());
    (tx, key)
}

/// Returns the wallet identified by the given public key or `None` such wallet doesn't exist.
fn try_get_wallet(testkit: &TestKit, pubkey: &PublicKey) -> Option<Wallet> {
    let snapshot = testkit.snapshot();
    CurrencySchema::new(&snapshot).wallet(pubkey)
}

/// Returns the wallet identified by the given public key.
fn get_wallet(testkit: &TestKit, pubkey: &PublicKey) -> Wallet {
    try_get_wallet(testkit, pubkey).expect("No wallet persisted")
}
