//! Inbound service tests with a fake peer set.

use std::{
    collections::HashSet,
    iter::{self, FromIterator},
    net::SocketAddr,
    str::FromStr,
    sync::Arc,
    time::Duration,
};

use futures::FutureExt;
use tokio::{sync::oneshot, task::JoinHandle, time::timeout};
use tower::{buffer::Buffer, builder::ServiceBuilder, util::BoxService, Service, ServiceExt};
use tracing::Span;

use zebra_chain::{
    amount::Amount,
    block::{Block, Height},
    fmt::humantime_seconds,
    parameters::Network::{self, *},
    serialization::ZcashDeserializeInto,
    transaction::{UnminedTx, UnminedTxId, VerifiedUnminedTx},
};
use zebra_consensus::{error::TransactionError, transaction, Config as ConsensusConfig};
use zebra_network::{
    constants::DEFAULT_MAX_CONNS_PER_IP, AddressBook, InventoryResponse, Request, Response,
};
use zebra_node_services::mempool;
use zebra_state::{ChainTipChange, Config as StateConfig, CHAIN_TIP_UPDATE_WAIT_LIMIT};
use zebra_test::mock_service::{MockService, PanicAssertion};

use crate::{
    components::{
        inbound::{downloads::MAX_INBOUND_CONCURRENCY, Inbound, InboundSetupData},
        mempool::{
            gossip_mempool_transaction_id, unmined_transactions_in_blocks, Config as MempoolConfig,
            Mempool, MempoolError, SameEffectsChainRejectionError, UnboxMempoolError,
        },
        sync::{self, BlockGossipError, SyncStatus, PEER_GOSSIP_DELAY},
    },
    BoxError,
};

use InventoryResponse::*;

/// Maximum time to wait for a network service request.
///
/// The default [`MockService`] value can be too short for some of these tests that take a little
/// longer than expected to actually send the network request.
///
/// Increasing this value causes the tests to take longer to complete, so it can't be too large.
const MAX_PEER_SET_REQUEST_DELAY: Duration = Duration::from_millis(500);

#[tokio::test(flavor = "multi_thread")]
async fn mempool_requests_for_transactions() {
    let (
        inbound_service,
        _mempool_guard,
        _committed_blocks,
        added_transactions,
        _mock_tx_verifier,
        mut peer_set,
        _state_guard,
        _chain_tip_change,
        sync_gossip_task_handle,
        tx_gossip_task_handle,
    ) = setup(true).await;

    let added_transactions: Vec<UnminedTx> = added_transactions
        .iter()
        .map(|t| t.transaction.clone())
        .collect();
    let added_transaction_ids: Vec<UnminedTxId> = added_transactions.iter().map(|t| t.id).collect();

    // Test `Request::MempoolTransactionIds`
    let response = inbound_service
        .clone()
        .oneshot(Request::MempoolTransactionIds)
        .await;
    match response {
        Ok(Response::TransactionIds(response)) => assert_eq!(response, added_transaction_ids),
        Ok(Response::Nil) => assert!(
            added_transaction_ids.is_empty(),
            "`MempoolTransactionIds` request should match added_transaction_ids {added_transaction_ids:?}, got Ok(Nil)"
        ),
        _ => unreachable!(
            "`MempoolTransactionIds` requests should always respond `Ok(Vec<UnminedTxId> | Nil)`, got {:?}",
            response
        ),
    };

    // Test `Request::TransactionsById`
    let hash_set = added_transaction_ids
        .iter()
        .copied()
        .collect::<HashSet<_>>();

    let response = inbound_service
        .clone()
        .oneshot(Request::TransactionsById(hash_set))
        .await;

    match response {
        Ok(Response::Transactions(response)) => {
            assert_eq!(
                response,
                added_transactions
                    .into_iter()
                    .map(Available)
                    .collect::<Vec<_>>(),
            )
        }
        _ => unreachable!("`TransactionsById` requests should always respond `Ok(Vec<UnminedTx>)`"),
    };

    // check that nothing unexpected happened
    peer_set.expect_no_requests().await;

    let sync_gossip_result = sync_gossip_task_handle.now_or_never();
    assert!(
        sync_gossip_result.is_none(),
        "unexpected error or panic in sync gossip task: {sync_gossip_result:?}",
    );

    let tx_gossip_result = tx_gossip_task_handle.now_or_never();
    assert!(
        tx_gossip_result.is_none(),
        "unexpected error or panic in transaction gossip task: {tx_gossip_result:?}",
    );
}

#[tokio::test(flavor = "multi_thread")]
async fn mempool_push_transaction() -> Result<(), crate::BoxError> {
    // get a block that has at least one non coinbase transaction
    let block: Arc<Block> =
        zebra_test::vectors::BLOCK_MAINNET_982681_BYTES.zcash_deserialize_into()?;

    // use the first transaction that is not coinbase
    let tx = block.transactions[1].clone();
    let test_transaction_id = tx.unmined_id();

    let (
        inbound_service,
        _mempool_guard,
        _committed_blocks,
        _added_transactions,
        mut tx_verifier,
        mut peer_set,
        _state_guard,
        _chain_tip_change,
        sync_gossip_task_handle,
        tx_gossip_task_handle,
    ) = setup(false).await;

    // Test `Request::PushTransaction`
    let request = inbound_service
        .clone()
        .oneshot(Request::PushTransaction(tx.clone().into()));
    // Simulate a successful transaction verification
    let verification = tx_verifier.expect_request_that(|_| true).map(|responder| {
        let transaction = responder
            .request()
            .clone()
            .mempool_transaction()
            .expect("unexpected non-mempool request");

        // Set a dummy fee and sigops.
        responder.respond(transaction::Response::from(
            VerifiedUnminedTx::new(
                transaction,
                Amount::try_from(1_000_000).expect("invalid value"),
                0,
            )
            .expect("verification should pass"),
        ));
    });

    let (push_response, _) = futures::join!(request, verification);

    assert_eq!(
        push_response.expect("unexpected error response from inbound service"),
        Response::Nil,
        "`PushTransaction` requests should always respond `Ok(Nil)`",
    );

    // Wait for the mempool to store the transaction
    tokio::time::sleep(Duration::from_millis(100)).await;

    // Use `Request::MempoolTransactionIds` to check the transaction was inserted to mempool
    let mempool_response = inbound_service
        .clone()
        .oneshot(Request::MempoolTransactionIds)
        .await;

    assert_eq!(
        mempool_response.expect("unexpected error response from mempool"),
        Response::TransactionIds(vec![test_transaction_id]),
        "`MempoolTransactionIds` requests should always respond `Ok(Vec<UnminedTxId> | Nil)`",
    );

    // Make sure there is an additional request broadcasting the
    // inserted transaction to peers.
    let mut hs = HashSet::new();
    hs.insert(tx.unmined_id());
    peer_set
        .expect_request(Request::AdvertiseTransactionIds(hs))
        .await
        .respond(Response::Nil);

    let sync_gossip_result = sync_gossip_task_handle.now_or_never();
    assert!(
        sync_gossip_result.is_none(),
        "unexpected error or panic in sync gossip task: {sync_gossip_result:?}",
    );

    let tx_gossip_result = tx_gossip_task_handle.now_or_never();
    assert!(
        tx_gossip_result.is_none(),
        "unexpected error or panic in transaction gossip task: {tx_gossip_result:?}",
    );

    Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn mempool_advertise_transaction_ids() -> Result<(), crate::BoxError> {
    // get a block that has at least one non coinbase transaction
    let block: Block = zebra_test::vectors::BLOCK_MAINNET_982681_BYTES.zcash_deserialize_into()?;

    // use the first transaction that is not coinbase
    let test_transaction = block
        .transactions
        .into_iter()
        .find(|tx| !tx.is_coinbase())
        .expect("at least one non-coinbase transaction");
    let test_transaction_id = test_transaction.unmined_id();
    let txs = HashSet::from_iter([test_transaction_id]);

    let (
        inbound_service,
        _mempool_guard,
        _committed_blocks,
        _added_transactions,
        mut tx_verifier,
        mut peer_set,
        _state_guard,
        _chain_tip_change,
        sync_gossip_task_handle,
        tx_gossip_task_handle,
    ) = setup(false).await;

    // Test `Request::AdvertiseTransactionIds`
    let request = inbound_service
        .clone()
        .oneshot(Request::AdvertiseTransactionIds(txs.clone()));
    // Ensure the mocked peer set responds
    let peer_set_responder =
        peer_set
            .expect_request(Request::TransactionsById(txs))
            .map(|responder| {
                let unmined_transaction = UnminedTx::from(test_transaction.clone());
                responder.respond(Response::Transactions(vec![Available(unmined_transaction)]))
            });
    // Simulate a successful transaction verification
    let verification = tx_verifier.expect_request_that(|_| true).map(|responder| {
        let transaction = responder
            .request()
            .clone()
            .mempool_transaction()
            .expect("unexpected non-mempool request");

        // Set a dummy fee and sigops.
        responder.respond(transaction::Response::from(
            VerifiedUnminedTx::new(
                transaction,
                Amount::try_from(1_000_000).expect("invalid value"),
                0,
            )
            .expect("verification should pass"),
        ));
    });

    let (advertise_response, _, _) = futures::join!(request, peer_set_responder, verification);

    assert_eq!(
        advertise_response.expect("unexpected error response from inbound service"),
        Response::Nil,
        "`AdvertiseTransactionIds` requests should always respond `Ok(Nil)`",
    );

    // Wait for the mempool to store the transaction
    tokio::time::sleep(Duration::from_millis(100)).await;

    // Use `Request::MempoolTransactionIds` to check the transaction was inserted to mempool
    let mempool_response = inbound_service
        .clone()
        .oneshot(Request::MempoolTransactionIds)
        .await;

    assert_eq!(
        mempool_response.expect("unexpected error response from mempool"),
        Response::TransactionIds(vec![test_transaction_id]),
        "`MempoolTransactionIds` requests should always respond `Ok(Vec<UnminedTxId> | Nil)`",
    );

    // Make sure there is an additional request broadcasting the
    // inserted transaction to peers.
    let mut hs = HashSet::new();
    hs.insert(test_transaction.unmined_id());
    peer_set
        .expect_request(Request::AdvertiseTransactionIds(hs))
        .await
        .respond(Response::Nil);

    let sync_gossip_result = sync_gossip_task_handle.now_or_never();
    assert!(
        sync_gossip_result.is_none(),
        "unexpected error or panic in sync gossip task: {sync_gossip_result:?}",
    );

    let tx_gossip_result = tx_gossip_task_handle.now_or_never();
    assert!(
        tx_gossip_result.is_none(),
        "unexpected error or panic in transaction gossip task: {tx_gossip_result:?}",
    );

    Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn mempool_transaction_expiration() -> Result<(), crate::BoxError> {
    // Get a block that has at least one non coinbase transaction
    let block: Block = zebra_test::vectors::BLOCK_MAINNET_982681_BYTES.zcash_deserialize_into()?;

    // Use the first transaction that is not coinbase to test expiration
    let tx1 = &*(block.transactions[1]).clone();
    let mut tx1_id = tx1.unmined_id();

    // Change the expiration height of the transaction to block 3
    let mut tx1 = tx1.clone();
    *tx1.expiry_height_mut() = zebra_chain::block::Height(3);

    // Use the second transaction that is not coinbase to trigger `remove_expired_transactions()`
    let tx2 = block.transactions[2].clone();
    let mut tx2_id = tx2.unmined_id();

    // Get services
    let (
        inbound_service,
        mempool,
        _committed_blocks,
        _added_transactions,
        mut tx_verifier,
        mut peer_set,
        state_service,
        _chain_tip_change,
        sync_gossip_task_handle,
        tx_gossip_task_handle,
    ) = setup(false).await;

    // Push test transaction
    let request = inbound_service
        .clone()
        .oneshot(Request::PushTransaction(tx1.clone().into()));
    // Simulate a successful transaction verification
    let verification = tx_verifier.expect_request_that(|_| true).map(|responder| {
        tx1_id = responder.request().tx_id();
        let transaction = responder
            .request()
            .clone()
            .mempool_transaction()
            .expect("unexpected non-mempool request");

        // Set a dummy fee and sigops.
        responder.respond(transaction::Response::from(
            VerifiedUnminedTx::new(
                transaction,
                Amount::try_from(1_000_000).expect("invalid value"),
                0,
            )
            .expect("verification should pass"),
        ));
    });

    let (push_response, _) = futures::join!(request, verification);

    assert_eq!(
        push_response.expect("unexpected error response from inbound service"),
        Response::Nil,
        "`PushTransaction` requests should always respond `Ok(Nil)`",
    );

    // Wait for the mempool to store the transaction
    tokio::time::sleep(Duration::from_millis(100)).await;

    // Use `Request::MempoolTransactionIds` to check the transaction was inserted to mempool
    let mempool_response = inbound_service
        .clone()
        .oneshot(Request::MempoolTransactionIds)
        .await;

    assert_eq!(
        mempool_response.expect("unexpected error response from mempool"),
        Response::TransactionIds(vec![tx1_id]),
        "`MempoolTransactionIds` requests should always respond `Ok(Vec<UnminedTxId> | Nil)`",
    );

    // Add a new block to the state (make the chain tip advance)
    let block_two: Arc<Block> = zebra_test::vectors::BLOCK_MAINNET_2_BYTES
        .zcash_deserialize_into()
        .unwrap();
    state_service
        .clone()
        .oneshot(zebra_state::Request::CommitCheckpointVerifiedBlock(
            block_two.clone().into(),
        ))
        .await
        .unwrap();

    // Test transaction 1 is gossiped
    let mut hs = HashSet::new();
    hs.insert(tx1_id);

    // Transaction and Block IDs are gossipped, in any order, after waiting for the gossip delay
    tokio::time::sleep(PEER_GOSSIP_DELAY).await;
    let possible_requests = &mut [
        Request::AdvertiseTransactionIds(hs),
        Request::AdvertiseBlock(block_two.hash()),
    ]
    .to_vec();

    peer_set
        .expect_request_that(|request| {
            let is_possible = possible_requests.contains(request);

            *possible_requests = possible_requests
                .clone()
                .into_iter()
                .filter(|possible| possible != request)
                .collect();

            is_possible
        })
        .await
        .respond(Response::Nil);

    peer_set
        .expect_request_that(|request| {
            let is_possible = possible_requests.contains(request);

            *possible_requests = possible_requests
                .clone()
                .into_iter()
                .filter(|possible| possible != request)
                .collect();

            is_possible
        })
        .await
        .respond(Response::Nil);

    // Make sure tx1 is still in the mempool as it is not expired yet.
    let request = inbound_service
        .clone()
        .oneshot(Request::MempoolTransactionIds)
        .await;

    match request {
        Ok(Response::TransactionIds(response)) => {
            assert_eq!(response, vec![tx1_id])
        }
        Ok(Response::Nil) => panic!(
            "response to `MempoolTransactionIds` request should match {:?}",
            vec![tx1_id]
        ),
        _ => unreachable!(
            "`MempoolTransactionIds` requests should always respond `Ok(Vec<UnminedTxId> | Nil)`"
        ),
    };

    // As our test transaction will expire at a block height greater or equal to 3 we need to push block 3.
    let block_three: Arc<Block> = zebra_test::vectors::BLOCK_MAINNET_3_BYTES
        .zcash_deserialize_into()
        .unwrap();
    state_service
        .clone()
        .oneshot(zebra_state::Request::CommitCheckpointVerifiedBlock(
            block_three.clone().into(),
        ))
        .await
        .unwrap();

    // Test the block is gossiped, after waiting for the multi-gossip delay
    tokio::time::sleep(PEER_GOSSIP_DELAY).await;
    peer_set
        .expect_request(Request::AdvertiseBlock(block_three.hash()))
        .await
        .respond(Response::Nil);

    // Push a second transaction to trigger `remove_expired_transactions()`
    let request = inbound_service
        .clone()
        .oneshot(Request::PushTransaction(tx2.clone().into()));
    // Simulate a successful transaction verification
    let verification = tx_verifier.expect_request_that(|_| true).map(|responder| {
        tx2_id = responder.request().tx_id();
        let transaction = responder
            .request()
            .clone()
            .mempool_transaction()
            .expect("unexpected non-mempool request");

        // Set a dummy fee and sigops.
        responder.respond(transaction::Response::from(
            VerifiedUnminedTx::new(
                transaction,
                Amount::try_from(1_000_000).expect("invalid value"),
                0,
            )
            .expect("verification should pass"),
        ));
    });

    let (push_response, _) = futures::join!(request, verification);

    assert_eq!(
        push_response.expect("unexpected error response from inbound service"),
        Response::Nil,
        "`PushTransaction` requests should always respond `Ok(Nil)`",
    );

    // Wait for the mempool to store the transaction
    tokio::time::sleep(Duration::from_millis(100)).await;

    // Use `Request::MempoolTransactionIds` to check the transaction was inserted to mempool
    let mempool_response = inbound_service
        .clone()
        .oneshot(Request::MempoolTransactionIds)
        .await;

    // Only tx2 will be in the mempool while tx1 was expired
    assert_eq!(
        mempool_response.expect("unexpected error response from mempool"),
        Response::TransactionIds(vec![tx2_id]),
        "`MempoolTransactionIds` requests should always respond `Ok(Vec<UnminedTxId> | Nil)`",
    );

    // Check if tx1 was added to the rejected list as well
    let response = mempool
        .clone()
        .oneshot(mempool::Request::Queue(vec![tx1_id.into()]))
        .await
        .unwrap();

    let queued_responses = match response {
        mempool::Response::Queued(queue_responses) => queue_responses,
        _ => unreachable!("will never happen in this test"),
    };

    assert_eq!(queued_responses.len(), 1);
    assert_eq!(
        queued_responses
            .into_iter()
            .next()
            .unwrap()
            .unbox_mempool_error(),
        MempoolError::StorageEffectsChain(SameEffectsChainRejectionError::Expired)
    );

    // Test transaction 2 is gossiped, after waiting for the multi-gossip delay
    tokio::time::sleep(PEER_GOSSIP_DELAY).await;

    let mut hs = HashSet::new();
    hs.insert(tx2_id);
    peer_set
        .expect_request(Request::AdvertiseTransactionIds(hs))
        .await
        .respond(Response::Nil);

    // Add all the rest of the continuous blocks we have to test tx2 will never expire.
    let more_blocks: Vec<Arc<Block>> = vec![
        zebra_test::vectors::BLOCK_MAINNET_4_BYTES
            .zcash_deserialize_into()
            .unwrap(),
        zebra_test::vectors::BLOCK_MAINNET_5_BYTES
            .zcash_deserialize_into()
            .unwrap(),
        zebra_test::vectors::BLOCK_MAINNET_6_BYTES
            .zcash_deserialize_into()
            .unwrap(),
    ];
    for block in more_blocks {
        state_service
            .clone()
            .oneshot(zebra_state::Request::CommitCheckpointVerifiedBlock(
                block.clone().into(),
            ))
            .await
            .unwrap();

        // Test the block is gossiped, after waiting for the multi-gossip delay
        tokio::time::sleep(PEER_GOSSIP_DELAY).await;
        peer_set
            .expect_request(Request::AdvertiseBlock(block.hash()))
            .await
            .respond(Response::Nil);

        let request = inbound_service
            .clone()
            .oneshot(Request::MempoolTransactionIds)
            .await;

        // tx2 is still in the mempool as the blockchain progress because the zero expiration height
        match request {
            Ok(Response::TransactionIds(response)) => {
                assert_eq!(response, vec![tx2_id])
            }
            Ok(Response::Nil) => panic!(
                "response to `MempoolTransactionIds` request should match {:?}",
                vec![tx2_id]
            ),
            _ => unreachable!(
                "`MempoolTransactionIds` requests should always respond `Ok(Vec<UnminedTxId> | Nil)`"
            ),
        };
    }

    // check that nothing unexpected happened
    peer_set.expect_no_requests().await;

    let sync_gossip_result = sync_gossip_task_handle.now_or_never();
    assert!(
        sync_gossip_result.is_none(),
        "unexpected error or panic in sync gossip task: {sync_gossip_result:?}",
    );

    let tx_gossip_result = tx_gossip_task_handle.now_or_never();
    assert!(
        tx_gossip_result.is_none(),
        "unexpected error or panic in transaction gossip task: {tx_gossip_result:?}",
    );

    Ok(())
}

/// Test that the inbound downloader rejects blocks above the lookahead limit.
///
/// TODO: also test that it rejects blocks behind the tip limit. (Needs ~100 fake blocks.)
#[tokio::test(flavor = "multi_thread")]
async fn inbound_block_height_lookahead_limit() -> Result<(), crate::BoxError> {
    // Get services
    let (
        inbound_service,
        _mempool,
        _committed_blocks,
        _added_transactions,
        mut tx_verifier,
        mut peer_set,
        state_service,
        mut chain_tip_change,
        sync_gossip_task_handle,
        tx_gossip_task_handle,
    ) = setup(false).await;

    // Get the next block
    let block: Arc<Block> = zebra_test::vectors::BLOCK_MAINNET_2_BYTES.zcash_deserialize_into()?;
    let block_hash = block.hash();

    // Push test block hash
    let _request = inbound_service
        .clone()
        .oneshot(Request::AdvertiseBlock(block_hash))
        .await?;

    // Block is fetched, and committed to the state
    peer_set
        .expect_request(Request::BlocksByHash(iter::once(block_hash).collect()))
        .await
        .respond(Response::Blocks(vec![Available(block)]));

    // Wait for the chain tip update
    if let Err(timeout_error) = timeout(
        CHAIN_TIP_UPDATE_WAIT_LIMIT,
        chain_tip_change.wait_for_tip_change(),
    )
    .await
    .map(|change_result| change_result.expect("unexpected chain tip update failure"))
    {
        info!(
            timeout = ?humantime_seconds(CHAIN_TIP_UPDATE_WAIT_LIMIT),
            ?timeout_error,
            "timeout waiting for chain tip change after committing block"
        );
    }

    // check that nothing unexpected happened
    peer_set.expect_no_requests().await;
    tx_verifier.expect_no_requests().await;

    // Get a block that is a long way away from genesis
    let block: Arc<Block> =
        zebra_test::vectors::BLOCK_MAINNET_982681_BYTES.zcash_deserialize_into()?;
    let block_hash = block.hash();

    // Push test block hash
    let _request = inbound_service
        .clone()
        .oneshot(Request::AdvertiseBlock(block_hash))
        .await?;

    // Block is fetched, but the downloader drops it because it is too high
    peer_set
        .expect_request(Request::BlocksByHash(iter::once(block_hash).collect()))
        .await
        .respond(Response::Blocks(vec![Available(block)]));

    let response = state_service
        .clone()
        .oneshot(zebra_state::Request::Depth(block_hash))
        .await?;
    assert_eq!(response, zebra_state::Response::Depth(None));

    // TODO: check that the block is not queued in the checkpoint verifier or non-finalized state

    // check that nothing unexpected happened
    peer_set.expect_no_requests().await;
    tx_verifier.expect_no_requests().await;

    let sync_gossip_result = sync_gossip_task_handle.now_or_never();
    assert!(
        sync_gossip_result.is_none(),
        "unexpected error or panic in sync gossip task: {sync_gossip_result:?}",
    );

    let tx_gossip_result = tx_gossip_task_handle.now_or_never();
    assert!(
        tx_gossip_result.is_none(),
        "unexpected error or panic in transaction gossip task: {tx_gossip_result:?}",
    );

    Ok(())
}

/// Setup a fake Zebra network stack, with fake peer set.
///
/// Adds some initial state blocks, and mempool transactions if `add_transactions` is true.
///
/// Uses a real block verifier, but a fake transaction verifier.
/// Does not run a block syncer task.
async fn setup(
    add_transactions: bool,
) -> (
    Buffer<
        BoxService<zebra_network::Request, zebra_network::Response, BoxError>,
        zebra_network::Request,
    >,
    Buffer<BoxService<mempool::Request, mempool::Response, BoxError>, mempool::Request>,
    Vec<Arc<Block>>,
    Vec<VerifiedUnminedTx>,
    MockService<transaction::Request, transaction::Response, PanicAssertion, TransactionError>,
    MockService<Request, Response, PanicAssertion>,
    Buffer<BoxService<zebra_state::Request, zebra_state::Response, BoxError>, zebra_state::Request>,
    ChainTipChange,
    JoinHandle<Result<(), BlockGossipError>>,
    JoinHandle<Result<(), BoxError>>,
) {
    let _init_guard = zebra_test::init();

    let network = Mainnet;
    let consensus_config = ConsensusConfig::default();
    let state_config = StateConfig::ephemeral();
    let address_book = AddressBook::new(
        SocketAddr::from_str("0.0.0.0:0").unwrap(),
        Mainnet,
        DEFAULT_MAX_CONNS_PER_IP,
        Span::none(),
    );
    let address_book = Arc::new(std::sync::Mutex::new(address_book));
    let (sync_status, mut recent_syncs) = SyncStatus::new();

    // UTXO verification doesn't matter for these tests.
    let (state, _read_only_state_service, latest_chain_tip, mut chain_tip_change) =
        zebra_state::init(state_config.clone(), network, Height::MAX, 0);

    let mut state_service = ServiceBuilder::new().buffer(1).service(state);

    // Download task panics and timeouts are propagated to the tests that use Groth16 verifiers.
    let (block_verifier, _transaction_verifier, _groth16_download_handle, _max_checkpoint_height) =
        zebra_consensus::router::init(
            consensus_config.clone(),
            network,
            state_service.clone(),
            true,
        )
        .await;

    let mut peer_set = MockService::build()
        .with_max_request_delay(MAX_PEER_SET_REQUEST_DELAY)
        .for_unit_tests();
    let buffered_peer_set = Buffer::new(BoxService::new(peer_set.clone()), 10);

    let mock_tx_verifier = MockService::build().for_unit_tests();
    let buffered_tx_verifier = Buffer::new(BoxService::new(mock_tx_verifier.clone()), 10);

    let mut committed_blocks = Vec::new();

    // Push the genesis block to the state.
    // This must be done before creating the mempool to avoid `chain_tip_change`
    // returning "reset" which would clear the mempool.
    let genesis_block: Arc<Block> = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES
        .zcash_deserialize_into()
        .unwrap();
    state_service
        .ready()
        .await
        .unwrap()
        .call(zebra_state::Request::CommitCheckpointVerifiedBlock(
            genesis_block.clone().into(),
        ))
        .await
        .unwrap();
    committed_blocks.push(genesis_block);

    // Wait for the chain tip update
    if let Err(timeout_error) = timeout(
        CHAIN_TIP_UPDATE_WAIT_LIMIT,
        chain_tip_change.wait_for_tip_change(),
    )
    .await
    .map(|change_result| change_result.expect("unexpected chain tip update failure"))
    {
        info!(
            timeout = ?humantime_seconds(CHAIN_TIP_UPDATE_WAIT_LIMIT),
            ?timeout_error,
            "timeout waiting for chain tip change after committing block"
        );
    }

    // Also push block 1.
    // Block one is a network upgrade and the mempool will be cleared at it,
    // let all our tests start after this event.
    let block_one: Arc<Block> = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
        .zcash_deserialize_into()
        .unwrap();
    state_service
        .clone()
        .oneshot(zebra_state::Request::CommitCheckpointVerifiedBlock(
            block_one.clone().into(),
        ))
        .await
        .unwrap();
    committed_blocks.push(block_one);

    // Don't wait for the chain tip update here, we wait for expect_request(AdvertiseBlock) below,
    // which is called by the gossip_best_tip_block_hashes task once the chain tip changes.

    let (mut mempool_service, transaction_receiver) = Mempool::new(
        &MempoolConfig::default(),
        buffered_peer_set.clone(),
        state_service.clone(),
        buffered_tx_verifier.clone(),
        sync_status.clone(),
        latest_chain_tip.clone(),
        chain_tip_change.clone(),
    );

    // Pretend we're close to tip
    SyncStatus::sync_close_to_tip(&mut recent_syncs);

    let sync_gossip_task_handle = tokio::spawn(sync::gossip_best_tip_block_hashes(
        sync_status.clone(),
        chain_tip_change.clone(),
        peer_set.clone(),
    ));

    let tx_gossip_task_handle = tokio::spawn(gossip_mempool_transaction_id(
        transaction_receiver,
        peer_set.clone(),
    ));

    // Make sure there is an additional request broadcasting the
    // committed blocks to peers.
    //
    // (The genesis block gets skipped, because block 1 is committed before the task is spawned.)
    for block in committed_blocks.iter().skip(1) {
        peer_set
            .expect_request(Request::AdvertiseBlock(block.hash()))
            .await
            .respond(Response::Nil);
    }

    // Enable the mempool
    // Note: this needs to be done after the mock peer set service has received the AdvertiseBlock
    // request to ensure that the call to `last_tip_change` returns the chain tip block for block_one
    // and not the genesis block, or else the transactions from the genesis block will be added to
    // the mempool storage's rejection list and tests will fail.
    mempool_service.enable(&mut recent_syncs).await;

    // Add transactions to the mempool, skipping verification and broadcast
    let mut added_transactions = Vec::new();
    if add_transactions {
        added_transactions.extend(add_some_stuff_to_mempool(&mut mempool_service, network));
    }

    let mempool_service = BoxService::new(mempool_service);
    let mempool_service = ServiceBuilder::new().buffer(1).service(mempool_service);

    let (setup_tx, setup_rx) = oneshot::channel();

    let inbound_service = ServiceBuilder::new()
        .load_shed()
        .service(Inbound::new(MAX_INBOUND_CONCURRENCY, setup_rx));
    let inbound_service = BoxService::new(inbound_service);
    let inbound_service = ServiceBuilder::new().buffer(1).service(inbound_service);

    let setup_data = InboundSetupData {
        address_book,
        block_download_peer_set: buffered_peer_set,
        block_verifier,
        mempool: mempool_service.clone(),
        state: state_service.clone(),
        latest_chain_tip,
    };
    let r = setup_tx.send(setup_data);
    // We can't expect or unwrap because the returned Result does not implement Debug
    assert!(r.is_ok(), "unexpected setup channel send failure");

    (
        inbound_service,
        mempool_service,
        committed_blocks,
        added_transactions,
        mock_tx_verifier,
        peer_set,
        state_service,
        chain_tip_change,
        sync_gossip_task_handle,
        tx_gossip_task_handle,
    )
}

/// Manually add a transaction to the mempool storage.
///
/// Skips some mempool functionality, like transaction verification and peer broadcasts.
fn add_some_stuff_to_mempool(
    mempool_service: &mut Mempool,
    network: Network,
) -> Vec<VerifiedUnminedTx> {
    // get the genesis block coinbase transaction from the Zcash blockchain.
    let genesis_transactions: Vec<_> = unmined_transactions_in_blocks(..=0, network)
        .take(1)
        .collect();

    // Insert the genesis block coinbase transaction into the mempool storage.
    mempool_service
        .storage()
        .insert(genesis_transactions[0].clone())
        .unwrap();

    genesis_transactions
}
