//! Contains various tests for checking forge commands related to verifying contracts on Etherscan
//! and Sourcify.

use crate::utils::{self, EnvExternalities};
use alloy_network::Ethereum;
use alloy_primitives::{Address, U256, hex};
use anvil::{NodeConfig, spawn};
use axum::{
    Router,
    extract::Query,
    http::{StatusCode, header},
    response::IntoResponse,
};
use forge_script_sequence::ScriptSequence;
use foundry_common::retry::Retry;
use foundry_compilers::PathStyle;
use foundry_evm::traces::CallKind;
use foundry_test_utils::{
    forgetest, forgetest_async, str,
    util::{OutputExt, SOLC_VERSION, TestCommand, TestProject},
};
use std::{
    collections::HashMap,
    sync::{Arc, Mutex},
    time::Duration,
};
use tokio::net::TcpListener;

/// Adds a `Unique` contract to the source directory of the project that can be imported as
/// `import {Unique} from "./unique.sol";`
fn add_unique(prj: &TestProject) {
    let timestamp = utils::millis_since_epoch();
    prj.add_source(
        "unique",
        &format!(
            r#"
contract Unique {{
    uint public _timestamp = {timestamp};
}}
"#
        ),
    );
}

fn add_verify_target(prj: &TestProject) {
    prj.add_source(
        "Verify.sol",
        r#"
import {Unique} from "./unique.sol";
contract Verify is Unique {
function doStuff() external {}
}
"#,
    );
}

fn add_single_verify_target_file(prj: &TestProject) {
    let timestamp = utils::millis_since_epoch();
    let contract = format!(
        r#"
contract Unique {{
    uint public _timestamp = {timestamp};
}}
contract Verify is Unique {{
function doStuff() external {{}}
}}
"#
    );

    prj.add_source("Verify.sol", &contract);
}

fn add_verify_target_with_constructor(prj: &TestProject) {
    prj.add_source(
        "Verify.sol",
        r#"
import {Unique} from "./unique.sol";
contract Verify is Unique {
    struct SomeStruct {
        uint256 a;
        string str;
    }

    constructor(SomeStruct memory st, address owner) {}
}
"#,
    );
}

fn parse_verification_result(cmd: &mut TestCommand, retries: u32) -> eyre::Result<()> {
    // Give Etherscan some time to verify the contract.
    Retry::new(retries, Duration::from_secs(30)).run(|| -> eyre::Result<()> {
        let output = cmd.execute();
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);
        test_debug!("stdout: {stdout}\nstderr: {stderr}");
        if stderr.contains("Contract successfully verified") {
            return Ok(());
        }
        eyre::bail!("Failed to get verification, stdout: {stdout}, stderr: {stderr}");
    })
}

fn verify_check(
    guid: String,
    chain: String,
    etherscan_api_key: Option<String>,
    verifier: Option<String>,
    mut cmd: TestCommand,
) {
    let mut args = vec!["verify-check", &guid, "--chain-id", &chain];

    if let Some(etherscan_api_key) = &etherscan_api_key {
        args.push("--etherscan-api-key");
        args.push(etherscan_api_key);
    }

    if let Some(verifier) = &verifier {
        args.push("--verifier");
        args.push(verifier);
    }
    cmd.forge_fuse().args(args);

    parse_verification_result(&mut cmd, 6).expect("Failed to verify check")
}

fn await_verification_response(info: EnvExternalities, mut cmd: TestCommand) {
    let guid = {
        // Give Etherscan some time to detect the transaction.
        Retry::new(5, Duration::from_secs(60))
            .run(|| -> eyre::Result<String> {
                let output = cmd.execute();
                let out = String::from_utf8_lossy(&output.stdout);
                utils::parse_verification_guid(&out).ok_or_else(|| {
                    eyre::eyre!(
                        "Failed to get guid, stdout: {}, stderr: {}",
                        out,
                        String::from_utf8_lossy(&output.stderr)
                    )
                })
            })
            .expect("Failed to get verify guid")
    };

    // verify-check
    let etherscan = (!info.etherscan.is_empty()).then_some(info.etherscan.clone());
    let verifier = (!info.verifier.is_empty()).then_some(info.verifier.clone());
    verify_check(guid, info.chain.to_string(), etherscan, verifier, cmd);
}

fn deploy_contract(
    info: &EnvExternalities,
    contract_path: &str,
    prj: TestProject,
    cmd: &mut TestCommand,
) -> String {
    add_unique(&prj);
    add_verify_target(&prj);
    let output = cmd
        .forge_fuse()
        .arg("create")
        .args(info.create_args())
        .arg(contract_path)
        .assert_success()
        .get_output()
        .stdout_lossy();
    utils::parse_deployed_address(output.as_str())
        .unwrap_or_else(|| panic!("Failed to parse deployer {output}"))
}

fn verify_on_chain(info: Option<EnvExternalities>, prj: TestProject, mut cmd: TestCommand) {
    // only execute if keys present
    if let Some(info) = info {
        test_debug!("verifying on {}", info.chain);

        let contract_path = "src/Verify.sol:Verify";
        let address = deploy_contract(&info, contract_path, prj, &mut cmd);

        let mut args = vec![
            "--chain-id".to_string(),
            info.chain.to_string(),
            address,
            contract_path.to_string(),
        ];

        if !info.etherscan.is_empty() {
            args.push("--etherscan-api-key".to_string());
            args.push(info.etherscan.clone());
        }

        if !info.verifier.is_empty() {
            args.push("--verifier".to_string());
            args.push(info.verifier.clone());
        }
        cmd.forge_fuse().arg("verify-contract").root_arg().args(args);

        await_verification_response(info, cmd)
    }
}

fn guess_constructor_args(info: Option<EnvExternalities>, prj: TestProject, mut cmd: TestCommand) {
    // only execute if keys present
    if let Some(info) = info {
        test_debug!("verifying on {}", info.chain);
        add_unique(&prj);
        add_verify_target_with_constructor(&prj);

        let contract_path = "src/Verify.sol:Verify";
        let output = cmd
            .arg("create")
            .args(info.create_args())
            .arg(contract_path)
            .args(vec![
                "--constructor-args",
                "(239,SomeString)",
                "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
            ])
            .assert_success()
            .get_output()
            .stdout_lossy();

        let address = utils::parse_deployed_address(output.as_str())
            .unwrap_or_else(|| panic!("Failed to parse deployer {output}"));

        cmd.forge_fuse().arg("verify-contract").root_arg().args([
            "--rpc-url".to_string(),
            info.rpc.clone(),
            address,
            contract_path.to_string(),
            "--etherscan-api-key".to_string(),
            info.etherscan.clone(),
            "--verifier".to_string(),
            info.verifier.clone(),
            "--guess-constructor-args".to_string(),
        ]);

        await_verification_response(info, cmd)
    }
}

/// Executes create --verify on the given chain
fn create_verify_on_chain(info: Option<EnvExternalities>, prj: TestProject, mut cmd: TestCommand) {
    // only execute if keys present
    if let Some(info) = info {
        test_debug!("verifying on {}", info.chain);
        add_single_verify_target_file(&prj);

        let contract_path = "src/Verify.sol:Verify";
        let assert = cmd
            .arg("create")
            .args(info.create_args())
            .args([contract_path, "--etherscan-api-key", info.etherscan.as_str(), "--verify"])
            .assert_success();
        let output = assert.get_output();
        let stderr = String::from_utf8_lossy(&output.stderr);
        assert!(stderr.contains("Contract successfully verified"), "stderr: {stderr}");
    }
}

// tests `create && contract-verify && verify-check` on Fantom testnet if correct env vars are set
forgetest!(can_verify_random_contract_fantom_testnet, |prj, cmd| {
    verify_on_chain(EnvExternalities::ftm_testnet(), prj, cmd);
});

// tests `create && contract-verify && verify-check` on Optimism kovan if correct env vars are set
forgetest!(can_verify_random_contract_optimism_kovan, |prj, cmd| {
    verify_on_chain(EnvExternalities::optimism_kovan(), prj, cmd);
});

// tests `create && contract-verify && verify-check` on Sepolia testnet if correct env vars are set
forgetest!(can_verify_random_contract_sepolia, |prj, cmd| {
    // Implicitly tests `--verifier etherscan` on Sepolia testnet
    verify_on_chain(EnvExternalities::sepolia_etherscan(), prj, cmd);
});

// tests that `verify-contract --verifier etherscan` also submits to Sourcify on Sepolia
forgetest!(can_verify_contract_sepolia_etherscan_also_runs_sourcify, |prj, cmd| {
    if let Some(info) = EnvExternalities::sepolia_etherscan() {
        test_debug!("verifying on {}", info.chain);
        add_unique(&prj);
        add_verify_target(&prj);
        let contract_path = "src/Verify.sol:Verify";

        let deploy_output = cmd
            .forge_fuse()
            .arg("create")
            .args(info.create_args())
            .args([contract_path, "--broadcast"])
            .assert_success()
            .get_output()
            .stdout_lossy();
        let address = utils::parse_deployed_address(deploy_output.as_str())
            .unwrap_or_else(|| panic!("Failed to parse deployer {deploy_output}"));

        let output = cmd
            .forge_fuse()
            .arg("verify-contract")
            .root_arg()
            .args([
                "--chain-id",
                info.chain.as_ref(),
                &address,
                contract_path,
                "--etherscan-api-key",
                info.etherscan.as_str(),
                "--verifier",
                info.verifier.as_str(),
            ])
            .assert_success()
            .get_output()
            .stderr_lossy();

        assert!(output.contains("Verifying on etherscan"), "Etherscan run missing: {output}");
        assert!(
            output.contains("Verification Job ID")
                || output.contains("Contract source code already fully verified"),
            "Sourcify submission did not succeed: {output}"
        );
        assert!(
            !output.contains("sourcify verification failed"),
            "Sourcify failure warning logged: {output}"
        );
    }
});

// tests `create --verify on Sepolia testnet if correct env vars are set
// SEPOLIA_RPC_URL=https://rpc.sepolia.org
// TEST_PRIVATE_KEY=0x...
// ETHERSCAN_API_KEY=<API_KEY>
forgetest!(can_create_verify_random_contract_sepolia_etherscan, |prj, cmd| {
    // Implicitly tests `--verifier etherscan` on Sepolia testnet
    create_verify_on_chain(EnvExternalities::sepolia_etherscan(), prj, cmd);
});

// tests that `create --verify --verifier etherscan` also submits to Sourcify on Sepolia
forgetest!(can_create_verify_sepolia_etherscan_also_runs_sourcify, |prj, cmd| {
    if let Some(info) = EnvExternalities::sepolia_etherscan() {
        test_debug!("verifying on {}", info.chain);
        add_single_verify_target_file(&prj);

        let contract_path = "src/Verify.sol:Verify";
        let output = cmd
            .arg("create")
            .args(info.create_args())
            .args([
                contract_path,
                "--etherscan-api-key",
                info.etherscan.as_str(),
                "--verify",
                "--broadcast",
            ])
            .assert_success()
            .get_output()
            .stderr_lossy();

        assert!(output.contains("Verifying on etherscan"), "Etherscan run missing: {output}");
        assert!(
            output.contains("Verification Job ID")
                || output.contains("Contract source code already fully verified"),
            "Sourcify submission did not succeed: {output}"
        );
        assert!(
            !output.contains("sourcify verification failed"),
            "Sourcify failure warning logged: {output}"
        );
    }
});

// tests `create --verify --verifier sourcify` on Sepolia testnet
forgetest!(can_create_verify_random_contract_sepolia_sourcify, |prj, cmd| {
    verify_on_chain(EnvExternalities::sepolia_sourcify(), prj, cmd);
});

// tests `create --verify --verifier sourcify` with etherscan api key set
// <https://github.com/foundry-rs/foundry/issues/10000>
forgetest!(
    can_create_verify_random_contract_sepolia_sourcify_with_etherscan_api_key_set,
    |prj, cmd| {
        verify_on_chain(EnvExternalities::sepolia_sourcify_with_etherscan_api_key_set(), prj, cmd);
    }
);

// tests `create --verify --verifier blockscout` on Sepolia testnet
forgetest!(can_create_verify_random_contract_sepolia_blockscout, |prj, cmd| {
    verify_on_chain(EnvExternalities::sepolia_blockscout(), prj, cmd);
});

// tests `create --verify --verifier blockscout` on Sepolia testnet with etherscan api key set
forgetest!(
    can_create_verify_random_contract_sepolia_blockscout_with_etherscan_api_key_set,
    |prj, cmd| {
        verify_on_chain(
            EnvExternalities::sepolia_blockscout_with_etherscan_api_key_set(),
            prj,
            cmd,
        );
    }
);

// tests `create && contract-verify --guess-constructor-args && verify-check` on Goerli testnet if
// correct env vars are set
forgetest!(can_guess_constructor_args, |prj, cmd| {
    guess_constructor_args(EnvExternalities::goerli(), prj, cmd);
});

// tests `create && verify-contract && verify-check` on sepolia with default sourcify verifier
forgetest!(can_verify_random_contract_sepolia_default_sourcify, |prj, cmd| {
    verify_on_chain(EnvExternalities::sepolia_empty_verifier(), prj, cmd);
});

// Tests that verify properly validates verifier arguments.
// <https://github.com/foundry-rs/foundry/issues/11430>
forgetest_init!(can_validate_verifier_settings, |prj, cmd| {
    prj.initialize_default_contracts();
    // Build the project to create the cache.
    cmd.forge_fuse().arg("build").assert_success();
    // No verifier URL.
    cmd.forge_fuse()
        .args([
            "verify-contract",
            "--rpc-url",
            "https://rpc.sepolia-api.lisk.com",
            "--verifier",
            "blockscout",
            "0x19b248616E4964f43F611b5871CE1250f360E9d3",
            "src/Counter.sol:Counter",
        ])
        .assert_failure()
        .stderr_eq(str![[r#"
Start verifying contract `0x19b248616E4964f43F611b5871CE1250f360E9d3` deployed on 4202
Error: No verifier URL specified for verifier blockscout

"#]]);

    // Unknown Etherscan chain.
    cmd.forge_fuse()
        .args([
            "verify-contract",
            "--rpc-url",
            "https://rpc.sepolia-api.lisk.com",
            "--verifier",
            "etherscan",
            "0x19b248616E4964f43F611b5871CE1250f360E9d3",
            "src/Counter.sol:Counter",
        ])
        .assert_failure()
        .stderr_eq(str![[r#"
Start verifying contract `0x19b248616E4964f43F611b5871CE1250f360E9d3` deployed on 4202
Error: No known Etherscan API URL for chain `4202`. To fix this, please:
1. Specify a `url` when using Etherscan verifier
2. Verify the chain `4202` is correct

"#]]);

    cmd.forge_fuse()
        .args([
            "verify-contract",
            "--rpc-url",
            "https://rpc.sepolia-api.lisk.com",
            "--verifier",
            "blockscout",
            "--verifier-url",
            "https://sepolia-blockscout.lisk.com/api",
            "0x19b248616E4964f43F611b5871CE1250f360E9d3",
            "src/Counter.sol:Counter",
        ])
        .assert_success()
        .stdout_eq(str![""])
        .stderr_eq(str![[r#"
Start verifying contract `0x19b248616E4964f43F611b5871CE1250f360E9d3` deployed on 4202

Verifying on blockscout...
Contract [src/Counter.sol:Counter] "0x19b248616E4964f43F611b5871CE1250f360E9d3" is already verified. Skipping verification.

"#]]);

    // Unknown Etherscan chain, explicit Blockscout verifier, and ETHERSCAN_API_KEY set.
    // This should still use the explicit verifier URL instead of trying to resolve an
    // Etherscan API URL for the chain.
    let cmd = cmd.forge_fuse();
    cmd.env("ETHERSCAN_API_KEY", "dummy");
    cmd.args([
        "verify-contract",
        "--rpc-url",
        "https://rpc.sepolia-api.lisk.com",
        "--verifier",
        "blockscout",
        "--verifier-url",
        "https://sepolia-blockscout.lisk.com/api",
        "0x19b248616E4964f43F611b5871CE1250f360E9d3",
        "src/Counter.sol:Counter",
    ])
    .assert_success()
    .stdout_eq(str![""])
    .stderr_eq(str![[r#"
Start verifying contract `0x19b248616E4964f43F611b5871CE1250f360E9d3` deployed on 4202

Verifying on blockscout...
Contract [src/Counter.sol:Counter] "0x19b248616E4964f43F611b5871CE1250f360E9d3" is already verified. Skipping verification.

"#]]);
});

// Tests that `forge script --broadcast --verify` fails before broadcasting when
// the verifier rejects the API key (credential preflight check).
forgetest_async!(script_fails_early_on_bad_verifier_credentials, |prj, cmd| {
    foundry_test_utils::util::initialize(prj.root());
    prj.add_script(
        "Deploy.s.sol",
        r#"
import "forge-std/Script.sol";
contract Noop {}
contract Deploy is Script {
    function run() external {
        vm.startBroadcast();
        new Noop();
        vm.stopBroadcast();
    }
}
"#,
    );

    let (_api, handle) = spawn(NodeConfig::test()).await;
    let wallet = handle.dev_wallets().next().unwrap();
    let pk = hex::encode(wallet.credential().to_bytes());

    let (verifier_url, _server) =
        spawn_mock_verifier(r#"{"status":"0","message":"NOTOK","result":"Invalid API Key"}"#).await;

    let output = cmd
        .forge_fuse()
        .args([
            "script",
            "script/Deploy.s.sol:Deploy",
            "--rpc-url",
            handle.http_endpoint().as_str(),
            "--private-key",
            pk.as_str(),
            "--broadcast",
            "--verify",
            "--verifier",
            "custom",
            "--verifier-url",
            verifier_url.as_str(),
            "--verifier-api-key",
            "FAKE_KEY_1234",
        ])
        .execute();

    assert!(!output.status.success(), "expected command to fail");
    let stderr = output.stderr_lossy();
    assert!(
        stderr.contains("Verification preflight check failed"),
        "expected preflight error in stderr, got: {stderr}"
    );
    // The broadcast phase prints "ONCHAIN EXECUTION COMPLETE" and "Sending transactions";
    // neither must appear if the preflight check stopped execution before broadcasting.
    let stdout = output.stdout_lossy();
    assert!(
        !stdout.contains("ONCHAIN EXECUTION COMPLETE") && !stdout.contains("Sending transactions"),
        "transactions were broadcast but preflight check should have prevented it: {stdout}"
    );
});

/// Spawns a local HTTP server that returns the given body for Etherscan-style ABI requests.
async fn spawn_mock_verifier(body: &'static str) -> (String, tokio::task::JoinHandle<()>) {
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();
    let app =
        Router::new().fallback(move |Query(query): Query<HashMap<String, String>>| async move {
            if query.get("module").is_some_and(|value| value == "contract")
                && query.get("action").is_some_and(|value| value == "getabi")
                && query.contains_key("address")
                && query.contains_key("apikey")
            {
                body
            } else {
                r#"{"status":"0","message":"NOTOK","result":"Contract source code not verified"}"#
            }
        });
    let handle = tokio::spawn(async move {
        axum::serve(listener, app).await.unwrap();
    });
    (format!("http://{addr}"), handle)
}

/// Spawns a local HTTP server mocking the full Etherscan verification flow: the `getabi` preflight
/// returns "not verified", `verifysourcecode` returns a submission GUID, and `checkverifystatus`
/// reports success. Returns the server URL and the GUID it emits.
async fn spawn_full_mock_verifier() -> (String, &'static str, tokio::task::JoinHandle<()>) {
    const GUID: &str = "mockguid1976000000000000000000000000000000000000000";
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();
    let app = Router::new().fallback(move |uri: axum::http::Uri, body: String| async move {
        let combined = format!("{}&{body}", uri.query().unwrap_or_default());
        if combined.contains("verifysourcecode") {
            format!(r#"{{"status":"1","message":"OK","result":"{GUID}"}}"#)
        } else if combined.contains("checkverifystatus") {
            r#"{"status":"1","message":"OK","result":"Pass - Verified"}"#.to_string()
        } else {
            r#"{"status":"0","message":"NOTOK","result":"Contract source code not verified"}"#
                .to_string()
        }
    });
    let handle = tokio::spawn(async move {
        axum::serve(listener, app).await.unwrap();
    });
    (format!("http://{addr}"), GUID, handle)
}

// Tests that `forge create --broadcast --verify --json` keeps stdout clean (valid JSON only) and
// does not leak the verification submission GUID/URL into stdout, while still reporting it on
// stderr. <https://github.com/foundry-rs/foundry/issues/1976>
forgetest_async!(create_verify_json_keeps_stdout_clean, |prj, cmd| {
    prj.initialize_default_contracts();
    let (_api, handle) = spawn(NodeConfig::test()).await;
    let wallet = handle.dev_wallets().next().unwrap();
    let pk = hex::encode(wallet.credential().to_bytes());

    let (verifier_url, guid, _server) = spawn_full_mock_verifier().await;

    let output = cmd
        .forge_fuse()
        .args([
            "create",
            "src/Counter.sol:Counter",
            "--rpc-url",
            handle.http_endpoint().as_str(),
            "--private-key",
            pk.as_str(),
            "--broadcast",
            "--verify",
            "--verifier",
            "custom",
            "--verifier-url",
            verifier_url.as_str(),
            "--verifier-api-key",
            "VALID_KEY",
            "--json",
        ])
        .execute();

    assert!(output.status.success(), "expected command to succeed");

    let stdout = output.stdout_lossy();
    // stdout must be a single valid JSON document (the deployment result).
    serde_json::from_str::<serde_json::Value>(stdout.trim())
        .unwrap_or_else(|e| panic!("stdout is not valid JSON ({e}): {stdout}"));
    // The verification submission GUID must not pollute stdout.
    assert!(!stdout.contains(guid), "verification GUID leaked into stdout: {stdout}");

    // The GUID/URL is still reported to the user on stderr.
    let stderr = output.stderr_lossy();
    assert!(stderr.contains(guid), "expected verification GUID on stderr, got: {stderr}");
});

// Tests that `forge script --broadcast --verify --json` keeps stdout clean (valid JSON Lines only)
// and does not leak the verification submission GUID/URL into stdout, while still reporting it on
// stderr. <https://github.com/foundry-rs/foundry/issues/1976>
forgetest_async!(script_verify_json_keeps_stdout_clean, |prj, cmd| {
    foundry_test_utils::util::initialize(prj.root());
    prj.add_script(
        "Deploy.s.sol",
        r#"
import "forge-std/Script.sol";
contract Noop {}
contract Deploy is Script {
    function run() external {
        vm.startBroadcast();
        new Noop();
        vm.stopBroadcast();
    }
}
"#,
    );

    let (_api, handle) = spawn(NodeConfig::test()).await;
    let wallet = handle.dev_wallets().next().unwrap();
    let pk = hex::encode(wallet.credential().to_bytes());

    let (verifier_url, guid, _server) = spawn_full_mock_verifier().await;

    let output = cmd
        .forge_fuse()
        .args([
            "script",
            "script/Deploy.s.sol:Deploy",
            "--rpc-url",
            handle.http_endpoint().as_str(),
            "--private-key",
            pk.as_str(),
            "--broadcast",
            "--verify",
            "--verifier",
            "custom",
            "--verifier-url",
            verifier_url.as_str(),
            "--verifier-api-key",
            "VALID_KEY",
            "--json",
        ])
        .execute();

    assert!(output.status.success(), "expected command to succeed");

    let stdout = output.stdout_lossy();
    for line in stdout.lines().filter(|line| !line.trim().is_empty()) {
        serde_json::from_str::<serde_json::Value>(line)
            .unwrap_or_else(|e| panic!("stdout line is not valid JSON ({e}): {line}"));
    }
    assert!(!stdout.contains(guid), "verification GUID leaked into stdout: {stdout}");

    let stderr = output.stderr_lossy();
    assert!(stderr.contains(guid), "expected verification GUID on stderr, got: {stderr}");
});

// Regression test for <https://github.com/foundry-rs/foundry/issues/10164>. The contract created
// by the script is compiled in a different project and can only be identified from its factory's
// verified Standard JSON input.
forgetest_async!(script_verifies_external_create2_contract, |prj, cmd| {
    const SUBMISSION_KEY: &str = "submission-key";
    const GUID: &str = "external-create2-guid";

    foundry_test_utils::util::initialize(prj.root());
    let external = TestProject::new("external-create2", PathStyle::Dapptools);
    foundry_test_utils::util::initialize(external.root());
    external.add_source(
        "External.sol",
        r#"
contract Child { uint256 public immutable value; constructor(uint256 value_) { value = value_; } }
contract Executor {
    function deploy(bytes memory initCode) external returns (address deployed) {
        assembly { deployed := create2(0, add(initCode, 32), mload(initCode), 0) }
        require(deployed != address(0));
    }
}
contract ExternalFactory {
    Executor public immutable executor;
    constructor() { executor = new Executor(); }
    function deployChild(uint256 value) external returns (address) {
        return executor.deploy(abi.encodePacked(type(Child).creationCode, abi.encode(value)));
    }
}
"#,
    );
    external.write_config(foundry_config::Config {
        solc: Some(foundry_config::SolcReq::Version(SOLC_VERSION.parse().unwrap())),
        optimizer: Some(true),
        optimizer_runs: Some(777),
        ..Default::default()
    });

    let (_api, anvil) = spawn(NodeConfig::test()).await;
    let wallet = anvil.dev_wallets().next().unwrap();
    let private_key = hex::encode(wallet.credential().to_bytes());
    let rpc = anvil.http_endpoint();
    let create = external
        .forge_command()
        .forge_fuse()
        .args([
            "create",
            "src/External.sol:ExternalFactory",
            "--broadcast",
            "--json",
            "--rpc-url",
            rpc.as_str(),
            "--private-key",
            private_key.as_str(),
        ])
        .assert_success()
        .get_output()
        .stdout_lossy();
    let create: serde_json::Value = serde_json::from_str(&create).unwrap();
    let factory = create["deployedTo"].as_str().unwrap().to_string();
    let selector = &alloy_primitives::keccak256("executor()")[..4];
    let call: serde_json::Value = reqwest::Client::new()
        .post(rpc.as_str())
        .json(&serde_json::json!({
            "jsonrpc": "2.0", "id": 1, "method": "eth_call",
            "params": [{"to": factory, "data": format!("0x{}", hex::encode(selector))}, "latest"]
        }))
        .send()
        .await
        .unwrap()
        .json()
        .await
        .unwrap();
    let executor_result = call["result"].as_str().unwrap();
    let executor = format!("0x{}", &executor_result[executor_result.len() - 40..]);

    let standard_json = external
        .forge_command()
        .forge_fuse()
        .args([
            "verify-contract",
            factory.as_str(),
            "src/External.sol:ExternalFactory",
            "--show-standard-json-input",
        ])
        .assert_success()
        .get_output()
        .stdout_lossy();
    let standard_json: serde_json::Value = serde_json::from_str(standard_json.trim()).unwrap();
    assert_eq!(standard_json["settings"]["optimizer"]["runs"], 777);

    // Use the compiler build recorded by Forge's artifact, not a guessed release suffix.
    let artifact: serde_json::Value = serde_json::from_reader(
        std::fs::File::open(external.artifacts().join("External.sol/ExternalFactory.json"))
            .unwrap(),
    )
    .unwrap();
    let metadata: serde_json::Value =
        serde_json::from_str(artifact["rawMetadata"].as_str().unwrap()).unwrap();
    let compiler_version = format!("v{}", metadata["compiler"]["version"].as_str().unwrap());

    #[derive(Default)]
    struct Requests {
        sources: Vec<HashMap<String, String>>,
        submissions: Vec<HashMap<String, String>>,
    }
    let requests = Arc::new(Mutex::new(Requests::default()));
    let state = requests.clone();
    let source_json = standard_json.clone();
    let factory_for_server = factory.to_lowercase();
    let compiler_version_for_server = compiler_version.clone();

    // Verification requests are trusted and must retain normal redirect handling. External source
    // discovery uses the same selected endpoint, but remains on the non-redirecting client.
    let redirect_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let redirect_url = format!("http://{}", redirect_listener.local_addr().unwrap());
    let redirect_state = requests.clone();
    let redirect_app = Router::new().fallback(move |uri: axum::http::Uri, body: String| {
        let state = redirect_state.clone();
        async move {
            let encoded = if body.is_empty() { uri.query().unwrap_or_default() } else { &body };
            let form = url::form_urlencoded::parse(encoded.as_bytes())
                .into_owned().collect::<HashMap<_, _>>();
            match form.get("action").map(String::as_str) {
                Some("verifysourcecode") => {
                    state.lock().unwrap().submissions.push(form);
                    format!(r#"{{"status":"1","message":"OK","result":"{GUID}"}}"#)
                }
                Some("checkverifystatus") => {
                    r#"{"status":"1","message":"OK","result":"Pass - Verified"}"#.to_string()
                }
                _ => r#"{"status":"0","message":"NOTOK","result":"Contract source code not verified"}"#.to_string(),
            }
        }
    });
    let _redirect_server =
        tokio::spawn(async move { axum::serve(redirect_listener, redirect_app).await.unwrap() });

    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let verifier_url = format!("http://{}", listener.local_addr().unwrap());
    let app = Router::new().fallback(move |uri: axum::http::Uri, body: String| {
        let state = state.clone();
        let source_json = source_json.clone();
        let factory = factory_for_server.clone();
        let compiler_version = compiler_version_for_server.clone();
        let redirect_url = redirect_url.clone();
        async move {
            let encoded = if body.is_empty() { uri.query().unwrap_or_default() } else { &body };
            let form = url::form_urlencoded::parse(encoded.as_bytes())
                .into_owned().collect::<HashMap<_, _>>();
            match form.get("action").map(String::as_str) {
                Some("getsourcecode") => {
                    state.lock().unwrap().sources.push(form.clone());
                    if form.get("address").is_some_and(|address| address.to_lowercase() == factory) {
                        serde_json::json!({"status":"1","message":"OK","result":[{
                            "SourceCode": source_json.to_string(), "CompilerVersion": compiler_version,
                            "ContractName": "ExternalFactory", "ABI": "[]",
                            "OptimizationUsed": "1", "OptimizationRuns": "777",
                            "ConstructorArguments": "", "EVMVersion": "osaka", "IsProxy": "0"
                        }]}).to_string().into_response()
                    } else {
                        r#"{"status":"0","message":"NOTOK","result":"Contract source code not verified"}"#.into_response()
                    }
                }
                Some("verifysourcecode" | "checkverifystatus") => (
                    StatusCode::TEMPORARY_REDIRECT,
                    [(header::LOCATION, redirect_url)],
                )
                    .into_response(),
                _ => r#"{"status":"0","message":"NOTOK","result":"Contract source code not verified"}"#.into_response(),
            }
        }
    });
    let _server = tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });

    prj.add_source(
        "IExternalFactory.sol",
        "interface IExternalFactory { function deployChild(uint256) external returns (address); }",
    );
    prj.add_script("Deploy.s.sol", &format!(r#"
import "forge-std/Script.sol";
import {{IExternalFactory}} from "../src/IExternalFactory.sol";
contract Deploy is Script {{
    function run() external {{ vm.startBroadcast(); IExternalFactory({factory}).deployChild(42); vm.stopBroadcast(); }}
}}
"#));

    let output = cmd
        .forge_fuse()
        .args([
            "script",
            "script/Deploy.s.sol:Deploy",
            "--broadcast",
            "--verify",
            "--verify-external",
            "--verifier",
            "custom",
            "--verifier-url",
            verifier_url.as_str(),
            "--verifier-api-key",
            SUBMISSION_KEY,
            "--rpc-url",
            rpc.as_str(),
            "--private-key",
            private_key.as_str(),
        ])
        .execute();
    assert!(output.status.success(), "script failed: {}", output.stderr_lossy());

    assert!(!prj.root().join("src/External.sol").exists());
    assert!(!prj.artifacts().join("External.sol/Child.json").exists());
    let submission = {
        let requests = requests.lock().unwrap();
        assert!(requests.sources.len() >= 2, "source requests: {:?}", requests.sources);
        let source_addresses = requests
            .sources
            .iter()
            .map(|request| request["address"].to_lowercase())
            .collect::<Vec<_>>();
        let expected = [executor.to_lowercase(), factory.to_lowercase()];
        assert!(
            source_addresses.windows(2).any(|addresses| addresses == expected),
            "source request order: {source_addresses:?}"
        );
        assert!(requests.sources.iter().all(|request| request["apikey"] == SUBMISSION_KEY));
        assert_eq!(requests.submissions.len(), 1, "script stderr: {}", output.stderr_lossy());
        requests.submissions[0].clone()
    };
    assert_eq!(submission["apikey"], SUBMISSION_KEY);
    assert_eq!(submission["contractname"], "src/External.sol:Child");
    assert_eq!(submission["codeformat"], "solidity-standard-json-input");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&submission["sourceCode"]).unwrap(),
        standard_json
    );
    assert_eq!(submission["compilerversion"], compiler_version);
    assert_eq!(submission["constructorArguements"], format!("{:064x}", 42));
    let broadcast_path = prj.root().join("broadcast/Deploy.s.sol/31337/run-latest.json");
    let broadcast = std::fs::read_to_string(&broadcast_path).unwrap();
    let mut sequence: ScriptSequence<Ethereum> = serde_json::from_str(&broadcast).unwrap();
    let child_address = submission["contractaddress"].parse::<Address>().unwrap();
    let child = sequence
        .transactions
        .iter()
        .flat_map(|transaction| &transaction.additional_contracts)
        .find(|contract| contract.address == child_address)
        .expect("submitted contract is not a traced nested creation");
    assert_eq!(child.call_kind, CallKind::Create2);
    assert_eq!(
        &child.creator_code_addresses[..2],
        &[executor.parse::<Address>().unwrap(), factory.parse::<Address>().unwrap()]
    );

    let value_selector = &alloy_primitives::keccak256("value()")[..4];
    let value: serde_json::Value = reqwest::Client::new()
        .post(rpc.as_str())
        .json(&serde_json::json!({
            "jsonrpc": "2.0", "id": 2, "method": "eth_call",
            "params": [{"to": child_address, "data": format!("0x{}", hex::encode(value_selector))}, "latest"]
        }))
        .send()
        .await
        .unwrap()
        .json()
        .await
        .unwrap();
    assert_eq!(value["result"].as_str().unwrap().parse::<U256>().unwrap(), U256::from(42));

    // Old broadcast logs do not contain creator provenance. An explicitly requested external
    // verification must report that skipped attempt as a failure rather than `All (0)` success.
    sequence
        .transactions
        .iter_mut()
        .flat_map(|transaction| &mut transaction.additional_contracts)
        .find(|contract| contract.address == child_address)
        .unwrap()
        .creator_code_addresses
        .clear();
    std::fs::write(&broadcast_path, serde_json::to_vec_pretty(&sequence).unwrap()).unwrap();

    let failed = prj
        .forge_command()
        .forge_fuse()
        .args([
            "script",
            "script/Deploy.s.sol:Deploy",
            "--broadcast",
            "--resume",
            "--verify",
            "--verify-external",
            "--verifier",
            "custom",
            "--verifier-url",
            verifier_url.as_str(),
            "--verifier-api-key",
            SUBMISSION_KEY,
            "--rpc-url",
            rpc.as_str(),
            "--private-key",
            private_key.as_str(),
        ])
        .execute();
    assert!(!failed.status.success(), "resume unexpectedly succeeded: {}", failed.stderr_lossy());
    let stderr = failed.stderr_lossy();
    assert!(stderr.contains("creator provenance is unavailable"), "{stderr}");
    assert!(stderr.contains("Not all (0 / 1) contracts were verified"), "{stderr}");
    assert!(!stderr.contains("All (0) contracts were verified"), "{stderr}");
});

// Tests that the preflight check passes (does not block deploy) when the verifier responds
// with ContractCodeNotVerified (the normal "valid key, unknown address" response).
forgetest_async!(create_preflight_passes_on_contract_not_verified, |prj, cmd| {
    prj.initialize_default_contracts();
    let (_api, handle) = spawn(NodeConfig::test()).await;
    let wallet = handle.dev_wallets().next().unwrap();
    let pk = hex::encode(wallet.credential().to_bytes());

    // Server returns a well-formed "source code not verified" Etherscan response.
    let (verifier_url, _server) = spawn_mock_verifier(
        r#"{"status":"0","message":"NOTOK","result":"Contract source code not verified"}"#,
    )
    .await;

    let output = cmd
        .forge_fuse()
        .args([
            "create",
            "src/Counter.sol:Counter",
            "--rpc-url",
            handle.http_endpoint().as_str(),
            "--private-key",
            pk.as_str(),
            "--verify",
            "--verifier",
            "custom",
            "--verifier-url",
            verifier_url.as_str(),
            "--verifier-api-key",
            "VALID_KEY",
        ])
        .execute();

    // Preflight must pass — the command may fail for other reasons (e.g. post-deploy
    // verification), but it must NOT fail with the preflight error.
    let stderr = output.stderr_lossy();
    assert!(
        !stderr.contains("Verification preflight check failed"),
        "preflight should not block on ContractCodeNotVerified, got: {stderr}"
    );
});

// Tests that the preflight check fails (blocks deploy) when the verifier explicitly
// rejects the API key with an InvalidApiKey response.
forgetest_async!(create_preflight_fails_on_invalid_api_key, |prj, cmd| {
    prj.initialize_default_contracts();
    let (_api, handle) = spawn(NodeConfig::test()).await;
    let wallet = handle.dev_wallets().next().unwrap();
    let pk = hex::encode(wallet.credential().to_bytes());

    // Server returns a well-formed "invalid API key" Etherscan response.
    let (verifier_url, _server) =
        spawn_mock_verifier(r#"{"status":"0","message":"NOTOK","result":"Invalid API Key"}"#).await;

    let output = cmd
        .forge_fuse()
        .args([
            "create",
            "src/Counter.sol:Counter",
            "--rpc-url",
            handle.http_endpoint().as_str(),
            "--private-key",
            pk.as_str(),
            "--verify",
            "--verifier",
            "custom",
            "--verifier-url",
            verifier_url.as_str(),
            "--verifier-api-key",
            "BAD_KEY",
        ])
        .execute();

    assert!(!output.status.success(), "expected command to fail");
    let stderr = output.stderr_lossy();
    assert!(
        stderr.contains("Verification preflight check failed"),
        "expected preflight error in stderr, got: {stderr}"
    );
    let stdout = output.stdout_lossy();
    assert!(
        !stdout.contains("Contract Address"),
        "contract was deployed but preflight check should have prevented it"
    );
});

// Tests that the preflight check does NOT block deployment when the verifier responds
// with a rate-limit error (transient, not an auth failure).
forgetest_async!(create_preflight_warns_on_rate_limit, |prj, cmd| {
    prj.initialize_default_contracts();
    let (_api, handle) = spawn(NodeConfig::test()).await;
    let wallet = handle.dev_wallets().next().unwrap();
    let pk = hex::encode(wallet.credential().to_bytes());

    // Server returns a well-formed "rate limit exceeded" Etherscan response.
    let (verifier_url, _server) = spawn_mock_verifier(
        r#"{"status":"0","message":"NOTOK","result":"Max rate limit reached"}"#,
    )
    .await;

    let output = cmd
        .forge_fuse()
        .args([
            "create",
            "src/Counter.sol:Counter",
            "--rpc-url",
            handle.http_endpoint().as_str(),
            "--private-key",
            pk.as_str(),
            "--verify",
            "--verifier",
            "blockscout",
            "--verifier-url",
            verifier_url.as_str(),
            "--verifier-api-key",
            "VALID_KEY",
        ])
        .execute();

    // Rate limit must not block the deploy.
    let stderr = output.stderr_lossy();
    assert!(
        !stderr.contains("Verification preflight check failed"),
        "preflight should not block on rate limit, got: {stderr}"
    );
    assert!(
        stderr.contains("verifier credential check inconclusive"),
        "preflight should warn on rate limit, got: {stderr}"
    );
});
