//! Test submitblock RPC method on Regtest.
//!
//! This test will get block templates via the `getblocktemplate` RPC method and submit them as new blocks
//! via the `submitblock` RPC method on Regtest.

use std::{net::SocketAddr, sync::Arc, time::Duration};

use color_eyre::eyre::{eyre, Context, Result};
use tower::BoxError;
use tracing::*;

use zebra_chain::{
    block::{Block, Height},
    parameters::{testnet::REGTEST_NU5_ACTIVATION_HEIGHT, Network, NetworkUpgrade},
    primitives::byte_array::increment_big_endian,
    serialization::{ZcashDeserializeInto, ZcashSerialize},
};
use zebra_node_services::rpc_client::RpcRequestClient;
use zebra_rpc::{
    constants::MISSING_BLOCK_ERROR_CODE,
    methods::{
        get_block_template_rpcs::{
            get_block_template::{
                proposal::TimeSource, proposal_block_from_template, GetBlockTemplate,
            },
            types::submit_block,
        },
        hex_data::HexData,
    },
    server::OPENED_RPC_ENDPOINT_MSG,
};
use zebra_test::args;

use crate::common::{
    config::{os_assigned_rpc_port_config, read_listen_addr_from_logs, testdir},
    launch::ZebradTestDirExt,
};

/// Number of blocks that should be submitted before the test is considered successful.
const NUM_BLOCKS_TO_SUBMIT: usize = 200;

pub(crate) async fn submit_blocks_test() -> Result<()> {
    let _init_guard = zebra_test::init();
    info!("starting regtest submit_blocks test");

    let network = Network::new_regtest(None, None);
    let mut config = os_assigned_rpc_port_config(false, &network)?;
    config.mempool.debug_enable_at_height = Some(0);

    let mut zebrad = testdir()?
        .with_config(&mut config)?
        .spawn_child(args!["start"])?;

    let rpc_address = read_listen_addr_from_logs(&mut zebrad, OPENED_RPC_ENDPOINT_MSG)?;

    info!("waiting for zebrad to start");

    tokio::time::sleep(Duration::from_secs(30)).await;

    info!("attempting to submit blocks");
    submit_blocks(network, rpc_address).await?;

    zebrad.kill(false)?;

    let output = zebrad.wait_with_output()?;
    let output = output.assert_failure()?;

    // [Note on port conflict](#Note on port conflict)
    output
        .assert_was_killed()
        .wrap_err("Possible port conflict. Are there other acceptance tests running?")
}

/// Get block templates and submit blocks
async fn submit_blocks(network: Network, rpc_address: SocketAddr) -> Result<()> {
    let client = RpcRequestClient::new(rpc_address);

    for _ in 1..=NUM_BLOCKS_TO_SUBMIT {
        let (mut block, height) = client
            .block_from_template(Height(REGTEST_NU5_ACTIVATION_HEIGHT))
            .await?;

        while !network.disable_pow()
            && zebra_consensus::difficulty_is_valid(&block.header, &network, &height, &block.hash())
                .is_err()
        {
            increment_big_endian(Arc::make_mut(&mut block.header).nonce.as_mut());
        }

        if height.0 % 40 == 0 {
            info!(?block, ?height, "submitting block");
        }

        client.submit_block(block).await?;
    }

    Ok(())
}

pub trait MiningRpcMethods {
    async fn block_from_template(&self, nu5_activation_height: Height) -> Result<(Block, Height)>;
    async fn submit_block(&self, block: Block) -> Result<()>;
    async fn submit_block_from_template(&self) -> Result<(Block, Height)>;
    async fn get_block(&self, height: i32) -> Result<Option<Arc<Block>>, BoxError>;
}

impl MiningRpcMethods for RpcRequestClient {
    async fn block_from_template(&self, nu5_activation_height: Height) -> Result<(Block, Height)> {
        let block_template: GetBlockTemplate = self
            .json_result_from_call("getblocktemplate", "[]".to_string())
            .await
            .expect("response should be success output with a serialized `GetBlockTemplate`");

        let height = Height(block_template.height);

        let network_upgrade = if height < nu5_activation_height {
            NetworkUpgrade::Canopy
        } else {
            NetworkUpgrade::Nu5
        };

        Ok((
            proposal_block_from_template(&block_template, TimeSource::default(), network_upgrade)?,
            height,
        ))
    }

    async fn submit_block(&self, block: Block) -> Result<()> {
        let block_data = hex::encode(block.zcash_serialize_to_vec()?);

        let submit_block_response: submit_block::Response = self
            .json_result_from_call("submitblock", format!(r#"["{block_data}"]"#))
            .await
            .map_err(|err| eyre!(err))?;

        match submit_block_response {
            submit_block::Response::Accepted => Ok(()),
            submit_block::Response::ErrorResponse(err) => {
                Err(eyre!("block submission failed: {err:?}"))
            }
        }
    }

    async fn submit_block_from_template(&self) -> Result<(Block, Height)> {
        let (block, height) = self
            .block_from_template(Height(REGTEST_NU5_ACTIVATION_HEIGHT))
            .await?;

        self.submit_block(block.clone()).await?;

        Ok((block, height))
    }

    async fn get_block(&self, height: i32) -> Result<Option<Arc<Block>>, BoxError> {
        match self
            .json_result_from_call("getblock", format!(r#"["{}", 0]"#, height))
            .await
        {
            Ok(HexData(raw_block)) => {
                let block = raw_block.zcash_deserialize_into::<Block>()?;
                Ok(Some(Arc::new(block)))
            }
            Err(err)
                if err
                    .downcast_ref::<jsonrpc_core::Error>()
                    .is_some_and(|err| err.code == MISSING_BLOCK_ERROR_CODE) =>
            {
                Ok(None)
            }
            Err(err) => Err(err),
        }
    }
}
