use cairo_lang_starknet::casm_contract_class::CasmContractClass;
use cairo_vm::felt::Felt252;
use num_traits::{Num, Zero};
use starknet_in_rust::services::api::contract_classes::compiled_class::CompiledClass;
use starknet_in_rust::utils::calculate_sn_keccak;
use starknet_in_rust::EntryPointType;
use starknet_in_rust::{
    definitions::{block_context::BlockContext, constants::TRANSACTION_VERSION},
    execution::{
        execution_entry_point::ExecutionEntryPoint, CallInfo, CallType, OrderedEvent,
        OrderedL2ToL1Message, TransactionExecutionContext,
    },
    state::cached_state::CachedState,
    state::{in_memory_state_reader::InMemoryStateReader, ExecutionResourcesManager},
    utils::{Address, ClassHash},
};
use std::{collections::HashMap, sync::Arc, vec};

#[test]
fn test_multiple_syscall() {
    //  Create program and entry point types for contract class
    let program_data = include_bytes!("../starknet_programs/cairo1/multi_syscall_test.casm");
    let contract_class: CasmContractClass = serde_json::from_slice(program_data).unwrap();

    // Create state reader with class hash data
    let mut contract_class_cache: HashMap<[u8; 32], _> = HashMap::new();

    let address = Address(1111.into());
    let class_hash: ClassHash = [1; 32];
    let nonce = Felt252::zero();

    contract_class_cache.insert(class_hash, CompiledClass::Casm(Arc::new(contract_class)));
    let mut state_reader = InMemoryStateReader::default();
    state_reader
        .address_to_class_hash_mut()
        .insert(address.clone(), class_hash);
    state_reader
        .address_to_nonce_mut()
        .insert(address.clone(), nonce);

    // Create state from the state_reader and contract cache.
    let mut state = CachedState::new(Arc::new(state_reader), contract_class_cache.clone());

    // Create an execution entry point
    let calldata = [].to_vec();
    let caller_address = Address(0000.into());
    let entry_point_type = EntryPointType::External;

    // Block for get_caller_address.
    {
        let call_info = test_syscall(
            "caller_address",
            address.clone(),
            vec![],
            caller_address.clone(),
            entry_point_type,
            class_hash,
            &mut state,
        );
        assert_eq!(call_info.retdata, vec![caller_address.clone().0])
    }

    // Block for get_contact_address.
    {
        let call_info = test_syscall(
            "contract_address",
            address.clone(),
            vec![],
            caller_address.clone(),
            entry_point_type,
            class_hash,
            &mut state,
        );
        assert_eq!(call_info.retdata, vec![address.clone().0])
    }
    // Block for get_execution_info_syscall.
    {
        let call_info = test_syscall(
            "execution_info_syscall",
            address.clone(),
            calldata.clone(),
            caller_address.clone(),
            entry_point_type,
            class_hash,
            &mut state,
        );
        assert_eq!(call_info.retdata, vec![0.into(), 1111.into()]);
    }

    // Block for library_call_syscall
    {
        let entrypoint_selector =
            Felt252::from_bytes_be(&calculate_sn_keccak("get_number".as_bytes()));
        let new_call_data = vec![
            Felt252::from_bytes_be(&class_hash),
            entrypoint_selector,
            Felt252::from(25),
        ];
        let call_info = test_syscall(
            "test_library_call_syscall",
            address.clone(),
            new_call_data,
            caller_address.clone(),
            entry_point_type,
            class_hash,
            &mut state,
        );
        assert_eq!(call_info.retdata, vec![25.into()])
    }

    // Block for call_contract_syscall
    {
        let entrypoint_selector =
            Felt252::from_bytes_be(&calculate_sn_keccak("get_number".as_bytes()));
        let new_call_data = vec![entrypoint_selector, Felt252::from(25)];
        let call_info = test_syscall(
            "test_call_contract_syscall",
            address.clone(),
            new_call_data,
            caller_address.clone(),
            entry_point_type,
            class_hash,
            &mut state,
        );
        assert_eq!(call_info.retdata, vec![25.into()])
    }

    // Block for send_message_to_l1_syscall
    {
        let new_call_data = vec![2222.into(), Felt252::from(25), Felt252::from(30)];
        let call_info = test_syscall(
            "test_send_message_to_l1",
            address.clone(),
            new_call_data,
            caller_address.clone(),
            entry_point_type,
            class_hash,
            &mut state,
        );
        assert_eq!(
            call_info.l2_to_l1_messages,
            vec![OrderedL2ToL1Message {
                order: 0,
                to_address: Address(2222.into()),
                payload: vec![Felt252::from(25), Felt252::from(30)],
            },]
        )
    }

    // Block for read write
    {
        let call_info = test_syscall(
            "read",
            address.clone(),
            calldata.clone(),
            caller_address.clone(),
            entry_point_type,
            class_hash,
            &mut state,
        );
        assert_eq!(
            call_info.retdata,
            vec![Felt252::from_str_radix("310939249775", 10).unwrap()]
        )
    }

    // Block for emit
    {
        let call_info = test_syscall(
            "trigger_events",
            address,
            calldata,
            caller_address,
            entry_point_type,
            class_hash,
            &mut state,
        );
        assert_eq!(
            call_info.events,
            vec![
                OrderedEvent {
                    order: 0,
                    keys: vec![Felt252::from_str_radix(
                        "1533133552972353850845856330693290141476612241335297758062928121906575244541",
                        10
                    )
                    .unwrap()],
                    data: vec![1.into()]
                },
                OrderedEvent {
                    order: 1,
                    keys: vec![Felt252::from_str_radix(
                        "1533133552972353850845856330693290141476612241335297758062928121906575244541",
                        10
                    )
                    .unwrap()],
                    data: vec![2.into()]
                },
                OrderedEvent {
                    order: 2,
                    keys: vec![Felt252::from_str_radix(
                        "1533133552972353850845856330693290141476612241335297758062928121906575244541",
                        10
                    )
                    .unwrap()],
                    data: vec![3.into()]
                }
            ]
        )
    }
}

fn test_syscall(
    entrypoint_selector: &str,
    address: Address,
    calldata: Vec<Felt252>,
    caller_address: Address,
    entry_point_type: EntryPointType,
    class_hash: [u8; 32],
    state: &mut CachedState<InMemoryStateReader>,
) -> CallInfo {
    let entrypoint_selector =
        Felt252::from_bytes_be(&calculate_sn_keccak(entrypoint_selector.as_bytes()));
    let exec_entry_point = ExecutionEntryPoint::new(
        address,
        calldata,
        Felt252::new(entrypoint_selector),
        caller_address,
        entry_point_type,
        Some(CallType::Delegate),
        Some(class_hash),
        100000,
    );

    // Execute the entrypoint
    let block_context = BlockContext::default();
    let mut tx_execution_context = TransactionExecutionContext::new(
        Address(0.into()),
        Felt252::zero(),
        Vec::new(),
        0,
        10.into(),
        block_context.invoke_tx_max_n_steps(),
        TRANSACTION_VERSION.clone(),
    );
    let mut resources_manager = ExecutionResourcesManager::default();
    exec_entry_point
        .execute(
            state,
            &block_context,
            &mut resources_manager,
            &mut tx_execution_context,
            false,
            block_context.invoke_tx_max_n_steps(),
        )
        .unwrap()
        .call_info
        .unwrap()
}