//! Implementation of the `testing` namespace.
//!
//! This exposes `testing_buildBlockV1`, intended for non-production/debug use.
//!
//! # Enabling the testing namespace
//!
//! The `testing_` namespace is disabled by default for security reasons.
//! To enable it, add `testing` to the `--http.api` flag when starting the node:
//!
//! ```sh
//! reth node --http --http.api eth,testing
//! ```
//!
//! **Warning:** This namespace allows building arbitrary blocks. Never expose it
//! on public-facing RPC endpoints without proper authentication.

use alloy_consensus::Transaction;
use alloy_eips::{eip1559::calculate_block_gas_limit, eip2718::Decodable2718};
use alloy_evm::{Evm, RecoveredTx};
use alloy_primitives::{
    map::{DefaultHashBuilder, HashSet},
    Address, Bytes, B256, U256,
};
use alloy_rlp::Encodable;
use alloy_rpc_types_engine::{
    BlobsBundleV2, ExecutionData, ExecutionPayloadEnvelopeV5, ExecutionPayloadSidecar,
    ExecutionPayloadV3, ForkchoiceState, PayloadAttributes, PraguePayloadFields,
};
use async_trait::async_trait;
use jsonrpsee::core::RpcResult;
use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
use reth_consensus_common::validation::MAX_RLP_BLOCK_SIZE;
use reth_engine_primitives::ConsensusEngineHandle;
use reth_errors::RethError;
use reth_ethereum_engine_primitives::EthBuiltPayload;
use reth_evm::{execute::BlockBuilder, ConfigureEvm, NextBlockEnvAttributes};
use reth_payload_primitives::{BuiltPayload, PayloadTypes};
use reth_primitives_traits::{
    transaction::{recover::try_recover_signers, signed::RecoveryError},
    AlloyBlockHeader as BlockTrait, Block as _, HeaderTy, TxTy,
};
use reth_revm::{database::StateProviderDatabase, db::State};
use reth_rpc_api::{TestingApiServer, TestingBuildBlockRequestV1};
use reth_rpc_eth_api::{helpers::Call, FromEthApiError};
use reth_rpc_eth_types::EthApiError;
use reth_storage_api::{BlockReader, BlockReaderIdExt, HeaderProvider};
use reth_transaction_pool::{BestTransactionsAttributes, PoolTransaction, TransactionPool};
use revm::context::Block;
use std::sync::Arc;
use tracing::debug;

/// Testing API handler.
#[derive(Debug, Clone)]
pub struct TestingApi<
    Eth,
    Evm,
    Payload: PayloadTypes = reth_ethereum_engine_primitives::EthEngineTypes,
> {
    eth_api: Eth,
    evm_config: Evm,
    /// Desired gas limit to move toward while respecting the consensus gas limit bounds.
    desired_gas_limit: u64,
    engine_handle: ConsensusEngineHandle<Payload>,
    /// If true, skip invalid transactions instead of failing.
    skip_invalid_transactions: bool,
    /// If set, override the block gas limit in `testing_buildBlockV1`.
    gas_limit_override: Option<u64>,
}

impl<Eth, Evm, Payload: PayloadTypes> TestingApi<Eth, Evm, Payload> {
    /// Create a new testing API handler.
    pub const fn new(
        eth_api: Eth,
        evm_config: Evm,
        desired_gas_limit: u64,
        engine_handle: ConsensusEngineHandle<Payload>,
    ) -> Self {
        Self {
            eth_api,
            evm_config,
            desired_gas_limit,
            engine_handle,
            skip_invalid_transactions: false,
            gas_limit_override: None,
        }
    }

    /// Enable skipping invalid transactions instead of failing.
    /// When a transaction fails, all subsequent transactions from the same sender are also
    /// skipped.
    pub const fn with_skip_invalid_transactions(mut self) -> Self {
        self.skip_invalid_transactions = true;
        self
    }

    /// Override the gas limit used by `testing_buildBlockV1`.
    pub const fn with_gas_limit_override(mut self, gas_limit: u64) -> Self {
        self.gas_limit_override = Some(gas_limit);
        self
    }
}

impl<Eth, Evm, Payload> TestingApi<Eth, Evm, Payload>
where
    Payload: PayloadTypes<
        ExecutionData = ExecutionData,
        BuiltPayload: BuiltPayload<Primitives = Evm::Primitives>,
    >,
    Eth: Call<
        Provider: BlockReader<Header = HeaderTy<Evm::Primitives>>
                      + BlockReaderIdExt<Header = HeaderTy<Evm::Primitives>>
                      + ChainSpecProvider<ChainSpec: EthereumHardforks>,
        Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TxTy<Evm::Primitives>>>,
    >,
    Evm: ConfigureEvm<NextBlockEnvCtx = NextBlockEnvAttributes> + 'static,
{
    async fn build_payload_v1(
        &self,
        request: TestingBuildBlockRequestV1,
        skip_invalid_transactions: bool,
        use_pool_transactions: bool,
    ) -> Result<EthBuiltPayload<Evm::Primitives>, Eth::Error> {
        let evm_config = self.evm_config.clone();
        let desired_gas_limit = self.desired_gas_limit;
        let gas_limit_override = self.gas_limit_override;
        self.eth_api
            .spawn_with_state_at_block(request.parent_block_hash, move |eth_api, state| {
                let state = state.database.0;
                let parent = eth_api
                    .provider()
                    .sealed_header_by_hash(request.parent_block_hash)?
                    .ok_or_else(|| {
                    EthApiError::HeaderNotFound(request.parent_block_hash.into())
                })?;

                let chain_spec = eth_api.provider().chain_spec();
                let is_amsterdam = chain_spec
                    .is_amsterdam_active_at_timestamp(request.payload_attributes.timestamp);
                let is_osaka =
                    chain_spec.is_osaka_active_at_timestamp(request.payload_attributes.timestamp);
                let mut db = State::builder()
                    .with_bundle_update()
                    .with_database(StateProviderDatabase::new(&state))
                    .with_bal_builder_if(is_amsterdam)
                    .build();

                let withdrawals = request.payload_attributes.withdrawals.clone();
                let withdrawals_rlp_length = withdrawals.as_ref().map(|w| w.length()).unwrap_or(0);

                let env_attrs = NextBlockEnvAttributes {
                    timestamp: request.payload_attributes.timestamp,
                    suggested_fee_recipient: request.payload_attributes.suggested_fee_recipient,
                    prev_randao: request.payload_attributes.prev_randao,
                    gas_limit: gas_limit_override.unwrap_or_else(|| {
                        calculate_block_gas_limit(parent.gas_limit(), desired_gas_limit)
                    }),
                    parent_beacon_block_root: request.payload_attributes.parent_beacon_block_root,
                    withdrawals: withdrawals.map(Into::into),
                    extra_data: request.extra_data.unwrap_or_default(),
                    slot_number: request.payload_attributes.slot_number,
                };

                let mut builder = evm_config
                    .builder_for_next_block(&mut db, &parent, env_attrs)
                    .map_err(RethError::other)
                    .map_err(Eth::Error::from_eth_err)?;
                builder.apply_pre_execution_changes().map_err(Eth::Error::from_eth_err)?;

                let mut total_fees = U256::ZERO;
                let base_fee = builder.evm_mut().block().basefee();

                let mut invalid_senders: HashSet<Address, DefaultHashBuilder> = HashSet::default();
                let mut block_transactions_rlp_length = 0usize;

                // If no transactions are provided in the request, use transactions from the pool.
                let recovered_txs = if use_pool_transactions {
                    let mut best_txs = eth_api.pool().best_transactions_with_attributes(
                        BestTransactionsAttributes::new(
                            base_fee,
                            builder
                                .evm_mut()
                                .block()
                                .blob_gasprice()
                                .map(|gasprice| gasprice as u64),
                        ),
                    );
                    best_txs.no_updates();
                    best_txs.map(|tx| tx.to_consensus()).collect()
                } else {
                    // Decode and recover all transactions in parallel
                    try_recover_signers(&request.transactions, |tx| {
                        TxTy::<Evm::Primitives>::decode_2718_exact(tx.as_ref())
                            .map_err(RecoveryError::from_source)
                    })
                    .or(Err(EthApiError::InvalidTransactionSignature))?
                };
                let allow_skip_invalid_transactions =
                    skip_invalid_transactions || use_pool_transactions;

                for (idx, tx) in recovered_txs.into_iter().enumerate() {
                    let signer = tx.signer();
                    if allow_skip_invalid_transactions && invalid_senders.contains(&signer) {
                        continue;
                    }

                    // EIP-7934: Check estimated block size before adding transaction
                    let tx_rlp_len = tx.tx().length();
                    if is_osaka {
                        // 1KB overhead for block header
                        let estimated_block_size = block_transactions_rlp_length +
                            tx_rlp_len +
                            withdrawals_rlp_length +
                            1024;
                        if estimated_block_size > MAX_RLP_BLOCK_SIZE {
                            if allow_skip_invalid_transactions {
                                debug!(
                                    target: "rpc::testing",
                                    tx_idx = idx,
                                    ?signer,
                                    estimated_block_size,
                                    max_size = MAX_RLP_BLOCK_SIZE,
                                    "Skipping transaction: would exceed block size limit"
                                );
                                invalid_senders.insert(signer);
                                continue;
                            }
                            return Err(Eth::Error::from_eth_err(EthApiError::InvalidParams(
                                format!(
                                    "transaction at index {} would exceed max block size: {} > {}",
                                    idx, estimated_block_size, MAX_RLP_BLOCK_SIZE
                                ),
                            )));
                        }
                    }

                    let tip = tx.effective_tip_per_gas(base_fee).unwrap_or_default();
                    let gas_used = match builder.execute_transaction(tx) {
                        Ok(gas_used) => gas_used.tx_gas_used(),
                        Err(err) => {
                            if allow_skip_invalid_transactions {
                                debug!(
                                    target: "rpc::testing",
                                    tx_idx = idx,
                                    ?signer,
                                    error = ?err,
                                    "Skipping invalid transaction"
                                );
                                invalid_senders.insert(signer);
                                continue;
                            }
                            debug!(
                                target: "rpc::testing",
                                tx_idx = idx,
                                ?signer,
                                error = ?err,
                                "Transaction execution failed"
                            );
                            return Err(Eth::Error::from_eth_err(err));
                        }
                    };

                    block_transactions_rlp_length += tx_rlp_len;
                    total_fees += U256::from(tip) * U256::from(gas_used);
                }
                let outcome = builder.finish(&state, None).map_err(Eth::Error::from_eth_err)?;

                let has_requests = outcome.block.requests_hash().is_some();
                let requests = has_requests.then_some(outcome.execution_result.requests);
                let block_access_list = outcome
                    .block_access_list
                    .map(|block_access_list| alloy_rlp::encode(&block_access_list).into());

                Ok(EthBuiltPayload::new(
                    Arc::new(outcome.block),
                    total_fees,
                    requests,
                    block_access_list,
                ))
            })
            .await
    }

    async fn build_block_v1(
        &self,
        request: TestingBuildBlockRequestV1,
        use_pool_transactions: bool,
    ) -> Result<ExecutionPayloadEnvelopeV5, Eth::Error> {
        let payload = self
            .build_payload_v1(request, self.skip_invalid_transactions, use_pool_transactions)
            .await?;
        let fees = payload.fees();
        let requests = payload.requests().unwrap_or_default();
        let block = Arc::unwrap_or_clone(payload.into_block_arc());
        let block_hash = block.hash();
        let block = block.into_block().into_ethereum_block();

        Ok(ExecutionPayloadEnvelopeV5 {
            execution_payload: ExecutionPayloadV3::from_block_unchecked(block_hash, &block),
            block_value: fees,
            blobs_bundle: BlobsBundleV2::empty(),
            should_override_builder: false,
            execution_requests: requests,
        })
    }

    async fn commit_block_v1(
        &self,
        payload_attributes: PayloadAttributes,
        transactions: Option<Vec<Bytes>>,
        extra_data: Option<Bytes>,
    ) -> Result<B256, Eth::Error> {
        let parent = self
            .eth_api
            .provider()
            .latest_header()
            .map_err(EthApiError::from)?
            .ok_or_else(|| EthApiError::HeaderNotFound(alloy_eips::BlockId::latest()))?;
        let safe_block_hash = self
            .eth_api
            .provider()
            .safe_header()
            .map_err(EthApiError::from)?
            .map(|header| header.hash())
            .unwrap_or_else(|| parent.hash());
        let finalized_block_hash = self
            .eth_api
            .provider()
            .finalized_header()
            .map_err(EthApiError::from)?
            .map(|header| header.hash())
            .unwrap_or_else(|| parent.hash());

        let use_pool_transactions = transactions.is_none();
        let payload = self
            .build_payload_v1(
                TestingBuildBlockRequestV1 {
                    parent_block_hash: parent.hash(),
                    payload_attributes,
                    transactions: transactions.unwrap_or_default(),
                    extra_data,
                },
                false,
                use_pool_transactions,
            )
            .await?;

        let block_hash = payload.block().hash();
        let requests = payload.requests();
        let block_access_list = payload.block_access_list().cloned();
        let block = Arc::unwrap_or_clone(payload.into_block_arc()).into_sealed_block();
        let execution_data = Payload::block_to_payload(block, block_access_list);
        let execution_data = match (requests, execution_data.sidecar.cancun()) {
            (Some(requests), Some(cancun)) => ExecutionData::new(
                execution_data.payload,
                ExecutionPayloadSidecar::v4(cancun.clone(), PraguePayloadFields::new(requests)),
            ),
            _ => execution_data,
        };
        let status = self
            .engine_handle
            .new_payload(execution_data)
            .await
            .map_err(RethError::other)
            .map_err(Eth::Error::from_eth_err)?;
        if !status.is_valid() {
            return Err(Eth::Error::from_eth_err(EthApiError::InvalidParams(format!(
                "new payload returned non-valid status: {:?}",
                status.status
            ))));
        }

        let fcu = self
            .engine_handle
            .fork_choice_updated(
                ForkchoiceState {
                    head_block_hash: block_hash,
                    safe_block_hash,
                    finalized_block_hash,
                },
                None,
            )
            .await
            .map_err(RethError::other)
            .map_err(Eth::Error::from_eth_err)?;
        if !fcu.is_valid() {
            return Err(Eth::Error::from_eth_err(EthApiError::InvalidParams(format!(
                "forkchoice update returned non-valid status: {:?}",
                fcu.payload_status.status
            ))));
        }

        Ok(block_hash)
    }
}

#[async_trait]
impl<Eth, Evm, Payload> TestingApiServer for TestingApi<Eth, Evm, Payload>
where
    Payload: PayloadTypes<
        ExecutionData = ExecutionData,
        BuiltPayload: BuiltPayload<Primitives = Evm::Primitives>,
    >,
    Eth: Call<
        Provider: BlockReader<Header = HeaderTy<Evm::Primitives>>
                      + BlockReaderIdExt<Header = HeaderTy<Evm::Primitives>>
                      + ChainSpecProvider<ChainSpec: EthereumHardforks>,
        Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TxTy<Evm::Primitives>>>,
    >,
    Evm: ConfigureEvm<NextBlockEnvCtx = NextBlockEnvAttributes> + 'static,
{
    /// Handles `testing_buildBlockV1` by gating concurrency via a semaphore and offloading heavy
    /// work to the blocking pool to avoid stalling the async runtime.
    async fn build_block_v1(
        &self,
        parent_block_hash: B256,
        payload_attributes: PayloadAttributes,
        transactions: Option<Vec<Bytes>>,
        extra_data: Option<Bytes>,
    ) -> RpcResult<ExecutionPayloadEnvelopeV5> {
        let use_pool_transactions = transactions.is_none();
        let request = TestingBuildBlockRequestV1 {
            parent_block_hash,
            payload_attributes,
            transactions: transactions.unwrap_or_default(),
            extra_data,
        };
        self.build_block_v1(request, use_pool_transactions).await.map_err(Into::into)
    }

    /// Handles `testing_commitBlockV1` by building on the current canonical head, then submitting
    /// the payload and advancing forkchoice through the same engine handle used by the Engine API.
    async fn commit_block_v1(
        &self,
        payload_attributes: PayloadAttributes,
        transactions: Option<Vec<Bytes>>,
        extra_data: Option<Bytes>,
    ) -> RpcResult<B256> {
        self.commit_block_v1(payload_attributes, transactions, extra_data).await.map_err(Into::into)
    }
}
