#![cfg(test)]

use super::*;
use crate::mock::*;

use frame_support::{assert_err, assert_ok, dispatch};
use sp_core::offchain::{
    testing,
    testing::{TestOffchainExt, TestTransactionPoolExt},
    OffchainDbExt, OffchainWorkerExt, TransactionPoolExt,
};
use sp_runtime::{testing::UintAuthorityId, DispatchError};

use ghost_networks::BridgedInflationCurve;
use pallet_staking::EraPayout;

const MAX_DEVIATION_DEPTH: u64 = 10;

fn prepare_evm_network(
    maybe_network_id: Option<u32>,
    maybe_incoming_commission: Option<u32>,
) -> NetworkData {
    let network_data = NetworkData {
        chain_name: "Ethereum".into(),
        default_endpoints: get_rpc_endpoints(),
        finality_delay: 69,
        rate_limit_delay: 69,
        block_distance: 69,
        network_type: ghost_networks::NetworkType::Evm,
        gatekeeper: get_gatekeeper(),
        topic_name: get_topic_name(),
        incoming_fee: maybe_incoming_commission.unwrap_or_default(),
        outgoing_fee: 0,
    };

    assert_ok!(Networks::register_network(
        RuntimeOrigin::root(),
        maybe_network_id.unwrap_or(1u32),
        network_data.clone()
    ));

    network_data
}

fn do_clap_from_first_authority(
    session_index: u32,
    network_id: u32,
    authority: u64,
) -> dispatch::DispatchResult {
    let (transaction_hash, receiver, amount) = get_mocked_metadata();
    let clap = Clap {
        block_number: 420,
        removed: false,
        transaction_hash,
        session_index,
        authority_index: 0,
        network_id,
        receiver,
        amount,
    };
    let authority = UintAuthorityId::from(authority);
    let signature = authority.sign(&clap.encode()).unwrap();

    SlowClap::pre_dispatch(&crate::Call::slow_clap {
        clap: clap.clone(),
        signature: signature.clone(),
    })
    .map_err(|e| <&'static str>::from(e))?;

    SlowClap::slow_clap(RuntimeOrigin::none(), clap, signature)
}

fn do_clap_from(
    session_index: u32,
    network_id: u32,
    authority_index: u32,
    transaction_removed: bool,
) -> dispatch::DispatchResult {
    let (transaction_hash, receiver, amount) = get_mocked_metadata();
    let clap = Clap {
        block_number: 420,
        removed: transaction_removed,
        transaction_hash,
        session_index,
        authority_index,
        network_id,
        receiver,
        amount,
    };
    let authority = UintAuthorityId::from((authority_index + 1) as u64);
    let signature = authority.sign(&clap.encode()).unwrap();

    SlowClap::pre_dispatch(&crate::Call::slow_clap {
        clap: clap.clone(),
        signature: signature.clone(),
    })
    .map_err(|e| <&'static str>::from(e))?;

    SlowClap::slow_clap(RuntimeOrigin::none(), clap, signature)
}

#[test]
fn test_throttling_slash_function() {
    let dummy_offence = ThrottlingOffence {
        session_index: 0,
        validator_set_count: 50,
        offenders: vec![()],
    };

    assert_eq!(dummy_offence.slash_fraction(1), Perbill::zero());
    assert_eq!(dummy_offence.slash_fraction(5), Perbill::zero());
    assert_eq!(
        dummy_offence.slash_fraction(7),
        Perbill::from_parts(4200000)
    );
    assert_eq!(
        dummy_offence.slash_fraction(17),
        Perbill::from_parts(46200000)
    );
}

#[test]
fn test_median_calculations_are_correct() {
    new_test_ext().execute_with(|| {
        let data = BTreeMap::from([
            (
                0u32,
                SessionAuthorityInfo {
                    claps: 0,
                    disabled: true,
                },
            ),
            (
                3u32,
                SessionAuthorityInfo {
                    claps: 1,
                    disabled: false,
                },
            ),
        ]);
        assert_eq!(SlowClap::calculate_median_claps(&data, 4), 0);

        let data = BTreeMap::from([
            (
                0u32,
                SessionAuthorityInfo {
                    claps: 0,
                    disabled: false,
                },
            ),
            (
                1u32,
                SessionAuthorityInfo {
                    claps: 69,
                    disabled: false,
                },
            ),
            (
                2u32,
                SessionAuthorityInfo {
                    claps: 69,
                    disabled: false,
                },
            ),
            (
                3u32,
                SessionAuthorityInfo {
                    claps: 420,
                    disabled: false,
                },
            ),
        ]);
        assert_eq!(SlowClap::calculate_median_claps(&data, 4), 69);

        let data = BTreeMap::from([
            (
                0u32,
                SessionAuthorityInfo {
                    claps: 31,
                    disabled: false,
                },
            ),
            (
                1u32,
                SessionAuthorityInfo {
                    claps: 420,
                    disabled: true,
                },
            ),
            (
                2u32,
                SessionAuthorityInfo {
                    claps: 69,
                    disabled: false,
                },
            ),
            (
                3u32,
                SessionAuthorityInfo {
                    claps: 156,
                    disabled: true,
                },
            ),
        ]);
        assert_eq!(SlowClap::calculate_median_claps(&data, 4), 50);

        let data = BTreeMap::from([
            (
                0u32,
                SessionAuthorityInfo {
                    claps: 0,
                    disabled: true,
                },
            ),
            (
                1u32,
                SessionAuthorityInfo {
                    claps: 420,
                    disabled: false,
                },
            ),
            (
                2u32,
                SessionAuthorityInfo {
                    claps: 0,
                    disabled: true,
                },
            ),
            (
                3u32,
                SessionAuthorityInfo {
                    claps: 0,
                    disabled: false,
                },
            ),
        ]);
        assert_eq!(SlowClap::calculate_median_claps(&data, 4), 210);

        let data = BTreeMap::from([
            (
                0u32,
                SessionAuthorityInfo {
                    claps: 0,
                    disabled: false,
                },
            ),
            (
                1u32,
                SessionAuthorityInfo {
                    claps: 420,
                    disabled: true,
                },
            ),
            (
                2u32,
                SessionAuthorityInfo {
                    claps: 69,
                    disabled: true,
                },
            ),
            (
                3u32,
                SessionAuthorityInfo {
                    claps: 0,
                    disabled: false,
                },
            ),
        ]);
        assert_eq!(SlowClap::calculate_median_claps(&data, 4), 0);
    });
}

#[test]
fn request_body_is_correct_for_get_block_number() {
    let (offchain, _) = TestOffchainExt::new();
    let mut t = sp_io::TestExternalities::default();
    t.register_extension(OffchainWorkerExt::new(offchain));

    t.execute_with(|| {
        let network_data = prepare_evm_network(Some(1), None);
        let request_body = SlowClap::prepare_request_body_for_latest_block(&network_data);
        assert_eq!(
            core::str::from_utf8(&request_body).unwrap(),
            r#"{"id":0,"jsonrpc":"2.0","method":"eth_blockNumber"}"#
        );
    });
}

#[test]
fn request_body_is_correct_for_get_logs() {
    let (offchain, _) = TestOffchainExt::new();
    let mut t = sp_io::TestExternalities::default();
    t.register_extension(OffchainWorkerExt::new(offchain));

    t.execute_with(|| {
        let from_block: u64 = 420;
        let to_block: u64 = 1337;
        let network_data = prepare_evm_network(Some(1), None);
        let request_body = SlowClap::prepare_request_body_for_latest_transfers(
            from_block, to_block, &network_data);
        assert_eq!(core::str::from_utf8(&request_body).unwrap(),
            r#"{"id":0,"jsonrpc":"2.0","method":"eth_getLogs","params":[{"fromBlock":"0x1a4","toBlock":"0x539","address":"0x4d224452801ACEd8B2F0aebE155379bb5D594381","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]}]}"#,
        );
    });
}

#[test]
fn should_make_http_call_for_block_number() {
    let (offchain, state) = TestOffchainExt::new();
    let mut t = sp_io::TestExternalities::default();
    t.register_extension(OffchainWorkerExt::new(offchain));

    evm_block_response(&mut state.write());

    let _: Result<(), OffchainErr<u32>> = t.execute_with(|| {
        let rpc_endpoint = get_rpc_endpoint();
        let network_data = prepare_evm_network(Some(1), None);
        let request_body = SlowClap::prepare_request_body_for_latest_block(&network_data);
        let raw_response = SlowClap::fetch_from_remote(&rpc_endpoint, &request_body)?;
        assert_eq!(raw_response.len(), 45usize); // precalculated
        Ok(())
    });
}

#[test]
fn should_make_http_call_for_logs() {
    let (offchain, state) = TestOffchainExt::new();
    let mut t = sp_io::TestExternalities::default();
    t.register_extension(OffchainWorkerExt::new(offchain));

    evm_logs_response(&mut state.write());

    let _: Result<(), OffchainErr<u32>> = t.execute_with(|| {
        let from_block: u64 = 20335770;
        let to_block: u64 = 20335858;
        let rpc_endpoint = get_rpc_endpoint();

        let network_data = prepare_evm_network(Some(1), None);
        let request_body = SlowClap::prepare_request_body_for_latest_transfers(
            from_block,
            to_block,
            &network_data,
        );
        let raw_response = SlowClap::fetch_from_remote(&rpc_endpoint, &request_body)?;
        assert_eq!(raw_response.len(), 1805); // precalculated
        Ok(())
    });
}

#[test]
fn should_make_http_call_and_parse_block_number() {
    let (offchain, state) = TestOffchainExt::new();
    let mut t = sp_io::TestExternalities::default();
    t.register_extension(OffchainWorkerExt::new(offchain));

    evm_block_response(&mut state.write());

    let _: Result<(), OffchainErr<u32>> = t.execute_with(|| {
        let rpc_endpoint = get_rpc_endpoint();
        let network_data = prepare_evm_network(Some(1), None);

        let request_body = SlowClap::prepare_request_body_for_latest_block(&network_data);
        let raw_response = SlowClap::fetch_from_remote(&rpc_endpoint, &request_body)?;
        let maybe_evm_block_number =
            SlowClap::apply_evm_response(&raw_response, 69, Default::default(), 420, 1)?;

        assert_eq!(maybe_evm_block_number, Some(20335745));
        Ok(())
    });
}

#[test]
fn should_make_http_call_and_parse_logs() {
    let (offchain, state) = TestOffchainExt::new();
    let (pool, _) = TestTransactionPoolExt::new();
    let mut t = sp_io::TestExternalities::default();

    t.register_extension(OffchainDbExt::new(offchain.clone()));
    t.register_extension(OffchainWorkerExt::new(offchain));
    t.register_extension(TransactionPoolExt::new(pool));

    evm_logs_response(&mut state.write());

    let _: Result<(), OffchainErr<u32>> = t.execute_with(|| {
        let session_index = advance_session_and_get_index();
        let rpc_endpoint = get_rpc_endpoint();

        let from_block: u64 = 20335770;
        let to_block: u64 = 20335858;
        let network_id: u32 = 1;

        let network_data = prepare_evm_network(Some(network_id), None);
        let request_body = SlowClap::prepare_request_body_for_latest_transfers(
            from_block,
            to_block,
            &network_data,
        );

        let raw_response = SlowClap::fetch_from_remote(&rpc_endpoint, &request_body)?;
        match SlowClap::parse_evm_response(&raw_response)? {
            EvmResponseType::BlockNumber(_) => assert_eq!(1, 0), // force break
            EvmResponseType::TransactionLogs(evm_logs) => assert_eq!(evm_logs.len(), 2),
        }

        let maybe_evm_block_number = SlowClap::apply_evm_response(
            &raw_response,
            1,
            UintAuthorityId::from(2),
            session_index,
            network_id,
        )?;

        assert_eq!(maybe_evm_block_number, None);
        Ok(())
    });
}

#[test]
fn should_clear_sesions_based_on_history_depth() {
    let (network_id, transaction_hash, unique_transaction_hash) =
        generate_unique_hash(None, None, None, None);

    new_test_ext().execute_with(|| {
        let session_index = advance_session_and_get_index();
        let history_depth = HistoryDepth::get();
        let storage_key = (session_index, transaction_hash, unique_transaction_hash);

        assert_claps_info_correct(&storage_key, &session_index, 0);
        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            false
        );

        assert_ok!(do_clap_from(session_index, network_id, 0, false));
        assert_ok!(do_clap_from(session_index, network_id, 1, false));
        assert_ok!(do_clap_from(session_index, network_id, 2, false));

        assert_claps_info_correct(&storage_key, &session_index, 3);
        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            true
        );

        for _ in 0..history_depth {
            advance_session();
        }

        assert_claps_info_correct(&storage_key, &session_index, 0);
        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            false
        );
    });
}

#[test]
fn should_collect_commission_accordingly() {
    let (network_id, _, _) = generate_unique_hash(None, None, None, None);
    let (_, _, amount) = get_mocked_metadata();

    new_test_ext().execute_with(|| {
        let _ = prepare_evm_network(Some(network_id), Some(500_000_000));
        let session_index = advance_session_and_get_index();

        assert_eq!(Networks::accumulated_commission(), 0);
        assert_ok!(do_clap_from(session_index, network_id, 0, false));
        assert_ok!(do_clap_from(session_index, network_id, 1, false));
        assert_ok!(do_clap_from(session_index, network_id, 2, false));
        assert_eq!(Networks::accumulated_commission(), amount.saturating_div(2));
    });
}

#[test]
fn should_increase_gatkeeper_amount_accordingly() {
    let (network_id, _, _) = generate_unique_hash(None, None, None, None);
    let (_, _, amount) = get_mocked_metadata();

    new_test_ext().execute_with(|| {
        let _ = prepare_evm_network(Some(network_id), Some(500_000_000));
        let session_index = advance_session_and_get_index();

        assert_eq!(Networks::gatekeeper_amount(network_id), 0);
        assert_eq!(Networks::bridged_imbalance().bridged_in, 0);
        assert_eq!(Networks::bridged_imbalance().bridged_out, 0);

        assert_ok!(do_clap_from(session_index, network_id, 0, false));
        assert_ok!(do_clap_from(session_index, network_id, 1, false));
        assert_ok!(do_clap_from(session_index, network_id, 2, false));

        assert_eq!(Networks::gatekeeper_amount(network_id), amount);
        assert_eq!(
            Networks::bridged_imbalance().bridged_in,
            amount.saturating_div(2)
        );
        assert_eq!(Networks::bridged_imbalance().bridged_out, 0);
    });
}

#[test]
fn should_mark_clapped_transaction_when_clap_received() {
    let (network_id, transaction_hash, unique_transaction_hash) =
        generate_unique_hash(None, None, None, None);

    new_test_ext().execute_with(|| {
        let session_index = advance_session_and_get_index();
        let storage_key = (session_index, transaction_hash, unique_transaction_hash);

        assert_claps_info_correct(&storage_key, &session_index, 0);
        assert_ok!(do_clap_from(session_index, network_id, 0, false));
        assert_claps_info_correct(&storage_key, &session_index, 1);
        assert_ok!(do_clap_from(session_index, network_id, 1, false));
        assert_claps_info_correct(&storage_key, &session_index, 2);
        assert_ok!(do_clap_from(session_index, network_id, 2, false));
        assert_claps_info_correct(&storage_key, &session_index, 3);
    });
}

#[test]
fn should_applause_and_take_next_claps() {
    let (network_id, transaction_hash, unique_transaction_hash) =
        generate_unique_hash(None, None, None, None);
    let (_, receiver, amount) = get_mocked_metadata();

    new_test_ext().execute_with(|| {
        let session_index = advance_session_and_get_index();
        let storage_key = (session_index, transaction_hash, unique_transaction_hash);

        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            false
        );
        assert_eq!(Balances::balance(&receiver), 0);
        assert_ok!(do_clap_from(session_index, network_id, 0, false));
        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            false
        );
        assert_eq!(Balances::balance(&receiver), 0);
        assert_ok!(do_clap_from(session_index, network_id, 1, false));
        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            true
        );
        assert_eq!(Balances::balance(&receiver), amount);
        assert_ok!(do_clap_from(session_index, network_id, 2, false));
        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            true
        );
        assert_eq!(Balances::balance(&receiver), amount);
    });
}

#[test]
fn should_throw_error_on_clap_duplication() {
    let (network_id, transaction_hash, unique_transaction_hash) =
        generate_unique_hash(None, None, None, None);

    new_test_ext().execute_with(|| {
        let session_index = advance_session_and_get_index();
        let storage_key = (session_index, transaction_hash, unique_transaction_hash);

        assert_claps_info_correct(&storage_key, &session_index, 0);
        assert_ok!(do_clap_from(session_index, network_id, 0, false));
        assert_claps_info_correct(&storage_key, &session_index, 1);
        assert_err!(
            do_clap_from(session_index, network_id, 0, false),
            Error::<Runtime>::AlreadyClapped
        );
        assert_claps_info_correct(&storage_key, &session_index, 1);
    });
}

#[test]
fn should_throw_error_on_removal_of_unregistered_clap() {
    let (network_id, transaction_hash, unique_transaction_hash) =
        generate_unique_hash(None, None, None, None);

    new_test_ext().execute_with(|| {
        let session_index = advance_session_and_get_index();
        let storage_key = (session_index, transaction_hash, unique_transaction_hash);

        assert_claps_info_correct(&storage_key, &session_index, 0);
        assert_err!(
            do_clap_from(session_index, network_id, 0, true),
            Error::<Runtime>::UnregisteredClapRemove
        );
        assert_claps_info_correct(&storage_key, &session_index, 0);
    });
}

#[test]
fn should_throw_error_if_session_index_is_not_current() {
    let (network_id, transaction_hash, unique_transaction_hash) =
        generate_unique_hash(None, None, None, None);

    let bad_signer = 777;
    let bad_signer_id = 5;

    new_test_ext().execute_with(|| {
        let mut session_and_indexes = Vec::new();

        for deviation in 1..MAX_DEVIATION_DEPTH {
            advance_session_with_authority(deviation);
            session_and_indexes.push((deviation, Session::current_index()));
        }

        for chunk in session_and_indexes.chunks(3).into_iter() {
            let authority_curr = chunk[1].0;
            let authority_prev = chunk[0].0;
            let authority_next = chunk[2].0;

            let session_index_curr = chunk[1].1;
            let session_index_prev = chunk[0].1;
            let session_index_next = chunk[2].1;

            let storage_key_curr = (
                session_index_curr,
                transaction_hash,
                unique_transaction_hash,
            );
            let storage_key_prev = (
                session_index_prev,
                transaction_hash,
                unique_transaction_hash,
            );
            let storage_key_next = (
                session_index_next,
                transaction_hash,
                unique_transaction_hash,
            );

            assert_claps_info_correct(&storage_key_curr, &session_index_curr, 0);
            assert_claps_info_correct(&storage_key_prev, &session_index_prev, 0);
            assert_claps_info_correct(&storage_key_next, &session_index_next, 0);

            assert_invalid_signing_address(session_index_curr, network_id, bad_signer_id);
            assert_invalid_signing_address(session_index_prev, network_id, bad_signer_id);
            assert_invalid_signing_address(session_index_prev, network_id, bad_signer_id);

            assert_transaction_has_bad_signature(session_index_curr, network_id, bad_signer);
            assert_transaction_has_bad_signature(session_index_prev, network_id, bad_signer);
            assert_transaction_has_bad_signature(session_index_prev, network_id, bad_signer);

            assert_transaction_has_bad_signature(session_index_curr, network_id, authority_prev);
            assert_transaction_has_bad_signature(session_index_curr, network_id, authority_next);

            assert_transaction_has_bad_signature(session_index_prev, network_id, authority_curr);
            assert_transaction_has_bad_signature(session_index_prev, network_id, authority_next);

            assert_transaction_has_bad_signature(session_index_next, network_id, authority_prev);
            assert_transaction_has_bad_signature(session_index_next, network_id, authority_curr);

            assert_claps_info_correct(&storage_key_curr, &session_index_curr, 0);
            assert_claps_info_correct(&storage_key_prev, &session_index_prev, 0);
            assert_claps_info_correct(&storage_key_next, &session_index_next, 0);

            assert_ok!(do_clap_from_first_authority(
                session_index_curr,
                network_id,
                authority_curr
            ));
            assert_ok!(do_clap_from_first_authority(
                session_index_prev,
                network_id,
                authority_prev
            ));
            assert_err!(
                do_clap_from_first_authority(session_index_next, network_id, authority_next),
                DispatchError::Other("Transaction has a bad signature")
            );

            assert_claps_info_correct(&storage_key_curr, &session_index_curr, 1);
            assert_claps_info_correct(&storage_key_prev, &session_index_prev, 1);
            assert_claps_info_correct(&storage_key_next, &session_index_next, 0);
        }
    });
}

#[test]
fn should_throw_error_if_signer_has_incorrect_index() {
    let (network_id, transaction_hash, unique_transaction_hash) =
        generate_unique_hash(None, None, None, None);

    new_test_ext().execute_with(|| {
        let session_index = advance_session_and_get_index();
        let storage_key = (session_index, transaction_hash, unique_transaction_hash);

        assert_claps_info_correct(&storage_key, &session_index, 0);
        assert_invalid_signing_address(session_index, network_id, 69);
        assert_transaction_has_bad_signature(session_index, network_id, 69);
        assert_invalid_signing_address(session_index, network_id, 420);
        assert_transaction_has_bad_signature(session_index, network_id, 420);
        assert_invalid_signing_address(session_index, network_id, 1337);
        assert_transaction_has_bad_signature(session_index, network_id, 1337);
        assert_claps_info_correct(&storage_key, &session_index, 0);
    });
}

#[test]
fn should_throw_error_if_validator_disabled_and_ignore_later() {
    let (network_id, transaction_hash, unique_transaction_hash) =
        generate_unique_hash(None, None, None, None);

    new_test_ext().execute_with(|| {
        let session_index = advance_session_and_get_index();
        let storage_key = (session_index, transaction_hash, unique_transaction_hash);

        assert_claps_info_correct(&storage_key, &session_index, 0);
        assert_eq!(Session::disable_index(0), true);
        assert_err!(
            do_clap_from(session_index, network_id, 0, false),
            DispatchError::Other("Invalid signing address")
        );

        assert_eq!(pallet::ReceivedClaps::<Runtime>::get(&storage_key).len(), 0);
        assert_eq!(
            pallet::ClapsInSession::<Runtime>::get(&session_index).len(),
            1
        );
        assert_eq!(
            pallet::ClapsInSession::<Runtime>::get(&session_index)
                .into_iter()
                .filter(|(_, v)| !v.disabled)
                .collect::<Vec<_>>()
                .len(),
            0
        );

        assert_ok!(do_clap_from(session_index, network_id, 1, false));
        assert_eq!(Session::disable_index(1), true);

        assert_eq!(pallet::ReceivedClaps::<Runtime>::get(&storage_key).len(), 1);
        assert_eq!(
            pallet::ClapsInSession::<Runtime>::get(&session_index).len(),
            2
        );
        assert_eq!(
            pallet::ClapsInSession::<Runtime>::get(&session_index)
                .into_iter()
                .filter(|(_, v)| !v.disabled)
                .collect::<Vec<_>>()
                .len(),
            0
        );
    });
}

#[test]
fn should_clap_without_applause_on_gatekeeper_amount_overflow() {
    let big_amount: u64 = u64::MAX;
    let first_receiver: u64 = 1337;
    let second_receiver: u64 = 420;
    let (network_id, transaction_hash, unique_transaction_hash) =
        generate_unique_hash(None, None, Some(first_receiver), Some(big_amount));

    new_test_ext().execute_with(|| {
        let session_index = advance_session_and_get_index();
        let storage_key_first = (session_index, transaction_hash, unique_transaction_hash);

        for authority_index in 0..=2 {
            let clap = Clap {
                block_number: 420,
                removed: false,
                transaction_hash,
                session_index,
                authority_index,
                network_id,
                receiver: first_receiver,
                amount: big_amount,
            };
            let authority = UintAuthorityId::from((authority_index + 1) as u64);
            let signature = authority.sign(&clap.encode()).unwrap();
            assert_ok!(SlowClap::slow_clap(RuntimeOrigin::none(), clap, signature));
        }

        assert_claps_info_correct(&storage_key_first, &session_index, 3);
        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key_first),
            true
        );
        assert_eq!(Balances::balance(&first_receiver), big_amount);
        assert_eq!(Balances::balance(&second_receiver), 0);

        for authority_index in 0..=2 {
            let clap = Clap {
                block_number: 420,
                removed: false,
                transaction_hash: H256::repeat_byte(3u8),
                session_index,
                authority_index,
                network_id,
                receiver: second_receiver,
                amount: 420,
            };
            let authority = UintAuthorityId::from((authority_index + 1) as u64);
            let signature = authority.sign(&clap.encode()).unwrap();
            assert_ok!(SlowClap::slow_clap(RuntimeOrigin::none(), clap, signature));
        }

        assert_eq!(Balances::balance(&first_receiver), big_amount);
        assert_eq!(Balances::balance(&second_receiver), 0);

        assert_eq!(Networks::gatekeeper_amount(network_id), big_amount);
        assert_eq!(Networks::bridged_imbalance().bridged_in, big_amount);
        assert_eq!(Networks::bridged_imbalance().bridged_out, 0);
    });
}

#[test]
fn should_clap_without_applause_on_commission_overflow() {
    let big_amount: u64 = u64::MAX;
    let first_receiver: u64 = 1337;
    let second_receiver: u64 = 420;
    let network_id_other: u32 = 69;
    let (network_id, transaction_hash, unique_transaction_hash) =
        generate_unique_hash(None, None, Some(first_receiver), Some(big_amount));

    new_test_ext().execute_with(|| {
        let _ = prepare_evm_network(Some(network_id), Some(0));
        let _ = prepare_evm_network(Some(network_id_other), Some(0));
        let session_index = advance_session_and_get_index();
        let storage_key_first = (session_index, transaction_hash, unique_transaction_hash);

        for authority_index in 0..=2 {
            let clap = Clap {
                block_number: 420,
                removed: false,
                transaction_hash,
                session_index,
                authority_index,
                network_id,
                receiver: first_receiver,
                amount: big_amount,
            };
            let authority = UintAuthorityId::from((authority_index + 1) as u64);
            let signature = authority.sign(&clap.encode()).unwrap();
            assert_ok!(SlowClap::slow_clap(RuntimeOrigin::none(), clap, signature));
        }

        assert_claps_info_correct(&storage_key_first, &session_index, 3);
        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key_first),
            true
        );
        assert_eq!(Balances::balance(&first_receiver), big_amount);
        assert_eq!(Balances::balance(&second_receiver), 0);

        for authority_index in 0..=2 {
            let clap = Clap {
                block_number: 420,
                removed: false,
                transaction_hash: H256::repeat_byte(3u8),
                session_index,
                authority_index,
                network_id: network_id_other,
                receiver: 420,
                amount: big_amount,
            };
            let authority = UintAuthorityId::from((authority_index + 1) as u64);
            let signature = authority.sign(&clap.encode()).unwrap();
            assert_ok!(SlowClap::slow_clap(RuntimeOrigin::none(), clap, signature));
        }

        assert_eq!(Balances::balance(&first_receiver), big_amount);
        assert_eq!(Balances::balance(&second_receiver), 0);

        assert_eq!(Networks::gatekeeper_amount(network_id), big_amount);
        assert_eq!(Networks::gatekeeper_amount(network_id_other), big_amount);
        assert_eq!(Networks::bridged_imbalance().bridged_in, big_amount);
        assert_eq!(Networks::bridged_imbalance().bridged_out, 0);
    });
}

#[test]
fn should_nullify_commission_on_finalize() {
    let total_staked = 69_000_000;
    let total_issuance = 100_000_000;

    let (network_id, _, _) = generate_unique_hash(None, None, None, None);
    let (_, _, amount) = get_mocked_metadata();

    new_test_ext().execute_with(|| {
        let _ = prepare_evm_network(Some(network_id), Some(1_000_000_000));
        let session_index = advance_session_and_get_index();

        assert_eq!(Networks::accumulated_commission(), 0);
        assert_ok!(do_clap_from(session_index, network_id, 0, false));
        assert_ok!(do_clap_from(session_index, network_id, 1, false));
        assert_eq!(Networks::accumulated_commission(), amount);
        assert_eq!(Networks::is_nullification_period(), false);

        assert_eq!(
            BridgedInflationCurve::<RewardCurve, Runtime>::era_payout(
                total_staked,
                total_issuance,
                0u64
            ),
            (420000000000000u64, 0u64)
        ); // precomputed values
        assert_eq!(Networks::is_nullification_period(), true);
        Networks::on_finalize(System::block_number());

        assert_eq!(Networks::is_nullification_period(), false);
        assert_eq!(Networks::accumulated_commission(), 0);
        assert_ok!(do_clap_from(session_index, network_id, 2, false));
        assert_eq!(Networks::accumulated_commission(), 0);
    });
}

#[test]
fn should_avoid_applause_during_nullification_period() {
    let zero: u64 = 0u64;
    let total_staked = 69_000_000;
    let total_issuance = 100_000_000;

    let (network_id, _, _) = generate_unique_hash(None, None, None, None);
    let (_, receiver, amount) = get_mocked_metadata();

    new_test_ext().execute_with(|| {
        let _ = prepare_evm_network(Some(network_id), Some(0));
        let session_index = advance_session_and_get_index();

        assert_eq!(Networks::is_nullification_period(), false);
        assert_eq!(
            BridgedInflationCurve::<RewardCurve, Runtime>::era_payout(
                total_staked,
                total_issuance,
                zero
            ),
            (zero, zero)
        );
        assert_eq!(Networks::is_nullification_period(), true);

        assert_ok!(do_clap_from(session_index, network_id, 0, false));
        assert_ok!(do_clap_from(session_index, network_id, 1, false));
        assert_eq!(Balances::balance(&receiver), 0);

        Networks::on_finalize(System::block_number());
        assert_eq!(Networks::is_nullification_period(), false);

        assert_ok!(do_clap_from(session_index, network_id, 2, false));
        assert_eq!(Balances::balance(&receiver), amount);
    });
}

#[test]
fn should_self_applause_after_diabled() {
    let zero = 0u64;
    let (network_id, transaction_hash, unique_transaction_hash) =
        generate_unique_hash(None, None, None, None);
    let (_, receiver, amount) = get_mocked_metadata();

    new_test_ext().execute_with(|| {
        let _ = prepare_evm_network(Some(network_id), Some(0));
        let session_index = advance_session_and_get_index();
        let storage_key = (session_index, transaction_hash, unique_transaction_hash);

        assert_err!(
            SlowClap::self_applause(
                RuntimeOrigin::signed(receiver),
                network_id,
                session_index,
                transaction_hash,
                receiver,
                amount,
            ),
            Error::<Runtime>::NotEnoughClaps,
        );

        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            false
        );
        assert_eq!(Balances::balance(&receiver), zero);

        assert_ok!(do_clap_from(session_index, network_id, 0, false));
        advance_session();
        let curr_session_index = Session::session_index();

        pallet::ClapsInSession::<Runtime>::mutate(&session_index, |claps| {
            claps.entry(1 as AuthIndex)
                .and_modify(|individual| (*individual).disabled = true)
                .or_insert(SessionAuthorityInfo {
                    claps: 0u32,
                    disabled: true,
                });
        });

        pallet::ClapsInSession::<Runtime>::mutate(&curr_session_index, |claps| {
            claps.entry(2 as AuthIndex)
                .and_modify(|individual| (*individual).disabled = true)
                .or_insert(SessionAuthorityInfo {
                    claps: 0u32,
                    disabled: true,
                });
        });

        assert_ok!(SlowClap::self_applause(
            RuntimeOrigin::signed(receiver),
            network_id,
            session_index,
            transaction_hash,
            receiver,
            amount,
        ));
        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            true
        );
        assert_eq!(Balances::balance(&receiver), amount);
    });
}

#[test]
fn should_self_applause_if_enough_claps() {
    let zero = 0u64;
    let (network_id, transaction_hash, unique_transaction_hash) =
        generate_unique_hash(None, None, None, None);
    let (_, receiver, amount) = get_mocked_metadata();

    new_test_ext().execute_with(|| {
        let _ = prepare_evm_network(Some(network_id), Some(0));
        let session_index = advance_session_and_get_index();
        let storage_key = (session_index, transaction_hash, unique_transaction_hash);

        assert_err!(
            SlowClap::self_applause(
                RuntimeOrigin::signed(receiver),
                network_id,
                session_index,
                transaction_hash,
                receiver,
                amount,
            ),
            Error::<Runtime>::NotEnoughClaps,
        );

        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            false
        );
        assert_eq!(Balances::balance(&receiver), zero);

        assert_ok!(do_clap_from(session_index, network_id, 0, false));
        advance_session();

        let mut fake_received_clap =
            BoundedBTreeSet::<AuthIndex, <Runtime as pallet::Config>::MaxAuthorities>::new();
        assert_eq!(fake_received_clap.try_insert(1).unwrap(), true);
        assert_eq!(fake_received_clap.try_insert(2).unwrap(), true);

        pallet::ReceivedClaps::<Runtime>::insert(&storage_key, fake_received_clap);

        assert_ok!(SlowClap::self_applause(
            RuntimeOrigin::signed(receiver),
            network_id,
            session_index,
            transaction_hash,
            receiver,
            amount,
        ));
        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            true
        );
        assert_eq!(Balances::balance(&receiver), amount);
    });
}

#[test]
fn should_avoid_session_overlap_on_mended_session_index() {
    let (network_id, transaction_hash, unique_transaction_hash) =
        generate_unique_hash(None, None, None, None);
    let (_, receiver, amount) = get_mocked_metadata();

    new_test_ext().execute_with(|| {
        let _ = prepare_evm_network(Some(network_id), Some(0));
        let session_index = advance_session_and_get_index();
        let storage_key = (session_index, transaction_hash, unique_transaction_hash);

        assert_ok!(do_clap_from(session_index, network_id, 0, false));
        advance_session();

        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            false
        );
        assert_eq!(Balances::balance(&receiver), 0u64);

        assert_ok!(do_clap_from(session_index, network_id, 1, false));
        assert_ok!(do_clap_from(session_index, network_id, 2, false));

        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            true
        );
        assert_eq!(Balances::balance(&receiver), amount);
    });
}

#[test]
fn should_emit_event_on_each_clap_and_on_applause() {
    let (network_id, transaction_hash, unique_transaction_hash) =
        generate_unique_hash(None, None, None, None);
    let (_, receiver, amount) = get_mocked_metadata();

    new_test_ext().execute_with(|| {
        let commission = Perbill::from_parts(100_000_000); // 10%
        let commission_amount = commission.mul_ceil(amount);
        let amount_after_commission = amount.saturating_sub(commission_amount);
        let _ = prepare_evm_network(Some(network_id), Some(100_000_000));
        let session_index = advance_session_and_get_index();
        let storage_key = (session_index, transaction_hash, unique_transaction_hash);

        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            false
        );
        assert_claps_info_correct(&storage_key, &session_index, 0);
        assert_ok!(do_clap_from(session_index, network_id, 0, false));
        System::assert_last_event(RuntimeEvent::SlowClap(crate::Event::Clapped {
            receiver: receiver.clone(),
            authority_id: 0,
            network_id,
            transaction_hash,
            amount,
        }));

        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            false
        );
        assert_claps_info_correct(&storage_key, &session_index, 1);
        assert_ok!(do_clap_from(session_index, network_id, 1, false));
        let binding = System::events();
        let last_two_events = binding.iter().rev().take(5).collect::<Vec<_>>();
        assert_eq!(
            last_two_events[0].event,
            RuntimeEvent::SlowClap(crate::Event::Applaused {
                network_id,
                receiver: receiver.clone(),
                received_amount: amount_after_commission,
            })
        );
        assert_eq!(
            last_two_events[4].event,
            RuntimeEvent::SlowClap(crate::Event::Clapped {
                receiver: receiver.clone(),
                authority_id: 1,
                network_id,
                transaction_hash,
                amount,
            })
        );

        assert_eq!(
            pallet::ApplausesForTransaction::<Runtime>::get(&storage_key),
            true
        );
        assert_claps_info_correct(&storage_key, &session_index, 2);
        assert_ok!(do_clap_from(session_index, network_id, 2, false));
        System::assert_last_event(RuntimeEvent::SlowClap(crate::Event::Clapped {
            receiver: receiver.clone(),
            authority_id: 2,
            network_id,
            transaction_hash,
            amount,
        }));
    });
}

#[test]
fn should_not_fail_on_sub_existential_balance() {
    let (network_id, transaction_hash, unique_transaction_hash) =
        generate_unique_hash(None, None, None, None);
    let (_, receiver, amount) = get_mocked_metadata();

    new_test_ext().execute_with(|| {
        let _ = prepare_evm_network(Some(network_id), Some(1_000_000_000)); // 100%
        let session_index = advance_session_and_get_index();
        let received_claps_key = (session_index, transaction_hash, unique_transaction_hash);

        assert_eq!(Networks::accumulated_commission(), 0);
        assert_eq!(Networks::gatekeeper_amount(network_id), 0);
        assert_eq!(Networks::bridged_imbalance().bridged_in, 0);
        assert_eq!(Networks::bridged_imbalance().bridged_out, 0);
        assert_eq!(Balances::balance(&receiver), 0);
        assert_eq!(
            SlowClap::applauses_for_transaction(&received_claps_key),
            false
        );

        assert_ok!(do_clap_from(session_index, network_id, 0, false));
        assert_ok!(do_clap_from(session_index, network_id, 1, false));
        assert_ok!(do_clap_from(session_index, network_id, 2, false));

        assert_eq!(Networks::accumulated_commission(), amount);
        assert_eq!(Networks::gatekeeper_amount(network_id), amount);
        assert_eq!(Networks::bridged_imbalance().bridged_in, 0);
        assert_eq!(Networks::bridged_imbalance().bridged_out, 0);
        assert_eq!(Balances::balance(&receiver), 0);
        assert_eq!(
            SlowClap::applauses_for_transaction(&received_claps_key),
            true
        );
    });
}

fn advance_session_and_get_index() -> u32 {
    advance_session();
    assert_eq!(Session::validators(), Vec::<u64>::new());
    advance_session();

    Session::session_index()
}

fn generate_unique_hash(
    maybe_network_id: Option<u32>,
    maybe_transaction_hash: Option<H256>,
    maybe_receiver: Option<u64>,
    maybe_amount: Option<u64>,
) -> (u32, H256, H256) {
    let network_id = maybe_network_id.unwrap_or(1);
    let (transaction_hash, receiver, amount) = get_mocked_metadata();

    let transaction_hash = maybe_transaction_hash.unwrap_or(transaction_hash);
    let receiver = maybe_receiver.unwrap_or(receiver);
    let amount = maybe_amount.unwrap_or(amount);

    let unique_transaction_hash = SlowClap::generate_unique_hash(&receiver, &amount, &network_id);

    (network_id, transaction_hash, unique_transaction_hash)
}

fn assert_claps_info_correct(storage_key: &(u32, H256, H256), session_index: &u32, index: usize) {
    assert_eq!(
        pallet::ReceivedClaps::<Runtime>::get(storage_key).len(),
        index
    );
    assert_eq!(
        pallet::ClapsInSession::<Runtime>::get(session_index).len(),
        index
    );
}

fn assert_transaction_has_bad_signature(session_index: u32, network_id: u32, authority: u64) {
    assert_err!(
        do_clap_from_first_authority(session_index, network_id, authority),
        DispatchError::Other("Transaction has a bad signature")
    );
}

fn assert_invalid_signing_address(session_index: u32, network_id: u32, authority_index: u32) {
    assert_err!(
        do_clap_from(session_index, network_id, authority_index, false),
        DispatchError::Other("Invalid signing address")
    );
}

fn get_rpc_endpoint() -> Vec<u8> {
    b"https://rpc.endpoint.network.com".to_vec()
}

fn get_rpc_endpoints() -> Vec<Vec<u8>> {
    vec![
        get_rpc_endpoint(),
        b"https://other.endpoint.network.com".to_vec(),
    ]
}

fn get_gatekeeper() -> Vec<u8> {
    b"0x4d224452801ACEd8B2F0aebE155379bb5D594381".to_vec()
}

fn get_topic_name() -> Vec<u8> {
    b"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef".to_vec()
}

fn get_mocked_metadata() -> (H256, u64, u64) {
    let transaction_hash = H256::repeat_byte(69u8);
    let receiver: u64 = 1337;
    let amount: u64 = 420_000_000_000_000;

    (transaction_hash, receiver, amount)
}

fn evm_block_response(state: &mut testing::OffchainState) {
    let expected_body = br#"{"id":0,"jsonrpc":"2.0","method":"eth_blockNumber"}"#.to_vec();
    state.expect_request(testing::PendingRequest {
        method: "POST".into(),
        uri: "https://rpc.endpoint.network.com".into(),
        headers: vec![
            ("Accept".to_string(), "application/json".to_string()),
            ("Content-Type".to_string(), "application/json".to_string()),
        ],
        response: Some(b"{\"id\":0,\"jsonrpc\":\"2.0\",\"result\":\"0x1364c81\"}".to_vec()),
        body: expected_body,
        sent: true,
        ..Default::default()
    });
}

fn evm_logs_response(state: &mut testing::OffchainState) {
    let expected_body = br#"{"id":0,"jsonrpc":"2.0","method":"eth_getLogs","params":[{"fromBlock":"0x1364c9a","toBlock":"0x1364cf2","address":"0x4d224452801ACEd8B2F0aebE155379bb5D594381","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]}]}"#.to_vec();

    let expected_response = br#"{
        "id":0,
        "jsonrpc":"2.0",
        "result":[
            {
                "address":"0x4d224452801aced8b2f0aebe155379bb5d594381",
                "topics":[
                    "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
                    "0x000000000000000000000000d91efec7e42f80156d1d9f660a69847188950747",
                    "0x0000000000000000000000005e611dfbe71b988cf2a3e5fe4191b5e3d4c4212a"
                ],
                "data":"0x000000000000000000000000000000000000000000000046f0d522a71fdac000",
                "blockNumber":"0x1364c9d",
                "transactionHash":"0xa82f2fe87f4ba01ab3bd2cd4d0fe75a26f0e9a37e2badc004a0e38f9d088a758",
                "transactionIndex":"0x11",
                "blockHash":"0x4b08f82d5a948c0bc5c23c8ddce55e5b4536d7454be53668bbd985ef13dca04a",
                "logIndex":"0x92",
                "removed":false
            },
            {
                "address":"0x4d224452801aced8b2f0aebe155379bb5d594381",
                "topics":[
                    "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
                    "0x0000000000000000000000005954ab967bc958940b7eb73ee84797dc8a2afbb9",
                    "0x000000000000000000000000465165be7cacdbd2cbc8334f549fab9783ad6e7a"
                ],
                "data":"0x0000000000000000000000000000000000000000000000027c790defec959e75",
                "blockNumber":"0x1364cf1",
                "transactionHash":"0xd9f02b79a90db7536b0afb5e24bbc1f4319dc3d8c57af7c39941acd249ec053a",
                "transactionIndex":"0x2c",
                "blockHash":"0x6876b18f5081b5d24d5a73107a667a7713256f6e51edbbe52bf8df24e340b0f7",
                "logIndex":"0x14b",
                "removed":false
            }
        ]
    }"#.to_vec();

    state.expect_request(testing::PendingRequest {
        method: "POST".into(),
        uri: "https://rpc.endpoint.network.com".into(),
        headers: vec![
            ("Accept".to_string(), "application/json".to_string()),
            ("Content-Type".to_string(), "application/json".to_string()),
        ],
        response_headers: vec![
            ("Accept".to_string(), "application/json".to_string()),
            ("Content-Type".to_string(), "application/json".to_string()),
        ],
        body: expected_body,
        response: Some(expected_response),
        sent: true,
        ..Default::default()
    });
}
