// Copyright 2019. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

use rand::rngs::OsRng;
use tari_common::configuration::Network;
use tari_common_types::types::{
    ComAndPubSignature,
    Commitment,
    FixedHash,
    PrivateKey,
    PublicKey,
    RangeProof,
    Signature,
};
use tari_core::{
    blocks::{BlockAccumulatedData, BlockHeader, BlockHeaderAccumulatedData, ChainHeader, UpdateBlockAccumulatedData},
    chain_storage::{create_lmdb_database, BlockchainBackend, ChainStorageError, DbKey, DbTransaction, DbValue},
    consensus::{ConsensusManager, ConsensusManagerBuilder},
    covenants::Covenant,
    test_helpers::blockchain::create_test_db,
    transactions::transaction_components::{
        EncryptedOpenings,
        KernelFeatures,
        OutputFeatures,
        TransactionKernel,
        TransactionKernelVersion,
        TransactionOutput,
        TransactionOutputVersion,
    },
    tx,
};
use tari_crypto::keys::{PublicKey as PKtrait, SecretKey as SKtrait};
use tari_script::TariScript;
use tari_storage::lmdb_store::LMDBConfig;
use tari_test_utils::paths::create_temporary_data_path;

use crate::helpers::database::create_orphan_block;

#[test]
fn test_lmdb_insert_contains_delete_and_fetch_orphan() {
    let network = Network::LocalNet;
    let consensus = ConsensusManagerBuilder::new(network).build();
    let mut db = create_test_db();
    let txs = vec![
        (tx!(1000.into(), fee: 4.into(), inputs: 2, outputs: 1)).0,
        (tx!(2000.into(), fee: 6.into(), inputs: 1, outputs: 1)).0,
    ];
    let orphan = create_orphan_block(10, txs, &consensus);
    let hash = orphan.hash();
    assert!(!db.contains(&DbKey::OrphanBlock(hash)).unwrap());

    let mut txn = DbTransaction::new();
    txn.insert_orphan(orphan.clone().into());
    db.write(txn).unwrap();

    assert!(db.contains(&DbKey::OrphanBlock(hash)).unwrap());
    if let Some(DbValue::OrphanBlock(retrieved_orphan)) = db.fetch(&DbKey::OrphanBlock(hash)).unwrap() {
        assert_eq!(*retrieved_orphan, orphan);
    } else {
        panic!();
    }

    let mut txn = DbTransaction::new();
    txn.delete_orphan(hash);
    assert!(db.write(txn).is_ok());
    assert!(!db.contains(&DbKey::OrphanBlock(hash)).unwrap());
}

#[test]
fn test_kernel_order() {
    let mut db = create_test_db();

    let block_hash = FixedHash::zero();
    let mut kernels = Vec::with_capacity(2000);
    let version = TransactionKernelVersion::V0;
    let features = KernelFeatures::default();
    for _i in 0..2000 {
        let pvt_key = PrivateKey::random(&mut OsRng);
        let pub_key = PublicKey::from_secret_key(&pvt_key);
        let commitment = Commitment::from_public_key(&pub_key);
        let sig = Signature::new(pub_key, pvt_key);
        let kernel = TransactionKernel::new(version, features, 0.into(), 0, commitment, sig, None);
        kernels.push(kernel);
    }
    kernels.sort();

    for (i, kernel) in kernels.iter().enumerate().take(2000) {
        let mut tx = DbTransaction::new();
        tx.insert_kernel(kernel.clone(), block_hash, i as u32);
        db.write(tx).unwrap();
    }

    let read_kernels = db.fetch_kernels_in_block(&block_hash).unwrap();
    assert_eq!(kernels.len(), read_kernels.len());
    for i in 0..2000 {
        assert_eq!(kernels[i], read_kernels[i]);
    }
}

#[test]
fn test_utxo_order() {
    let mut db = create_test_db();

    let block_data = BlockAccumulatedData::default();
    let header = BlockHeader::new(0);
    let block_hash = header.hash();
    let mut utxos = Vec::with_capacity(2000);
    let version = TransactionOutputVersion::V0;
    let features = OutputFeatures::default();
    let script = script!(Nop).unwrap();
    let proof = RangeProof::default();
    let sig = ComAndPubSignature::default();
    let covenant = Covenant::default();
    let encrypt = EncryptedOpenings::default();
    for _i in 0..2000 {
        let pvt_key = PrivateKey::random(&mut OsRng);
        let pub_key = PublicKey::from_secret_key(&pvt_key);
        let commitment = Commitment::from_public_key(&pub_key);
        let utxo = TransactionOutput::new(
            version,
            features.clone(),
            commitment,
            Some(proof.clone()),
            script.clone(),
            pub_key,
            sig.clone(),
            covenant.clone(),
            encrypt,
            0.into(),
        );
        utxos.push(utxo);
    }
    utxos.sort();

    for (i, utxo) in utxos.iter().enumerate().take(2000) {
        let mut tx = DbTransaction::new();
        tx.insert_utxo(utxo.clone(), block_hash, 0, i as u32, 0);
        db.write(tx).unwrap();
    }

    let mut tx = DbTransaction::new();
    let data = BlockHeaderAccumulatedData {
        hash: header.hash(),
        ..Default::default()
    };
    let chainheader = ChainHeader::try_construct(header, data).unwrap();
    let sum = block_data.kernel_sum().clone();
    let (kernels, utxo_set, deleted) = block_data.dissolve();
    let update_data = UpdateBlockAccumulatedData {
        kernel_hash_set: Some(kernels),
        utxo_hash_set: Some(utxo_set),
        deleted_diff: Some(deleted.into()),
        kernel_sum: Some(sum),
    };
    tx.insert_chain_header(chainheader);
    tx.update_block_accumulated_data(block_hash, update_data);

    db.write(tx).unwrap();

    let read_utxos = db.fetch_outputs_in_block_with_spend_state(&block_hash).unwrap();
    assert_eq!(utxos.len(), read_utxos.len());
    for i in 0..2000 {
        assert_eq!(&utxos[i], read_utxos[i].to_transaction_output().unwrap());
    }
}

#[test]
fn test_lmdb_file_lock() {
    // Create temporary test folder
    let temp_path = create_temporary_data_path();

    // Perform test
    {
        let consensus_manager = ConsensusManager::builder(Network::LocalNet).build();
        let db = create_lmdb_database(&temp_path, LMDBConfig::default(), consensus_manager.clone()).unwrap();

        match create_lmdb_database(&temp_path, LMDBConfig::default(), consensus_manager.clone()) {
            Err(ChainStorageError::CannotAcquireFileLock) => {},
            _ => panic!("Should not be able to make this db"),
        }

        drop(db);

        let _db2 = create_lmdb_database(&temp_path, LMDBConfig::default(), consensus_manager)
            .expect("Should be able to make a new lmdb now");
    }

    // Cleanup test data - in Windows the LMBD `set_mapsize` sets file size equals to map size; Linux use sparse files
    if std::path::Path::new(&temp_path).exists() {
        std::fs::remove_dir_all(&temp_path).expect("Could not clear temp storage for db");
    }
}
