// 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, RngCore};
use tari_common::configuration::Network;
use tari_common_types::types::BlockHash;
use tari_core::{
    blocks::{genesis_block, Block, BlockHeader},
    chain_storage::{
        create_lmdb_database,
        BlockAddResult,
        BlockchainBackend,
        BlockchainDatabase,
        BlockchainDatabaseConfig,
        ChainStorageError,
        DbTransaction,
        MmrTree,
        Validators,
    },
    consensus::{emission::Emission, ConsensusConstantsBuilder, ConsensusManager, ConsensusManagerBuilder},
    proof_of_work::Difficulty,
    test_helpers::blockchain::{
        create_store_with_consensus,
        create_store_with_consensus_and_validators,
        create_test_blockchain_db,
        create_test_db,
        TempDatabase,
    },
    transactions::{
        tari_amount::{uT, MicroMinotari, T},
        test_helpers::spend_utxos,
        CryptoFactories,
    },
    tx,
    txn_schema,
    validation::{mocks::MockValidator, DifficultyCalculator, ValidationError},
};
use tari_storage::lmdb_store::LMDBConfig;
use tari_test_utils::{paths::create_temporary_data_path, unpack_enum};

// use crate::helpers::database::create_test_db;
// use crate::helpers::database::create_store;
use crate::helpers::{
    block_builders::{
        append_block,
        chain_block,
        create_chain_header,
        create_genesis_block,
        find_header_with_achieved_difficulty,
        generate_block_with_achieved_difficulty,
        generate_new_block,
        generate_new_block_with_achieved_difficulty,
        generate_new_block_with_coinbase,
    },
    database::create_orphan_block,
    sample_blockchains::{create_new_blockchain, create_new_blockchain_lmdb},
};

#[test]
fn test_fetch_nonexistent_header() {
    let network = Network::LocalNet;
    let _consensus_manager = ConsensusManagerBuilder::new(network).build();
    let store = create_test_blockchain_db();

    assert_eq!(store.fetch_header(1).unwrap(), None);
}

#[test]
fn test_insert_and_fetch_header() {
    let network = Network::LocalNet;
    let _consensus_manager = ConsensusManagerBuilder::new(network).build();
    let store = create_test_blockchain_db();
    let genesis_block = store.fetch_tip_header().unwrap();
    let mut header1 = BlockHeader::from_previous(genesis_block.header());

    header1.kernel_mmr_size += 1;
    header1.output_mmr_size += 1;

    let chain1 = create_chain_header(header1.clone(), genesis_block.accumulated_data());

    store.insert_valid_headers(vec![chain1.clone()]).unwrap();
    let mut header2 = BlockHeader::from_previous(&header1);
    header2.kernel_mmr_size += 2;
    header2.output_mmr_size += 2;
    let chain2 = create_chain_header(header2.clone(), chain1.accumulated_data());

    store.insert_valid_headers(vec![chain2]).unwrap();
    store.fetch_header(0).unwrap();

    assert_eq!(store.fetch_header(1).unwrap().unwrap(), header1);
    assert_eq!(store.fetch_header(2).unwrap().unwrap(), header2);
}

#[test]
fn test_insert_and_fetch_orphan() {
    let network = Network::LocalNet;
    let consensus_manager = ConsensusManagerBuilder::new(network).build();
    let store = create_test_blockchain_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_manager);
    let orphan_hash = orphan.hash();
    let mut txn = DbTransaction::new();
    txn.insert_orphan(orphan.clone().into());
    assert!(store.commit(txn).is_ok());
    assert_eq!(store.fetch_orphan(orphan_hash).unwrap(), orphan);
}

#[test]
fn test_store_and_retrieve_block() {
    let (db, blocks, _, _) = create_new_blockchain(Network::LocalNet);
    let hash = blocks[0].hash();
    // Check the metadata
    let metadata = db.get_chain_metadata().unwrap();
    assert_eq!(metadata.best_block_height(), 0);
    assert_eq!(metadata.best_block(), hash);
    assert_eq!(metadata.horizon_block(metadata.best_block_height()), 0);
    // Fetch the block back
    let block0 = db.fetch_block(0, true).unwrap();
    assert_eq!(block0.confirmations(), 1);
    // Compare the blocks
    let block0 = Block::from(block0);
    assert_eq!(blocks[0].block(), &block0);
}

#[test]
fn test_add_multiple_blocks() {
    // Create new database with genesis block
    let network = Network::LocalNet;
    let consensus_manager = ConsensusManagerBuilder::new(network).build();
    let store = create_store_with_consensus(consensus_manager.clone());
    let metadata = store.get_chain_metadata().unwrap();
    assert_eq!(metadata.best_block_height(), 0);
    let block0 = store.fetch_block(0, true).unwrap();
    assert_eq!(metadata.best_block(), block0.hash());
    // Add another block
    let block1 = append_block(
        &store,
        &block0.try_into_chain_block().unwrap(),
        vec![],
        &consensus_manager,
        Difficulty::min(),
    )
    .unwrap();
    let metadata = store.get_chain_metadata().unwrap();
    let hash = block1.hash();
    assert_eq!(metadata.best_block_height(), 1);
    assert_eq!(metadata.best_block(), hash);
    // Adding blocks is idempotent
    assert_eq!(
        store.add_block(block1.to_arc_block()).unwrap(),
        BlockAddResult::BlockExists
    );
    // Check the metadata
    let metadata = store.get_chain_metadata().unwrap();
    assert_eq!(metadata.best_block_height(), 1);
    assert_eq!(metadata.best_block(), hash);
}

#[test]
fn test_checkpoints() {
    let network = Network::LocalNet;
    let (db, blocks, outputs, consensus_manager) = create_new_blockchain(network);

    let txn = txn_schema!(
        from: vec![outputs[0][0].clone()],
        to: vec![MicroMinotari(5_000), MicroMinotari(6_000)]
    );
    let (txn, _) = spend_utxos(txn);
    let block1 = append_block(&db, &blocks[0], vec![txn], &consensus_manager, Difficulty::min()).unwrap();
    // Get the checkpoint
    let block_a = db.fetch_block(0, false).unwrap();
    assert_eq!(block_a.confirmations(), 2);
    assert_eq!(blocks[0].block(), block_a.block());
    let block_b = db.fetch_block(1, false).unwrap();
    assert_eq!(block_b.confirmations(), 1);
    let block1 = serde_json::to_string(block1.block()).unwrap();
    let block_b = serde_json::to_string(&Block::from(block_b)).unwrap();
    assert_eq!(block1, block_b);
}

#[test]
#[allow(clippy::identity_op)]
fn test_rewind_to_height() {
    // let _ = env_logger::builder().is_test(true).try_init();
    let network = Network::LocalNet;
    let (mut db, mut blocks, mut outputs, consensus_manager) = create_new_blockchain(network);

    // Block 1
    let schema = vec![txn_schema!(from: vec![outputs[0][0].clone()], to: vec![6 * T, 3 * T])];
    unpack_enum!(
        BlockAddResult::Ok(_b1) =
            generate_new_block(&mut db, &mut blocks, &mut outputs, schema, &consensus_manager).unwrap()
    );
    // Block 2
    let schema = vec![txn_schema!(from: vec![outputs[1][0].clone()], to: vec![3 * T, 1 * T])];
    unpack_enum!(
        BlockAddResult::Ok(_b2) =
            generate_new_block(&mut db, &mut blocks, &mut outputs, schema, &consensus_manager).unwrap()
    );
    // Block 3
    let schema = vec![
        txn_schema!(from: vec![outputs[2][0].clone()], to: vec![2 * T, 500_000 * uT]),
        txn_schema!(from: vec![outputs[1][1].clone()], to: vec![500_000 * uT]),
    ];
    unpack_enum!(
        BlockAddResult::Ok(_b3) =
            generate_new_block(&mut db, &mut blocks, &mut outputs, schema, &consensus_manager).unwrap()
    );

    db.rewind_to_height(3).unwrap();
    assert_eq!(db.get_height().unwrap(), 3);

    // Invalid rewind
    assert!(db.rewind_to_height(4).is_err());
    assert_eq!(db.get_height().unwrap(), 3);
    db.rewind_to_height(1).unwrap();
    assert_eq!(db.get_height().unwrap(), 1);
}

#[test]
fn test_coverage_chain_storage() {
    let validators = Validators::new(
        MockValidator::new(true),
        MockValidator::new(true),
        MockValidator::new(true),
    );
    let network = Network::LocalNet;
    let rules = ConsensusManagerBuilder::new(network).build();
    let db = create_test_db();
    assert_eq!(db.kernel_count().unwrap(), 0);
    let store = BlockchainDatabase::new(
        db,
        rules.clone(),
        validators,
        BlockchainDatabaseConfig::default(),
        DifficultyCalculator::new(rules.clone(), Default::default()),
    )
    .unwrap();

    let block0 = store.fetch_block(0, true).unwrap();
    append_block(
        &store,
        &block0.clone().try_into_chain_block().unwrap(),
        vec![],
        &rules,
        Difficulty::min(),
    )
    .unwrap();
    assert_eq!(store.fetch_all_reorgs().unwrap(), vec![]);
    assert_eq!(store.fetch_mmr_size(MmrTree::Kernel).unwrap(), 2);
    assert_eq!(store.fetch_mmr_size(MmrTree::Utxo).unwrap(), 2);

    let mut txn = DbTransaction::new();
    txn.insert_bad_block(*block0.hash(), 0);
    store.commit(txn).unwrap();
    let bad_blocks = txn.fetch_bad_blocks();
    assert_eq!(bad_blocks.len(), 1);
    assert_eq!(bad_blocks[0].0, *block0.hash());
}

#[test]
fn test_rewind_past_horizon_height() {
    let network = Network::LocalNet;
    let block0 = genesis_block::get_esmeralda_genesis_block();
    let consensus_manager = ConsensusManagerBuilder::new(network).with_block(block0.clone()).build();
    let validators = Validators::new(
        MockValidator::new(true),
        MockValidator::new(true),
        MockValidator::new(true),
    );
    let db = create_test_db();
    let config = BlockchainDatabaseConfig {
        orphan_storage_capacity: 3,
        pruning_horizon: 2,
        pruning_interval: 1,
        ..Default::default()
    };
    let store = BlockchainDatabase::new(
        db,
        consensus_manager.clone(),
        validators,
        config,
        DifficultyCalculator::new(consensus_manager.clone(), Default::default()),
    )
    .unwrap();

    let block1 = append_block(&store, &block0, vec![], &consensus_manager, Difficulty::min()).unwrap();
    let block2 = append_block(&store, &block1, vec![], &consensus_manager, Difficulty::min()).unwrap();
    let block3 = append_block(&store, &block2, vec![], &consensus_manager, Difficulty::min()).unwrap();
    let _block4 = append_block(&store, &block3, vec![], &consensus_manager, Difficulty::min()).unwrap();

    let metadata = store.get_chain_metadata().unwrap();
    assert_eq!(metadata.best_block_height(), 4);
    // we should not be able to rewind to the future
    assert!(store.rewind_to_height(metadata.best_block_height() + 1).is_err());
    let horizon_height = metadata.pruned_height();
    assert_eq!(horizon_height, 2);
    // rewinding past pruning horizon should set us to height 0 so we can resync from gen block.
    assert!(store.rewind_to_height(horizon_height - 1).is_ok());
    let metadata = store.get_chain_metadata().unwrap();
    assert_eq!(metadata.best_block_height(), 0);
}

#[test]
#[allow(clippy::too_many_lines)]
fn test_handle_tip_reorg_with_zero_conf() {
    // GB --> A1 --> A2 --> A3(Low PoW)      [Main Chain]
    //          \--> B2 --> B3 -- B4 --> B5(Highest PoW)  [Forked Chain]

    // Create Main Chain
    let network = Network::LocalNet;
    let (mut store, mut blocks, mut outputs, consensus_manager) = create_new_blockchain(network);
    // Block A1
    let txs = vec![txn_schema!(
        from: vec![outputs[0][0].clone()],
        to: vec![10 * T, 10 * T, 10 * T, 10 * T]
    )];
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager,
    )
    .unwrap();
    // Block A2
    let txs_1 = txn_schema!(from: vec![outputs[1][3].clone()], to: vec![6 * T]);
    let (tx_1, utxos_1) = spend_utxos(txs_1);
    // create zero conf
    let txs_2 = txn_schema!(from: vec![utxos_1[0].clone()], to: vec![4 * T]);
    let (tx_2, utxos_2) = spend_utxos(txs_2);
    let txns = vec![tx_1, tx_2];

    outputs.push(utxos_2);
    generate_block_with_achieved_difficulty(&mut store, &mut blocks, txns, Difficulty::from(3), &consensus_manager)
        .unwrap();

    // Block A3
    let txs = vec![txn_schema!(from: vec![outputs[2][0].clone()], to: vec![2 * T])];
    assert!(generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager
    )
    .is_ok());
    assert_eq!(store.get_chain_metadata().unwrap().best_block_height(), 3);

    // Create Forked Chain

    let mut orphan_store = create_store_with_consensus(consensus_manager.clone());
    orphan_store.add_block(blocks[1].to_arc_block()).unwrap();
    let mut orphan_blocks = vec![blocks[0].clone(), blocks[1].clone()];
    let mut orphan_outputs = vec![outputs[0].clone(), outputs[1].clone()];
    // Block B2
    let txs = vec![txn_schema!(
        from: vec![
            orphan_outputs[1][0].clone(),
            orphan_outputs[1][1].clone(),
            orphan_outputs[1][2].clone(),
            orphan_outputs[1][3].clone(),
            orphan_outputs[1][4].clone(),
        ],
        to: vec![5 * T, 5 * T, 5 * T, 5 * T, 5 * T]
    )];
    generate_new_block_with_achieved_difficulty(
        &mut orphan_store,
        &mut orphan_blocks,
        &mut orphan_outputs,
        txs,
        Difficulty::from(7),
        &consensus_manager,
    )
    .unwrap();

    // Adding B2 to the main chain will produce a reorg to GB->A1->B2.
    if let Ok(BlockAddResult::ChainReorg { .. }) = store.add_block(orphan_blocks[2].to_arc_block()) {
    } else {
        panic!();
    }

    assert_eq!(store.fetch_tip_header().unwrap().header(), orphan_blocks[2].header());

    // Check that B2 was removed from the block orphans and A2 has been orphaned.
    assert!(store.fetch_orphan(*orphan_blocks[2].hash()).is_err());
    assert!(store.fetch_orphan(*blocks[2].hash()).is_ok());
    assert_eq!(store.get_chain_metadata().unwrap().best_block_height(), 2);

    // Block B3
    let txs = vec![
        txn_schema!(from: vec![orphan_outputs[2][0].clone()], to: vec![3 * T]),
        txn_schema!(from: vec![orphan_outputs[2][1].clone()], to: vec![3 * T]),
        txn_schema!(from: vec![orphan_outputs[2][2].clone()], to: vec![3 * T]),
        txn_schema!(from: vec![orphan_outputs[2][3].clone()], to: vec![3 * T]),
        txn_schema!(from: vec![orphan_outputs[2][4].clone()], to: vec![3 * T]),
        txn_schema!(from: vec![orphan_outputs[2][5].clone()], to: vec![3 * T]),
    ];
    assert!(generate_new_block_with_achieved_difficulty(
        &mut orphan_store,
        &mut orphan_blocks,
        &mut orphan_outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager
    )
    .is_ok());

    if let Ok(BlockAddResult::Ok { .. }) = store.add_block(orphan_blocks[3].to_arc_block()) {
    } else {
        panic!();
    }
    // Block B4
    let txs = vec![
        txn_schema!(from: vec![orphan_outputs[3][0].clone()], to: vec![1500000 * uT]),
        txn_schema!(from: vec![orphan_outputs[3][1].clone()], to: vec![1500000 * uT]),
    ];
    assert!(generate_new_block_with_achieved_difficulty(
        &mut orphan_store,
        &mut orphan_blocks,
        &mut orphan_outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager
    )
    .is_ok());

    if let Ok(BlockAddResult::Ok { .. }) = store.add_block(orphan_blocks[4].to_arc_block()) {
    } else {
        panic!();
    }

    // Block B5
    let txs = vec![
        txn_schema!(from: vec![orphan_outputs[4][0].clone()], to: vec![50000 * uT]),
        txn_schema!(from: vec![orphan_outputs[4][1].clone()], to: vec![50000 * uT]),
    ];
    assert!(generate_new_block_with_achieved_difficulty(
        &mut orphan_store,
        &mut orphan_blocks,
        &mut orphan_outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager
    )
    .is_ok());

    if let Ok(BlockAddResult::Ok { .. }) = store.add_block(orphan_blocks[5].to_arc_block()) {
    } else {
        panic!();
    }
    assert_eq!(store.get_chain_metadata().unwrap().best_block_height(), 5);
}
#[test]
#[allow(clippy::too_many_lines)]
fn test_handle_tip_reorg() {
    // GB --> A1 --> A2(Low PoW)      [Main Chain]
    //          \--> B2(Highest PoW)  [Forked Chain]
    // Initially, the main chain is GB->A1->A2. B2 has a higher accumulated PoW and when B2 is added the main chain is
    // reorged to GB->A1->B2

    // Create Main Chain
    let network = Network::LocalNet;
    let (mut store, mut blocks, mut outputs, consensus_manager) = create_new_blockchain(network);
    // Block A1
    let txs = vec![txn_schema!(
        from: vec![outputs[0][0].clone()],
        to: vec![10 * T, 10 * T, 10 * T, 10 * T]
    )];
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager,
    )
    .unwrap();
    // Block A2
    let txs = vec![txn_schema!(from: vec![outputs[1][3].clone()], to: vec![6 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(3),
        &consensus_manager,
    )
    .unwrap();

    // Create Forked Chain

    let mut orphan_store = create_store_with_consensus(consensus_manager.clone());
    orphan_store.add_block(blocks[1].to_arc_block()).unwrap();
    let mut orphan_blocks = vec![blocks[0].clone(), blocks[1].clone()];
    let mut orphan_outputs = vec![outputs[0].clone(), outputs[1].clone()];
    // Block B2
    let txs = vec![txn_schema!(from: vec![orphan_outputs[1][0].clone()], to: vec![5 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut orphan_store,
        &mut orphan_blocks,
        &mut orphan_outputs,
        txs,
        Difficulty::from(7),
        &consensus_manager,
    )
    .unwrap();

    // Adding B2 to the main chain will produce a reorg to GB->A1->B2.
    if let Ok(BlockAddResult::ChainReorg { .. }) = store.add_block(orphan_blocks[2].to_arc_block()) {
    } else {
        panic!();
    }
    assert_eq!(store.fetch_tip_header().unwrap().header(), orphan_blocks[2].header());

    // Check that B2 was removed from the block orphans and A2 has been orphaned.
    assert!(store.fetch_orphan(*orphan_blocks[2].hash()).is_err());
    assert!(store.fetch_orphan(*blocks[2].hash()).is_ok());
}

#[test]
fn test_handle_tip_reset() {
    // GB --> A1 --> A2(Low PoW)      [Main Chain]
    //          \--> B2(Highest PoW)  [Forked Chain]
    // Initially, the main chain is GB->A1->A2. B2 has a higher accumulated PoW and when B2 is added the main chain is
    // reorged to GB->A1->B2

    // Create Main Chain
    let network = Network::LocalNet;
    let (mut store, mut blocks, mut outputs, consensus_manager) = create_new_blockchain(network);
    // Block A1
    let txs = vec![txn_schema!(
        from: vec![outputs[0][0].clone()],
        to: vec![10 * T, 10 * T, 10 * T, 10 * T]
    )];
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager,
    )
    .unwrap();
    // Block A2
    let txs = vec![txn_schema!(from: vec![outputs[1][3].clone()], to: vec![6 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(3),
        &consensus_manager,
    )
    .unwrap();

    // Create Forked Chain

    let mut orphan_store = create_store_with_consensus(consensus_manager.clone());
    orphan_store.add_block(blocks[1].to_arc_block()).unwrap();
    let mut orphan_blocks = vec![blocks[0].clone(), blocks[1].clone()];
    let mut orphan_outputs = vec![outputs[0].clone(), outputs[1].clone()];
    // Block B2
    let txs = vec![txn_schema!(from: vec![orphan_outputs[1][0].clone()], to: vec![5 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut orphan_store,
        &mut orphan_blocks,
        &mut orphan_outputs,
        txs,
        Difficulty::from(7),
        &consensus_manager,
    )
    .unwrap();

    // Adding B2 to the main chain will produce a reorg to GB->A1->B2.
    if let Ok(BlockAddResult::ChainReorg { .. }) = store.add_block(orphan_blocks[2].to_arc_block()) {
    } else {
        panic!();
    }

    assert_eq!(store.fetch_tip_header().unwrap().header().height, 2);
    store.rewind_to_height(1).unwrap();
    assert_eq!(store.fetch_tip_header().unwrap().header().height, 1);
    // both tips should be in the orphan pool
    assert!(store.fetch_orphan(*orphan_blocks[2].hash()).is_ok());
    assert!(store.fetch_orphan(*blocks[2].hash()).is_ok());
    store.swap_to_highest_pow_chain().unwrap();
    // should no be on B2

    assert_eq!(store.fetch_tip_header().unwrap().header().height, 2);
    assert_eq!(store.fetch_tip_header().unwrap().hash(), orphan_blocks[2].hash());
    assert!(store.fetch_orphan(*blocks[2].hash()).is_ok());

    store.swap_to_highest_pow_chain().unwrap();
    // Chain should not have swapped
    assert_eq!(store.fetch_tip_header().unwrap().hash(), orphan_blocks[2].hash());
    assert!(store.fetch_orphan(*blocks[2].hash()).is_ok());

    // lets reset to A1 again
    store.rewind_to_height(1).unwrap();
    assert_eq!(store.fetch_tip_header().unwrap().header().height, 1);
    store.cleanup_all_orphans().unwrap();
    store.swap_to_highest_pow_chain().unwrap();
    // current main chain should be the highest so is it still?
    assert_eq!(store.fetch_tip_header().unwrap().header().height, 1);
    assert_eq!(store.fetch_tip_header().unwrap().hash(), blocks[1].hash());
}

#[test]
#[allow(clippy::identity_op)]
#[allow(clippy::too_many_lines)]
fn test_handle_reorg() {
    // GB --> A1 --> A2 --> A3 -----> A4(Low PoW)     [Main Chain]
    //          \--> B2 --> B3(?) --> B4(Medium PoW)  [Forked Chain 1]
    //                        \-----> C4(Highest PoW) [Forked Chain 2]
    // Initially, the main chain is GB->A1->A2->A3->A4 with orphaned blocks B2, B4, C4. When B3 arrives late and is
    // added to the blockchain then a reorg is triggered and the main chain is reorganized to GB->A1->B2->B3->C4.

    // Create Main Chain
    let network = Network::LocalNet;
    let (mut store, mut blocks, mut outputs, consensus_manager) = create_new_blockchain(network);
    // Block A1
    let txs = vec![txn_schema!(
        from: vec![outputs[0][0].clone()],
        to: vec![10 * T, 10 * T, 10 * T, 10 * T]
    )];
    assert!(generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager
    )
    .is_ok());
    // Block A2
    let txs = vec![txn_schema!(from: vec![outputs[1][3].clone()], to: vec![6 * T])];
    assert!(generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(3),
        &consensus_manager
    )
    .is_ok());
    // Block A3
    let txs = vec![txn_schema!(from: vec![outputs[2][0].clone()], to: vec![2 * T])];
    assert!(generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager
    )
    .is_ok());
    // Block A4
    let txs = vec![txn_schema!(from: vec![outputs[1][0].clone()], to: vec![2 * T])];
    assert!(generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager
    )
    .is_ok());

    // Create Forked Chain 1
    let mut orphan1_store = create_store_with_consensus(consensus_manager.clone());
    orphan1_store
        .add_block(blocks[1].to_arc_block())
        .unwrap()
        .assert_added(); // A1
    let mut orphan1_blocks = vec![blocks[0].clone(), blocks[1].clone()];
    let mut orphan1_outputs = vec![outputs[0].clone(), outputs[1].clone()];
    // Block B2
    let txs = vec![txn_schema!(from: vec![orphan1_outputs[1][0].clone()], to: vec![5 * T])];
    assert!(generate_new_block_with_achieved_difficulty(
        &mut orphan1_store,
        &mut orphan1_blocks,
        &mut orphan1_outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager
    )
    .is_ok());
    // Block B3
    let txs = vec![
        txn_schema!(from: vec![orphan1_outputs[1][3].clone()], to: vec![3 * T]),
        txn_schema!(from: vec![orphan1_outputs[2][0].clone()], to: vec![3 * T]),
    ];
    assert!(generate_new_block_with_achieved_difficulty(
        &mut orphan1_store,
        &mut orphan1_blocks,
        &mut orphan1_outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager
    )
    .is_ok());
    // Block B4
    let txs = vec![txn_schema!(from: vec![orphan1_outputs[3][0].clone()], to: vec![1 * T])];
    assert!(generate_new_block_with_achieved_difficulty(
        &mut orphan1_store,
        &mut orphan1_blocks,
        &mut orphan1_outputs,
        txs,
        Difficulty::from(5),
        &consensus_manager
    )
    .is_ok());

    // Create Forked Chain 2
    let mut orphan2_store = create_store_with_consensus(consensus_manager.clone());
    orphan2_store
        .add_block(blocks[1].to_arc_block())
        .unwrap()
        .assert_added(); // A1
    orphan2_store
        .add_block(orphan1_blocks[2].to_arc_block())
        .unwrap()
        .assert_added(); // B2
    orphan2_store
        .add_block(orphan1_blocks[3].to_arc_block())
        .unwrap()
        .assert_added(); // B3
    let mut orphan2_blocks = vec![
        blocks[0].clone(),
        blocks[1].clone(),
        orphan1_blocks[2].clone(),
        orphan1_blocks[3].clone(),
    ];
    let mut orphan2_outputs = vec![
        outputs[0].clone(),
        outputs[1].clone(),
        orphan1_outputs[2].clone(),
        orphan1_outputs[3].clone(),
    ];
    // Block C4
    let txs = vec![txn_schema!(from: vec![orphan2_outputs[3][1].clone()], to: vec![1 * T])];
    assert!(generate_new_block_with_achieved_difficulty(
        &mut orphan2_store,
        &mut orphan2_blocks,
        &mut orphan2_outputs,
        txs,
        Difficulty::from(20),
        &consensus_manager
    )
    .is_ok());

    // Now add the fork blocks C4, B2, B4 and B3 (out of order) to the first DB and observe a reorg. Blocks are added
    // out of order to test the forward and reverse chaining.
    store
        .add_block(orphan2_blocks[4].to_arc_block())
        .unwrap()
        .assert_orphaned(); // C4
    store
        .add_block(orphan1_blocks[2].to_arc_block())
        .unwrap()
        .assert_orphaned(); // B2
    store
        .add_block(orphan1_blocks[4].to_arc_block())
        .unwrap()
        .assert_orphaned(); // B4
    store
        .add_block(orphan1_blocks[3].to_arc_block())
        .unwrap()
        .assert_reorg(3, 3); // B3
    assert_eq!(store.fetch_tip_header().unwrap().header(), orphan2_blocks[4].header());

    // Check that B2,B3 and C4 were removed from the block orphans and A2,A3,A4 and B4 has been orphaned.
    assert!(store.fetch_orphan(*orphan1_blocks[2].hash()).is_err()); // B2
    assert!(store.fetch_orphan(*orphan1_blocks[3].hash()).is_err()); // B3
    assert!(store.fetch_orphan(*orphan2_blocks[4].hash()).is_err()); // C4
    assert!(store.fetch_orphan(*blocks[2].hash()).is_ok()); // A2
    assert!(store.fetch_orphan(*blocks[3].hash()).is_ok()); // A3
    assert!(store.fetch_orphan(*blocks[4].hash()).is_ok()); // A4
    assert!(store.fetch_orphan(*blocks[4].hash()).is_ok()); // B4
}

#[test]
#[allow(clippy::too_many_lines)]
fn test_reorgs_should_update_orphan_tips() {
    // Create a main chain GB -> A1 -> A2
    // Create an orphan chain GB -> B1
    // Add a block B2 that forces a reorg to B2
    // Check that A2 is in the orphan chain tips db
    // Add a block A3 that forces a reorg to A3
    // Check that B2 is in the orphan chain tips db
    // Add 2 blocks B3 and B4 that force a reorg to B4
    // Check that A3 is in the orphan chain tips db
    // Add 2 blocks A4 and A5 that force a reorg to A5
    // Check that B4 is in the orphan chain tips db

    let network = Network::LocalNet;
    let (store, blocks, outputs, consensus_manager) = create_new_blockchain(network);

    // Create "A" Chain
    let mut a_store = create_store_with_consensus(consensus_manager.clone());
    let mut a_blocks = vec![blocks[0].clone()];
    let mut a_outputs = vec![outputs[0].clone()];

    // Block A1
    let txs = vec![txn_schema!(from: vec![a_outputs[0][0].clone()], to: vec![50 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut a_store,
        &mut a_blocks,
        &mut a_outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager,
    )
    .unwrap();

    store.add_block(a_blocks[1].to_arc_block()).unwrap().assert_added();

    // Block A2
    let txs = vec![txn_schema!(from: vec![a_outputs[1][1].clone()], to: vec![30 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut a_store,
        &mut a_blocks,
        &mut a_outputs,
        txs,
        Difficulty::from(3),
        &consensus_manager,
    )
    .unwrap();

    store.add_block(a_blocks[2].to_arc_block()).unwrap().assert_added();
    let a2_hash = *a_blocks[2].hash();

    // Create "B" Chain
    let mut b_store = create_store_with_consensus(consensus_manager.clone());
    let mut b_blocks = vec![blocks[0].clone()];
    let mut b_outputs = vec![outputs[0].clone()];

    // Block B1
    let txs = vec![txn_schema!(from: vec![b_outputs[0][0].clone()], to: vec![50 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut b_store,
        &mut b_blocks,
        &mut b_outputs,
        txs,
        Difficulty::from(2),
        &consensus_manager,
    )
    .unwrap();

    store.add_block(b_blocks[1].to_arc_block()).unwrap().assert_orphaned();
    let b1_hash = *b_blocks[1].hash();

    // check that B1 is in orphan tips
    let orphan_tip_b1 = store
        .db_read_access()
        .unwrap()
        .fetch_orphan_chain_tip_by_hash(&b1_hash)
        .unwrap();
    assert!(orphan_tip_b1.is_some());
    assert_eq!(orphan_tip_b1.unwrap().hash(), &b1_hash);

    // Block B2
    let txs = vec![txn_schema!(from: vec![b_outputs[1][0].clone()], to: vec![40 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut b_store,
        &mut b_blocks,
        &mut b_outputs,
        txs,
        Difficulty::from(4),
        &consensus_manager,
    )
    .unwrap();

    store.add_block(b_blocks[2].to_arc_block()).unwrap().assert_reorg(2, 2);
    let b2_hash = *b_blocks[2].hash();

    // check that A2 is now in the orphan chain tip db
    let orphan_tip_a2 = store
        .db_read_access()
        .unwrap()
        .fetch_orphan_chain_tip_by_hash(&a2_hash)
        .unwrap();
    assert!(orphan_tip_a2.is_some());
    assert_eq!(orphan_tip_a2.unwrap().hash(), &a2_hash);

    // check that B1 was removed from orphan chain tips
    let orphan_tip_b1 = store
        .db_read_access()
        .unwrap()
        .fetch_orphan_chain_tip_by_hash(&b1_hash)
        .unwrap();
    assert!(orphan_tip_b1.is_none());

    // Block A3
    let txs = vec![txn_schema!(from: vec![a_outputs[2][0].clone()], to: vec![25 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut a_store,
        &mut a_blocks,
        &mut a_outputs,
        txs,
        Difficulty::from(5), // A chain accumulated difficulty 9
        &consensus_manager,
    )
    .unwrap();

    store.add_block(a_blocks[3].to_arc_block()).unwrap().assert_reorg(3, 2);
    let a3_hash = *a_blocks[3].hash();

    // check that B2 is now in the orphan chain tip db
    let orphan_tip_b2 = store
        .db_read_access()
        .unwrap()
        .fetch_orphan_chain_tip_by_hash(&b2_hash)
        .unwrap();
    assert!(orphan_tip_b2.is_some());
    assert_eq!(orphan_tip_b2.unwrap().hash(), &b2_hash);

    // check that A2 was removed from orphan chain tips
    let orphan_tip_a2 = store
        .db_read_access()
        .unwrap()
        .fetch_orphan_chain_tip_by_hash(&a2_hash)
        .unwrap();
    assert!(orphan_tip_a2.is_none());

    // Block B3
    let txs = vec![txn_schema!(from: vec![b_outputs[2][0].clone()], to: vec![30 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut b_store,
        &mut b_blocks,
        &mut b_outputs,
        txs,
        Difficulty::from(1), // B chain accumulated difficulty 7
        &consensus_manager,
    )
    .unwrap();

    store.add_block(b_blocks[3].to_arc_block()).unwrap().assert_orphaned();
    let b3_hash = *b_blocks[3].hash();

    // Block B4
    let txs = vec![txn_schema!(from: vec![b_outputs[3][0].clone()], to: vec![20 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut b_store,
        &mut b_blocks,
        &mut b_outputs,
        txs,
        Difficulty::from(5), // B chain accumulated difficulty 12
        &consensus_manager,
    )
    .unwrap();

    store.add_block(b_blocks[4].to_arc_block()).unwrap().assert_reorg(4, 3);
    let b4_hash = *b_blocks[4].hash();

    // check that A3 is now in the orphan chain tip db
    let orphan_tip_a3 = store
        .db_read_access()
        .unwrap()
        .fetch_orphan_chain_tip_by_hash(&a3_hash)
        .unwrap();
    assert!(orphan_tip_a3.is_some());
    assert_eq!(orphan_tip_a3.unwrap().hash(), &a3_hash);

    // check that B3 was removed from orphan chain tips
    let orphan_tip_b3 = store
        .db_read_access()
        .unwrap()
        .fetch_orphan_chain_tip_by_hash(&b3_hash)
        .unwrap();
    assert!(orphan_tip_b3.is_none());

    // Block A4
    let txs = vec![txn_schema!(from: vec![a_outputs[3][0].clone()], to: vec![20 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut a_store,
        &mut a_blocks,
        &mut a_outputs,
        txs,
        Difficulty::from(2), // A chain accumulated difficulty 11
        &consensus_manager,
    )
    .unwrap();

    store.add_block(a_blocks[4].to_arc_block()).unwrap().assert_orphaned();

    // Block A5
    let txs = vec![txn_schema!(from: vec![a_outputs[4][0].clone()], to: vec![10 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut a_store,
        &mut a_blocks,
        &mut a_outputs,
        txs,
        Difficulty::from(4), // A chain accumulated difficulty 15
        &consensus_manager,
    )
    .unwrap();

    store.add_block(a_blocks[5].to_arc_block()).unwrap().assert_reorg(5, 4);

    // check that B4 is now in the orphan chain tip db
    let orphan_tip_b4 = store
        .db_read_access()
        .unwrap()
        .fetch_orphan_chain_tip_by_hash(&b4_hash)
        .unwrap();
    assert!(orphan_tip_b4.is_some());
    assert_eq!(orphan_tip_b4.unwrap().hash(), &b4_hash);

    // check that A3 was removed from orphan chain tips
    let orphan_tip_a3 = store
        .db_read_access()
        .unwrap()
        .fetch_orphan_chain_tip_by_hash(&a3_hash)
        .unwrap();
    assert!(orphan_tip_a3.is_none());

    // Check that B1 - B4 are orphans
    assert!(store.fetch_orphan(*b_blocks[1].hash()).is_ok()); // B1
    assert!(store.fetch_orphan(*b_blocks[2].hash()).is_ok()); // B2
    assert!(store.fetch_orphan(*b_blocks[3].hash()).is_ok()); // B3
    assert!(store.fetch_orphan(*b_blocks[4].hash()).is_ok()); // B4

    // And blocks A1 - A5 are not
    assert!(store.fetch_orphan(*a_blocks[1].hash()).is_err()); // A1
    assert!(store.fetch_orphan(*a_blocks[2].hash()).is_err()); // A2
    assert!(store.fetch_orphan(*a_blocks[3].hash()).is_err()); // A3
    assert!(store.fetch_orphan(*a_blocks[4].hash()).is_err()); // A4
    assert!(store.fetch_orphan(*a_blocks[5].hash()).is_err()); // A5
}

#[test]
fn test_handle_reorg_with_no_removed_blocks() {
    // GB --> A1
    //          \--> B2 (?) --> B3)
    // Initially, the main chain is GB->A1 with orphaned blocks B3. When B2 arrives late and is
    // added to the blockchain then a reorg is triggered and the main chain is reorganized to GB->A1->B2->B3.

    // Create Main Chain
    let network = Network::LocalNet;
    let (mut store, mut blocks, mut outputs, consensus_manager) = create_new_blockchain(network);

    // Block A1
    let txs = vec![txn_schema!(
        from: vec![outputs[0][0].clone()],
        to: vec![10 * T, 10 * T, 10 * T, 10 * T]
    )];
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager,
    )
    .unwrap();

    // Create Forked Chain 1
    let mut orphan1_store = create_store_with_consensus(consensus_manager.clone());
    orphan1_store.add_block(blocks[1].to_arc_block()).unwrap(); // A1
    let mut orphan1_blocks = vec![blocks[0].clone(), blocks[1].clone()];
    let mut orphan1_outputs = vec![outputs[0].clone(), outputs[1].clone()];
    // Block B2
    let txs = vec![txn_schema!(from: vec![orphan1_outputs[1][0].clone()], to: vec![5 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut orphan1_store,
        &mut orphan1_blocks,
        &mut orphan1_outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager,
    )
    .unwrap();
    // Block B3
    let txs = vec![
        txn_schema!(from: vec![orphan1_outputs[1][3].clone()], to: vec![3 * T]),
        txn_schema!(from: vec![orphan1_outputs[2][0].clone()], to: vec![3 * T]),
    ];
    generate_new_block_with_achieved_difficulty(
        &mut orphan1_store,
        &mut orphan1_blocks,
        &mut orphan1_outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager,
    )
    .unwrap();

    // Now add the fork blocks B3 and B2 (out of order) to the first DB and ensure a reorg.
    // see https://github.com/tari-project/tari/issues/2101#issuecomment-679188619
    store.add_block(orphan1_blocks[3].to_arc_block()).unwrap(); // B3
    let result = store.add_block(orphan1_blocks[2].to_arc_block()).unwrap(); // B2
    match result {
        BlockAddResult::Ok(_) => panic!("Adding multiple blocks without removing any failed to cause a reorg!"),
        BlockAddResult::ChainReorg { removed, added } => {
            assert_eq!(added.len(), 2);
            assert_eq!(removed.len(), 0);
        },
        _ => panic!(),
    }

    assert_eq!(store.fetch_tip_header().unwrap().header(), orphan1_blocks[3].header());
}

#[test]
fn test_handle_reorg_failure_recovery() {
    // GB --> A1 --> A2 --> A3 -----> A4(Low PoW)     [Main Chain]
    //          \--> B2 --> B3(double spend - rejected by db)  [Forked Chain 1]
    //          \--> B2 --> B3'(validation failed)      [Forked Chain 1]
    // Checks the following cases:
    // 1. recovery from failure to commit reorged blocks, and
    // 2. recovery from failed block validation.

    let block_validator = MockValidator::new(true);
    let validators = Validators::new(block_validator, MockValidator::new(true), MockValidator::new(true));
    // Create Main Chain
    let network = Network::LocalNet;
    let (mut store, mut blocks, mut outputs, consensus_manager) =
        create_new_blockchain_lmdb(network, validators, Default::default());
    // Block A1
    let txs = vec![txn_schema!(
        from: vec![outputs[0][0].clone()],
        to: vec![10 * T, 10 * T, 10 * T, 10 * T]
    )];
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager,
    )
    .unwrap();
    // Block A2
    let txs = vec![txn_schema!(from: vec![outputs[1][3].clone()], to: vec![6 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager,
    )
    .unwrap();
    // Block A3
    let txs = vec![txn_schema!(from: vec![outputs[2][0].clone()], to: vec![2 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(2),
        &consensus_manager,
    )
    .unwrap();
    // Block A4
    let txs = vec![txn_schema!(from: vec![outputs[1][0].clone()], to: vec![2 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(2),
        &consensus_manager,
    )
    .unwrap();

    // Create Forked Chain 1
    let mut orphan1_store = create_store_with_consensus(consensus_manager.clone());
    orphan1_store.add_block(blocks[1].to_arc_block()).unwrap(); // A1
    let mut orphan1_blocks = vec![blocks[0].clone(), blocks[1].clone()];
    let mut orphan1_outputs = vec![outputs[0].clone(), outputs[1].clone()];
    // Block B2
    let txs = vec![txn_schema!(from: vec![orphan1_outputs[1][0].clone()], to: vec![5 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut orphan1_store,
        &mut orphan1_blocks,
        &mut orphan1_outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager,
    )
    .unwrap();
    // Block B3 (Incorrect height)
    let double_spend_block = {
        let schemas = vec![
            txn_schema!(from: vec![orphan1_outputs[1][3].clone()], to: vec![3 * T]),
            // Double spend
            // txn_schema!(from: vec![orphan1_outputs[1][3].clone()], to: vec![3 * T]),
        ];
        let mut txns = Vec::new();
        let mut block_utxos = Vec::new();
        for schema in schemas {
            let (tx, mut utxos) = spend_utxos(schema);
            txns.push(tx);
            block_utxos.append(&mut utxos);
        }
        orphan1_outputs.push(block_utxos);

        let template = chain_block(orphan1_blocks.last().unwrap().block(), txns, &consensus_manager);
        let mut block = orphan1_store.prepare_new_block(template).unwrap();
        block.header.nonce = OsRng.next_u64();
        block.header.height += 1;
        find_header_with_achieved_difficulty(&mut block.header, Difficulty::from_u64(2).unwrap());
        block
    };

    // Add an orphaned B2
    let result = store.add_block(orphan1_blocks[2].to_arc_block()).unwrap(); // B2
    unpack_enum!(BlockAddResult::OrphanBlock = result);

    // Add invalid block B3. Our database should recover
    let res = store.add_block(double_spend_block.into()).unwrap(); // B3
    unpack_enum!(BlockAddResult::OrphanBlock = res);
    let tip_header = store.fetch_tip_header().unwrap();
    assert_eq!(tip_header.height(), 4);
    assert_eq!(tip_header.header(), blocks[4].header());

    assert!(store.fetch_orphan(*blocks[2].hash()).is_err()); // A2
    assert!(store.fetch_orphan(*blocks[3].hash()).is_err()); // A3
    assert!(store.fetch_orphan(*blocks[4].hash()).is_err()); // A4
}

#[test]
fn test_store_and_retrieve_blocks() {
    let validators = Validators::new(
        MockValidator::new(true),
        MockValidator::new(true),
        MockValidator::new(true),
    );
    let network = Network::LocalNet;
    let rules = ConsensusManagerBuilder::new(network).build();
    let db = create_test_db();
    let store = BlockchainDatabase::new(
        db,
        rules.clone(),
        validators,
        BlockchainDatabaseConfig::default(),
        DifficultyCalculator::new(rules.clone(), Default::default()),
    )
    .unwrap();

    let block0 = store.fetch_block(0, true).unwrap();
    let block1 = append_block(
        &store,
        &block0.clone().try_into_chain_block().unwrap(),
        vec![],
        &rules,
        Difficulty::min(),
    )
    .unwrap();
    let block2 = append_block(&store, &block1, vec![], &rules, Difficulty::min()).unwrap();
    assert_eq!(
        store.fetch_block(0, true).unwrap().try_into_chain_block().unwrap(),
        block0.clone().try_into_chain_block().unwrap()
    );
    assert_eq!(
        store.fetch_block(1, true).unwrap().try_into_chain_block().unwrap(),
        block1
    );
    assert_eq!(
        store.fetch_block(2, true).unwrap().try_into_chain_block().unwrap(),
        block2
    );

    let block3 = append_block(&store, &block2, vec![], &rules, Difficulty::min()).unwrap();
    assert_eq!(
        store.fetch_block(0, true).unwrap().try_into_chain_block().unwrap(),
        block0.try_into_chain_block().unwrap()
    );
    assert_eq!(
        store.fetch_block(1, true).unwrap().try_into_chain_block().unwrap(),
        block1
    );
    assert_eq!(
        store.fetch_block(2, true).unwrap().try_into_chain_block().unwrap(),
        block2
    );
    assert_eq!(
        store.fetch_block(3, true).unwrap().try_into_chain_block().unwrap(),
        block3
    );
}

#[test]
#[allow(clippy::identity_op)]
fn test_store_and_retrieve_blocks_from_contents() {
    let network = Network::LocalNet;
    let (mut db, mut blocks, mut outputs, consensus_manager) = create_new_blockchain(network);

    // Block 1
    let schema = vec![txn_schema!(from: vec![outputs[0][0].clone()], to: vec![6 * T, 3 * T])];
    unpack_enum!(
        BlockAddResult::Ok(_b1) =
            generate_new_block(&mut db, &mut blocks, &mut outputs, schema, &consensus_manager).unwrap()
    );
    // Block 2
    let schema = vec![txn_schema!(from: vec![outputs[1][0].clone()], to: vec![3 * T, 1 * T])];
    unpack_enum!(
        BlockAddResult::Ok(_b2) =
            generate_new_block(&mut db, &mut blocks, &mut outputs, schema, &consensus_manager).unwrap()
    );
    let kernel_sig = blocks[1].block().body.kernels()[0].clone().excess_sig;
    let utxo_commit = blocks.last().unwrap().block().body.outputs()[0].clone().commitment;
    assert_eq!(
        db.fetch_block_with_kernel(kernel_sig)
            .unwrap()
            .unwrap()
            .try_into_chain_block()
            .unwrap(),
        blocks[1]
    );

    assert_eq!(
        db.fetch_block_with_utxo(utxo_commit)
            .unwrap()
            .unwrap()
            .try_into_chain_block()
            .unwrap(),
        blocks[2]
    );
}

#[test]
fn test_restore_metadata_and_pruning_horizon_update() {
    // Perform test
    let validators = Validators::new(
        MockValidator::new(true),
        MockValidator::new(true),
        MockValidator::new(true),
    );
    let network = Network::LocalNet;
    let block0 = genesis_block::get_esmeralda_genesis_block();
    let rules = ConsensusManagerBuilder::new(network).with_block(block0.clone()).build();
    let mut config = BlockchainDatabaseConfig::default();
    let block_hash: BlockHash;
    let temp_path = create_temporary_data_path();
    {
        let mut db = TempDatabase::from_path(&temp_path);
        db.disable_delete_on_drop();
        config.pruning_horizon = 1000;
        let db = BlockchainDatabase::new(
            db,
            rules.clone(),
            validators.clone(),
            config,
            DifficultyCalculator::new(rules.clone(), Default::default()),
        )
        .unwrap();

        let block1 = append_block(&db, &block0, vec![], &rules, Difficulty::min()).unwrap();
        db.add_block(block1.to_arc_block()).unwrap();
        block_hash = *block1.hash();
        let metadata = db.get_chain_metadata().unwrap();
        assert_eq!(metadata.best_block_height(), 1);
        assert_eq!(metadata.best_block(), &block_hash);
        assert_eq!(metadata.pruning_horizon(), 1000);
    }
    // Restore blockchain db with larger pruning horizon

    {
        config.pruning_horizon = 2000;
        let mut db = TempDatabase::from_path(&temp_path);
        db.disable_delete_on_drop();
        let db = BlockchainDatabase::new(
            db,
            rules.clone(),
            validators.clone(),
            config,
            DifficultyCalculator::new(rules.clone(), Default::default()),
        )
        .unwrap();

        let metadata = db.get_chain_metadata().unwrap();
        assert_eq!(metadata.best_block_height(), 1);
        assert_eq!(metadata.best_block(), &block_hash);
        assert_eq!(metadata.pruning_horizon(), 2000);
    }
    // Restore blockchain db with smaller pruning horizon update
    {
        config.pruning_horizon = 900;
        let db = TempDatabase::from_path(&temp_path);
        let db = BlockchainDatabase::new(
            db,
            rules.clone(),
            validators,
            config,
            DifficultyCalculator::new(rules, Default::default()),
        )
        .unwrap();

        let metadata = db.get_chain_metadata().unwrap();
        assert_eq!(metadata.best_block_height(), 1);
        assert_eq!(metadata.best_block(), &block_hash);
        assert_eq!(metadata.pruning_horizon(), 900);
    }
}
static EMISSION: [u64; 2] = [10, 10];
#[test]
fn test_invalid_block() {
    let factories = CryptoFactories::default();
    let network = Network::LocalNet;
    let consensus_constants = ConsensusConstantsBuilder::new(network)
        .with_emission_amounts(100_000_000.into(), &EMISSION, 100.into())
        .build();
    let (block0, output) = create_genesis_block( &consensus_constants);
    let consensus_manager = ConsensusManagerBuilder::new(network)
        .add_consensus_constants(consensus_constants)
        .with_block(block0.clone())
        .build();
    let validator = MockValidator::new(true);
    let is_valid = validator.shared_flag();
    let validators = Validators::new(MockValidator::new(true), MockValidator::new(true), validator);
    let mut store = create_store_with_consensus_and_validators(consensus_manager.clone(), validators);

    let mut blocks = vec![block0];
    let mut outputs = vec![vec![output]];
    let block0_hash = *blocks[0].hash();
    let metadata = store.get_chain_metadata().unwrap();
    assert_eq!(metadata.best_block_height(), 0);
    assert_eq!(metadata.best_block(), &block0_hash);
    assert_eq!(store.fetch_block(0, true).unwrap().block().hash(), block0_hash);
    assert!(store.fetch_block(1, true).is_err());

    // Block 1
    let txs = vec![txn_schema!(
        from: vec![outputs[0][0].clone()],
        to: vec![10 * T, 5 * T, 10 * T, 15 * T]
    )];
    let coinbase_value = consensus_manager.emission_schedule().block_reward(1);
    unpack_enum!(
        BlockAddResult::Ok(_b1) = generate_new_block_with_coinbase(
            &mut store,
            &factories,
            &mut blocks,
            &mut outputs,
            txs,
            coinbase_value,
            &consensus_manager
        )
        .unwrap()
    );
    let block1_hash = *blocks[1].hash();
    let metadata = store.get_chain_metadata().unwrap();
    assert_eq!(metadata.best_block_height(), 1);
    assert_eq!(metadata.best_block(), &block1_hash);
    assert_eq!(store.fetch_block(0, true).unwrap().hash(), &block0_hash);
    assert_eq!(store.fetch_block(1, true).unwrap().hash(), &block1_hash);
    assert!(store.fetch_block(2, true).is_err());

    // Invalid Block 2 - Double spends genesis block output
    is_valid.set(false);
    let txs = vec![txn_schema!(from: vec![outputs[0][0].clone()], to: vec![20 * T, 20 * T])];
    let coinbase_value = consensus_manager.emission_schedule().block_reward(2);
    unpack_enum!(
        ChainStorageError::InvalidOperation(_msg) = generate_new_block_with_coinbase(
            &mut store,
            &factories,
            &mut blocks,
            &mut outputs,
            txs,
            coinbase_value,
            &consensus_manager
        )
        .unwrap_err()
    );
    let metadata = store.get_chain_metadata().unwrap();
    assert_eq!(metadata.best_block_height(), 1);
    assert_eq!(metadata.best_block(), &block1_hash);
    assert_eq!(store.fetch_block(0, true).unwrap().hash(), &block0_hash);
    assert_eq!(store.fetch_block(1, true).unwrap().hash(), &block1_hash);
    assert!(store.fetch_block(2, true).is_err());

    // Valid Block 2
    is_valid.set(true);
    let txs = vec![txn_schema!(from: vec![outputs[1][0].clone()], to: vec![4 * T, 4 * T])];
    let coinbase_value = consensus_manager.emission_schedule().block_reward(2);
    unpack_enum!(
        BlockAddResult::Ok(_b1) = generate_new_block_with_coinbase(
            &mut store,
            &factories,
            &mut blocks,
            &mut outputs,
            txs,
            coinbase_value,
            &consensus_manager
        )
        .unwrap()
    );
    let block2_hash = blocks[2].hash();
    let metadata = store.get_chain_metadata().unwrap();
    assert_eq!(metadata.best_block_height(), 2);
    assert_eq!(metadata.best_block(), block2_hash);
    assert_eq!(store.fetch_block(0, true).unwrap().hash(), &block0_hash);
    assert_eq!(store.fetch_block(1, true).unwrap().hash(), &block1_hash);
    assert_eq!(store.fetch_block(2, true).unwrap().hash(), block2_hash);
    assert!(store.fetch_block(3, true).is_err());
}

#[test]
fn test_orphan_cleanup_on_block_add() {
    let network = Network::LocalNet;
    let consensus_manager = ConsensusManagerBuilder::new(network).build();
    let validators = Validators::new(
        MockValidator::new(true),
        MockValidator::new(true),
        MockValidator::new(true),
    );
    let db = create_test_db();
    let config = BlockchainDatabaseConfig {
        orphan_storage_capacity: 3,
        pruning_horizon: 0,
        pruning_interval: 50,
        ..Default::default()
    };
    let store = BlockchainDatabase::new(
        db,
        consensus_manager.clone(),
        validators,
        config,
        DifficultyCalculator::new(consensus_manager.clone(), Default::default()),
    )
    .unwrap();

    let orphan1 = create_orphan_block(500, vec![], &consensus_manager);
    let orphan2 = create_orphan_block(5, vec![], &consensus_manager);
    let orphan3 = create_orphan_block(30, vec![], &consensus_manager);
    let orphan4 = create_orphan_block(700, vec![], &consensus_manager);
    let orphan5 = create_orphan_block(43, vec![], &consensus_manager);
    let orphan6 = create_orphan_block(75, vec![], &consensus_manager);
    let orphan7 = create_orphan_block(150, vec![], &consensus_manager);
    let orphan1_hash = orphan1.hash();
    let orphan2_hash = orphan2.hash();
    let orphan3_hash = orphan3.hash();
    let orphan4_hash = orphan4.hash();
    let orphan5_hash = orphan5.hash();
    let orphan6_hash = orphan6.hash();
    let orphan7_hash = orphan7.hash();
    assert_eq!(
        store.add_block(orphan1.clone().into()).unwrap(),
        BlockAddResult::OrphanBlock
    );
    assert_eq!(store.add_block(orphan2.into()).unwrap(), BlockAddResult::OrphanBlock);
    assert_eq!(store.add_block(orphan3.into()).unwrap(), BlockAddResult::OrphanBlock);
    assert_eq!(
        store.add_block(orphan4.clone().into()).unwrap(),
        BlockAddResult::OrphanBlock
    );
    assert_eq!(store.add_block(orphan5.into()).unwrap(), BlockAddResult::OrphanBlock);
    assert_eq!(store.add_block(orphan6.into()).unwrap(), BlockAddResult::OrphanBlock);
    assert_eq!(
        store.add_block(orphan7.clone().into()).unwrap(),
        BlockAddResult::OrphanBlock
    );

    store.cleanup_orphans().unwrap();
    assert_eq!(store.db_read_access().unwrap().orphan_count().unwrap(), 3);
    assert_eq!(store.fetch_orphan(orphan1_hash).unwrap(), orphan1);
    assert!(store.fetch_orphan(orphan2_hash).is_err());
    assert!(store.fetch_orphan(orphan3_hash).is_err());
    assert_eq!(store.fetch_orphan(orphan4_hash).unwrap(), orphan4);
    assert!(store.fetch_orphan(orphan5_hash).is_err());
    assert!(store.fetch_orphan(orphan6_hash).is_err());
    assert_eq!(store.fetch_orphan(orphan7_hash).unwrap(), orphan7);
}

#[test]
fn test_horizon_height_orphan_cleanup() {
    let network = Network::LocalNet;
    let block0 = genesis_block::get_esmeralda_genesis_block();
    let consensus_manager = ConsensusManagerBuilder::new(network).with_block(block0.clone()).build();
    let validators = Validators::new(
        MockValidator::new(true),
        MockValidator::new(true),
        MockValidator::new(true),
    );
    let db = create_test_db();
    let config = BlockchainDatabaseConfig {
        orphan_storage_capacity: 3,
        pruning_horizon: 2,
        pruning_interval: 50,
        ..Default::default()
    };
    let store = BlockchainDatabase::new(
        db,
        consensus_manager.clone(),
        validators,
        config,
        DifficultyCalculator::new(consensus_manager.clone(), Default::default()),
    )
    .unwrap();
    let orphan1 = create_orphan_block(2, vec![], &consensus_manager);
    let orphan2 = create_orphan_block(3, vec![], &consensus_manager);
    let orphan3 = create_orphan_block(1, vec![], &consensus_manager);
    let orphan4 = create_orphan_block(4, vec![], &consensus_manager);
    let orphan1_hash = orphan1.hash();
    let orphan2_hash = orphan2.hash();
    let orphan3_hash = orphan3.hash();
    let orphan4_hash = orphan4.hash();
    assert_eq!(store.add_block(orphan1.into()).unwrap(), BlockAddResult::OrphanBlock);
    assert_eq!(
        store.add_block(orphan2.clone().into()).unwrap(),
        BlockAddResult::OrphanBlock
    );
    assert_eq!(store.add_block(orphan3.into()).unwrap(), BlockAddResult::OrphanBlock);
    assert_eq!(store.db_read_access().unwrap().orphan_count().unwrap(), 3);

    let block1 = append_block(&store, &block0, vec![], &consensus_manager, Difficulty::min()).unwrap();
    let block2 = append_block(&store, &block1, vec![], &consensus_manager, Difficulty::min()).unwrap();
    let block3 = append_block(&store, &block2, vec![], &consensus_manager, Difficulty::min()).unwrap();
    let _block4 = append_block(&store, &block3, vec![], &consensus_manager, Difficulty::min()).unwrap();

    // Adding another orphan block will trigger the orphan cleanup as the storage limit was reached
    assert_eq!(
        store.add_block(orphan4.clone().into()).unwrap(),
        BlockAddResult::OrphanBlock
    );

    assert_eq!(store.db_read_access().unwrap().orphan_count().unwrap(), 2);
    assert!(store.fetch_orphan(orphan1_hash).is_err());
    assert_eq!(store.fetch_orphan(orphan2_hash).unwrap(), orphan2);
    assert!(store.fetch_orphan(orphan3_hash).is_err());
    assert_eq!(store.fetch_orphan(orphan4_hash).unwrap(), orphan4);
}

#[test]
#[allow(clippy::too_many_lines)]
fn test_orphan_cleanup_on_reorg() {
    // Create Main Chain
    let network = Network::LocalNet;
    let factories = CryptoFactories::default();
    let consensus_constants = ConsensusConstantsBuilder::new(network).build();
    let (block0, output) = create_genesis_block( &consensus_constants);
    let consensus_manager = ConsensusManagerBuilder::new(network)
        .add_consensus_constants(consensus_constants)
        .with_block(block0.clone())
        .build();
    let validators = Validators::new(
        MockValidator::new(true),
        MockValidator::new(true),
        MockValidator::new(true),
    );
    let db = create_test_db();
    let config = BlockchainDatabaseConfig {
        orphan_storage_capacity: 3,
        pruning_horizon: 0,
        pruning_interval: 50,
        ..Default::default()
    };
    let mut store = BlockchainDatabase::new(
        db,
        consensus_manager.clone(),
        validators,
        config,
        DifficultyCalculator::new(consensus_manager.clone(), Default::default()),
    )
    .unwrap();
    let mut blocks = vec![block0];
    let mut outputs = vec![vec![output]];

    // Block A1
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        vec![],
        Difficulty::from(2),
        &consensus_manager,
    )
    .unwrap();
    // Block A2
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        vec![],
        Difficulty::from(3),
        &consensus_manager,
    )
    .unwrap();
    // Block A3
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        vec![],
        Difficulty::from(3),
        &consensus_manager,
    )
    .unwrap();
    // Block A4
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        vec![],
        Difficulty::from(3),
        &consensus_manager,
    )
    .unwrap();

    // Create Forked Chain
    let mut orphan_store = create_store_with_consensus(consensus_manager.clone());
    let mut orphan_blocks = vec![blocks[0].clone()];
    let mut orphan_outputs = vec![outputs[0].clone()];
    // Block B1
    generate_new_block_with_achieved_difficulty(
        &mut orphan_store,
        &mut orphan_blocks,
        &mut orphan_outputs,
        vec![],
        Difficulty::from(2),
        &consensus_manager,
    )
    .unwrap();
    // Block B2
    generate_new_block_with_achieved_difficulty(
        &mut orphan_store,
        &mut orphan_blocks,
        &mut orphan_outputs,
        vec![],
        Difficulty::from(10),
        &consensus_manager,
    )
    .unwrap();
    // Block B3
    generate_new_block_with_achieved_difficulty(
        &mut orphan_store,
        &mut orphan_blocks,
        &mut orphan_outputs,
        vec![],
        Difficulty::from(15),
        &consensus_manager,
    )
    .unwrap();

    // Fill orphan block pool
    let orphan1 = create_orphan_block(1, vec![], &consensus_manager);
    let orphan2 = create_orphan_block(1, vec![], &consensus_manager);
    assert_eq!(store.add_block(orphan1.into()).unwrap(), BlockAddResult::OrphanBlock);
    assert_eq!(store.add_block(orphan2.into()).unwrap(), BlockAddResult::OrphanBlock);

    // Adding B1 and B2 to the main chain will produce a reorg from GB->A1->A2->A3->A4 to GB->B1->B2->B3.
    assert_eq!(
        store.add_block(orphan_blocks[1].to_arc_block()).unwrap(),
        BlockAddResult::OrphanBlock
    );

    if let Ok(BlockAddResult::ChainReorg { .. }) = store.add_block(orphan_blocks[2].to_arc_block()) {
    } else {
        panic!();
    }

    // Check that A2, A3 and A4 is in the orphan block pool, A1 and the other orphans were discarded by the orphan
    // cleanup.
    store.cleanup_orphans().unwrap();
    assert_eq!(store.db_read_access().unwrap().orphan_count().unwrap(), 3);
    assert_eq!(store.fetch_orphan(*blocks[2].hash()).unwrap(), *blocks[2].block());
    assert_eq!(store.fetch_orphan(*blocks[3].hash()).unwrap(), *blocks[3].block());
    assert_eq!(store.fetch_orphan(*blocks[4].hash()).unwrap(), *blocks[4].block());
}

#[test]
fn test_orphan_cleanup_delete_all_orphans() {
    let path = create_temporary_data_path();
    let network = Network::LocalNet;
    let validators = Validators::new(
        MockValidator::new(true),
        MockValidator::new(true),
        MockValidator::new(true),
    );
    let mut config = BlockchainDatabaseConfig {
        orphan_storage_capacity: 5,
        pruning_horizon: 0,
        pruning_interval: 50,
        ..Default::default()
    };
    // Test cleanup during runtime
    {
        let consensus_manager = ConsensusManager::builder(network).build();
        let db = create_lmdb_database(&path, LMDBConfig::default(), consensus_manager.clone()).unwrap();
        let store = BlockchainDatabase::new(
            db,
            consensus_manager.clone(),
            validators.clone(),
            config,
            DifficultyCalculator::new(consensus_manager.clone(), Default::default()),
        )
        .unwrap();

        let orphan1 = create_orphan_block(500, vec![], &consensus_manager);
        let orphan2 = create_orphan_block(5, vec![], &consensus_manager);
        let orphan3 = create_orphan_block(30, vec![], &consensus_manager);
        let orphan4 = create_orphan_block(700, vec![], &consensus_manager);
        let orphan5 = create_orphan_block(43, vec![], &consensus_manager);

        // Add orphans and verify
        assert_eq!(
            store.add_block(orphan1.clone().into()).unwrap(),
            BlockAddResult::OrphanBlock
        );
        assert_eq!(
            store.add_block(orphan2.clone().into()).unwrap(),
            BlockAddResult::OrphanBlock
        );
        assert_eq!(
            store.add_block(orphan3.clone().into()).unwrap(),
            BlockAddResult::OrphanBlock
        );
        assert_eq!(
            store.add_block(orphan4.clone().into()).unwrap(),
            BlockAddResult::OrphanBlock
        );
        assert_eq!(
            store.add_block(orphan5.clone().into()).unwrap(),
            BlockAddResult::OrphanBlock
        );
        assert_eq!(store.db_read_access().unwrap().orphan_count().unwrap(), 5);

        // Cleanup orphans and verify
        assert!(store.cleanup_all_orphans().is_ok());
        assert_eq!(store.db_read_access().unwrap().orphan_count().unwrap(), 0);

        // Add orphans again
        assert_eq!(store.add_block(orphan1.into()).unwrap(), BlockAddResult::OrphanBlock);
        assert_eq!(store.add_block(orphan2.into()).unwrap(), BlockAddResult::OrphanBlock);
        assert_eq!(store.add_block(orphan3.into()).unwrap(), BlockAddResult::OrphanBlock);
        assert_eq!(store.add_block(orphan4.into()).unwrap(), BlockAddResult::OrphanBlock);
        assert_eq!(store.add_block(orphan5.into()).unwrap(), BlockAddResult::OrphanBlock);
    }

    // Test orphans are present on open
    {
        let consensus_manager = ConsensusManager::builder(Network::LocalNet).build();
        let db = create_lmdb_database(&path, LMDBConfig::default(), consensus_manager.clone()).unwrap();
        let store = BlockchainDatabase::new(
            db,
            consensus_manager.clone(),
            validators.clone(),
            config,
            DifficultyCalculator::new(consensus_manager, Default::default()),
        )
        .unwrap();
        assert_eq!(store.db_read_access().unwrap().orphan_count().unwrap(), 5);
    }

    // Test orphans cleanup on open
    {
        let consensus_manager = ConsensusManager::builder(Network::LocalNet).build();
        let db = create_lmdb_database(&path, LMDBConfig::default(), consensus_manager.clone()).unwrap();
        config.cleanup_orphans_at_startup = true;
        let store = BlockchainDatabase::new(
            db,
            consensus_manager.clone(),
            validators,
            config,
            DifficultyCalculator::new(consensus_manager, Default::default()),
        )
        .unwrap();
        assert_eq!(store.db_read_access().unwrap().orphan_count().unwrap(), 0);
    }

    if std::path::Path::new(&path).exists() {
        std::fs::remove_dir_all(&path).expect("Could not clean up directory")
    }
}

#[test]
fn test_fails_validation() {
    let network = Network::LocalNet;
    let factories = CryptoFactories::default();
    let consensus_constants = ConsensusConstantsBuilder::new(network).build();
    let (block0, output) = create_genesis_block(&factories, &consensus_constants);
    let consensus_manager = ConsensusManagerBuilder::new(network)
        .add_consensus_constants(consensus_constants)
        .with_block(block0.clone())
        .build();
    let validators = Validators::new(
        MockValidator::new(false),
        MockValidator::new(true),
        MockValidator::new(true),
    );
    let db = create_test_db();
    let config = BlockchainDatabaseConfig {
        orphan_storage_capacity: 3,
        pruning_horizon: 0,
        pruning_interval: 50,
        ..Default::default()
    };
    let mut store = BlockchainDatabase::new(
        db,
        consensus_manager.clone(),
        validators,
        config,
        DifficultyCalculator::new(consensus_manager.clone(), Default::default()),
    )
    .unwrap();
    let mut blocks = vec![block0];
    let mut outputs = vec![vec![]];

    let schemas = vec![txn_schema!(from: vec![output.clone()], to: vec![2 * T, 500_000 * uT])];
    let err = generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        schemas,
        Difficulty::from(3),
        &consensus_manager,
    )
    .unwrap_err();
    unpack_enum!(ChainStorageError::ValidationError { source } = err);
    unpack_enum!(ValidationError::CustomError(_s) = source);

    let metadata = store.get_chain_metadata().unwrap();
    assert_eq!(metadata.best_block_height(), 0);
}

#[test]
fn pruned_mode_cleanup_and_fetch_block() {
    let network = Network::LocalNet;
    let block0 = genesis_block::get_esmeralda_genesis_block();
    let consensus_manager = ConsensusManagerBuilder::new(network).with_block(block0.clone()).build();
    let validators = Validators::new(
        MockValidator::new(true),
        MockValidator::new(true),
        MockValidator::new(true),
    );
    let db = create_test_db();
    let config = BlockchainDatabaseConfig {
        orphan_storage_capacity: 3,
        pruning_horizon: 3,
        pruning_interval: 1,
        ..Default::default()
    };
    let store = BlockchainDatabase::new(
        db,
        consensus_manager.clone(),
        validators,
        config,
        DifficultyCalculator::new(consensus_manager.clone(), Default::default()),
    )
    .unwrap();
    let block1 = append_block(&store, &block0, vec![], &consensus_manager, Difficulty::min()).unwrap();
    let block2 = append_block(&store, &block1, vec![], &consensus_manager, Difficulty::min()).unwrap();
    let block3 = append_block(&store, &block2, vec![], &consensus_manager, Difficulty::min()).unwrap();

    let metadata = store.get_chain_metadata().unwrap();
    assert_eq!(metadata.pruned_height(), 0);

    let block4 = append_block(&store, &block3, vec![], &consensus_manager, Difficulty::min()).unwrap();
    let _block5 = append_block(&store, &block4, vec![], &consensus_manager, Difficulty::min()).unwrap();

    let metadata = store.get_chain_metadata().unwrap();
    assert_eq!(metadata.pruned_height(), 2);
    assert_eq!(metadata.best_block_height(), 5);
    assert_eq!(metadata.pruning_horizon(), 3);
}

mod malleability {
    use tari_common_types::types::{ComAndPubSignature, RangeProof};
    use tari_core::{
        blocks::Block,
        covenant,
        transactions::{test_helpers::generate_keys, transaction_components::TransactionOutputVersion},
    };
    use tari_script::{Opcode, TariScript};
    use tari_utilities::hex::Hex;

    use crate::helpers::block_malleability::*;

    mod input {
        use tari_core::transactions::transaction_components::TransactionInputVersion;
        use tari_script::StackItem;

        use super::*;

        // This test hightlights that the "version" field is not being included in the input hash
        // so a consensus change is needed for the input to include it
        #[test]
        fn test_version() {
            check_input_malleability(|block: &mut Block| {
                let input = &mut block.body.inputs_mut()[0];
                let mod_version = match input.version {
                    TransactionInputVersion::V0 => TransactionInputVersion::V1,
                    _ => TransactionInputVersion::V0,
                };
                input.version = mod_version;
            });
        }

        #[test]
        fn test_spent_output() {
            check_input_malleability(|block: &mut Block| {
                // to modify the spent output, we will substitue it for a copy of a different output
                // we will use one of the outputs of the current transaction
                // because of how the test blockchain is created, they will never be equal
                let output = &block.body.outputs()[0].clone();
                let input = &mut block.body.inputs_mut()[0];
                input.add_output_data(
                    output.version,
                    output.features.clone(),
                    output.commitment.clone(),
                    output.script.clone(),
                    output.sender_offset_public_key.clone(),
                    output.covenant.clone(),
                    output.encrypted_data,
                    output.minimum_value_promise,
                );
            });
        }

        #[test]
        fn test_input_data() {
            check_input_malleability(|block: &mut Block| {
                block.body.inputs_mut()[0]
                    .input_data
                    .push(StackItem::Hash(*b"I can't do whatever I want......"))
                    .unwrap();
            });
        }

        #[test]
        fn test_script_signature() {
            check_input_malleability(|block: &mut Block| {
                let input = &mut block.body.inputs_mut()[0];
                input.script_signature = ComAndPubSignature::default();
            });
        }
    }

    mod output {
        use super::*;

        #[test]
        fn test_version() {
            check_output_malleability(|block: &mut Block| {
                let output = &mut block.body.outputs_mut()[0];
                let mod_version = match output.version {
                    TransactionOutputVersion::V0 => TransactionOutputVersion::V1,
                    _ => TransactionOutputVersion::V0,
                };
                output.version = mod_version;
            });
        }

        #[test]
        fn test_features() {
            check_output_malleability(|block: &mut Block| {
                let output = &mut block.body.outputs_mut()[0];
                output.features.maturity += 1;
            });
        }

        #[test]
        fn test_commitment() {
            check_output_malleability(|block: &mut Block| {
                let output = &mut block.body.outputs_mut()[0];
                let mod_commitment = &output.commitment + &output.commitment;
                output.commitment = mod_commitment;
            });
        }

        #[test]
        fn test_proof() {
            check_output_malleability(|block: &mut Block| {
                let output = &mut block.body.outputs_mut()[0];
                let mod_proof = RangeProof::from_hex(&(output.proof.as_ref().unwrap().to_hex() + "00")).unwrap();
                output.proof = Some(mod_proof);
            });
        }

        #[test]
        fn test_script() {
            check_output_malleability(|block: &mut Block| {
                let output = &mut block.body.outputs_mut()[0];
                let mut script_bytes = output.script.to_bytes();
                Opcode::PushZero.to_bytes(&mut script_bytes);
                let mod_script = TariScript::from_bytes(&script_bytes).unwrap();
                output.script = mod_script;
            });
        }

        // This test hightlights that the "sender_offset_public_key" field is not being included in the output hash
        // so a consensus change is needed for the output to include it
        #[test]
        fn test_sender_offset_public_key() {
            check_output_malleability(|block: &mut Block| {
                let output = &mut block.body.outputs_mut()[0];

                // "gerate_keys" should return a random, different key than the present one
                let mod_pk = generate_keys().pk;
                output.sender_offset_public_key = mod_pk;
            });
        }

        #[test]
        fn test_metadata_signature() {
            check_output_malleability(|block: &mut Block| {
                let output = &mut block.body.outputs_mut()[0];
                output.metadata_signature = ComAndPubSignature::default();
            });
        }

        #[test]
        fn test_covenant() {
            check_output_malleability(|block: &mut Block| {
                let output = &mut block.body.outputs_mut()[0];
                let mod_covenant = covenant!(absolute_height(@uint(42))).unwrap();
                output.covenant = mod_covenant;
            });
        }
    }

    mod kernel {
        use tari_common_types::types::Signature;
        use tari_core::transactions::MicroMinotari;

        use super::*;

        // the "version" field only has one value (V0) so malleability test is not possible for it
        // the "features" field has only a constant value at the moment, so no malleability test possible

        #[test]
        fn test_fee() {
            check_kernel_malleability(|block: &mut Block| {
                let kernel = &mut block.body.kernels_mut()[0];
                kernel.fee += MicroMinotari::from(1);
            });
        }

        #[test]
        fn test_lock_height() {
            check_kernel_malleability(|block: &mut Block| {
                let kernel = &mut block.body.kernels_mut()[0];
                kernel.lock_height += 1;
            });
        }

        #[test]
        fn test_excess() {
            check_kernel_malleability(|block: &mut Block| {
                let kernel = &mut block.body.kernels_mut()[0];
                let mod_excess = &kernel.excess + &kernel.excess;
                kernel.excess = mod_excess;
            });
        }

        #[test]
        fn test_excess_sig() {
            check_kernel_malleability(|block: &mut Block| {
                let kernel = &mut block.body.kernels_mut()[0];
                // "gerate_keys" should return a group of random keys, different from the ones in the field
                let keys = generate_keys();
                kernel.excess_sig = Signature::new(keys.pk, keys.k);
            });
        }
    }
}

#[allow(clippy::identity_op)]
#[test]
fn test_fetch_deleted_position_block_hash() {
    // Create Main Chain
    let network = Network::LocalNet;
    let (mut store, mut blocks, mut outputs, consensus_manager) = create_new_blockchain(network);
    // Block 1
    let txs = vec![txn_schema!(
        from: vec![outputs[0][0].clone()],
        to: vec![11 * T, 12 * T, 13 * T, 14 * T]
    )];
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(1),
        &consensus_manager,
    )
    .unwrap()
    .assert_added();
    // Block 2
    let txs = vec![txn_schema!(from: vec![outputs[1][3].clone()], to: vec![6 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(3),
        &consensus_manager,
    )
    .unwrap()
    .assert_added();
    // Blocks 3 - 12 so we can test the search in the bottom and top half
    for i in 0..10 {
        generate_new_block_with_achieved_difficulty(
            &mut store,
            &mut blocks,
            &mut outputs,
            vec![],
            Difficulty::from(4 + i),
            &consensus_manager,
        )
        .unwrap()
        .assert_added();
    }
    // Block 13
    let txs = vec![txn_schema!(from: vec![outputs[2][0].clone()], to: vec![2 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(30),
        &consensus_manager,
    )
    .unwrap()
    .assert_added();
    // Block 14
    let txs = vec![txn_schema!(from: vec![outputs[13][0].clone()], to: vec![1 * T])];
    generate_new_block_with_achieved_difficulty(
        &mut store,
        &mut blocks,
        &mut outputs,
        txs,
        Difficulty::from(50),
        &consensus_manager,
    )
    .unwrap()
    .assert_added();

    let block1_hash = store.fetch_header(1).unwrap().unwrap().hash();
    let block2_hash = store.fetch_header(2).unwrap().unwrap().hash();
    let block13_hash = store.fetch_header(13).unwrap().unwrap().hash();
    let block14_hash = store.fetch_header(14).unwrap().unwrap().hash();

    let deleted_positions = store
        .fetch_complete_deleted_bitmap_at(block14_hash)
        .unwrap()
        .bitmap()
        .to_vec();

    let headers = store
        .fetch_header_hash_by_deleted_mmr_positions(deleted_positions)
        .unwrap();
    let mut headers = headers.into_iter().map(Option::unwrap).collect::<Vec<_>>();
    headers.sort_by(|(a, _), (b, _)| a.cmp(b));

    assert_eq!(headers[3], (14, block14_hash));
    assert_eq!(headers[2], (13, block13_hash));
    assert_eq!(headers[1], (2, block2_hash));
    assert_eq!(headers[0], (1, block1_hash));
}
