//! Contains various tests for checking forge commands related to config values

use alloy_primitives::{Address, B256, U256};
use foundry_cli::utils as forge_utils;
use foundry_compilers::{
    artifacts::{BytecodeHash, OptimizerDetails, RevertStrings, YulDetails},
    solc::Solc,
};
use foundry_config::{
    CompilationRestrictions, Config, Eip1559FeeEstimatePreset, FsPermissions, FuzzConfig,
    FuzzCorpusConfig, InvariantConfig, SettingsOverrides, SolcReq, SymbolicConfig,
    SymbolicExplorationOrder, SymbolicStorageLayout, TracingConfig,
    cache::{CachedChains, CachedEndpoints, StorageCachingConfig},
    filter::GlobMatcher,
    fs_permissions::{FsAccessPermission, PathPermission},
};
use foundry_evm::opts::EvmOpts;
use foundry_test_utils::{
    foundry_compilers::artifacts::{EvmVersion, remappings::Remapping},
    util::{OTHER_SOLC_VERSION, OutputExt, TestCommand, pretty_err},
};
use path_slash::PathBufExt;
use semver::VersionReq;
use serde_json::Value;
use similar_asserts::assert_eq;
use std::{
    fs,
    path::{Path, PathBuf},
    str::FromStr,
    thread,
};

const DEFAULT_CONFIG: &str = r#"[profile.default]
src = "src"
test = "test"
script = "script"
out = "out"
libs = ["lib"]
remappings = ["forge-std/=lib/forge-std/src/"]
auto_detect_remappings = true
libraries = []
cache = true
cache_path = "cache"
dynamic_test_linking = true
snapshots = "snapshots"
gas_snapshot_check = false
gas_snapshot_emit = true
broadcast = "broadcast"
allow_paths = []
include_paths = []
skip = []
force = false
evm_version = "osaka"
gas_reports = ["*"]
gas_reports_ignore = []
gas_reports_include_tests = false
auto_detect_solc = true
offline = false
optimizer = false
optimizer_runs = 200
verbosity = 0
eth_rpc_accept_invalid_certs = false
eth_rpc_no_proxy = false
eth_rpc_curl = false
ignored_error_codes = [
    "license",
    "code-size",
    "init-code-size",
    "transient-storage",
    "transfer-deprecated",
    "natspec-memory-safe-assembly-deprecated",
]
ignored_error_codes_from = []
ignored_warnings_from = []
deny = "never"
test_failures_file = "cache/test-failures"
mutation_dir = "cache/mutation"
show_progress = false
ffi = false
live_logs = false
allow_internal_expect_revert = false
always_use_create_2_factory = false
eip1559_fee_estimate = "market"
prompt_timeout = 120
sender = "0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38"
tx_origin = "0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38"
initial_balance = "0xffffffffffffffffffffffff"
block_number = 1
gas_limit = 1073741824
block_base_fee_per_gas = 0
block_coinbase = "0x0000000000000000000000000000000000000000"
block_timestamp = 1
block_difficulty = 0
block_prevrandao = "0x0000000000000000000000000000000000000000000000000000000000000000"
memory_limit = 134217728
extra_output = []
extra_output_files = []
names = false
sizes = false
via_ir = false
via_ssa_cfg = false
experimental = false
ast = false
no_storage_caching = false
no_rpc_rate_limit = false
use_literal_content = false
bytecode_hash = "ipfs"
cbor_metadata = true
sparse_mode = false
build_info = false
isolate = true
disable_block_gas_limit = false
enable_tx_gas_limit = false
unchecked_cheatcode_artifacts = false
create2_library_salt = "0x0000000000000000000000000000000000000000000000000000000000000000"
create2_deployer = "0x4e59b44847b379578588920ca78fbf26c0b4956c"
assertions_revert = true
legacy_assertions = false
celo = false
bypass_prevrandao = false
transaction_timeout = 120
additional_compiler_profiles = []
compilation_restrictions = []
script_execution_protection = true

[profile.default.rpc_storage_caching]
chains = "all"
endpoints = "all"

[[profile.default.fs_permissions]]
access = "read"
path = "out"

[fmt]
line_length = 120
tab_width = 4
style = "space"
bracket_spacing = false
int_types = "long"
multiline_func_header = "attributes_first"
quote_style = "double"
number_underscore = "preserve"
hex_underscore = "remove"
single_line_statement_blocks = "preserve"
override_spacing = false
wrap_comments = false
docs_style = "preserve"
ignore = []
contract_new_lines = false
sort_imports = false
namespace_import_style = "prefer_plain"
pow_no_space = false
prefer_compact = "all"
single_line_imports = false

[lint]
severity = []
exclude_lints = []
ignore = []
lint_on_build = true

[lint.lint_specific]
mixed_case_exceptions = [
    "ERC",
    "URI",
    "ID",
    "URL",
    "API",
    "JSON",
    "XML",
    "HTML",
    "HTTP",
    "HTTPS",
]
multi_contract_file_exceptions = []

[doc]
out = "docs"
title = ""
book = "book.toml"
homepage = "README.md"
ignore = []

[fuzz]
runs = 256
fail_on_revert = true
max_test_rejects = 65536
dictionary_weight = 40
include_storage = true
include_push_bytes = true
max_fuzz_dictionary_addresses = 15728640
max_fuzz_dictionary_values = 9830400
max_fuzz_dictionary_literals = 6553600
gas_report_samples = 256
frontier_limit = 256
corpus_gzip = true
corpus_min_mutations = 5
corpus_min_size = 0
show_edge_coverage = false
evm_edge_coverage_collision_free = true
evm_edge_coverage_include_call_depth = false
sancov_edges = false
sancov_trace_cmp = false
corpus_random_sequence_weight = 10
payable_value_weight = 0
mutation_weight_splice = 1
mutation_weight_repeat = 1
mutation_weight_interleave = 1
mutation_weight_prefix = 1
mutation_weight_suffix = 1
mutation_weight_abi = 1
mutation_weight_cmp = 1
failure_persist_dir = "cache/fuzz"
show_logs = false

[invariant]
runs = 256
depth = 500
min_depth = 1
depth_mode = "fixed"
workers = "auto"
fail_on_revert = false
call_override = false
dictionary_weight = 80
include_storage = true
include_push_bytes = true
max_fuzz_dictionary_addresses = 15728640
max_fuzz_dictionary_values = 9830400
max_fuzz_dictionary_literals = 6553600
shrink_run_limit = 5000
max_assume_rejects = 65536
gas_report_samples = 256
frontier_limit = 256
corpus_gzip = true
corpus_min_mutations = 5
corpus_min_size = 0
show_edge_coverage = false
evm_edge_coverage_collision_free = true
evm_edge_coverage_include_call_depth = false
sancov_edges = false
sancov_trace_cmp = false
corpus_random_sequence_weight = 10
payable_value_weight = 15
mutation_weight_splice = 1
mutation_weight_repeat = 1
mutation_weight_interleave = 1
mutation_weight_prefix = 1
mutation_weight_suffix = 1
mutation_weight_abi = 1
mutation_weight_cmp = 1
failure_persist_dir = "cache/invariant"
show_metrics = true
show_solidity = false
check_interval = 1

[symbolic]
enabled = false
seed_corpus = false
use_fuzz_corpus = false
corpus_seed_limit = 32
use_fuzz_frontiers = false
frontier_limit = 256
solver = "z3"
timeout = 30
max_depth = 10000
max_paths = 1024
invariant_depth = 10
exploration_order = "bfs"
max_solver_queries = 10000
default_dynamic_length = 2
max_dynamic_length = 256
array_lengths = []
max_calldata_bytes = 4096
symbolic_call_targets = false
dump_smt = false
storage_layout = "solidity"

[coverage]
report = ["summary"]
lcov_version = "1.0.0"
ir_minimum = false
include_libs = false
exclude_tests = false
skip_files = []

[mutation]
include_operators = []
exclude_operators = []

[tracing]
verbosity = 0
disable_labels = false
compact_labels = false
decode_internal = false
external_identification_timeout = 5

[vyper]

[bind_json]
out = "utils/JsonBindings.sol"
include = []
exclude = []

"#;

// tests all config values that are in use
forgetest!(can_extract_config_values, |prj, cmd| {
    // explicitly set all values
    let input = Config {
        profile: Config::DEFAULT_PROFILE,
        // `profiles` is not serialized.
        profiles: vec![],
        root: ".".into(),
        extends: None,
        src: "test-src".into(),
        test: "test-test".into(),
        script: "test-script".into(),
        out: "out-test".into(),
        libs: vec!["lib-test".into()],
        cache: true,
        dynamic_test_linking: true,
        cache_path: "test-cache".into(),
        snapshots: "snapshots".into(),
        gas_snapshot_check: false,
        gas_snapshot_emit: true,
        broadcast: "broadcast".into(),
        force: true,
        evm_version: EvmVersion::Byzantium,
        hardfork: None,
        gas_reports: vec!["Contract".to_string()],
        gas_reports_ignore: vec![],
        gas_reports_include_tests: false,
        solc: Some(SolcReq::Local(PathBuf::from("custom-solc"))),
        auto_detect_solc: false,
        auto_detect_remappings: true,
        offline: true,
        optimizer: Some(false),
        optimizer_runs: Some(1000),
        optimizer_details: Some(OptimizerDetails {
            yul: Some(false),
            yul_details: Some(YulDetails { stack_allocation: Some(true), ..Default::default() }),
            ..Default::default()
        }),
        model_checker: None,
        extra_output: Default::default(),
        extra_output_files: Default::default(),
        names: true,
        sizes: true,
        test_pattern: None,
        test_pattern_inverse: None,
        contract_pattern: None,
        contract_pattern_inverse: None,
        path_pattern: None,
        path_pattern_inverse: None,
        coverage_pattern_inverse: None,
        test_failures_file: "test-cache/test-failures".into(),
        mutation_dir: "test-cache/mutation".into(),
        threads: None,
        show_progress: false,
        fuzz: FuzzConfig {
            runs: 1000,
            max_test_rejects: 100203,
            seed: Some(U256::from(1000)),
            failure_persist_dir: Some("test-cache/fuzz".into()),
            show_logs: false,
            ..Default::default()
        },
        invariant: InvariantConfig {
            runs: 256,
            failure_persist_dir: Some("test-cache/fuzz".into()),
            corpus: FuzzCorpusConfig {
                corpus_dir: Some("cache/invariant/corpus".into()),
                ..Default::default()
            },
            ..Default::default()
        },
        symbolic: SymbolicConfig {
            enabled: true,
            seed_corpus: true,
            use_fuzz_corpus: true,
            corpus_seed_limit: 17,
            use_fuzz_frontiers: true,
            frontier_limit: 11,
            frontier_ids: vec![4, 9],
            frontier_pcs: vec![123, 456],
            frontier_selectors: vec!["0x12345678".to_string(), "deadbeef".to_string()],
            solver: "custom-z3".to_string(),
            solver_command: None,
            solver_portfolio: Vec::new(),
            timeout: Some(7),
            loop_bound: Some(64),
            depth: Some(222),
            width: Some(33),
            max_depth: 123,
            max_paths: 456,
            invariant_depth: 5,
            exploration_order: SymbolicExplorationOrder::Dfs,
            max_solver_queries: 789,
            default_dynamic_length: 3,
            max_dynamic_length: 99,
            array_lengths: vec![1, 2, 3],
            dynamic_lengths: std::collections::BTreeMap::from([("data".to_string(), vec![4, 5])]),
            default_array_lengths: vec![6, 7],
            default_bytes_lengths: vec![8, 9],
            max_calldata_bytes: 2048,
            symbolic_call_targets: true,
            dump_smt: true,
            storage_layout: SymbolicStorageLayout::Generic,
        },
        coverage: Default::default(),
        mutation: Default::default(),
        tracing: TracingConfig { verbosity: 2, compact_labels: true, ..Default::default() },
        ffi: true,
        live_logs: true,
        allow_internal_expect_revert: false,
        always_use_create_2_factory: false,
        eip1559_fee_estimate: Eip1559FeeEstimatePreset::Market,
        prompt_timeout: 0,
        sender: "00a329c0648769A73afAc7F9381D08FB43dBEA72".parse().unwrap(),
        tx_origin: "00a329c0648769A73afAc7F9F81E08FB43dBEA72".parse().unwrap(),
        initial_balance: U256::from(0xffffffffffffffffffffffffu128),
        block_number: U256::from(10),
        fork_block_number: Some(200),
        chain: Some(9999.into()),
        gas_limit: 99_000_000u64.into(),
        code_size_limit: Some(100000),
        gas_price: Some(999),
        block_base_fee_per_gas: 10,
        block_coinbase: Address::random(),
        block_timestamp: U256::from(10),
        block_difficulty: 10,
        block_prevrandao: B256::random(),
        block_gas_limit: Some(100u64.into()),
        disable_block_gas_limit: false,
        enable_tx_gas_limit: false,
        memory_limit: 1 << 27,
        eth_rpc_url: Some("localhost".to_string()),
        eth_rpc_accept_invalid_certs: false,
        eth_rpc_no_proxy: false,
        eth_rpc_jwt: None,
        eth_rpc_timeout: None,
        eth_rpc_headers: None,
        eth_rpc_curl: false,
        etherscan_api_key: None,
        etherscan: Default::default(),
        verbosity: 4,
        remappings: vec![Remapping::from_str("forge-std/=lib/forge-std/").unwrap().into()],
        libraries: vec![
            "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6".to_string(),
        ],
        ignored_error_codes: vec![],
        ignored_error_codes_from: vec![],
        ignored_file_paths: vec![],
        deny: foundry_config::DenyLevel::Never,
        deny_warnings: false,
        via_ir: true,
        ast: false,
        rpc_storage_caching: StorageCachingConfig {
            chains: CachedChains::None,
            endpoints: CachedEndpoints::Remote,
        },
        no_storage_caching: true,
        no_rpc_rate_limit: true,
        use_literal_content: false,
        bytecode_hash: Default::default(),
        cbor_metadata: true,
        revert_strings: Some(RevertStrings::Strip),
        sparse_mode: true,
        allow_paths: vec![],
        include_paths: vec![],
        rpc_endpoints: Default::default(),
        build_info: false,
        build_info_path: None,
        fmt: Default::default(),
        lint: Default::default(),
        doc: Default::default(),
        bind_json: Default::default(),
        fs_permissions: Default::default(),
        labels: Default::default(),
        isolate: true,
        unchecked_cheatcode_artifacts: false,
        create2_library_salt: Config::DEFAULT_CREATE2_LIBRARY_SALT,
        create2_deployer: Config::DEFAULT_CREATE2_DEPLOYER,
        vyper: Default::default(),
        skip: vec![],
        dependencies: Default::default(),
        soldeer: Default::default(),
        warnings: vec![],
        assertions_revert: true,
        legacy_assertions: false,
        extra_args: vec![],
        experimental: false,
        via_ssa_cfg: false,
        networks: Default::default(),
        transaction_timeout: 120,
        additional_compiler_profiles: Default::default(),
        compilation_restrictions: Default::default(),
        script_execution_protection: true,
        _non_exhaustive: (),
    };
    prj.write_config(input.clone());
    let config = cmd.config();
    similar_asserts::assert_eq!(input, config);
});

// tests config gets printed to std out
forgetest!(can_show_config, |prj, cmd| {
    let expected =
        Config::load_with_root(prj.root()).unwrap().to_string_pretty().unwrap().trim().to_string();
    let output = cmd.arg("config").assert_success().get_output().stdout_lossy().trim().to_string();
    assert_eq!(expected, output);
});

forgetest!(can_select_profile_with_cli, |prj, cmd| {
    prj.create_file(
        Config::FILE_NAME,
        r#"
[profile.default]
optimizer = false
optimizer_runs = 200

[profile.ci]
optimizer = true
optimizer_runs = 1
"#,
    );
    cmd.env("foundry_profile", "default");

    let config = cmd.args(["--profile", "ci"]).config();

    assert_eq!(config.optimizer, Some(true));
    assert_eq!(config.optimizer_runs, Some(1));
});

// checks that config works
// - foundry.toml is properly generated
// - paths are resolved properly
// - config supports overrides from env, and cli
forgetest_init!(can_override_config, |prj, cmd| {
    prj.initialize_default_contracts();
    cmd.set_current_dir(prj.root());
    let foundry_toml = prj.root().join(Config::FILE_NAME);
    assert!(foundry_toml.exists());

    let profile = Config::load_with_root(prj.root()).unwrap();
    // ensure that the auto-generated internal remapping for forge-std's ds-test exists
    assert_eq!(profile.remappings.len(), 1);
    assert_eq!("forge-std/=lib/forge-std/src/", profile.remappings[0].to_string());

    // ensure remappings contain test
    assert_eq!("forge-std/=lib/forge-std/src/", profile.remappings[0].to_string());
    // the loaded config has resolved, absolute paths
    assert_eq!(
        "forge-std/=lib/forge-std/src/",
        Remapping::from(profile.remappings[0].clone()).to_string()
    );

    let expected = profile.to_string_pretty().unwrap().trim().to_string();
    let output = cmd.arg("config").assert_success().get_output().stdout_lossy().trim().to_string();
    assert_eq!(expected, output);

    // remappings work
    let remappings_txt =
        prj.create_file("remappings.txt", "ds-test/=lib/forge-std/lib/ds-test/from-file/");
    let config = forge_utils::load_config_with_root(Some(prj.root())).unwrap();
    assert_eq!(
        format!(
            "ds-test/={}/",
            prj.root().join("lib/forge-std/lib/ds-test/from-file").to_slash_lossy()
        ),
        Remapping::from(config.remappings[0].clone()).to_string()
    );

    let config =
        prj.config_from_output(["--remappings", "ds-test/=lib/forge-std/lib/ds-test/from-cli"]);
    assert_eq!(
        format!(
            "ds-test/={}/",
            prj.root().join("lib/forge-std/lib/ds-test/from-cli").to_slash_lossy()
        ),
        Remapping::from(config.remappings[0].clone()).to_string()
    );

    let config = prj.config_from_output(["--remappings", "other-key/=lib/other/"]);
    assert_eq!(config.remappings.len(), 3);
    assert_eq!(
        format!("other-key/={}/", prj.root().join("lib/other").to_slash_lossy()),
        // As CLI has the higher priority, it'll be found at the first slot.
        Remapping::from(config.remappings[0].clone()).to_string()
    );

    pretty_err(&remappings_txt, fs::remove_file(&remappings_txt));

    let expected = profile.into_basic().to_string_pretty().unwrap().trim().to_string();
    let output = cmd
        .forge_fuse()
        .args(["config", "--basic"])
        .assert_success()
        .get_output()
        .stdout_lossy()
        .trim()
        .to_string();
    assert_eq!(expected, output);
});

forgetest_init!(can_parse_remappings_correctly, |prj, cmd| {
    prj.initialize_default_contracts();
    cmd.set_current_dir(prj.root());
    let foundry_toml = prj.root().join(Config::FILE_NAME);
    assert!(foundry_toml.exists());

    let profile = Config::load_with_root(prj.root()).unwrap();
    // ensure that the auto-generated internal remapping for forge-std's ds-test exists
    assert_eq!(profile.remappings.len(), 1);
    let r = &profile.remappings[0];
    assert_eq!("forge-std/=lib/forge-std/src/", r.to_string());

    // the loaded config has resolved, absolute paths
    assert_eq!("forge-std/=lib/forge-std/src/", Remapping::from(r.clone()).to_string());

    let expected = profile.to_string_pretty().unwrap().trim().to_string();
    let output = cmd.arg("config").assert_success().get_output().stdout_lossy().trim().to_string();
    assert_eq!(expected, output);

    let install = |cmd: &mut TestCommand, dep: &str| {
        cmd.forge_fuse().args(["install", dep]).assert_success().stdout_eq(str![""]).stderr_eq(
            str![[r#"
Installing solmate in [..] (url: https://github.com/transmissions11/solmate, tag: None)
...
    Installed solmate[..]

"#]],
        );
    };

    install(&mut cmd, "transmissions11/solmate");
    let profile = Config::load_with_root(prj.root()).unwrap();
    // remappings work
    let remappings_txt = prj.create_file(
        "remappings.txt",
        "solmate/=lib/solmate/src/\nsolmate-contracts/=lib/solmate/src/",
    );
    let config = forge_utils::load_config_with_root(Some(prj.root())).unwrap();
    // trailing slashes are removed on windows `to_slash_lossy`
    let path = prj.root().join("lib/solmate/src/").to_slash_lossy().into_owned();
    #[cfg(windows)]
    let path = path + "/";
    assert_eq!(
        format!("solmate/={path}"),
        Remapping::from(config.remappings[0].clone()).to_string()
    );
    // As this is an user-generated remapping, it is not removed, even if it points to the same
    // location.
    assert_eq!(
        format!("solmate-contracts/={path}"),
        Remapping::from(config.remappings[1].clone()).to_string()
    );
    pretty_err(&remappings_txt, fs::remove_file(&remappings_txt));

    let expected = profile.into_basic().to_string_pretty().unwrap().trim().to_string();
    let output = cmd
        .forge_fuse()
        .args(["config", "--basic"])
        .assert_success()
        .get_output()
        .stdout_lossy()
        .trim()
        .to_string();
    assert_eq!(expected, output);
});

forgetest_init!(can_detect_config_vals, |prj, _cmd| {
    prj.initialize_default_contracts();
    let url = "http://127.0.0.1:8545";
    let config = prj.config_from_output(["--no-auto-detect", "--rpc-url", url]);
    assert!(!config.auto_detect_solc);
    assert_eq!(config.eth_rpc_url, Some(url.to_string()));

    let mut config = Config::load_with_root(prj.root()).unwrap();
    config.eth_rpc_url = Some("http://127.0.0.1:8545".to_string());
    config.auto_detect_solc = false;
    // write to `foundry.toml`
    prj.create_file(
        Config::FILE_NAME,
        &config.to_string_pretty().unwrap().replace("eth_rpc_url", "eth-rpc-url"),
    );
    let config = prj.config_from_output(["--force"]);
    assert!(!config.auto_detect_solc);
    assert_eq!(config.eth_rpc_url, Some(url.to_string()));
});

// checks that `clean` removes dapptools style paths
forgetest_init!(can_get_evm_opts, |prj, _cmd| {
    prj.initialize_default_contracts();
    let url = "http://127.0.0.1:8545";
    let config = prj.config_from_output(["--rpc-url", url, "--ffi"]);
    assert_eq!(config.eth_rpc_url, Some(url.to_string()));
    assert!(config.ffi);

    unsafe {
        std::env::set_var("FOUNDRY_ETH_RPC_URL", url);
    }
    let figment = Config::figment_with_root(prj.root()).merge(("debug", false));
    let evm_opts: EvmOpts = figment.extract().unwrap();
    assert_eq!(evm_opts.fork_url, Some(url.to_string()));
    unsafe {
        std::env::remove_var("FOUNDRY_ETH_RPC_URL");
    }
});

// Regression test for <https://github.com/foundry-rs/foundry/issues/14538>:
// the bare `ETH_RPC_URL` env var must NOT cause `forge` commands to set
// `eth_rpc_url` (which would silently fork all `forge test` runs).
// Only `--rpc-url`, `foundry.toml`, the `FOUNDRY_ETH_RPC_URL` env var, or
// cheatcodes should configure forking.
forgetest_init!(eth_rpc_url_env_does_not_set_fork_url, |prj, _cmd| {
    prj.initialize_default_contracts();
    let url = "http://127.0.0.1:8545";

    let mut cmd = prj.forge_bin();
    cmd.arg("config")
        .arg("--root")
        .arg(prj.root())
        .arg("--json")
        .env("ETH_RPC_URL", url)
        // Make sure the figment-style env var is not set in the test environment.
        .env_remove("FOUNDRY_ETH_RPC_URL");
    let output = cmd.output().unwrap();
    let stdout = String::from_utf8_lossy(&output.stdout);
    let config: Config = serde_json::from_str(stdout.as_ref()).unwrap();
    assert_eq!(
        config.eth_rpc_url, None,
        "bare ETH_RPC_URL must not propagate to forge config (regression #14538)"
    );
});

// checks that we can set various config values
forgetest_init!(can_set_config_values, |prj, _cmd| {
    prj.initialize_default_contracts();
    let config =
        prj.config_from_output(["--via-ir", "--experimental", "--via-ssa-cfg", "--no-metadata"]);
    assert!(config.via_ir);
    assert!(config.experimental);
    assert!(config.via_ssa_cfg);
    assert_eq!(config.cbor_metadata, false);
    assert_eq!(config.bytecode_hash, BytecodeHash::None);
});

// tests that solc can be explicitly set
forgetest!(can_set_solc_explicitly, |prj, cmd| {
    prj.add_source(
        "Foo",
        r"
pragma solidity *;
contract Greeter {}
   ",
    );

    prj.update_config(|config| {
        config.solc = Some(OTHER_SOLC_VERSION.into());
    });

    cmd.arg("build").assert_success().stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!

"#]]);
});

// tests that `--use <solc>` works
forgetest!(can_use_solc, |prj, cmd| {
    prj.add_raw_source(
        "Foo",
        r"
pragma solidity *;
contract Foo {}
   ",
    );

    cmd.args(["build", "--use", OTHER_SOLC_VERSION]).assert_success().stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!

"#]]);

    cmd.forge_fuse()
        .args(["build", "--force", "--use", &format!("solc:{OTHER_SOLC_VERSION}")])
        .root_arg()
        .assert_success()
        .stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!

"#]]);

    // fails to use solc that does not exist
    cmd.forge_fuse().args(["build", "--use", "this/solc/does/not/exist"]);
    cmd.assert_failure().stderr_eq(str![[r#"
Error: `solc` this/solc/does/not/exist does not exist

"#]]);

    // `OTHER_SOLC_VERSION` was installed in previous step, so we can use the path to this directly
    let local_solc = Solc::find_or_install(&OTHER_SOLC_VERSION.parse().unwrap()).unwrap();
    cmd.forge_fuse()
        .args(["build", "--force", "--use"])
        .arg(local_solc.solc)
        .root_arg()
        .assert_success()
        .stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!

"#]]);
});

// test to ensure yul optimizer can be set as intended
forgetest!(can_set_yul_optimizer, |prj, cmd| {
    prj.update_config(|config| config.optimizer = Some(true));
    prj.add_source(
        "foo.sol",
        r"
contract Foo {
    function bar() public pure {
       assembly {
            let result_start := msize()
       }
    }
}
   ",
    );

    cmd.arg("build").assert_failure().stderr_eq(str![[r#"
Error: Compiler run failed:
Error (6553): The msize instruction cannot be used when the Yul optimizer is activated because it can change its semantics. Either disable the Yul optimizer or do not use the instruction.
 [FILE]:6:8:
  |
6 |        assembly {
  |        ^ (Relevant source part starts here and spans across multiple lines).

"#]]);

    // disable yul optimizer explicitly
    prj.update_config(|config| config.optimizer_details.get_or_insert_default().yul = Some(false));
    cmd.assert_success();
});

// tests that the lib triple can be parsed
forgetest_init!(can_parse_dapp_libraries, |_prj, cmd| {
    cmd.env(
        "DAPP_LIBRARIES",
        "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
    );
    let config = cmd.config();
    assert_eq!(
        config.libraries,
        vec!["src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6".to_string(),]
    );
});

// test that optimizer runs works
forgetest!(can_set_optimizer_runs, |prj, cmd| {
    // explicitly set optimizer runs
    prj.update_config(|config| config.optimizer_runs = Some(1337));

    let config = cmd.config();
    assert_eq!(config.optimizer_runs, Some(1337));

    let config = prj.config_from_output(["--optimizer-runs", "300"]);
    assert_eq!(config.optimizer_runs, Some(300));
});

// test that use_literal_content works
forgetest!(can_set_use_literal_content, |prj, cmd| {
    // explicitly set use_literal_content
    prj.update_config(|config| config.use_literal_content = false);

    let config = cmd.config();
    assert_eq!(config.use_literal_content, false);

    let config = prj.config_from_output(["--use-literal-content"]);
    assert_eq!(config.use_literal_content, true);
});

// <https://github.com/foundry-rs/foundry/issues/9665>
forgetest!(enable_optimizer_when_runs_set, |prj, cmd| {
    // explicitly set optimizer runs
    prj.update_config(|config| config.optimizer_runs = Some(1337));

    let config = cmd.config();
    assert!(config.optimizer.unwrap());
});

// test `optimizer_runs` set to 200 by default if optimizer enabled
forgetest!(optimizer_runs_default, |prj, cmd| {
    // explicitly set optimizer
    prj.update_config(|config| config.optimizer = Some(true));

    let config = cmd.config();
    assert_eq!(config.optimizer_runs, Some(200));
});

// test that gas_price can be set
forgetest!(can_set_gas_price, |prj, cmd| {
    // explicitly set gas_price
    prj.update_config(|config| config.gas_price = Some(1337));

    let config = cmd.config();
    assert_eq!(config.gas_price, Some(1337));

    let config = prj.config_from_output(["--gas-price", "300"]);
    assert_eq!(config.gas_price, Some(300));
});

// test that we can detect remappings from foundry.toml
forgetest_init!(can_detect_lib_foundry_toml, |prj, cmd| {
    prj.initialize_default_contracts();
    let config = cmd.config();
    let remappings = config.remappings.iter().cloned().map(Remapping::from).collect::<Vec<_>>();
    similar_asserts::assert_eq!(
        remappings,
        vec![
            // global
            "forge-std/=lib/forge-std/src/".parse().unwrap(),
        ]
    );

    // create a new lib directly in the `lib` folder with a remapping
    let mut config = config;
    config.remappings = vec![Remapping::from_str("nested/=lib/nested").unwrap().into()];
    let nested = prj.paths().libraries[0].join("nested-lib");
    pretty_err(&nested, fs::create_dir_all(&nested));
    let toml_file = nested.join("foundry.toml");
    pretty_err(&toml_file, fs::write(&toml_file, config.to_string_pretty().unwrap()));

    let config = cmd.config();
    let remappings = config.remappings.iter().cloned().map(Remapping::from).collect::<Vec<_>>();
    similar_asserts::assert_eq!(
        remappings,
        vec![
            // default
            "forge-std/=lib/forge-std/src/".parse().unwrap(),
            // remapping is local to the lib
            "nested-lib/=lib/nested-lib/src/".parse().unwrap(),
            // global
            "nested/=lib/nested-lib/lib/nested/".parse().unwrap(),
        ]
    );

    // nest another lib under the already nested lib
    let mut config = config;
    config.remappings = vec![Remapping::from_str("nested-twice/=lib/nested-twice").unwrap().into()];
    let nested = nested.join("lib/another-lib");
    pretty_err(&nested, fs::create_dir_all(&nested));
    let toml_file = nested.join("foundry.toml");
    pretty_err(&toml_file, fs::write(&toml_file, config.to_string_pretty().unwrap()));

    let another_config = cmd.config();
    let remappings =
        another_config.remappings.iter().cloned().map(Remapping::from).collect::<Vec<_>>();
    similar_asserts::assert_eq!(
        remappings,
        vec![
            // local to the lib
            "another-lib/=lib/nested-lib/lib/another-lib/src/".parse().unwrap(),
            // global
            "forge-std/=lib/forge-std/src/".parse().unwrap(),
            "nested-lib/=lib/nested-lib/src/".parse().unwrap(),
            // remappings local to the lib
            "nested-twice/=lib/nested-lib/lib/another-lib/lib/nested-twice/".parse().unwrap(),
            "nested/=lib/nested-lib/lib/nested/".parse().unwrap(),
        ]
    );

    config.src = "custom-source-dir".into();
    pretty_err(&toml_file, fs::write(&toml_file, config.to_string_pretty().unwrap()));
    let config = cmd.config();
    let remappings = config.remappings.iter().cloned().map(Remapping::from).collect::<Vec<_>>();
    similar_asserts::assert_eq!(
        remappings,
        vec![
            // local to the lib
            "another-lib/=lib/nested-lib/lib/another-lib/custom-source-dir/".parse().unwrap(),
            // global
            "forge-std/=lib/forge-std/src/".parse().unwrap(),
            "nested-lib/=lib/nested-lib/src/".parse().unwrap(),
            // remappings local to the lib
            "nested-twice/=lib/nested-lib/lib/another-lib/lib/nested-twice/".parse().unwrap(),
            "nested/=lib/nested-lib/lib/nested/".parse().unwrap(),
        ]
    );

    // check if lib path is absolute, it should deteect nested lib
    let mut config = cmd.config();
    config.libs = vec![nested];

    let remappings = config.remappings.iter().cloned().map(Remapping::from).collect::<Vec<_>>();
    similar_asserts::assert_eq!(
        remappings,
        vec![
            // local to the lib
            "another-lib/=lib/nested-lib/lib/another-lib/custom-source-dir/".parse().unwrap(),
            // global
            "forge-std/=lib/forge-std/src/".parse().unwrap(),
            "nested-lib/=lib/nested-lib/src/".parse().unwrap(),
            // remappings local to the lib
            "nested-twice/=lib/nested-lib/lib/another-lib/lib/nested-twice/".parse().unwrap(),
            "nested/=lib/nested-lib/lib/nested/".parse().unwrap(),
        ]
    );
});

// test remappings with closer paths are prioritised
// so that `dep/=lib/a/src` will take precedent over  `dep/=lib/a/lib/b/src`
forgetest_init!(can_prioritise_closer_lib_remappings, |prj, cmd| {
    prj.initialize_default_contracts();
    let config = cmd.config();

    // create a new lib directly in the `lib` folder with conflicting remapping `forge-std/`
    let mut config = config;
    config.remappings = vec![Remapping::from_str("forge-std/=lib/forge-std/src/").unwrap().into()];
    let nested = prj.paths().libraries[0].join("dep1");
    pretty_err(&nested, fs::create_dir_all(&nested));
    let toml_file = nested.join("foundry.toml");
    pretty_err(&toml_file, fs::write(&toml_file, config.to_string_pretty().unwrap()));

    let config = cmd.config();
    let remappings = config.get_all_remappings().collect::<Vec<_>>();
    similar_asserts::assert_eq!(
        remappings,
        vec![
            "dep1/=lib/dep1/src/".parse().unwrap(),
            "forge-std/=lib/forge-std/src/".parse().unwrap()
        ]
    );
});

// Test that remappings within root of the project have priority over remappings of sub-projects.
// E.g. `@utils/libraries` mapping from library shouldn't be added if project already has `@utils`
// remapping.
// See <https://github.com/foundry-rs/foundry/issues/9146>
// Test that
// - single file remapping is properly added, see
// <https://github.com/foundry-rs/foundry/issues/6706> and <https://github.com/foundry-rs/foundry/issues/8499>
// - project defined `@openzeppelin/contracts` remapping is added
// - library defined `@openzeppelin/contracts-upgradeable` remapping is added
// - library defined `@openzeppelin/contracts/upgradeable` remapping is not added as it conflicts
// with project defined `@openzeppelin/contracts` remapping
// See <https://github.com/foundry-rs/foundry/issues/9271>
forgetest_init!(can_prioritise_project_remappings, |prj, cmd| {
    prj.initialize_default_contracts();
    let mut config = cmd.config();
    // Add `@utils/` remapping in project config.
    config.remappings = vec![
        Remapping::from_str("@utils/libraries/Contract.sol=src/Contract.sol").unwrap().into(),
        Remapping::from_str("@utils/=src/").unwrap().into(),
        Remapping::from_str("@openzeppelin/contracts=lib/openzeppelin-contracts/").unwrap().into(),
    ];
    let proj_toml_file = prj.paths().root.join("foundry.toml");
    pretty_err(&proj_toml_file, fs::write(&proj_toml_file, config.to_string_pretty().unwrap()));

    // Create a new lib in the `lib` folder with conflicting `@utils/libraries` remapping.
    // This should be filtered out from final remappings as root project already has `@utils/`.
    let nested = prj.paths().libraries[0].join("dep1");
    pretty_err(&nested, fs::create_dir_all(&nested));
    let mut lib_config = Config::load_with_root(&nested).unwrap();
    lib_config.remappings = vec![
        Remapping::from_str("@utils/libraries/=src/").unwrap().into(),
        Remapping::from_str("@openzeppelin/contracts-upgradeable/=lib/openzeppelin-upgradeable/")
            .unwrap()
            .into(),
        Remapping::from_str(
            "@openzeppelin/contracts/upgradeable/=lib/openzeppelin-contracts/upgradeable/",
        )
        .unwrap()
        .into(),
    ];
    let lib_toml_file = nested.join("foundry.toml");
    pretty_err(&lib_toml_file, fs::write(&lib_toml_file, lib_config.to_string_pretty().unwrap()));

    cmd.args(["remappings", "--pretty"])
        .assert_success()
        .stdout_eq(str![[r#"
@utils/libraries/Contract.sol=src/Contract.sol
@utils/=src/
@openzeppelin/contracts/=lib/openzeppelin-contracts/
@openzeppelin/contracts-upgradeable/=lib/dep1/lib/openzeppelin-upgradeable/
dep1/=lib/dep1/src/
forge-std/=lib/forge-std/src/

"#]])
        .stderr_eq(str![[r#"
Global:


"#]]);
});

forgetest!(narrow_project_remapping_preserves_broad_dependency_fallback, |prj, cmd| {
    prj.update_config(|config| {
        config.remappings = vec![Remapping::from_str("pkg/sub/=src/local/").unwrap().into()];
    });
    prj.add_source(
        "Root.sol",
        r#"
import {Dep} from "pkg/Dep.sol";
import {Local} from "pkg/sub/Local.sol";

contract Root is Dep, Local {}
"#,
    );
    prj.add_source(
        "local/Local.sol",
        r#"
contract Local {
    function localValue() public pure returns (uint256) {
        return 1;
    }
}
"#,
    );
    let dependency = prj.root().join("lib/pkg/src");
    pretty_err(&dependency, fs::create_dir_all(&dependency));
    pretty_err(
        dependency.join("Dep.sol"),
        fs::write(
            dependency.join("Dep.sol"),
            r#"
contract Dep {
    function dependencyValue() public pure returns (uint256) {
        return 2;
    }
}
"#,
        ),
    );

    cmd.args(["remappings"]).assert_success().stdout_eq(str![[r#"
pkg/sub/=src/local/
pkg/=lib/pkg/src/

"#]]);
    cmd.forge_fuse().args(["build"]).assert_success();
    // Forge lint resolves imports through Solar independently of the solc build.
    cmd.forge_fuse().args(["lint"]).assert_success();
});

forgetest!(nested_config_remapping_refines_auto_detected_package_root, |prj, cmd| {
    let outer = prj.paths().libraries[0].join("outer");
    let outer_other = prj.paths().libraries[0].join("outer-other");
    let inner = outer.join("lib/inner");
    prj.update_config(|config| {
        config.remappings =
            vec![Remapping::from_str("test/:inner/=src/unrelated/").unwrap().into()];
    });
    pretty_err(&outer, fs::create_dir_all(outer.join("src")));
    pretty_err(&outer_other, fs::create_dir_all(outer_other.join("src")));
    pretty_err(&inner, fs::create_dir_all(inner.join("contracts")));
    pretty_err(&outer, fs::write(outer.join("foundry.toml"), "[profile.default]\n"));
    pretty_err(&outer, fs::write(outer.join("remappings.txt"), "inner/=lib/inner/contracts/\n"));
    pretty_err(&inner, fs::write(inner.join("Marker.sol"), "contract Marker {}\n"));
    pretty_err(&inner, fs::write(inner.join("contracts/I.sol"), "interface I {}\n"));
    pretty_err(
        &outer,
        fs::write(
            outer.join("src/Outer.sol"),
            "import {I} from \"inner/I.sol\"; contract Outer is I {}\n",
        ),
    );
    pretty_err(
        &outer_other,
        fs::write(
            outer_other.join("src/Other.sol"),
            "import {Marker} from \"inner/Marker.sol\"; contract Other is Marker {}\n",
        ),
    );
    prj.add_source(
        "UsesOuter.sol",
        "import {Outer} from \"outer/Outer.sol\"; import {Other} from \"outer-other/Other.sol\"; import {Marker} from \"inner/Marker.sol\"; contract UsesOuter is Outer, Other {}\n",
    );

    cmd.args(["remappings"]).assert_success().stdout_eq(str![[r#"
test/:inner/=src/unrelated/
lib/outer/:inner/=lib/outer/lib/inner/contracts/
inner/=lib/outer/lib/inner/
outer-other/=lib/outer-other/src/
outer/=lib/outer/src/

"#]]);
    cmd.forge_fuse().arg("build").assert_success();
    cmd.forge_fuse().arg("lint").assert_success();
});

forgetest!(cli_preserves_explicit_contextual_remapping_pair, |prj, cmd| {
    let dependency = prj.paths().libraries[0].join("dep");
    pretty_err(&dependency, fs::create_dir_all(dependency.join("src")));
    pretty_err(&dependency, fs::create_dir_all(dependency.join("pkg/contracts")));
    prj.update_config(|config| {
        config.auto_detect_remappings = false;
        config.remappings =
            ["dep/=lib/dep/src/", "lib/dep/:pkg/=lib/dep/pkg/contracts/", "pkg/=lib/dep/pkg/"]
                .map(|remapping| Remapping::from_str(remapping).unwrap().into())
                .into();
    });
    pretty_err(
        &dependency,
        fs::write(
            dependency.join("src/Dep.sol"),
            "import {Other} from \"pkg/Other.sol\"; contract Dep is Other {}\n",
        ),
    );
    pretty_err(
        &dependency,
        fs::write(dependency.join("pkg/contracts/Other.sol"), "contract Other {}\n"),
    );
    prj.add_source(
        "UsesDep.sol",
        "import {Dep} from \"dep/Dep.sol\"; contract UsesDep is Dep {}\n",
    );

    cmd.args(["build", "--remappings", "pkg/sub/=src/local/"]).assert_success();

    // A generated candidate that normalizes to the explicit contextual mapping must not cause the
    // explicit mapping to be tagged as generated and removed by the CLI merge.
    pretty_err(&dependency, fs::write(dependency.join("foundry.toml"), "[profile.default]\n"));
    pretty_err(&dependency, fs::write(dependency.join("remappings.txt"), "pkg/=pkg/contracts/\n"));
    prj.update_config(|config| config.auto_detect_remappings = true);
    cmd.forge_fuse()
        .args(["build", "--force", "--remappings", "pkg/sub/=src/local/"])
        .assert_success();
});

forgetest!(root_remapping_precedes_nested_refinement, |prj, cmd| {
    let outer = prj.paths().libraries[0].join("outer");
    let inner = outer.join("lib/inner");
    pretty_err(&outer, fs::create_dir_all(outer.join("src")));
    pretty_err(&inner, fs::create_dir_all(inner.join("contracts/sub")));
    pretty_err(prj.root(), fs::create_dir_all(prj.root().join("src/local")));
    prj.update_config(|config| {
        config.remappings = vec![Remapping::from_str("inner/sub/=src/local/").unwrap().into()];
    });
    pretty_err(&outer, fs::write(outer.join("foundry.toml"), "[profile.default]\n"));
    pretty_err(&outer, fs::write(outer.join("remappings.txt"), "inner/=lib/inner/contracts/\n"));
    pretty_err(&inner, fs::write(inner.join("Marker.sol"), "contract Marker {}\n"));
    pretty_err(&inner, fs::write(inner.join("contracts/I.sol"), "contract I {}\n"));
    pretty_err(
        prj.root(),
        fs::write(prj.root().join("src/local/Selected.sol"), "contract Selected {}\n"),
    );
    pretty_err(
        &inner,
        fs::write(inner.join("contracts/sub/Selected.sol"), "contract Shadowed {}\n"),
    );
    pretty_err(
        &outer,
        fs::write(
            outer.join("src/Outer.sol"),
            "import {I} from \"inner/I.sol\"; import {Selected} from \"inner/sub/Selected.sol\"; contract Outer is I, Selected {}\n",
        ),
    );
    prj.add_source(
        "UsesOuter.sol",
        "import {Outer} from \"outer/Outer.sol\"; import {Marker} from \"inner/Marker.sol\"; contract UsesOuter is Outer, Marker {}\n",
    );

    cmd.arg("build").assert_success();

    // CLI remappings are merged after dependency refinements are generated and must still retain
    // root precedence.
    prj.update_config(|config| config.remappings.clear());
    cmd.forge_fuse()
        .args(["build", "--force", "--remappings", "inner/sub/=src/local/"])
        .assert_success();
});

forgetest!(broad_project_remapping_suppresses_narrow_dependency_override, |prj, cmd| {
    prj.update_config(|config| {
        config.remappings = vec![Remapping::from_str("pkg/=src/local/").unwrap().into()];
    });
    prj.add_source(
        "Root.sol",
        r#"
import {Selected} from "pkg/sub/Selected.sol";

contract Root is Selected {}
"#,
    );
    prj.add_source(
        "local/sub/Selected.sol",
        r#"
contract Selected {
    function selectedValue() public pure returns (uint256) {
        return 1;
    }
}
"#,
    );

    let dependency = prj.root().join("lib/dep1");
    pretty_err(dependency.join("src/decoy"), fs::create_dir_all(dependency.join("src/decoy")));
    let mut dependency_config = Config::load_with_root(&dependency).unwrap();
    dependency_config.remappings = vec![Remapping::from_str("pkg/sub/=src/decoy/").unwrap().into()];
    pretty_err(
        dependency.join("foundry.toml"),
        fs::write(dependency.join("foundry.toml"), dependency_config.to_string_pretty().unwrap()),
    );
    pretty_err(
        dependency.join("src/decoy/Selected.sol"),
        fs::write(
            dependency.join("src/decoy/Selected.sol"),
            r#"
contract DependencyDecoy {}
"#,
        ),
    );

    cmd.args(["remappings"]).assert_success().stdout_eq(str![[r#"
pkg/=src/local/
dep1/=lib/dep1/src/

"#]]);
    cmd.forge_fuse().args(["build"]).assert_success();
    // Solar prefers the longest matching prefix, so this fails if the dependency override leaks.
    cmd.forge_fuse().args(["lint"]).assert_success();
});

// Verifies the contract invariant: `forge remappings` and `forge remappings --pretty` emit
// identical stdout, even when remappings have contexts. The context prefix is part of the
// machine-readable value and must survive `--pretty` mode.
forgetest!(remappings_pretty_keeps_context_on_stdout, |prj, cmd| {
    prj.update_config(|config| {
        config.auto_detect_remappings = false;
        config.remappings = vec![
            Remapping::from_str("@global/=lib/global/").unwrap().into(),
            Remapping::from_str("ctx-a:@scoped/=lib/a/").unwrap().into(),
            Remapping::from_str("ctx-b:@scoped/=lib/b/").unwrap().into(),
        ];
    });

    cmd.args(["remappings"]).assert_success().stdout_eq(str![[r#"
@global/=lib/global/
ctx-a:@scoped/=lib/a/
ctx-b:@scoped/=lib/b/

"#]]);

    cmd.forge_fuse()
        .args(["remappings", "--pretty"])
        .assert_success()
        .stdout_eq(str![[r#"
@global/=lib/global/
ctx-a:@scoped/=lib/a/
ctx-b:@scoped/=lib/b/

"#]])
        .stderr_eq(str![[r#"
Global:

Context: ctx-a

Context: ctx-b


"#]]);
});

// test to check that foundry.toml libs section updates on install
forgetest!(can_update_libs_section, |prj, cmd| {
    cmd.git_init();

    // explicitly set gas_price
    prj.update_config(|config| config.libs = vec!["node_modules".into()]);

    cmd.args(["install", "foundry-rs/forge-std"]).assert_success().stdout_eq(str![""]).stderr_eq(
        str![[r#"
Installing forge-std in [..] (url: https://github.com/foundry-rs/forge-std, tag: None)
...
    Installed forge-std[..]

"#]],
    );

    let config = cmd.forge_fuse().config();
    // `lib` was added automatically
    let expected = vec![PathBuf::from("node_modules"), PathBuf::from("lib")];
    assert_eq!(config.libs, expected);

    // additional install don't edit `libs`
    cmd.forge_fuse()
        .args(["install", "dapphub/ds-test"])
        .assert_success()
        .stdout_eq(str![""])
        .stderr_eq(str![[r#"
Installing ds-test in [..] (url: https://github.com/dapphub/ds-test, tag: None)
...
    Installed ds-test

"#]]);

    let config = cmd.forge_fuse().config();
    assert_eq!(config.libs, expected);
});

// test to check that loading the config emits warnings on the root foundry.toml and
// is silent for any libs
forgetest!(config_emit_warnings, |prj, cmd| {
    cmd.git_init();

    cmd.args(["install", "foundry-rs/forge-std"]).assert_success().stdout_eq(str![""]).stderr_eq(
        str![[r#"
Installing forge-std in [..] (url: https://github.com/foundry-rs/forge-std, tag: None)
...
    Installed forge-std[..]

"#]],
    );

    let faulty_toml = r"[default]
    src = 'src'
    out = 'out'
    libs = ['lib']";

    fs::write(prj.root().join("foundry.toml"), faulty_toml).unwrap();
    fs::write(prj.root().join("lib").join("forge-std").join("foundry.toml"), faulty_toml).unwrap();

    cmd.forge_fuse().args(["config"]).assert_success().stderr_eq(str![[r#"
Warning: Found unknown config section in foundry.toml: [default]
This notation for profiles has been deprecated and may result in the profile not being registered in future versions.
Please use [profile.default] instead or run `forge config --fix`.

"#]]);
});

forgetest_init!(can_skip_remappings_auto_detection, |prj, cmd| {
    prj.initialize_default_contracts();
    // explicitly set remapping and libraries
    prj.update_config(|config| {
        config.remappings = vec![Remapping::from_str("remapping/=lib/remapping/").unwrap().into()];
        config.auto_detect_remappings = false;
    });

    let config = cmd.config();

    // only loads remappings from foundry.toml
    assert_eq!(config.remappings.len(), 1);
    assert_eq!("remapping/=lib/remapping/", config.remappings[0].to_string());
});

forgetest_init!(can_parse_default_fs_permissions, |_prj, cmd| {
    let config = cmd.config();

    assert_eq!(config.fs_permissions.len(), 1);
    let permissions = config.fs_permissions.joined(Path::new("test"));
    let out_permission = permissions.find_permission(Path::new("test/out")).unwrap();
    assert_eq!(FsAccessPermission::Read, out_permission);
});

forgetest_init!(can_parse_custom_fs_permissions, |prj, cmd| {
    prj.initialize_default_contracts();
    // explicitly set fs permissions
    prj.update_config(|config| {
        config.fs_permissions = FsPermissions::new(vec![
            PathPermission::read("./read"),
            PathPermission::write("./write"),
            PathPermission::read_write("./write/contracts"),
        ]);
    });

    let config = cmd.config();

    assert_eq!(config.fs_permissions.len(), 3);

    // check read permission
    let permission = config.fs_permissions.find_permission(Path::new("./read")).unwrap();
    assert_eq!(permission, FsAccessPermission::Read);
    // check nested write permission
    let permission =
        config.fs_permissions.find_permission(Path::new("./write/MyContract.sol")).unwrap();
    assert_eq!(permission, FsAccessPermission::Write);
    // check nested read-write permission
    let permission = config
        .fs_permissions
        .find_permission(Path::new("./write/contracts/MyContract.sol"))
        .unwrap();
    assert_eq!(permission, FsAccessPermission::ReadWrite);
    // check no permission
    let permission =
        config.fs_permissions.find_permission(Path::new("./bogus")).unwrap_or_default();
    assert_eq!(permission, FsAccessPermission::None);
});

#[cfg(not(target_os = "windows"))]
forgetest_init!(can_resolve_symlink_fs_permissions, |prj, cmd| {
    prj.initialize_default_contracts();
    // write config in packages/files/config.json
    let config_path = prj.root().join("packages").join("files");
    fs::create_dir_all(&config_path).unwrap();
    fs::write(config_path.join("config.json"), "{ enabled: true }").unwrap();

    // symlink packages/files dir as links/
    std::os::unix::fs::symlink(
        Path::new("./packages/../packages/../packages/files"),
        prj.root().join("links"),
    )
    .unwrap();

    // write config, give read access to links/ symlink to packages/files/
    prj.update_config(|config| {
        config.fs_permissions =
            FsPermissions::new(vec![PathPermission::read(Path::new("./links/config.json"))]);
    });

    let config = cmd.config();
    let mut fs_permissions = config.fs_permissions;
    fs_permissions.join_all(prj.root());
    assert_eq!(fs_permissions.len(), 1);

    // read permission to file should be granted through symlink
    let permission = fs_permissions.find_permission(&config_path.join("config.json")).unwrap();
    assert_eq!(permission, FsAccessPermission::Read);
});

// tests if evm version is normalized for config output
forgetest!(normalize_config_evm_version, |_prj, cmd| {
    let output = cmd
        .args(["config", "--use", "0.8.0", "--json"])
        .assert_success()
        .get_output()
        .stdout_lossy();
    let config: Config = serde_json::from_str(&output).unwrap();
    assert_eq!(config.evm_version, EvmVersion::Istanbul);

    // See <https://github.com/foundry-rs/foundry/issues/7014>
    let output = cmd
        .forge_fuse()
        .args(["config", "--use", "0.8.17", "--json"])
        .assert_success()
        .get_output()
        .stdout_lossy();
    let config: Config = serde_json::from_str(&output).unwrap();
    assert_eq!(config.evm_version, EvmVersion::London);

    let output = cmd
        .forge_fuse()
        .args(["config", "--use", "0.8.18", "--json"])
        .assert_success()
        .get_output()
        .stdout_lossy();
    let config: Config = serde_json::from_str(&output).unwrap();
    assert_eq!(config.evm_version, EvmVersion::Paris);

    let output = cmd
        .forge_fuse()
        .args(["config", "--use", "0.8.23", "--json"])
        .assert_success()
        .get_output()
        .stdout_lossy();
    let config: Config = serde_json::from_str(&output).unwrap();
    assert_eq!(config.evm_version, EvmVersion::Shanghai);

    let output = cmd
        .forge_fuse()
        .args(["config", "--use", "0.8.26", "--json"])
        .assert_success()
        .get_output()
        .stdout_lossy();
    let config: Config = serde_json::from_str(&output).unwrap();
    assert_eq!(config.evm_version, EvmVersion::Cancun);

    let output = cmd
        .forge_fuse()
        .args(["config", "--use", "0.8.35", "--evm-version", "amsterdam", "--json"])
        .assert_success()
        .get_output()
        .stdout_lossy();
    let config: Config = serde_json::from_str(&output).unwrap();
    assert_eq!(config.evm_version, EvmVersion::Osaka);

    let output = cmd
        .forge_fuse()
        .args(["config", "--use", "0.8.36", "--evm-version", "amsterdam", "--json"])
        .assert_success()
        .get_output()
        .stdout_lossy();
    let config: Config = serde_json::from_str(&output).unwrap();
    assert_eq!(config.evm_version, EvmVersion::Amsterdam);
});

// Tests that root paths are properly resolved even if submodule specifies remappings for them.
// See <https://github.com/foundry-rs/foundry/issues/3440>
forgetest_init!(test_submodule_root_path_remappings, |prj, cmd| {
    prj.initialize_default_contracts();
    prj.add_script(
        "BaseScript.sol",
        r#"
import "forge-std/Script.sol";

contract BaseScript is Script {
}
   "#,
    );
    prj.add_script(
        "MyScript.sol",
        r#"
import "script/BaseScript.sol";

contract MyScript is BaseScript {
}
   "#,
    );

    let nested = prj.paths().libraries[0].join("another-dep");
    pretty_err(&nested, fs::create_dir_all(&nested));
    let mut lib_config = Config::load_with_root(&nested).unwrap();
    lib_config.remappings = vec![
        Remapping::from_str("test/=test/").unwrap().into(),
        Remapping::from_str("script/=script/").unwrap().into(),
    ];
    let lib_toml_file = nested.join("foundry.toml");
    pretty_err(&lib_toml_file, fs::write(&lib_toml_file, lib_config.to_string_pretty().unwrap()));
    cmd.forge_fuse().args(["build"]).assert_success();
});

// Tests that project remappings use config paths.
// For `src=src/contracts` config, remapping should be `src/contracts/ = src/contracts/`.
// For `src=src` config, remapping should be `src/ = src/`.
// <https://github.com/foundry-rs/foundry/issues/9454>
forgetest_init!(test_project_remappings, |prj, cmd| {
    prj.initialize_default_contracts();
    prj.update_config(|config| {
        config.src = "src/contracts".into();
        config.remappings = vec![Remapping::from_str("contracts/=src/contracts/").unwrap().into()];
    });

    // Add Counter.sol in `src/contracts` project dir.
    let src_dir = &prj.root().join("src/contracts");
    pretty_err(src_dir, fs::create_dir_all(src_dir));
    pretty_err(
        src_dir.join("Counter.sol"),
        fs::write(src_dir.join("Counter.sol"), "contract Counter{}"),
    );
    prj.add_test(
        "CounterTest.sol",
        r#"
import "contracts/Counter.sol";

contract CounterTest {
}
   "#,
    );
    cmd.forge_fuse().args(["build"]).assert_success();
});

forgetest_init!(test_default_config, |prj, cmd| {
    prj.write_config(Config::default());
    cmd.forge_fuse().args(["config"]).assert_success().stdout_eq(DEFAULT_CONFIG);

    cmd.forge_fuse().args(["config", "--json"]).assert_success().stdout_eq(str![[r#"
{
  "src": "src",
  "test": "test",
  "script": "script",
  "out": "out",
  "libs": [
    "lib"
  ],
  "remappings": [
    "forge-std/=lib/forge-std/src/"
  ],
  "auto_detect_remappings": true,
  "libraries": [],
  "cache": true,
  "cache_path": "cache",
  "dynamic_test_linking": true,
  "snapshots": "snapshots",
  "gas_snapshot_check": false,
  "gas_snapshot_emit": true,
  "broadcast": "broadcast",
  "allow_paths": [],
  "include_paths": [],
  "skip": [],
  "force": false,
  "evm_version": "osaka",
  "hardfork": null,
  "gas_reports": [
    "*"
  ],
  "gas_reports_ignore": [],
  "gas_reports_include_tests": false,
  "solc": null,
  "auto_detect_solc": true,
  "offline": false,
  "optimizer": false,
  "optimizer_runs": 200,
  "optimizer_details": null,
  "model_checker": null,
  "verbosity": 0,
  "eth_rpc_url": null,
  "eth_rpc_accept_invalid_certs": false,
  "eth_rpc_no_proxy": false,
  "eth_rpc_jwt": null,
  "eth_rpc_timeout": null,
  "eth_rpc_headers": null,
  "eth_rpc_curl": false,
  "etherscan_api_key": null,
  "ignored_error_codes": [
    "license",
    "code-size",
    "init-code-size",
    "transient-storage",
    "transfer-deprecated",
    "natspec-memory-safe-assembly-deprecated"
  ],
  "ignored_error_codes_from": [],
  "ignored_warnings_from": [],
  "deny": "never",
  "match_test": null,
  "no_match_test": null,
  "match_contract": null,
  "no_match_contract": null,
  "match_path": null,
  "no_match_path": null,
  "no_match_coverage": null,
  "test_failures_file": "cache/test-failures",
  "mutation_dir": "cache/mutation",
  "threads": null,
  "show_progress": false,
  "fuzz": {
    "runs": 256,
    "run": null,
    "worker": null,
    "fail_on_revert": true,
    "max_test_rejects": 65536,
    "seed": null,
    "dictionary_weight": 40,
    "include_storage": true,
    "include_push_bytes": true,
    "max_fuzz_dictionary_addresses": 15728640,
    "max_fuzz_dictionary_values": 9830400,
    "max_fuzz_dictionary_literals": 6553600,
    "gas_report_samples": 256,
    "corpus_dir": null,
    "frontier_dir": null,
    "frontier_limit": 256,
    "corpus_gzip": true,
    "corpus_min_mutations": 5,
    "corpus_min_size": 0,
    "show_edge_coverage": false,
    "evm_edge_coverage_collision_free": true,
    "evm_edge_coverage_include_call_depth": false,
    "sancov_edges": false,
    "sancov_trace_cmp": false,
    "corpus_random_sequence_weight": 10,
    "payable_value_weight": 0,
    "mutation_weight_splice": 1,
    "mutation_weight_repeat": 1,
    "mutation_weight_interleave": 1,
    "mutation_weight_prefix": 1,
    "mutation_weight_suffix": 1,
    "mutation_weight_abi": 1,
    "mutation_weight_cmp": 1,
    "failure_persist_dir": "cache/fuzz",
    "show_logs": false,
    "timeout": null
  },
  "invariant": {
    "runs": 256,
    "depth": 500,
    "min_depth": 1,
    "depth_mode": "fixed",
    "workers": "auto",
    "fail_on_revert": false,
    "call_override": false,
    "dictionary_weight": 80,
    "include_storage": true,
    "include_push_bytes": true,
    "max_fuzz_dictionary_addresses": 15728640,
    "max_fuzz_dictionary_values": 9830400,
    "max_fuzz_dictionary_literals": 6553600,
    "shrink_run_limit": 5000,
    "max_assume_rejects": 65536,
    "gas_report_samples": 256,
    "corpus_dir": null,
    "frontier_dir": null,
    "frontier_limit": 256,
    "corpus_gzip": true,
    "corpus_min_mutations": 5,
    "corpus_min_size": 0,
    "show_edge_coverage": false,
    "evm_edge_coverage_collision_free": true,
    "evm_edge_coverage_include_call_depth": false,
    "sancov_edges": false,
    "sancov_trace_cmp": false,
    "corpus_random_sequence_weight": 10,
    "payable_value_weight": 15,
    "mutation_weight_splice": 1,
    "mutation_weight_repeat": 1,
    "mutation_weight_interleave": 1,
    "mutation_weight_prefix": 1,
    "mutation_weight_suffix": 1,
    "mutation_weight_abi": 1,
    "mutation_weight_cmp": 1,
    "failure_persist_dir": "cache/invariant",
    "show_metrics": true,
    "timeout": null,
    "show_solidity": false,
    "max_time_delay": null,
    "max_block_delay": null,
    "check_interval": 1
  },
  "symbolic": {
    "enabled": false,
    "seed_corpus": false,
    "use_fuzz_corpus": false,
    "corpus_seed_limit": 32,
    "use_fuzz_frontiers": false,
    "frontier_limit": 256,
    "solver": "z3",
    "timeout": 30,
    "max_depth": 10000,
    "max_paths": 1024,
    "invariant_depth": 10,
    "exploration_order": "bfs",
    "max_solver_queries": 10000,
    "default_dynamic_length": 2,
    "max_dynamic_length": 256,
    "array_lengths": [],
    "max_calldata_bytes": 4096,
    "symbolic_call_targets": false,
    "dump_smt": false,
    "storage_layout": "solidity"
  },
  "coverage": {
    "report": [
      "summary"
    ],
    "lcov_version": "1.0.0",
    "ir_minimum": false,
    "report_file": null,
    "include_libs": false,
    "exclude_tests": false,
    "skip_files": []
  },
  "mutation": {
    "include_operators": [],
    "exclude_operators": [],
    "timeout": null,
    "optimizer_runs": null,
    "via_ir": null
  },
  "tracing": {
    "verbosity": 0,
    "disable_labels": false,
    "compact_labels": false,
    "decode_internal": false,
    "external_identification_timeout": 5
  },
  "ffi": false,
  "live_logs": false,
  "allow_internal_expect_revert": false,
  "always_use_create_2_factory": false,
  "eip1559_fee_estimate": "market",
  "prompt_timeout": 120,
  "sender": "0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38",
  "tx_origin": "0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38",
  "initial_balance": "0xffffffffffffffffffffffff",
  "block_number": 1,
  "fork_block_number": null,
  "chain_id": null,
  "gas_limit": 1073741824,
  "code_size_limit": null,
  "gas_price": null,
  "block_base_fee_per_gas": 0,
  "block_coinbase": "0x0000000000000000000000000000000000000000",
  "block_timestamp": 1,
  "block_difficulty": 0,
  "block_prevrandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "block_gas_limit": null,
  "memory_limit": 134217728,
  "extra_output": [],
  "extra_output_files": [],
  "names": false,
  "sizes": false,
  "via_ir": false,
  "via_ssa_cfg": false,
  "experimental": false,
  "ast": false,
  "rpc_storage_caching": {
    "chains": "all",
    "endpoints": "all"
  },
  "no_storage_caching": false,
  "no_rpc_rate_limit": false,
  "use_literal_content": false,
  "bytecode_hash": "ipfs",
  "cbor_metadata": true,
  "revert_strings": null,
  "sparse_mode": false,
  "build_info": false,
  "build_info_path": null,
  "fmt": {
    "line_length": 120,
    "tab_width": 4,
    "style": "space",
    "bracket_spacing": false,
    "int_types": "long",
    "multiline_func_header": "attributes_first",
    "quote_style": "double",
    "number_underscore": "preserve",
    "hex_underscore": "remove",
    "single_line_statement_blocks": "preserve",
    "override_spacing": false,
    "wrap_comments": false,
    "docs_style": "preserve",
    "ignore": [],
    "contract_new_lines": false,
    "sort_imports": false,
    "namespace_import_style": "prefer_plain",
    "pow_no_space": false,
    "prefer_compact": "all",
    "single_line_imports": false
  },
  "lint": {
    "severity": [],
    "exclude_lints": [],
    "ignore": [],
    "lint_on_build": true,
    "lint_specific": {
      "mixed_case_exceptions": [
        "ERC",
        "URI",
        "ID",
        "URL",
        "API",
        "JSON",
        "XML",
        "HTML",
        "HTTP",
        "HTTPS"
      ],
      "multi_contract_file_exceptions": []
    }
  },
  "doc": {
    "out": "docs",
    "title": "",
    "book": "book.toml",
    "homepage": "README.md",
    "ignore": []
  },
  "bind_json": {
    "out": "utils/JsonBindings.sol",
    "include": [],
    "exclude": []
  },
  "fs_permissions": [
    {
      "access": "read",
      "path": "out"
    }
  ],
  "isolate": true,
  "disable_block_gas_limit": false,
  "enable_tx_gas_limit": false,
  "unchecked_cheatcode_artifacts": false,
  "create2_library_salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "create2_deployer": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
  "vyper": {},
  "dependencies": null,
  "soldeer": null,
  "assertions_revert": true,
  "legacy_assertions": false,
  "network": null,
  "celo": false,
  "bypass_prevrandao": false,
  "transaction_timeout": 120,
  "additional_compiler_profiles": [],
  "compilation_restrictions": [],
  "script_execution_protection": true
}

"#]]);
});

forgetest_init!(test_optimizer_config, |prj, cmd| {
    prj.initialize_default_contracts();
    // Default settings: optimizer disabled, optimizer runs 200.
    cmd.forge_fuse().args(["config"]).assert_success().stdout_eq(str![[r#"
...
optimizer = false
optimizer_runs = 200
...

"#]]);

    // Optimizer set to true: optimizer runs set to default value of 200.
    prj.update_config(|config| config.optimizer = Some(true));
    cmd.forge_fuse().args(["config"]).assert_success().stdout_eq(str![[r#"
...
optimizer = true
optimizer_runs = 200
...

"#]]);

    // Optimizer runs set to 0: optimizer should be disabled, runs set to 0.
    prj.update_config(|config| {
        config.optimizer = None;
        config.optimizer_runs = Some(0);
    });
    cmd.forge_fuse().args(["config"]).assert_success().stdout_eq(str![[r#"
...
optimizer = false
optimizer_runs = 0
...

"#]]);

    // Optimizer runs set to 500: optimizer should be enabled, runs set to 500.
    prj.update_config(|config| {
        config.optimizer = None;
        config.optimizer_runs = Some(500);
    });
    cmd.forge_fuse().args(["config"]).assert_success().stdout_eq(str![[r#"
...
optimizer = true
optimizer_runs = 500
...

"#]]);

    // Optimizer disabled and runs set to 500: optimizer should be disabled, runs set to 500.
    prj.update_config(|config| {
        config.optimizer = Some(false);
        config.optimizer_runs = Some(500);
    });
    cmd.forge_fuse().args(["config"]).assert_success().stdout_eq(str![[r#"
...
optimizer = false
optimizer_runs = 500
...

"#]]);

    // Optimizer enabled and runs set to 0: optimizer should be enabled, runs set to 0.
    prj.update_config(|config| {
        config.optimizer = Some(true);
        config.optimizer_runs = Some(0);
    });
    cmd.forge_fuse().args(["config"]).assert_success().stdout_eq(str![[r#"
...
optimizer = true
optimizer_runs = 0
...

"#]]);
});

forgetest_init!(test_gas_snapshot_check_config, |prj, cmd| {
    prj.initialize_default_contracts();
    // Default settings: gas_snapshot_check disabled.
    cmd.forge_fuse().args(["config"]).assert_success().stdout_eq(str![[r#"
...
gas_snapshot_check = false
...

"#]]);

    prj.insert_ds_test();

    prj.add_source(
        "Flare.sol",
        r#"
contract Flare {
    bytes32[] public data;

    function run(uint256 n_) public {
        for (uint256 i = 0; i < n_; i++) {
            data.push(keccak256(abi.encodePacked(i)));
        }
    }
}
    "#,
    );

    let test_contract = |n: u32| {
        format!(
            r#"
import "./test.sol";
import "./Flare.sol";

interface Vm {{
    function startSnapshotGas(string memory name) external;
    function stopSnapshotGas() external returns (uint256);
}}

contract GasSnapshotCheckTest is DSTest {{
    Vm constant vm = Vm(HEVM_ADDRESS);

    Flare public flare;

    function setUp() public {{
        flare = new Flare();
    }}

    function testSnapshotGasSectionExternal() public {{
        vm.startSnapshotGas("testAssertGasExternal");
        flare.run({n});
        vm.stopSnapshotGas();
    }}
}}
        "#
        )
    };

    // Assert that gas_snapshot_check is disabled by default.
    prj.add_source("GasSnapshotCheckTest.sol", &test_contract(1));
    cmd.forge_fuse().args(["test"]).assert_success().stdout_eq(str![[r#"
...
Ran 1 test for src/GasSnapshotCheckTest.sol:GasSnapshotCheckTest
[PASS] testSnapshotGasSectionExternal() ([GAS])
Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]
...
"#]]);

    // Enable gas_snapshot_check.
    prj.update_config(|config| config.gas_snapshot_check = true);
    cmd.forge_fuse().args(["config"]).assert_success().stdout_eq(str![[r#"
...
gas_snapshot_check = true
...

"#]]);

    // Replace the test contract with a new one that will fail the gas snapshot check.
    prj.add_source("GasSnapshotCheckTest.sol", &test_contract(2));
    cmd.forge_fuse().args(["test"]).assert_failure().stderr_eq(str![[r#"
...
[GasSnapshotCheckTest] Failed to match snapshots:
- [testAssertGasExternal] [..] → [..]

Error: Snapshots differ from previous run
...
"#]]);

    // Disable gas_snapshot_check, assert that running the test will pass.
    prj.update_config(|config| config.gas_snapshot_check = false);
    cmd.forge_fuse().args(["test"]).assert_success().stdout_eq(str![[r#"
...
Ran 1 test for src/GasSnapshotCheckTest.sol:GasSnapshotCheckTest
[PASS] testSnapshotGasSectionExternal() ([GAS])
Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]
...
"#]]);

    // Re-enable gas_snapshot_check
    // Assert that the new value has been stored from the previous run and re-run the test.
    prj.update_config(|config| config.gas_snapshot_check = true);
    cmd.forge_fuse().args(["test"]).assert_success().stdout_eq(str![[r#"
...
Ran 1 test for src/GasSnapshotCheckTest.sol:GasSnapshotCheckTest
[PASS] testSnapshotGasSectionExternal() ([GAS])
Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]
...
"#]]);

    // Replace the test contract with a new one that will fail the gas_snapshot_check.
    prj.add_source("GasSnapshotCheckTest.sol", &test_contract(3));
    cmd.forge_fuse().args(["test"]).assert_failure().stderr_eq(str![[r#"
...
[GasSnapshotCheckTest] Failed to match snapshots:
- [testAssertGasExternal] [..] → [..]

Error: Snapshots differ from previous run
...
"#]]);

    // Test that `--gas-snapshot-check=false` flag can be used to disable the gas_snapshot_check.
    cmd.forge_fuse().args(["test", "--gas-snapshot-check=false"]).assert_success().stdout_eq(str![
        [r#"
...
Ran 1 test for src/GasSnapshotCheckTest.sol:GasSnapshotCheckTest
[PASS] testSnapshotGasSectionExternal() ([GAS])
Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]
...
"#]
    ]);

    // Disable gas_snapshot_check in the config file.
    // Enable using `FORGE_SNAPSHOT_CHECK` environment variable.
    // Assert that this will override the config file value.
    prj.update_config(|config| config.gas_snapshot_check = false);
    prj.add_source("GasSnapshotCheckTest.sol", &test_contract(4));
    cmd.forge_fuse();
    cmd.env("FORGE_SNAPSHOT_CHECK", "true");
    cmd.args(["test"]).assert_failure().stderr_eq(str![[r#"
...
[GasSnapshotCheckTest] Failed to match snapshots:
- [testAssertGasExternal] [..] → [..]

Error: Snapshots differ from previous run
...
"#]]);

    // Assert that `--gas-snapshot-check=true` flag can be used to enable the gas_snapshot_check
    // even when `FORGE_SNAPSHOT_CHECK` is set to false in the environment variable.
    cmd.forge_fuse();
    cmd.env("FORGE_SNAPSHOT_CHECK", "false");
    cmd.args(["test", "--gas-snapshot-check=true"]).assert_failure().stderr_eq(str![[r#"
...
[GasSnapshotCheckTest] Failed to match snapshots:
- [testAssertGasExternal] [..] → [..]

Error: Snapshots differ from previous run
...
"#]]);

    // Finally assert that `--gas-snapshot-check=false` flag can be used to disable the
    // gas_snapshot_check even when `FORGE_SNAPSHOT_CHECK` is set to true
    cmd.forge_fuse();
    cmd.env("FORGE_SNAPSHOT_CHECK", "true");
    cmd.args(["test", "--gas-snapshot-check=false"]).assert_success().stdout_eq(str![[r#"
...
Ran 1 test for src/GasSnapshotCheckTest.sol:GasSnapshotCheckTest
[PASS] testSnapshotGasSectionExternal() ([GAS])
Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]
...
"#]]);
});

forgetest_init!(test_gas_snapshot_emit_config, |prj, cmd| {
    prj.initialize_default_contracts();
    // Default settings: gas_snapshot_emit enabled.
    cmd.forge_fuse().args(["config"]).assert_success().stdout_eq(str![[r#"
...
gas_snapshot_emit = true
...
"#]]);

    prj.insert_ds_test();

    prj.add_source(
        "GasSnapshotEmitTest.sol",
        r#"
import "./test.sol";

interface Vm {
    function startSnapshotGas(string memory name) external;
    function stopSnapshotGas() external returns (uint256);
}

contract GasSnapshotEmitTest is DSTest {
    Vm constant vm = Vm(HEVM_ADDRESS);

    function testSnapshotGasSection() public {
        vm.startSnapshotGas("testSection");
        int n = 1;
        vm.stopSnapshotGas();
    }
}
    "#,
    );

    // Assert that gas_snapshot_emit is enabled by default.
    cmd.forge_fuse().args(["test"]).assert_success();
    // Assert that snapshots were emitted to disk.
    assert!(prj.root().join("snapshots/GasSnapshotEmitTest.json").exists());

    // Remove the snapshot file.
    fs::remove_file(prj.root().join("snapshots/GasSnapshotEmitTest.json")).unwrap();

    // Test that `--gas-snapshot-emit=false` flag can be used to disable writing snapshots.
    cmd.forge_fuse().args(["test", "--gas-snapshot-emit=false"]).assert_success();
    // Assert that snapshots were not emitted to disk.
    assert!(!prj.root().join("snapshots/GasSnapshotEmitTest.json").exists());

    // Test that environment variable `FORGE_SNAPSHOT_EMIT` can be used to disable writing
    // snapshots.
    cmd.forge_fuse();
    cmd.env("FORGE_SNAPSHOT_EMIT", "false");
    cmd.args(["test"]).assert_success();
    // Assert that snapshots were not emitted to disk.
    assert!(!prj.root().join("snapshots/GasSnapshotEmitTest.json").exists());

    // Test that `--gas-snapshot-emit=true` flag can be used to enable writing snapshots, even when
    // `FORGE_SNAPSHOT_EMIT` is set to false.
    cmd.forge_fuse();
    cmd.env("FORGE_SNAPSHOT_EMIT", "false");
    cmd.args(["test", "--gas-snapshot-emit=true"]).assert_success();
    // Assert that snapshots were emitted to disk.
    assert!(prj.root().join("snapshots/GasSnapshotEmitTest.json").exists());

    // Remove the snapshot file.
    fs::remove_file(prj.root().join("snapshots/GasSnapshotEmitTest.json")).unwrap();

    // Disable gas_snapshot_emit in the config file.
    prj.update_config(|config| config.gas_snapshot_emit = false);
    cmd.forge_fuse().args(["config"]).assert_success();

    // Test that snapshots are not emitted to disk, when disabled by config.
    cmd.forge_fuse().args(["test"]).assert_success();
    // Assert that snapshots were not emitted to disk.
    assert!(!prj.root().join("snapshots/GasSnapshotEmitTest.json").exists());

    // Test that `--gas-snapshot-emit=true` flag can be used to enable writing snapshots, when
    // disabled by config.
    cmd.forge_fuse();
    cmd.args(["test", "--gas-snapshot-emit=true"]).assert_success();
    // Assert that snapshots were emitted to disk.
    assert!(prj.root().join("snapshots/GasSnapshotEmitTest.json").exists());

    // Remove the snapshot file.
    fs::remove_file(prj.root().join("snapshots/GasSnapshotEmitTest.json")).unwrap();

    // Test that environment variable `FORGE_SNAPSHOT_EMIT` can be used to enable writing snapshots.
    cmd.forge_fuse();
    cmd.env("FORGE_SNAPSHOT_EMIT", "true");
    cmd.args(["test"]).assert_success();
    // Assert that snapshots were emitted to disk.
    assert!(prj.root().join("snapshots/GasSnapshotEmitTest.json").exists());

    // Remove the snapshot file.
    fs::remove_file(prj.root().join("snapshots/GasSnapshotEmitTest.json")).unwrap();

    // Test that `--gas-snapshot-emit=false` flag can be used to disable writing snapshots,
    // even when `FORGE_SNAPSHOT_EMIT` is set to true.
    cmd.forge_fuse().args(["test", "--gas-snapshot-emit=false"]).assert_success();

    // Assert that snapshots were not emitted to disk.
    assert!(!prj.root().join("snapshots/GasSnapshotEmitTest.json").exists());
});

// Tests compilation restrictions enables optimizer if optimizer runs set to a value higher than 0.
forgetest_init!(test_additional_compiler_profiles, |prj, cmd| {
    prj.initialize_default_contracts();
    prj.add_source(
        "v1/Counter.sol",
        r#"
contract Counter {
}
    "#,
    );

    prj.add_source(
        "v2/Counter.sol",
        r#"
contract Counter {
}
    "#,
    );

    prj.add_source(
        "v3/Counter.sol",
        r#"
contract Counter {
}
    "#,
    );

    // Additional profiles are defined with optimizer runs but without explicitly enabling
    // optimizer
    //
    // additional_compiler_profiles = [
    //   { name = "v1", optimizer_runs = 44444444, via_ir = true, evm_version = "cancun" },
    //   { name = "v2", optimizer_runs = 111, via_ir = true },
    //   { name = "v3", optimizer_runs = 800, evm_version = "istanbul", via_ir = false },
    // ]
    //
    // compilation_restrictions = [
    //   # v1
    //   { paths = "src/v1/[!i]*.sol", version = "0.8.16", optimizer_runs = 44444444 },
    //   # v2
    //   { paths = "src/v2/{Counter}.sol", optimizer_runs = 111 },
    //   # v3
    //   { paths = "src/v3/*", optimizer_runs = 800 },
    // ]
    let v1_profile = SettingsOverrides {
        name: "v1".to_string(),
        via_ir: Some(true),
        evm_version: Some(EvmVersion::Osaka),
        optimizer: None,
        optimizer_runs: Some(44444444),
        bytecode_hash: None,
    };
    let v1_restrictions = CompilationRestrictions {
        paths: GlobMatcher::from_str("src/v1/[!i]*.sol").unwrap(),
        version: Some(VersionReq::from_str("0.8.16").unwrap()),
        via_ir: None,
        bytecode_hash: None,
        min_optimizer_runs: None,
        optimizer_runs: Some(44444444),
        max_optimizer_runs: None,
        min_evm_version: None,
        evm_version: None,
        max_evm_version: None,
    };
    let v2_profile = SettingsOverrides {
        name: "v2".to_string(),
        via_ir: Some(true),
        evm_version: None,
        optimizer: None,
        optimizer_runs: Some(111),
        bytecode_hash: None,
    };
    let v2_restrictions = CompilationRestrictions {
        paths: GlobMatcher::from_str("src/v2/{Counter}.sol").unwrap(),
        version: None,
        via_ir: None,
        bytecode_hash: None,
        min_optimizer_runs: None,
        optimizer_runs: Some(111),
        max_optimizer_runs: None,
        min_evm_version: None,
        evm_version: None,
        max_evm_version: None,
    };
    let v3_profile = SettingsOverrides {
        name: "v3".to_string(),
        via_ir: Some(false),
        evm_version: Some(EvmVersion::Istanbul),
        optimizer: None,
        optimizer_runs: Some(800),
        bytecode_hash: None,
    };
    let v3_restrictions = CompilationRestrictions {
        paths: GlobMatcher::from_str("src/v3/*").unwrap(),
        version: None,
        via_ir: None,
        bytecode_hash: None,
        min_optimizer_runs: None,
        optimizer_runs: Some(800),
        max_optimizer_runs: None,
        min_evm_version: None,
        evm_version: None,
        max_evm_version: None,
    };
    let additional_compiler_profiles = vec![v1_profile, v2_profile, v3_profile];
    let compilation_restrictions = vec![v1_restrictions, v2_restrictions, v3_restrictions];
    prj.update_config(|config| {
        config.additional_compiler_profiles = additional_compiler_profiles;
        config.compilation_restrictions = compilation_restrictions;
    });
    // Should find and build all profiles satisfying settings restrictions.
    cmd.forge_fuse().args(["build"]).assert_success();
    prj.assert_artifacts_dir_exists();

    let artifact_settings =
        |artifact| -> (Option<Value>, Option<Value>, Option<Value>, Option<Value>) {
            let artifact: serde_json::Value = serde_json::from_reader(
                fs::File::open(prj.artifacts().join(artifact)).expect("no artifact"),
            )
            .expect("invalid artifact");
            let settings =
                artifact.get("metadata").unwrap().get("settings").unwrap().as_object().unwrap();
            let optimizer = settings.get("optimizer").unwrap();
            (
                settings.get("viaIR").cloned(),
                settings.get("evmVersion").cloned(),
                optimizer.get("enabled").cloned(),
                optimizer.get("runs").cloned(),
            )
        };

    let (via_ir, evm_version, enabled, runs) = artifact_settings("Counter.sol/Counter.json");
    assert_eq!(None, via_ir);
    assert_eq!("\"osaka\"", evm_version.unwrap().to_string());
    assert_eq!("false", enabled.unwrap().to_string());
    assert_eq!("200", runs.unwrap().to_string());

    let (via_ir, evm_version, enabled, runs) = artifact_settings("v1/Counter.sol/Counter.json");
    assert_eq!("true", via_ir.unwrap().to_string());
    assert_eq!("\"osaka\"", evm_version.unwrap().to_string());
    assert_eq!("true", enabled.unwrap().to_string());
    assert_eq!("44444444", runs.unwrap().to_string());

    let (via_ir, evm_version, enabled, runs) = artifact_settings("v2/Counter.sol/Counter.json");
    assert_eq!("true", via_ir.unwrap().to_string());
    assert_eq!("\"osaka\"", evm_version.unwrap().to_string());
    assert_eq!("true", enabled.unwrap().to_string());
    assert_eq!("111", runs.unwrap().to_string());

    let (via_ir, evm_version, enabled, runs) = artifact_settings("v3/Counter.sol/Counter.json");
    assert_eq!(None, via_ir);
    assert_eq!("\"istanbul\"", evm_version.unwrap().to_string());
    assert_eq!("true", enabled.unwrap().to_string());
    assert_eq!("800", runs.unwrap().to_string());
});

// <https://github.com/foundry-rs/foundry/issues/11227>
forgetest_init!(test_exclude_lints_config, |prj, cmd| {
    prj.initialize_default_contracts();
    prj.update_config(|config| {
        config.lint.exclude_lints = vec![
            "asm-keccak256".to_string(),
            "incorrect-shift".to_string(),
            "divide-before-multiply".to_string(),
            "mixed-case-variable".to_string(),
            "mixed-case-function".to_string(),
            "screaming-snake-case-const".to_string(),
            "screaming-snake-case-immutable".to_string(),
            "unwrapped-modifier-logic".to_string(),
        ]
    });
    cmd.args(["lint"]).assert_success().stdout_eq("");
});

// <https://github.com/foundry-rs/foundry/issues/6529>
forgetest_init!(test_fail_fast_config, |prj, cmd| {
    // Skip if we don't have at least 2 CPUs to run both tests in parallel.
    if thread::available_parallelism().map_or(1, |n| n.get()) < 2 {
        return;
    }

    prj.update_config(|config| {
        // Set large timeout for fuzzed tests so test campaign won't stop if fail fast not passed.
        config.fuzz.timeout = Some(3600);
    });
    prj.add_test(
        "AnotherCounterTest.sol",
        r#"
import {Test} from "forge-std/Test.sol";

contract AnotherCounterTest is Test {
    // This failure should stop all other tests.
    function test_Failure() public pure {
        require(false);
    }

    function testFuzz_SetNumber(uint256 x) public {
    }
}
"#,
    );
    cmd.args(["test", "--fail-fast"]).assert_failure();
});

forgetest!(config_deny_warnings_is_deprecated, |prj, cmd| {
    cmd.git_init();

    let faulty_toml = DEFAULT_CONFIG.replace(r#"deny = "never""#, "deny_warnings = true");

    fs::write(prj.root().join("foundry.toml"), faulty_toml).unwrap();
    cmd.forge_fuse().args(["config"]).assert_success().stderr_eq(str![[r#"
Warning: Key `deny_warnings` is being deprecated in favor of `deny = warnings`. It will be removed in future versions.

"#]]);
});

forgetest!(config_labels_is_deprecated, |prj, cmd| {
    cmd.git_init();

    fs::write(
        prj.root().join("foundry.toml"),
        r#"
[labels]
0x0000000000000000000000000000000000000001 = "Alice"
"#,
    )
    .unwrap();
    cmd.forge_fuse().args(["config"]).assert_success().stderr_eq(str![[r#"
Warning: Key `[labels]` is being deprecated in favor of `[tracing.labels]`. It will be removed in future versions.

"#]]);
});

// <https://github.com/foundry-rs/foundry/issues/5866>
forgetest!(no_warnings_on_external_sections, |prj, cmd| {
    cmd.git_init();

    let toml = r"[profile.default]
    src = 'src'
    out = 'out'

    # Custom sections for other tools
    [external.scopelint]
    some_flag = 1

    [external.forge_deploy]
    another_setting = 123";

    fs::write(prj.root().join("foundry.toml"), toml).unwrap();
    cmd.forge_fuse().args(["config"]).assert_success().stderr_eq(str![[r#"

"#]]);
});

// <https://github.com/foundry-rs/foundry/issues/10550>
forgetest!(config_warnings_on_unknown_keys, |prj, cmd| {
    cmd.git_init();

    let faulty_toml = r"[profile.default]
    src = 'src'
    out = 'out'
    solc_version = '0.8.18'
    foo = 'unknown'

    [profile.another]
    src = 'src'
    out = 'out'
    bar = 'another_unknown'";

    fs::write(prj.root().join("foundry.toml"), faulty_toml).unwrap();
    cmd.forge_fuse().args(["config"]).assert_success().stderr_eq(str![[r#"
Warning: Found unknown `bar` config for profile `another` defined in foundry.toml.
Warning: Found unknown `foo` config for profile `default` defined in foundry.toml.

"#]]);
});

forgetest_init!(test_ignored_file_paths_normalization, |prj, cmd| {
    fn gen_contract(name: &str) -> String {
        let fn_name = name.chars().next().unwrap().to_lowercase().to_string() + &name[1..];
        format!(
            r#"
contract {name} {{
    function {fn_name}() public returns (bool) {{ return true; }}
}}
"#
        )
    }

    // Update config to ignore warnings from specific files with various path formats
    prj.update_config(|config| {
        config.ignored_file_paths = vec![
            PathBuf::from("./test/IgnoredWithPrefix.sol"), // With "./" prefix
            PathBuf::from("src/IgnoredNoPrefix.sol"),      // Without "./" prefix
            PathBuf::from("./src/nested/IgnoredNested.sol"), // Nested path with prefix
        ];
    });

    // Create contracts that will generate warnings
    prj.add_source("IgnoredNoPrefix.sol", &gen_contract("IgnoredNoPrefix"));
    prj.add_test("IgnoredWithPrefix.sol", &gen_contract("IgnoredWithPrefix"));

    fs::create_dir_all(prj.root().join("src/nested")).unwrap();
    fs::write(prj.root().join("src/nested/IgnoredNested.sol"), gen_contract("IgnoredNested"))
        .unwrap();

    prj.add_source("NotIgnored.sol", &gen_contract("NotIgnored"));

    // Verify the config loads paths as specified (before normalization)
    let config = cmd.config();
    let raw_paths = vec![
        PathBuf::from("./test/IgnoredWithPrefix.sol"),
        PathBuf::from("src/IgnoredNoPrefix.sol"),
        PathBuf::from("./src/nested/IgnoredNested.sol"),
    ];
    assert_eq!(config.ignored_file_paths, raw_paths);

    // Build and verify compilation succeeds with just 1 warning:
    cmd.forge_fuse().args(["build"]).assert_success().stdout_eq(
        r#"[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful with warnings:
Warning (2018): Function state mutability can be restricted to pure
 [FILE]:5:5:
  |
5 |     function notIgnored() public returns (bool) { return true; }
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

"#,
    );
});

forgetest_init!(test_failures_file_normalization, |prj, cmd| {
    // Update config with custom path containing "./" prefix
    prj.update_config(|config| {
        config.test_failures_file = PathBuf::from("./my-custom-failures");
    });

    prj.add_test(
        "MixedTests.t.sol",
        r#"
import {Test} from "forge-std/Test.sol";

contract MixedTests is Test {
    function testPass() public pure {
        require(1 == 1);
    }

    function testFail() public pure {
        require(1 == 2, "testFail failed");
    }
}
"#,
    );

    // Run test and verify test_failures_file is created at the correct location
    cmd.args(["test"]).assert_failure();
    let failures_file = prj.root().join("my-custom-failures");
    assert!(failures_file.exists());
    assert!(fs::read_to_string(&failures_file).unwrap().contains("testFail"));

    // Verify --rerun works from subdirectory
    let rerun_output = cmd
        .forge_fuse()
        .current_dir(prj.root().join("src"))
        .args(["test", "--rerun"])
        .assert_failure()
        .get_output()
        .stdout_lossy();
    assert!(rerun_output.contains("Ran 1 test"));
    assert!(rerun_output.contains("testFail()"));
    assert!(!rerun_output.contains("[PASS] testPass()"));
});
