use {
    futures_util::StreamExt,
    serde_json::{json, Value},
    serial_test::serial,
    solana_client::{
        nonblocking,
        pubsub_client::PubsubClient,
        rpc_client::RpcClient,
        rpc_config::{
            RpcAccountInfoConfig, RpcBlockSubscribeConfig, RpcBlockSubscribeFilter,
            RpcProgramAccountsConfig,
        },
        rpc_response::SlotInfo,
    },
    solana_ledger::{blockstore::Blockstore, get_tmp_ledger_path},
    solana_rpc::{
        optimistically_confirmed_bank_tracker::OptimisticallyConfirmedBank,
        rpc::{create_test_transaction_entries, populate_blockstore_for_tests},
        rpc_pubsub_service::{PubSubConfig, PubSubService},
        rpc_subscriptions::RpcSubscriptions,
    },
    solana_runtime::{
        bank::Bank,
        bank_forks::BankForks,
        commitment::{BlockCommitmentCache, CommitmentSlots},
        genesis_utils::{create_genesis_config, GenesisConfigInfo},
    },
    solana_sdk::{
        clock::Slot,
        commitment_config::{CommitmentConfig, CommitmentLevel},
        native_token::sol_to_lamports,
        pubkey::Pubkey,
        rpc_port,
        signature::{Keypair, Signer},
        system_program, system_transaction,
    },
    solana_streamer::socket::SocketAddrSpace,
    solana_test_validator::TestValidator,
    solana_transaction_status::{
        BlockEncodingOptions, ConfirmedBlock, TransactionDetails, UiTransactionEncoding,
    },
    std::{
        collections::HashSet,
        net::{IpAddr, SocketAddr},
        sync::{
            atomic::{AtomicBool, AtomicU64, Ordering},
            Arc, RwLock,
        },
        thread::sleep,
        time::{Duration, Instant},
    },
    systemstat::Ipv4Addr,
};

#[test]
fn test_rpc_client() {
    solana_logger::setup();

    let alice = Keypair::new();
    let test_validator =
        TestValidator::with_no_fees(alice.pubkey(), None, SocketAddrSpace::Unspecified);

    let bob_pubkey = solana_sdk::pubkey::new_rand();

    let client = RpcClient::new(test_validator.rpc_url());

    assert_eq!(
        client.get_version().unwrap().solana_core,
        solana_version::semver!()
    );

    assert!(client.get_account(&bob_pubkey).is_err());

    assert_eq!(client.get_balance(&bob_pubkey).unwrap(), 0);

    let original_alice_balance = client.get_balance(&alice.pubkey()).unwrap();

    let blockhash = client.get_latest_blockhash().unwrap();

    let tx = system_transaction::transfer(&alice, &bob_pubkey, sol_to_lamports(20.0), blockhash);
    let signature = client.send_transaction(&tx).unwrap();

    let mut confirmed_tx = false;

    let now = Instant::now();
    while now.elapsed().as_secs() <= 20 {
        let response = client
            .confirm_transaction_with_commitment(&signature, CommitmentConfig::default())
            .unwrap();

        if response.value {
            confirmed_tx = true;
            break;
        }

        sleep(Duration::from_millis(500));
    }

    assert!(confirmed_tx);

    assert_eq!(
        client.get_balance(&bob_pubkey).unwrap(),
        sol_to_lamports(20.0)
    );
    assert_eq!(
        client.get_balance(&alice.pubkey()).unwrap(),
        original_alice_balance - sol_to_lamports(20.0)
    );
}

#[test]
#[serial]
fn test_account_subscription() {
    let pubsub_addr = SocketAddr::new(
        IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
        rpc_port::DEFAULT_RPC_PUBSUB_PORT,
    );
    let exit = Arc::new(AtomicBool::new(false));

    let GenesisConfigInfo {
        genesis_config,
        mint_keypair: alice,
        ..
    } = create_genesis_config(10_000);
    let bank = Bank::new_for_tests(&genesis_config);
    let blockhash = bank.last_blockhash();
    let bank_forks = Arc::new(RwLock::new(BankForks::new(bank)));
    let bank0 = bank_forks.read().unwrap().get(0).unwrap().clone();
    let bank1 = Bank::new_from_parent(&bank0, &Pubkey::default(), 1);
    bank_forks.write().unwrap().insert(bank1);
    let bob = Keypair::new();
    let max_complete_transaction_status_slot = Arc::new(AtomicU64::default());
    let subscriptions = Arc::new(RpcSubscriptions::new_for_tests(
        &exit,
        max_complete_transaction_status_slot,
        bank_forks.clone(),
        Arc::new(RwLock::new(BlockCommitmentCache::default())),
        OptimisticallyConfirmedBank::locked_from_bank_forks_root(&bank_forks),
    ));
    let (trigger, pubsub_service) =
        PubSubService::new(PubSubConfig::default(), &subscriptions, pubsub_addr);
    std::thread::sleep(Duration::from_millis(400));
    let config = Some(RpcAccountInfoConfig {
        commitment: Some(CommitmentConfig::finalized()),
        encoding: None,
        data_slice: None,
    });
    let (mut client, receiver) = PubsubClient::account_subscribe(
        &format!("ws://0.0.0.0:{}/", pubsub_addr.port()),
        &bob.pubkey(),
        config,
    )
    .unwrap();

    // Transfer 100 lamports from alice to bob
    let tx = system_transaction::transfer(&alice, &bob.pubkey(), 100, blockhash);
    bank_forks
        .write()
        .unwrap()
        .get(1)
        .unwrap()
        .process_transaction(&tx)
        .unwrap();
    let commitment_slots = CommitmentSlots {
        slot: 1,
        ..CommitmentSlots::default()
    };
    subscriptions.notify_subscribers(commitment_slots);
    let commitment_slots = CommitmentSlots {
        slot: 2,
        root: 1,
        highest_confirmed_slot: 1,
        highest_confirmed_root: 1,
    };
    subscriptions.notify_subscribers(commitment_slots);

    let expected = json!({
    "context": { "slot": 1 },
        "value": {
            "owner": system_program::id().to_string(),
            "lamports": 100,
            "data": "",
            "executable": false,
            "rentEpoch": 0,
        },
    });

    // Read notification
    let mut errors: Vec<(Value, Value)> = Vec::new();
    let response = receiver.recv();
    match response {
        Ok(response) => {
            let actual = serde_json::to_value(response).unwrap();
            if expected != actual {
                errors.push((expected, actual));
            }
        }
        Err(_) => eprintln!("unexpected websocket receive timeout"),
    }

    exit.store(true, Ordering::Relaxed);
    trigger.cancel();
    client.shutdown().unwrap();
    pubsub_service.close().unwrap();
    assert_eq!(errors, [].to_vec());
}

#[test]
#[serial]
fn test_block_subscription() {
    // setup BankForks
    let exit = Arc::new(AtomicBool::new(false));
    let GenesisConfigInfo {
        genesis_config,
        mint_keypair: alice,
        ..
    } = create_genesis_config(10_000);
    let bank = Bank::new_for_tests(&genesis_config);
    let rent_exempt_amount = bank.get_minimum_balance_for_rent_exemption(0);
    let bank_forks = Arc::new(RwLock::new(BankForks::new(bank)));

    // setup Blockstore
    let ledger_path = get_tmp_ledger_path!();
    let blockstore = Blockstore::open(&ledger_path).unwrap();
    let blockstore = Arc::new(blockstore);

    // populate ledger with test txs
    let bank = bank_forks.read().unwrap().working_bank();
    let keypair1 = Keypair::new();
    let keypair2 = Keypair::new();
    let keypair3 = Keypair::new();
    let max_complete_transaction_status_slot = Arc::new(AtomicU64::new(blockstore.max_root()));
    bank.transfer(rent_exempt_amount, &alice, &keypair2.pubkey())
        .unwrap();
    populate_blockstore_for_tests(
        create_test_transaction_entries(
            vec![&alice, &keypair1, &keypair2, &keypair3],
            bank.clone(),
        )
        .0,
        bank,
        blockstore.clone(),
        max_complete_transaction_status_slot,
    );
    let max_complete_transaction_status_slot = Arc::new(AtomicU64::default());
    // setup RpcSubscriptions && PubSubService
    let subscriptions = Arc::new(RpcSubscriptions::new_for_tests_with_blockstore(
        &exit,
        max_complete_transaction_status_slot,
        blockstore.clone(),
        bank_forks.clone(),
        Arc::new(RwLock::new(BlockCommitmentCache::default())),
        OptimisticallyConfirmedBank::locked_from_bank_forks_root(&bank_forks),
    ));
    let pubsub_addr = SocketAddr::new(
        IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
        rpc_port::DEFAULT_RPC_PUBSUB_PORT,
    );
    let pub_cfg = PubSubConfig {
        enable_block_subscription: true,
        ..PubSubConfig::default()
    };
    let (trigger, pubsub_service) = PubSubService::new(pub_cfg, &subscriptions, pubsub_addr);

    std::thread::sleep(Duration::from_millis(400));

    // setup PubsubClient
    let (mut client, receiver) = PubsubClient::block_subscribe(
        &format!("ws://0.0.0.0:{}/", pubsub_addr.port()),
        RpcBlockSubscribeFilter::All,
        Some(RpcBlockSubscribeConfig {
            commitment: Some(CommitmentConfig {
                commitment: CommitmentLevel::Confirmed,
            }),
            encoding: Some(UiTransactionEncoding::Json),
            transaction_details: Some(TransactionDetails::Signatures),
            show_rewards: None,
            max_supported_transaction_version: None,
        }),
    )
    .unwrap();

    // trigger Gossip notification
    let slot = bank_forks.read().unwrap().highest_slot();
    subscriptions.notify_gossip_subscribers(slot);
    let maybe_actual = receiver.recv_timeout(Duration::from_millis(400));
    match maybe_actual {
        Ok(actual) => {
            let versioned_block = blockstore.get_complete_block(slot, false).unwrap();
            let confirmed_block = ConfirmedBlock::from(versioned_block);
            let block = confirmed_block
                .encode_with_options(
                    UiTransactionEncoding::Json,
                    BlockEncodingOptions {
                        transaction_details: TransactionDetails::Signatures,
                        show_rewards: false,
                        max_supported_transaction_version: None,
                    },
                )
                .unwrap();
            assert_eq!(actual.value.slot, slot);
            assert!(block.eq(&actual.value.block.unwrap()));
        }
        Err(e) => {
            eprintln!("unexpected websocket receive timeout");
            assert_eq!(Some(e), None);
        }
    }

    // cleanup
    exit.store(true, Ordering::Relaxed);
    trigger.cancel();
    client.shutdown().unwrap();
    pubsub_service.close().unwrap();
}

#[test]
#[serial]
fn test_program_subscription() {
    let pubsub_addr = SocketAddr::new(
        IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
        rpc_port::DEFAULT_RPC_PUBSUB_PORT,
    );
    let exit = Arc::new(AtomicBool::new(false));

    let GenesisConfigInfo {
        genesis_config,
        mint_keypair: alice,
        ..
    } = create_genesis_config(10_000);
    let bank = Bank::new_for_tests(&genesis_config);
    let blockhash = bank.last_blockhash();
    let bank_forks = Arc::new(RwLock::new(BankForks::new(bank)));
    let bank0 = bank_forks.read().unwrap().get(0).unwrap().clone();
    let bank1 = Bank::new_from_parent(&bank0, &Pubkey::default(), 1);
    bank_forks.write().unwrap().insert(bank1);
    let bob = Keypair::new();
    let max_complete_transaction_status_slot = Arc::new(AtomicU64::default());
    let subscriptions = Arc::new(RpcSubscriptions::new_for_tests(
        &exit,
        max_complete_transaction_status_slot,
        bank_forks.clone(),
        Arc::new(RwLock::new(BlockCommitmentCache::default())),
        OptimisticallyConfirmedBank::locked_from_bank_forks_root(&bank_forks),
    ));
    let (trigger, pubsub_service) =
        PubSubService::new(PubSubConfig::default(), &subscriptions, pubsub_addr);
    std::thread::sleep(Duration::from_millis(400));
    let config = Some(RpcProgramAccountsConfig {
        ..RpcProgramAccountsConfig::default()
    });

    let program_id = Pubkey::new_unique();
    let (mut client, receiver) = PubsubClient::program_subscribe(
        &format!("ws://0.0.0.0:{}/", pubsub_addr.port()),
        &program_id,
        config,
    )
    .unwrap();

    // Create new program account at bob's address
    let tx = system_transaction::create_account(&alice, &bob, blockhash, 100, 0, &program_id);
    bank_forks
        .write()
        .unwrap()
        .get(1)
        .unwrap()
        .process_transaction(&tx)
        .unwrap();
    let commitment_slots = CommitmentSlots {
        slot: 1,
        ..CommitmentSlots::default()
    };
    subscriptions.notify_subscribers(commitment_slots);
    let commitment_slots = CommitmentSlots {
        slot: 2,
        root: 1,
        highest_confirmed_slot: 1,
        highest_confirmed_root: 1,
    };
    subscriptions.notify_subscribers(commitment_slots);

    // Poll notifications generated by the transfer
    let mut notifications = Vec::new();
    let mut pubkeys = HashSet::new();
    loop {
        let response = receiver.recv_timeout(Duration::from_millis(100));
        match response {
            Ok(response) => {
                notifications.push(response.clone());
                pubkeys.insert(response.value.pubkey);
            }
            Err(_) => {
                break;
            }
        }
    }

    // Shutdown
    exit.store(true, Ordering::Relaxed);
    trigger.cancel();
    client.shutdown().unwrap();
    pubsub_service.close().unwrap();

    assert_eq!(notifications.len(), 1);
    assert!(pubkeys.contains(&bob.pubkey().to_string()));
}

#[test]
#[serial]
fn test_root_subscription() {
    let pubsub_addr = SocketAddr::new(
        IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
        rpc_port::DEFAULT_RPC_PUBSUB_PORT,
    );
    let exit = Arc::new(AtomicBool::new(false));

    let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(10_000);
    let bank = Bank::new_for_tests(&genesis_config);
    let bank_forks = Arc::new(RwLock::new(BankForks::new(bank)));
    let bank0 = bank_forks.read().unwrap().get(0).unwrap().clone();
    let bank1 = Bank::new_from_parent(&bank0, &Pubkey::default(), 1);
    bank_forks.write().unwrap().insert(bank1);
    let max_complete_transaction_status_slot = Arc::new(AtomicU64::default());
    let subscriptions = Arc::new(RpcSubscriptions::new_for_tests(
        &exit,
        max_complete_transaction_status_slot,
        bank_forks.clone(),
        Arc::new(RwLock::new(BlockCommitmentCache::default())),
        OptimisticallyConfirmedBank::locked_from_bank_forks_root(&bank_forks),
    ));
    let (trigger, pubsub_service) =
        PubSubService::new(PubSubConfig::default(), &subscriptions, pubsub_addr);
    std::thread::sleep(Duration::from_millis(400));
    let (mut client, receiver) =
        PubsubClient::root_subscribe(&format!("ws://0.0.0.0:{}/", pubsub_addr.port())).unwrap();

    let roots = vec![1, 2, 3];
    subscriptions.notify_roots(roots.clone());

    // Read notifications
    let mut errors: Vec<(Slot, Slot)> = Vec::new();
    for expected in roots {
        let response = receiver.recv();
        match response {
            Ok(response) => {
                if expected != response {
                    errors.push((expected, response));
                }
            }
            Err(_) => eprintln!("unexpected websocket receive timeout"),
        }
    }

    exit.store(true, Ordering::Relaxed);
    trigger.cancel();
    client.shutdown().unwrap();
    pubsub_service.close().unwrap();
    assert_eq!(errors, [].to_vec());
}

#[test]
#[serial]
fn test_slot_subscription() {
    let pubsub_addr = SocketAddr::new(
        IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
        rpc_port::DEFAULT_RPC_PUBSUB_PORT,
    );
    let exit = Arc::new(AtomicBool::new(false));
    let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(10_000);
    let bank = Bank::new_for_tests(&genesis_config);
    let bank_forks = Arc::new(RwLock::new(BankForks::new(bank)));
    let optimistically_confirmed_bank =
        OptimisticallyConfirmedBank::locked_from_bank_forks_root(&bank_forks);
    let max_complete_transaction_status_slot = Arc::new(AtomicU64::default());
    let subscriptions = Arc::new(RpcSubscriptions::new_for_tests(
        &exit,
        max_complete_transaction_status_slot,
        bank_forks,
        Arc::new(RwLock::new(BlockCommitmentCache::default())),
        optimistically_confirmed_bank,
    ));
    let (trigger, pubsub_service) =
        PubSubService::new(PubSubConfig::default(), &subscriptions, pubsub_addr);
    std::thread::sleep(Duration::from_millis(400));

    let (mut client, receiver) =
        PubsubClient::slot_subscribe(&format!("ws://0.0.0.0:{}/", pubsub_addr.port())).unwrap();

    let mut errors: Vec<(SlotInfo, SlotInfo)> = Vec::new();

    for i in 0..3 {
        subscriptions.notify_slot(i + 1, i, i);

        let maybe_actual = receiver.recv_timeout(Duration::from_millis(400));

        match maybe_actual {
            Ok(actual) => {
                let expected = SlotInfo {
                    slot: i + 1,
                    parent: i,
                    root: i,
                };

                if actual != expected {
                    errors.push((actual, expected));
                }
            }
            Err(_err) => {
                eprintln!("unexpected websocket receive timeout");
                break;
            }
        }
    }

    exit.store(true, Ordering::Relaxed);
    trigger.cancel();
    client.shutdown().unwrap();
    pubsub_service.close().unwrap();

    assert_eq!(errors, [].to_vec());
}

#[tokio::test]
#[serial]
async fn test_slot_subscription_async() {
    let sync_service = Arc::new(AtomicU64::new(0));
    let sync_client = Arc::clone(&sync_service);
    fn wait_until(atomic: &Arc<AtomicU64>, value: u64) {
        while atomic.load(Ordering::Relaxed) != value {
            sleep(Duration::from_millis(1))
        }
    }

    let pubsub_addr = SocketAddr::new(
        IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
        rpc_port::DEFAULT_RPC_PUBSUB_PORT,
    );

    tokio::task::spawn_blocking(move || {
        let exit = Arc::new(AtomicBool::new(false));
        let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(10_000);
        let bank = Bank::new_for_tests(&genesis_config);
        let bank_forks = Arc::new(RwLock::new(BankForks::new(bank)));
        let optimistically_confirmed_bank =
            OptimisticallyConfirmedBank::locked_from_bank_forks_root(&bank_forks);
        let max_complete_transaction_status_slot = Arc::new(AtomicU64::default());
        let subscriptions = Arc::new(RpcSubscriptions::new_for_tests(
            &exit,
            max_complete_transaction_status_slot,
            bank_forks,
            Arc::new(RwLock::new(BlockCommitmentCache::default())),
            optimistically_confirmed_bank,
        ));
        let (trigger, pubsub_service) =
            PubSubService::new(PubSubConfig::default(), &subscriptions, pubsub_addr);
        sleep(Duration::from_millis(100));
        sync_service.store(1, Ordering::Relaxed);

        wait_until(&sync_service, 2);
        subscriptions.notify_slot(1, 0, 0);
        sync_service.store(3, Ordering::Relaxed);

        wait_until(&sync_service, 4);
        subscriptions.notify_slot(2, 1, 1);
        sync_service.store(5, Ordering::Relaxed);

        wait_until(&sync_service, 6);
        exit.store(true, Ordering::Relaxed);
        trigger.cancel();
        pubsub_service.close().unwrap();
    });

    wait_until(&sync_client, 1);
    let url = format!("ws://0.0.0.0:{}/", pubsub_addr.port());
    let pubsub_client = nonblocking::pubsub_client::PubsubClient::new(&url)
        .await
        .unwrap();
    let (mut notifications, unsubscribe) = pubsub_client.slot_subscribe().await.unwrap();
    sync_client.store(2, Ordering::Relaxed);

    wait_until(&sync_client, 3);
    assert_eq!(
        tokio::time::timeout(Duration::from_millis(25), notifications.next()).await,
        Ok(Some(SlotInfo {
            slot: 1,
            parent: 0,
            root: 0,
        }))
    );
    sync_client.store(4, Ordering::Relaxed);

    wait_until(&sync_client, 5);
    assert_eq!(
        tokio::time::timeout(Duration::from_millis(25), notifications.next()).await,
        Ok(Some(SlotInfo {
            slot: 2,
            parent: 1,
            root: 1,
        }))
    );
    sync_client.store(6, Ordering::Relaxed);

    unsubscribe().await;
}
