//! # foundry-config
//!
//! Foundry configuration.

#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![cfg_attr(docsrs, feature(doc_cfg))]

#[macro_use]
extern crate tracing;

use crate::cache::StorageCachingConfig;
use alloy_primitives::{Address, B256, FixedBytes, U256, address, map::AddressHashMap};
use eyre::{ContextCompat, WrapErr};
use figment::{
    Error, Figment, Metadata, Profile, Provider,
    providers::{Env, Format, Serialized, Toml},
    value::{Dict, Map, Value},
};
use filter::GlobMatcher;
use foundry_compilers::{
    ArtifactOutput, ConfigurableArtifacts, Graph, Project, ProjectPathsConfig,
    RestrictionsWithVersion, VyperLanguage,
    artifacts::{
        BytecodeHash, DebuggingSettings, EvmVersion, Libraries, ModelCheckerSettings,
        ModelCheckerTarget, Optimizer, OptimizerDetails, RevertStrings, Settings, SettingsMetadata,
        Severity,
        output_selection::{ContractOutputSelection, OutputSelection},
        remappings::{RelativeRemapping, Remapping},
        serde_helpers,
    },
    cache::SOLIDITY_FILES_CACHE_FILENAME,
    compilers::{
        Compiler,
        multi::{MultiCompiler, MultiCompilerSettings},
        solc::{Solc, SolcCompiler},
        vyper::{Vyper, VyperSettings},
    },
    error::SolcError,
    multi::{MultiCompilerParser, MultiCompilerRestrictions},
    solc::{CliSettings, SolcLanguage, SolcSettings},
};
use regex::Regex;
use semver::Version;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use std::{
    borrow::Cow,
    collections::BTreeMap,
    fs, io,
    path::{Path, PathBuf},
    str::FromStr,
};

mod macros;

pub mod utils;
pub use foundry_evm_hardforks::{
    ExecutionSpec, FoundryHardfork, FromEvmVersion, evm_spec_id, evm_spec_id_from_str,
};
pub use utils::*;

mod endpoints;
pub use endpoints::{
    ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl, RpcEndpoints,
    builtin_rpc_url,
};

mod etherscan;
pub use etherscan::EtherscanConfigError;
use etherscan::{EtherscanConfigs, EtherscanEnvProvider, ResolvedEtherscanConfig};

pub mod resolve;
pub use resolve::UnresolvedEnvVarError;

pub mod cache;
use cache::{Cache, ChainCache};

pub mod fmt;
pub use fmt::FormatterConfig;

pub mod lint;
pub use lint::{LinterConfig, Severity as LintSeverity};

pub mod fs_permissions;
pub use fs_permissions::FsPermissions;
use fs_permissions::PathPermission;

pub mod error;
use error::ExtractConfigError;
pub use error::SolidityErrorCode;

pub mod doc;
pub use doc::DocConfig;

pub mod filter;
pub use filter::SkipBuildFilters;

mod warning;
pub use warning::*;

pub mod fix;

// reexport so cli types can implement `figment::Provider` to easily merge compiler arguments
pub use alloy_chains::{Chain, NamedChain};
pub use figment;

pub mod providers;
pub use providers::Remappings;
use providers::*;

mod fuzz;
pub use fuzz::{FuzzConfig, FuzzCorpusConfig, FuzzCorpusMutationWeights, FuzzDictionaryConfig};

mod invariant;
pub use invariant::{InvariantConfig, InvariantDepthMode, InvariantWorkers};

mod symbolic;
pub use symbolic::{SymbolicConfig, SymbolicExplorationOrder, SymbolicStorageLayout};

mod coverage;
pub use coverage::{CoverageConfig, CoverageReportKind, parse_lcov_version};

mod trace;
pub use trace::TracingConfig;

mod fee;
pub use fee::Eip1559FeeEstimatePreset;

pub mod mutation;
pub use mutation::{MutationConfig, MutatorType};

mod inline;
pub use inline::{InlineConfig, InlineConfigError, NatSpec};

pub mod soldeer;
use soldeer::{SoldeerConfig, SoldeerDependencyConfig};

mod vyper;
pub use vyper::VyperConfig;

mod bind_json;
use bind_json::BindJsonConfig;

mod compilation;
pub use compilation::{CompilationRestrictions, SettingsOverrides};

pub mod extend;
use extend::Extends;

use foundry_evm_networks::NetworkConfigs;
pub use semver;

#[cfg(not(test))]
static SELECTED_PROFILE: std::sync::OnceLock<Profile> = std::sync::OnceLock::new();

/// Foundry configuration
///
/// # Defaults
///
/// All configuration values have a default, documented in the [fields](#fields)
/// section below. [`Config::default()`] returns the default values for
/// the default profile while [`Config::with_root()`] returns the values based on the given
/// directory. [`Config::load()`] starts with the default profile and merges various providers into
/// the config, same for [`Config::load_with_root()`], but there the default values are determined
/// by [`Config::with_root()`]
///
/// # Provider Details
///
/// `Config` is a Figment [`Provider`] with the following characteristics:
///
///   * **Profile**
///
///     The profile is set to the value of the `profile` field.
///
///   * **Metadata**
///
///     This provider is named `Foundry Config`. It does not specify a
///     [`Source`](figment::Source) and uses default interpolation.
///
///   * **Data**
///
///     The data emitted by this provider are the keys and values corresponding
///     to the fields and values of the structure. The dictionary is emitted to
///     the "default" meta-profile.
///
/// Note that these behaviors differ from those of [`Config::figment()`].
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Config {
    /// The selected profile. **(default: _default_ `default`)**
    ///
    /// **Note:** This field is never serialized nor deserialized. When a
    /// `Config` is merged into a `Figment` as a `Provider`, this profile is
    /// selected on the `Figment`. When a `Config` is extracted, this field is
    /// set to the extracting Figment's selected `Profile`.
    #[serde(skip)]
    pub profile: Profile,
    /// The list of all profiles defined in the config.
    ///
    /// See `profile`.
    #[serde(skip)]
    pub profiles: Vec<Profile>,

    /// The root path where the config detection started from, [`Config::with_root`].
    // We're skipping serialization here, so it won't be included in the [`Config::to_string()`]
    // representation, but will be deserialized from the `Figment` so that forge commands can
    // override it.
    #[serde(default = "root_default", skip_serializing)]
    pub root: PathBuf,

    /// Configuration for extending from another foundry.toml (base) file.
    ///
    /// Can be either a string path or an object with path and strategy.
    /// Base files cannot extend (inherit) other files.
    #[serde(default, skip_serializing)]
    pub extends: Option<Extends>,

    /// Path of the sources directory.
    ///
    /// Defaults to `src`.
    pub src: PathBuf,
    /// Path of the tests directory.
    pub test: PathBuf,
    /// Path of the scripts directory.
    pub script: PathBuf,
    /// Path to the artifacts directory.
    pub out: PathBuf,
    /// Paths to all library folders, such as `lib`, or `node_modules`.
    pub libs: Vec<PathBuf>,
    /// Remappings to use for this repo
    #[serde(serialize_with = "remappings_serde::serialize")]
    pub remappings: Vec<RelativeRemapping>,
    /// Whether to autodetect remappings.
    pub auto_detect_remappings: bool,
    /// Library addresses to link.
    pub libraries: Vec<String>,
    /// Whether to enable the build cache.
    pub cache: bool,
    /// The path to the cache store.
    pub cache_path: PathBuf,
    /// Whether to dynamically link tests.
    pub dynamic_test_linking: bool,
    /// Where the gas snapshots are stored.
    pub snapshots: PathBuf,
    /// Whether to check for differences against previously stored gas snapshots.
    pub gas_snapshot_check: bool,
    /// Whether to emit gas snapshots to disk.
    pub gas_snapshot_emit: bool,
    /// The path to store broadcast logs at.
    pub broadcast: PathBuf,
    /// Additional paths passed to `solc --allow-paths`.
    pub allow_paths: Vec<PathBuf>,
    /// Additional paths passed to `solc --include-path`.
    pub include_paths: Vec<PathBuf>,
    /// Glob patterns for file paths to skip when building and executing contracts.
    pub skip: Vec<GlobMatcher>,
    /// Whether to forcefully clean all project artifacts before running commands.
    pub force: bool,
    /// The EVM version to use when building contracts.
    #[serde(with = "from_str_lowercase")]
    pub evm_version: EvmVersion,
    /// The runtime hardfork to use when executing tests and scripts.
    pub hardfork: Option<FoundryHardfork>,
    /// List of contracts to generate gas reports for.
    pub gas_reports: Vec<String>,
    /// List of contracts to ignore for gas reports.
    pub gas_reports_ignore: Vec<String>,
    /// Whether to include gas reports for tests.
    pub gas_reports_include_tests: bool,
    /// The Solc instance to use if any.
    ///
    /// This takes precedence over `auto_detect_solc`, if a version is set then this overrides
    /// auto-detection.
    ///
    /// **Note** for backwards compatibility reasons this also accepts solc_version from the toml
    /// file, see `BackwardsCompatTomlProvider`.
    ///
    /// Avoid using this field directly; call the related `solc` methods instead.
    #[doc(hidden)]
    pub solc: Option<SolcReq>,
    /// Whether to autodetect the solc compiler version to use.
    pub auto_detect_solc: bool,
    /// Offline mode, if set, network access (downloading solc) is disallowed.
    ///
    /// Relationship with `auto_detect_solc`:
    ///    - if `auto_detect_solc = true` and `offline = true`, the required solc version(s) will
    ///      be auto detected but if the solc version is not installed, it will _not_ try to
    ///      install it
    pub offline: bool,
    /// Whether to activate optimizer
    pub optimizer: Option<bool>,
    /// The number of runs specifies roughly how often each opcode of the deployed code will be
    /// executed across the life-time of the contract. This means it is a trade-off parameter
    /// between code size (deploy cost) and code execution cost (cost after deployment).
    /// An `optimizer_runs` parameter of `1` will produce short but expensive code. In contrast, a
    /// larger `optimizer_runs` parameter will produce longer but more gas efficient code. The
    /// maximum value of the parameter is `2**32-1`.
    ///
    /// A common misconception is that this parameter specifies the number of iterations of the
    /// optimizer. This is not true: The optimizer will always run as many times as it can
    /// still improve the code.
    pub optimizer_runs: Option<usize>,
    /// Switch optimizer components on or off in detail.
    /// The "enabled" switch above provides two defaults which can be
    /// tweaked here. If "details" is given, "enabled" can be omitted.
    pub optimizer_details: Option<OptimizerDetails>,
    /// Model checker settings.
    pub model_checker: Option<ModelCheckerSettings>,
    /// Verbosity to use for global output.
    pub verbosity: u8,
    /// url of the rpc server that should be used for any rpc calls
    pub eth_rpc_url: Option<String>,
    /// Whether to accept invalid certificates for the rpc server.
    pub eth_rpc_accept_invalid_certs: bool,
    /// Whether to disable automatic proxy detection for the rpc server.
    ///
    /// This can help in sandboxed environments (e.g., Cursor IDE sandbox, macOS App Sandbox)
    /// where system proxy detection via SCDynamicStore causes crashes.
    pub eth_rpc_no_proxy: bool,
    /// JWT secret that should be used for any rpc calls
    pub eth_rpc_jwt: Option<String>,
    /// Timeout that should be used for any rpc calls
    pub eth_rpc_timeout: Option<u64>,
    /// Headers that should be used for any rpc calls
    ///
    /// # Example
    ///
    /// rpc_headers = ["x-custom-header:value", "x-another-header:another-value"]
    ///
    /// You can also the ETH_RPC_HEADERS env variable like so:
    /// `ETH_RPC_HEADERS="x-custom-header:value x-another-header:another-value"`
    pub eth_rpc_headers: Option<Vec<String>>,
    /// Print the equivalent curl command instead of making the RPC request.
    pub eth_rpc_curl: bool,
    /// etherscan API key, or alias for an `EtherscanConfig` in `etherscan` table
    pub etherscan_api_key: Option<String>,
    /// Multiple etherscan api configs and their aliases
    #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")]
    pub etherscan: EtherscanConfigs,
    /// List of solidity error codes to always silence in the compiler output.
    pub ignored_error_codes: Vec<SolidityErrorCode>,
    /// List of (path prefix, solidity error codes) to silence in the compiler output.
    pub ignored_error_codes_from: Vec<(PathBuf, Vec<SolidityErrorCode>)>,
    /// List of file paths to ignore.
    #[serde(rename = "ignored_warnings_from")]
    pub ignored_file_paths: Vec<PathBuf>,
    /// Diagnostic level (minimum) at which the process should finish with a non-zero exit.
    pub deny: DenyLevel,
    /// DEPRECATED: use `deny` instead.
    #[serde(default, skip_serializing)]
    pub deny_warnings: bool,
    /// Only run test functions matching the specified regex pattern.
    #[serde(rename = "match_test")]
    pub test_pattern: Option<RegexWrapper>,
    /// Only run test functions that do not match the specified regex pattern.
    #[serde(rename = "no_match_test")]
    pub test_pattern_inverse: Option<RegexWrapper>,
    /// Only run tests in contracts matching the specified regex pattern.
    #[serde(rename = "match_contract")]
    pub contract_pattern: Option<RegexWrapper>,
    /// Only run tests in contracts that do not match the specified regex pattern.
    #[serde(rename = "no_match_contract")]
    pub contract_pattern_inverse: Option<RegexWrapper>,
    /// Only run tests in source files matching the specified glob pattern.
    #[serde(rename = "match_path", with = "from_opt_glob")]
    pub path_pattern: Option<globset::Glob>,
    /// Only run tests in source files that do not match the specified glob pattern.
    #[serde(rename = "no_match_path", with = "from_opt_glob")]
    pub path_pattern_inverse: Option<globset::Glob>,
    /// Only show coverage for files that do not match the specified regex pattern.
    #[serde(rename = "no_match_coverage")]
    pub coverage_pattern_inverse: Option<RegexWrapper>,
    /// Path where last test run failures are recorded.
    pub test_failures_file: PathBuf,
    /// Path where mutation tests are cached, to resume running them
    pub mutation_dir: PathBuf,
    /// Max concurrent threads to use.
    pub threads: Option<usize>,
    /// Whether to show test execution progress.
    pub show_progress: bool,
    /// Configuration for fuzz testing
    pub fuzz: FuzzConfig,
    /// Configuration for invariant testing
    pub invariant: InvariantConfig,
    /// Configuration for symbolic testing
    pub symbolic: SymbolicConfig,
    /// Configuration for `forge coverage`
    pub coverage: CoverageConfig,
    /// Configuration for mutation testing
    pub mutation: MutationConfig,
    /// Configuration for trace rendering.
    pub tracing: TracingConfig,
    /// Whether to allow ffi cheatcodes in test
    pub ffi: bool,
    /// Whether to show `console.log` outputs in realtime during script/test execution
    pub live_logs: bool,
    /// Whether to allow `expectRevert` for internal functions.
    pub allow_internal_expect_revert: bool,
    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
    pub always_use_create_2_factory: bool,
    /// Controls how EIP-1559 fees are estimated for `forge script` broadcasts
    /// (`low` / `market` / `aggressive`). Defaults to `market`, which preserves
    /// the historical behavior (`base_fee * 2 + 20th-percentile priority fee`).
    #[serde(default)]
    pub eip1559_fee_estimate: Eip1559FeeEstimatePreset,
    /// Sets a timeout in seconds for vm.prompt cheatcodes
    pub prompt_timeout: u64,
    /// The address which will be executing all tests
    pub sender: Address,
    /// The tx.origin value during EVM execution
    pub tx_origin: Address,
    /// the initial balance of each deployed test contract
    pub initial_balance: U256,
    /// the block.number value during EVM execution
    #[serde(
        deserialize_with = "crate::deserialize_u64_to_u256",
        serialize_with = "crate::serialize_u64_or_u256"
    )]
    pub block_number: U256,
    /// pins the block number for the state fork
    pub fork_block_number: Option<u64>,
    /// The chain name or EIP-155 chain ID.
    #[serde(rename = "chain_id", alias = "chain")]
    pub chain: Option<Chain>,
    /// Block gas limit.
    pub gas_limit: GasLimit,
    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests.
    pub code_size_limit: Option<usize>,
    /// `tx.gasprice` value during EVM execution.
    ///
    /// This is an Option, so we can determine in fork mode whether to use the config's gas price
    /// (if set by user) or the remote client's gas price.
    pub gas_price: Option<u64>,
    /// The base fee in a block.
    pub block_base_fee_per_gas: u64,
    /// The `block.coinbase` value during EVM execution.
    pub block_coinbase: Address,
    /// The `block.timestamp` value during EVM execution.
    #[serde(
        deserialize_with = "crate::deserialize_u64_to_u256",
        serialize_with = "crate::serialize_u64_or_u256"
    )]
    pub block_timestamp: U256,
    /// The `block.difficulty` value during EVM execution.
    pub block_difficulty: u64,
    /// Before merge the `block.max_hash`, after merge it is `block.prevrandao`.
    pub block_prevrandao: B256,
    /// The `block.gaslimit` value during EVM execution.
    pub block_gas_limit: Option<GasLimit>,
    /// The memory limit per EVM execution in bytes.
    /// If this limit is exceeded, a `MemoryLimitOOG` result is thrown.
    ///
    /// The default is 128MiB.
    pub memory_limit: u64,
    /// Additional output selection for all contracts, such as "ir", "devdoc", "storageLayout",
    /// etc.
    ///
    /// See the [Solc Compiler Api](https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-api) for more information.
    ///
    /// The following values are always set because they're required by `forge`:
    /// ```json
    /// {
    ///   "*": [
    ///       "abi",
    ///       "evm.bytecode",
    ///       "evm.deployedBytecode",
    ///       "evm.methodIdentifiers"
    ///     ]
    /// }
    /// ```
    #[serde(default)]
    pub extra_output: Vec<ContractOutputSelection>,
    /// If set, a separate JSON file will be emitted for every contract depending on the
    /// selection, eg. `extra_output_files = ["metadata"]` will create a `metadata.json` for
    /// each contract in the project.
    ///
    /// See [Contract Metadata](https://docs.soliditylang.org/en/latest/metadata.html) for more information.
    ///
    /// The difference between `extra_output = ["metadata"]` and
    /// `extra_output_files = ["metadata"]` is that the former will include the
    /// contract's metadata in the contract's json artifact, whereas the latter will emit the
    /// output selection as separate files.
    #[serde(default)]
    pub extra_output_files: Vec<ContractOutputSelection>,
    /// Whether to print the names of the compiled contracts.
    pub names: bool,
    /// Whether to print the sizes of the compiled contracts.
    pub sizes: bool,
    /// If set to true, changes compilation pipeline to go through the Yul intermediate
    /// representation.
    pub via_ir: bool,
    /// Whether to turn on SSA CFG-based code generation via the IR.
    /// This is an experimental feature (as of 0.8.35) that requires `experimental` to be enabled.
    /// Enabling this option will also enable `via_ir` if it is not already enabled.
    pub via_ssa_cfg: bool,
    /// Whether to enable Solidity's experimental mode.
    ///
    /// This passes `--experimental` to solc, which is required by Solidity 0.8.35+ for
    /// experimental features.
    pub experimental: bool,
    /// Whether to include the AST as JSON in the compiler output.
    pub ast: bool,
    /// RPC storage caching settings determines what chains and endpoints to cache
    pub rpc_storage_caching: StorageCachingConfig,
    /// Disables storage caching entirely. This overrides any settings made in
    /// `rpc_storage_caching`
    pub no_storage_caching: bool,
    /// Disables rate limiting entirely. This overrides any settings made in
    /// `compute_units_per_second`
    pub no_rpc_rate_limit: bool,
    /// Multiple rpc endpoints and their aliases
    #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")]
    pub rpc_endpoints: RpcEndpoints,
    /// Whether to store the referenced sources in the metadata as literal data.
    pub use_literal_content: bool,
    /// Whether to include the metadata hash.
    ///
    /// The metadata hash is machine dependent. By default, this is set to [BytecodeHash::None] to allow for deterministic code, See: <https://docs.soliditylang.org/en/latest/metadata.html>
    #[serde(with = "from_str_lowercase")]
    pub bytecode_hash: BytecodeHash,
    /// Whether to append the metadata hash to the bytecode.
    ///
    /// If this is `false` and the `bytecode_hash` option above is not `None` solc will issue a
    /// warning.
    pub cbor_metadata: bool,
    /// How to treat revert (and require) reason strings.
    #[serde(with = "serde_helpers::display_from_str_opt")]
    pub revert_strings: Option<RevertStrings>,
    /// Whether to compile in sparse mode
    ///
    /// If this option is enabled, only the required contracts/files will be selected to be
    /// included in solc's output selection, see also [`OutputSelection`].
    pub sparse_mode: bool,
    /// Generates additional build info json files for every new build, containing the
    /// `CompilerInput` and `CompilerOutput`.
    pub build_info: bool,
    /// The path to the `build-info` directory that contains the build info json files.
    pub build_info_path: Option<PathBuf>,
    /// Configuration for `forge fmt`
    pub fmt: FormatterConfig,
    /// Configuration for `forge lint`
    pub lint: LinterConfig,
    /// Configuration for `forge doc`
    pub doc: DocConfig,
    /// Configuration for `forge bind-json`
    pub bind_json: BindJsonConfig,
    /// Configures the permissions of cheat codes that touch the file system.
    ///
    /// This includes what operations can be executed (read, write)
    pub fs_permissions: FsPermissions,

    /// Whether to enable call isolation.
    ///
    /// Useful for more correct gas accounting and EVM behavior in general.
    pub isolate: bool,

    /// Whether to disable the block gas limit checks.
    pub disable_block_gas_limit: bool,

    /// Whether to enable the tx gas limit checks as imposed by Osaka (EIP-7825).
    pub enable_tx_gas_limit: bool,

    /// Deprecated address-label alias; use [`TracingConfig::labels`].
    #[serde(default, skip_serializing_if = "AddressHashMap::is_empty")]
    pub labels: AddressHashMap<String>,

    /// Whether to enable safety checks for `vm.getCode` and `vm.getDeployedCode` invocations.
    /// If disabled, it is possible to access artifacts which were not recompiled or cached.
    pub unchecked_cheatcode_artifacts: bool,

    /// CREATE2 salt to use for the library deployment in scripts.
    pub create2_library_salt: B256,

    /// The CREATE2 deployer address to use.
    pub create2_deployer: Address,

    /// Configuration for Vyper compiler
    pub vyper: VyperConfig,

    /// Soldeer dependencies
    pub dependencies: Option<SoldeerDependencyConfig>,

    /// Soldeer custom configs
    pub soldeer: Option<SoldeerConfig>,

    /// Whether failed assertions should revert.
    ///
    /// Note that this only applies to native (cheatcode) assertions, invoked on Vm contract.
    pub assertions_revert: bool,

    /// Whether `failed()` should be invoked to check if the test have failed.
    pub legacy_assertions: bool,

    /// Optional additional CLI arguments to pass to `solc` binary.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub extra_args: Vec<String>,

    /// Networks with enabled features.
    #[serde(flatten)]
    pub networks: NetworkConfigs,

    /// Timeout for transactions in seconds.
    pub transaction_timeout: u64,

    /// Warnings gathered when loading the Config. See [`WarningsProvider`] for more information.
    #[serde(rename = "__warnings", default, skip_serializing)]
    pub warnings: Vec<Warning>,

    /// Additional settings profiles to use when compiling.
    #[serde(default)]
    pub additional_compiler_profiles: Vec<SettingsOverrides>,

    /// Restrictions on compilation of certain files.
    #[serde(default)]
    pub compilation_restrictions: Vec<CompilationRestrictions>,

    /// Whether to enable script execution protection.
    pub script_execution_protection: bool,

    /// PRIVATE: This structure may grow, As such, constructing this structure should
    /// _always_ be done using a public constructor or update syntax:
    ///
    /// ```ignore
    /// use foundry_config::Config;
    ///
    /// let config = Config { src: "other".into(), ..Default::default() };
    /// ```
    #[doc(hidden)]
    #[serde(skip)]
    pub _non_exhaustive: (),
}

/// Diagnostic level (minimum) at which the process should finish with a non-zero exit.
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Default, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum DenyLevel {
    /// Always exit with zero code.
    #[default]
    Never,
    /// Exit with a non-zero code if any warnings are found.
    Warnings,
    /// Exit with a non-zero code if any notes or warnings are found.
    Notes,
}

// Custom deserialization to make `DenyLevel` parsing case-insensitive and backwards compatible with
// booleans.
impl<'de> Deserialize<'de> for DenyLevel {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct DenyLevelVisitor;

        impl<'de> de::Visitor<'de> for DenyLevelVisitor {
            type Value = DenyLevel;

            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                formatter.write_str("one of the following strings: `never`, `warnings`, `notes`")
            }

            fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
            where
                E: de::Error,
            {
                Ok(DenyLevel::from(value))
            }

            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
            where
                E: de::Error,
            {
                DenyLevel::from_str(value).map_err(de::Error::custom)
            }
        }

        deserializer.deserialize_any(DenyLevelVisitor)
    }
}

impl FromStr for DenyLevel {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "warnings" | "warning" | "w" => Ok(Self::Warnings),
            "notes" | "note" | "n" => Ok(Self::Notes),
            "never" | "false" | "f" => Ok(Self::Never),
            _ => Err(format!(
                "unknown variant: found `{s}`, expected one of `never`, `warnings`, `notes`"
            )),
        }
    }
}

impl From<bool> for DenyLevel {
    fn from(deny: bool) -> Self {
        if deny { Self::Warnings } else { Self::Never }
    }
}

impl DenyLevel {
    /// Returns `true` if the deny level includes warnings.
    pub const fn warnings(&self) -> bool {
        match self {
            Self::Never => false,
            Self::Warnings | Self::Notes => true,
        }
    }

    /// Returns `true` if the deny level includes notes.
    pub const fn notes(&self) -> bool {
        match self {
            Self::Never | Self::Warnings => false,
            Self::Notes => true,
        }
    }

    /// Returns `true` if the deny level is set to never (only errors).
    pub const fn never(&self) -> bool {
        match self {
            Self::Never => true,
            Self::Warnings | Self::Notes => false,
        }
    }
}

/// Mapping of fallback standalone sections. See [`FallbackProfileProvider`].
pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")];

/// Deprecated keys and their replacements.
///
/// See [Warning::DeprecatedKey]
pub const DEPRECATIONS: &[(&str, &str)] =
    &[("cancun", "evm_version = Cancun"), ("deny_warnings", "deny = warnings")];

impl Config {
    /// The default profile: "default"
    pub const DEFAULT_PROFILE: Profile = Profile::Default;

    /// The hardhat profile: "hardhat"
    pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat");

    /// TOML section for profiles
    pub const PROFILE_SECTION: &'static str = "profile";

    /// External config sections, ignored from warnings.
    pub const EXTERNAL_SECTION: &'static str = "external";

    /// Standalone sections in the config which get integrated into the selected profile
    pub const STANDALONE_SECTIONS: &'static [&'static str] = &[
        "rpc_endpoints",
        "etherscan",
        "fmt",
        "lint",
        "doc",
        "fuzz",
        "invariant",
        "symbolic",
        "coverage",
        "mutation",
        "tracing",
        "labels",
        "dependencies",
        "soldeer",
        "vyper",
        "bind_json",
    ];

    pub(crate) fn is_standalone_section<T: ?Sized + PartialEq<str>>(section: &T) -> bool {
        section == Self::PROFILE_SECTION
            || section == Self::EXTERNAL_SECTION
            || Self::STANDALONE_SECTIONS.iter().any(|s| section == *s)
    }

    /// File name of config toml file
    pub const FILE_NAME: &'static str = "foundry.toml";

    /// The name of the directory foundry reserves for itself under the user's home directory: `~`
    pub const FOUNDRY_DIR_NAME: &'static str = ".foundry";

    /// Default address for tx.origin
    ///
    /// `0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38`
    pub const DEFAULT_SENDER: Address = address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38");

    /// Default salt for create2 library deployments
    pub const DEFAULT_CREATE2_LIBRARY_SALT: FixedBytes<32> = FixedBytes::<32>::ZERO;

    /// Default create2 deployer
    pub const DEFAULT_CREATE2_DEPLOYER: Address =
        address!("0x4e59b44847b379578588920ca78fbf26c0b4956c");

    /// Loads the `Config` from the current directory.
    ///
    /// See [`figment`](Self::figment) for more details.
    pub fn load() -> Result<Self, ExtractConfigError> {
        Self::from_provider(Self::figment())
    }

    /// Loads the `Config` with the given `providers` preset.
    ///
    /// See [`figment`](Self::figment) for more details.
    pub fn load_with_providers(providers: FigmentProviders) -> Result<Self, ExtractConfigError> {
        Self::from_provider(Self::default().to_figment(providers))
    }

    /// Loads the `Config` from the given root directory.
    ///
    /// See [`figment_with_root`](Self::figment_with_root) for more details.
    #[track_caller]
    pub fn load_with_root(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
        Self::from_provider(Self::figment_with_root(root.as_ref()))
    }

    /// Loads the `Config` from the given root directory, allowing profile fallback.
    ///
    /// Unlike [`load_with_root`](Self::load_with_root), if the selected profile (via
    /// `FOUNDRY_PROFILE`) does not exist in the config, this falls back to the default profile
    /// instead of returning an error. This is useful for loading nested lib/dependency configs
    /// that may not define all profiles the main project uses.
    #[track_caller]
    pub fn load_with_root_and_fallback(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
        let figment = Self::figment_with_root(root.as_ref());
        Self::from_figment_fallback(Figment::from(figment))
    }

    /// Attempts to extract a `Config` from `provider`, returning the result.
    ///
    /// # Example
    ///
    /// ```rust
    /// use figment::providers::{Env, Format, Toml};
    /// use foundry_config::Config;
    ///
    /// // Use foundry's default `Figment`, but allow values from `other.toml`
    /// // to supersede its values.
    /// let figment = Config::figment().merge(Toml::file("other.toml").nested());
    ///
    /// let config = Config::from_provider(figment);
    /// ```
    #[doc(alias = "try_from")]
    pub fn from_provider<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
        trace!("load config with provider: {:?}", provider.metadata());
        Self::from_figment(Figment::from(provider.legacy_labels()))
    }

    /// Applies an inline provider on top of the current config without reloading external
    /// providers such as `foundry.toml`, env vars, or remappings.
    pub fn merge_inline_provider<T: Provider>(&self, provider: T) -> Result<Self, Error> {
        let provider = Figment::from(provider.legacy_labels()).select(self.profile.clone());
        let invariant_corpus_random_sequence_weight_configured =
            self.invariant.corpus_random_sequence_weight_configured
                || provider.contains("invariant.corpus_random_sequence_weight")
                || provider
                    .extract_inner::<bool>("invariant.corpus_random_sequence_weight_configured")
                    .unwrap_or(false);
        let invariant_workers_configured = self.invariant.workers_configured
            || provider.contains("invariant.workers")
            || provider.extract_inner::<bool>("invariant.workers_configured").unwrap_or(false)
            || provider.extract_inner::<InvariantWorkers>("invariant.workers").is_ok();
        let figment = self.to_figment(FigmentProviders::None).merge(provider);
        let mut config = figment.extract::<Self>()?;
        config.profile = self.profile.clone();
        config.profiles = self.profiles.clone();
        config.invariant.corpus_random_sequence_weight_configured =
            invariant_corpus_random_sequence_weight_configured;
        config.invariant.workers_configured = invariant_workers_configured;
        config.normalize_hardfork_settings()?;

        Ok(config)
    }

    #[doc(hidden)]
    #[deprecated(note = "use `Config::from_provider` instead")]
    pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
        Self::from_provider(provider)
    }

    fn from_figment(figment: Figment) -> Result<Self, ExtractConfigError> {
        Self::from_figment_inner(figment, true)
    }

    /// Same as `from_figment` but allows unknown profiles, falling back to default profile.
    /// Used when loading nested lib configs that may not define all profiles.
    fn from_figment_fallback(figment: Figment) -> Result<Self, ExtractConfigError> {
        Self::from_figment_inner(figment, false)
    }

    fn from_figment_inner(
        figment: Figment,
        strict_profile: bool,
    ) -> Result<Self, ExtractConfigError> {
        let invariant_corpus_random_sequence_weight_configured = figment
            .extract_inner::<bool>("invariant.corpus_random_sequence_weight_configured")
            .unwrap_or_else(|_| {
                figment_value_is_configured(&figment, "invariant.corpus_random_sequence_weight")
            });
        let invariant_workers_configured = figment
            .extract_inner::<bool>("invariant.workers_configured")
            .unwrap_or_else(|_| figment_value_is_configured(&figment, "invariant.workers"));
        let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
        config.invariant.corpus_random_sequence_weight_configured =
            invariant_corpus_random_sequence_weight_configured;
        config.invariant.workers_configured = invariant_workers_configured;
        let selected_profile = figment.profile().clone();

        // The `"profile"` profile contains all the profiles as keys.
        fn add_profile(profiles: &mut Vec<Profile>, profile: &Profile) {
            if !profiles.contains(profile) {
                profiles.push(profile.clone());
            }
        }
        let figment = figment.select(Self::PROFILE_SECTION);
        if let Ok(data) = figment.data()
            && let Some(profiles) = data.get(&Profile::new(Self::PROFILE_SECTION))
        {
            for profile in profiles.keys() {
                add_profile(&mut config.profiles, &Profile::new(profile));
            }
        }
        add_profile(&mut config.profiles, &Self::DEFAULT_PROFILE);

        // Check if the selected profile exists.
        if config.profiles.contains(&selected_profile) {
            config.profile = selected_profile;
        } else {
            // Fall back to the default profile. In strict mode (top-level loads), emit a warning
            // so users are informed when an unknown profile (e.g. via `FOUNDRY_PROFILE`) is
            // selected; the silent fallback is reserved for nested lib configs.
            if strict_profile {
                config
                    .warnings
                    .push(Warning::UnknownProfile { profile: selected_profile.to_string() });
            }
            config.profile = Self::DEFAULT_PROFILE;
        }

        config.normalize_optimizer_settings();
        config.normalize_hardfork_settings().map_err(ExtractConfigError::new)?;

        // Validate optimizer_runs does not exceed u32::MAX (Solidity compiler limit)
        if let Some(runs) = config.optimizer_runs
            && runs > u32::MAX as usize
        {
            return Err(ExtractConfigError::new(Error::from(format!(
                "`optimizer_runs` value {} exceeds maximum allowed value of {}",
                runs,
                u32::MAX
            ))));
        }

        Ok(config)
    }

    fn normalize_hardfork_settings(&mut self) -> Result<(), Error> {
        let Some(hardfork) = self.hardfork else { return Ok(()) };
        self.networks = self.networks.normalize_for_hardfork(hardfork).map_err(Error::from)?;
        Ok(())
    }

    /// Returns the populated [Figment] using the requested [FigmentProviders] preset.
    ///
    /// This will merge various providers, such as env,toml,remappings into the figment if
    /// requested.
    pub fn to_figment(&self, providers: FigmentProviders) -> Figment {
        // Note that `Figment::from` here is a method on `Figment` rather than the `From` impl below

        if providers.is_none() {
            return Figment::from(self);
        }

        let root = self.root.as_path();
        let profile = Self::selected_profile();
        let mut figment = Figment::default().merge(DappHardhatDirProvider(root));

        // merge global foundry.toml file
        if let Some(global_toml) = Self::foundry_dir_toml().filter(|p| p.exists()) {
            figment = Self::merge_toml_provider(
                figment,
                TomlFileProvider::new(None, global_toml),
                profile.clone(),
            );
        }
        // merge local foundry.toml file
        figment = Self::merge_toml_provider(
            figment,
            TomlFileProvider::new(Some("FOUNDRY_CONFIG"), root.join(Self::FILE_NAME)),
            profile.clone(),
        );

        // merge environment variables
        figment = figment
            .merge(
                Env::prefixed("DAPP_")
                    .ignore(&["REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
                    .global()
                    .legacy_labels(),
            )
            .merge(
                Env::prefixed("DAPP_TEST_")
                    .ignore(&["CACHE", "FUZZ_RUNS", "DEPTH", "FFI", "FS_PERMISSIONS"])
                    .global()
                    .legacy_labels(),
            )
            .merge(DappEnvCompatProvider)
            .merge(EtherscanEnvProvider::default())
            .merge(
                Env::prefixed("FOUNDRY_")
                    .ignore(&["PROFILE", "REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
                    .map(|key| {
                        let key = key.as_str();
                        if Self::STANDALONE_SECTIONS.iter().any(|section| {
                            key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
                        }) {
                            key.replacen('_', ".", 1).into()
                        } else {
                            key.into()
                        }
                    })
                    .global()
                    .legacy_labels(),
            )
            .select(profile.clone());

        // only resolve remappings if all providers are requested
        if providers.is_all() {
            // we try to merge remappings after we've merged all other providers, this prevents
            // redundant fs lookups to determine the default remappings that are eventually updated
            // by other providers, like the toml file
            let remappings = RemappingsProvider {
                auto_detect_remappings: figment
                    .extract_inner::<bool>("auto_detect_remappings")
                    .unwrap_or(true),
                lib_paths: figment
                    .extract_inner::<Vec<PathBuf>>("libs")
                    .map(Cow::Owned)
                    .unwrap_or_else(|_| Cow::Borrowed(&self.libs)),
                root,
                remappings: figment.extract_inner::<Vec<Remapping>>("remappings"),
            };
            figment = figment.merge(remappings);
        }

        let invariant_corpus_random_sequence_weight_configured = self
            .invariant
            .corpus_random_sequence_weight_configured
            || figment
                .extract_inner::<bool>("invariant.corpus_random_sequence_weight_configured")
                .unwrap_or_else(|_| {
                    figment_value_is_configured(&figment, "invariant.corpus_random_sequence_weight")
                });
        let invariant_workers_configured = self.invariant.workers_configured
            || figment.extract_inner::<bool>("invariant.workers_configured").unwrap_or_else(|_| {
                figment.extract_inner::<InvariantWorkers>("invariant.workers").is_ok()
            });

        // normalize defaults
        figment = self.normalize_defaults(figment);
        if invariant_corpus_random_sequence_weight_configured {
            figment = figment.merge(("invariant.corpus_random_sequence_weight_configured", true));
        }
        if invariant_workers_configured {
            figment = figment.merge(("invariant.workers_configured", true));
        }

        Figment::from(self).merge(figment).select(profile)
    }

    /// The config supports relative paths and tracks the root path separately see
    /// `Config::with_root`
    ///
    /// This joins all relative paths with the current root and attempts to make them canonic
    #[must_use]
    pub fn canonic(self) -> Self {
        let root = self.root.clone();
        self.canonic_at(root)
    }

    /// Joins all relative paths with the given root so that paths that are defined as:
    ///
    /// ```toml
    /// [profile.default]
    /// src = "src"
    /// out = "./out"
    /// libs = ["lib", "/var/lib"]
    /// ```
    ///
    /// Will be made canonic with the given root:
    ///
    /// ```toml
    /// [profile.default]
    /// src = "<root>/src"
    /// out = "<root>/out"
    /// libs = ["<root>/lib", "/var/lib"]
    /// ```
    #[must_use]
    pub fn canonic_at(mut self, root: impl Into<PathBuf>) -> Self {
        let root = canonic(root);

        fn p(root: &Path, rem: &Path) -> PathBuf {
            canonic(root.join(rem))
        }

        self.src = p(&root, &self.src);
        self.test = p(&root, &self.test);
        self.script = p(&root, &self.script);
        self.out = p(&root, &self.out);
        self.broadcast = p(&root, &self.broadcast);
        self.cache_path = p(&root, &self.cache_path);
        self.snapshots = p(&root, &self.snapshots);
        self.test_failures_file = p(&root, &self.test_failures_file);

        if let Some(build_info_path) = self.build_info_path {
            self.build_info_path = Some(p(&root, &build_info_path));
        }

        self.libs = self.libs.into_iter().map(|lib| p(&root, &lib)).collect();

        self.remappings = self
            .remappings
            .into_iter()
            .map(|r| relative_remapping_preserving_context_boundary(r.into(), &root))
            .collect();

        self.allow_paths = self.allow_paths.into_iter().map(|allow| p(&root, &allow)).collect();

        self.include_paths = self.include_paths.into_iter().map(|allow| p(&root, &allow)).collect();

        self.fs_permissions.join_all(&root);

        if let Some(model_checker) = &mut self.model_checker {
            model_checker.contracts = std::mem::take(&mut model_checker.contracts)
                .into_iter()
                .map(|(path, contracts)| {
                    (format!("{}", p(&root, path.as_ref()).display()), contracts)
                })
                .collect();
        }

        self
    }

    /// Normalizes the evm version if a [SolcReq] is set
    pub fn normalized_evm_version(mut self) -> Self {
        self.normalize_evm_version();
        self
    }

    /// Normalizes optimizer settings.
    /// See <https://github.com/foundry-rs/foundry/issues/9665>
    pub const fn normalized_optimizer_settings(mut self) -> Self {
        self.normalize_optimizer_settings();
        self
    }

    /// Normalizes the evm version if a [SolcReq] is set to a valid version.
    pub fn normalize_evm_version(&mut self) {
        self.evm_version = self.get_normalized_evm_version();
    }

    /// Normalizes optimizer settings:
    /// - with default settings, optimizer is set to false and optimizer runs to 200
    /// - if optimizer is set and optimizer runs not specified, then optimizer runs is set to 200
    /// - enable optimizer if not explicitly set and optimizer runs set to a value greater than 0
    pub const fn normalize_optimizer_settings(&mut self) {
        match (self.optimizer, self.optimizer_runs) {
            // Default: set the optimizer to false and optimizer runs to 200.
            (None, None) => {
                self.optimizer = Some(false);
                self.optimizer_runs = Some(200);
            }
            // Set the optimizer runs to 200 if the `optimizer` config set.
            (Some(_), None) => self.optimizer_runs = Some(200),
            // Enables optimizer if the `optimizer_runs` has been set with a value greater than 0.
            (None, Some(runs)) => self.optimizer = Some(runs > 0),
            _ => {}
        }
    }

    /// Returns the normalized [EvmVersion] for the current solc version, or the configured one.
    pub fn get_normalized_evm_version(&self) -> EvmVersion {
        if let Some(version) = self.solc_version()
            && let Some(evm_version) = self.evm_version.normalize_version_solc(&version)
        {
            return evm_version;
        }
        self.evm_version
    }

    /// Returns a sanitized version of the Config where are paths are set correctly and potential
    /// duplicates are resolved
    ///
    /// See [`Self::canonic`]
    #[must_use]
    pub fn sanitized(self) -> Self {
        let mut config = self.canonic();

        config.sanitize_remappings();

        config.libs.sort_unstable();
        config.libs.dedup();

        config
    }

    /// Cleans up any duplicate `Remapping` and sorts them
    ///
    /// On windows this will convert any `\` in the remapping path into a `/`
    #[allow(clippy::missing_const_for_fn)]
    pub fn sanitize_remappings(&mut self) {
        #[cfg(target_os = "windows")]
        {
            // force `/` in remappings on windows
            use path_slash::PathBufExt;
            self.remappings.iter_mut().for_each(|r| {
                r.path.path = r.path.path.to_slash_lossy().into_owned().into();
            });
        }
    }

    /// Returns the directory in which dependencies should be installed
    ///
    /// Returns the first dir from `libs` that is not `node_modules` or `lib` if `libs` is empty
    pub fn install_lib_dir(&self) -> &Path {
        self.libs
            .iter()
            .find(|p| !p.ends_with("node_modules"))
            .map(|p| p.as_path())
            .unwrap_or_else(|| Path::new("lib"))
    }

    /// Serves as the entrypoint for obtaining the project.
    ///
    /// Returns the `Project` configured with all `solc` and path related values.
    ///
    /// *Note*: this also _cleans_ [`Project::cleanup`] the workspace if `force` is set to true.
    ///
    /// # Example
    ///
    /// ```
    /// use foundry_config::Config;
    /// let config = Config::load_with_root(".")?.sanitized();
    /// let project = config.project()?;
    /// # Ok::<_, eyre::Error>(())
    /// ```
    pub fn project(&self) -> Result<Project<MultiCompiler>, SolcError> {
        self.create_project(self.cache, false)
    }

    /// Same as [`Self::project()`] but sets configures the project to not emit artifacts and ignore
    /// cache.
    pub fn ephemeral_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
        self.create_project(false, true)
    }

    /// A cached, in-memory project that does not request any artifacts.
    ///
    /// Use this when you just want the source graph or the Solar compiler context.
    pub fn solar_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
        let ui_testing = std::env::var_os("FOUNDRY_LINT_UI_TESTING").is_some();
        let mut project = self.create_project(self.cache && !ui_testing, false)?;
        project.update_output_selection(|selection| {
            // We have to request something to populate `contracts` in the output and thus
            // artifacts.
            *selection = OutputSelection::common_output_selection(["abi".into()]);
        });
        Ok(project)
    }

    /// Builds mapping with additional settings profiles.
    fn additional_settings(
        &self,
        base: &MultiCompilerSettings,
    ) -> BTreeMap<String, MultiCompilerSettings> {
        let mut map = BTreeMap::new();

        for profile in &self.additional_compiler_profiles {
            let mut settings = base.clone();
            profile.apply(&mut settings);
            map.insert(profile.name.clone(), settings);
        }

        map
    }

    /// Resolves globs and builds a mapping from individual source files to their restrictions
    #[expect(clippy::disallowed_macros)]
    fn restrictions(
        &self,
        paths: &ProjectPathsConfig,
    ) -> Result<BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>>, SolcError>
    {
        let mut map: BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>> =
            BTreeMap::new();
        if self.compilation_restrictions.is_empty() {
            return Ok(BTreeMap::new());
        }

        let graph = Graph::<MultiCompilerParser>::resolve(paths)?;
        let (sources, _) = graph.into_sources();

        for res in &self.compilation_restrictions {
            for source in sources.keys().filter(|path| {
                if res.paths.is_match(path) {
                    true
                } else if let Ok(path) = path.strip_prefix(&paths.root) {
                    res.paths.is_match(path)
                } else {
                    false
                }
            }) {
                let res: RestrictionsWithVersion<_> =
                    res.clone().try_into().map_err(SolcError::msg)?;
                if map.contains_key(source) {
                    let value = map.remove(source.as_path()).unwrap();
                    if let Some(merged) = value.clone().merge(res) {
                        map.insert(source.clone(), merged);
                    } else {
                        // `sh_warn!` is a circular dependency, preventing us from using it here.
                        eprintln!(
                            "{}",
                            yansi::Paint::yellow(&format!(
                                "Failed to merge compilation restrictions for {}",
                                source.display()
                            ))
                        );
                        map.insert(source.clone(), value);
                    }
                } else {
                    map.insert(source.clone(), res);
                }
            }
        }

        Ok(map)
    }

    /// Creates a [`Project`] with the given `cached` and `no_artifacts` flags.
    ///
    /// Prefer using [`Self::project`] or [`Self::ephemeral_project`] instead.
    pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
        let settings = self.compiler_settings()?;
        let paths = self.project_paths();

        // Strip "./" prefix for consistent path matching
        let parse_path = |path: &PathBuf| path.strip_prefix("./").unwrap_or(path).to_path_buf();

        let mut builder = Project::builder()
            .artifacts(self.configured_artifacts_handler())
            .additional_settings(self.additional_settings(&settings))
            .restrictions(self.restrictions(&paths)?)
            .settings(settings)
            .paths(paths)
            .ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
            .ignore_error_codes_from(self.ignored_error_codes_from.iter().map(|(path, codes)| {
                (parse_path(path), codes.iter().copied().map(Into::into).collect())
            }))
            .ignore_paths(self.ignored_file_paths.iter().map(parse_path).collect())
            .set_compiler_severity_filter(if self.deny.warnings() {
                Severity::Warning
            } else {
                Severity::Error
            })
            .set_offline(self.offline)
            .set_cached(cached)
            .set_build_info(!no_artifacts && self.build_info)
            .set_no_artifacts(no_artifacts);

        if !self.skip.is_empty() {
            let filter = SkipBuildFilters::new(self.skip.clone(), self.root.clone());
            builder = builder.sparse_output(filter);
        }

        let project = builder.build(self.compiler()?)?;

        // `ProjectBuilder` slashes paths on Windows. Re-encode a contextual remapping's trailing
        // directory boundary with the native separator so a later `Remapping::to_string` does not
        // discard it while converting the context back to slash-separated solc syntax.
        #[cfg(windows)]
        let mut project = project;
        #[cfg(windows)]
        for remapping in &mut project.paths.remappings {
            if let Some(context) = &mut remapping.context
                && context.ends_with('/')
            {
                context.pop();
                context.push(std::path::MAIN_SEPARATOR);
            }
        }

        if self.force {
            // Warnings are intentionally dropped here because `sh_warn!` is a circular
            // dependency. Callers that need warnings should call `cleanup()` directly.
            let _ = self.cleanup(&project);
        }

        Ok(project)
    }

    /// Disables optimizations and enables viaIR with minimum optimization if `ir_minimum` is true.
    pub fn disable_optimizations(&self, project: &mut Project, ir_minimum: bool) {
        if ir_minimum {
            // Enable viaIR with minimum optimization: https://github.com/ethereum/solidity/issues/12533#issuecomment-1013073350
            // And also in new releases of Solidity: https://github.com/ethereum/solidity/issues/13972#issuecomment-1628632202
            project.settings.solc.settings = std::mem::take(&mut project.settings.solc.settings)
                .with_via_ir_minimum_optimization();

            // Sanitize settings for solc 0.8.4 if version cannot be detected: https://github.com/foundry-rs/foundry/issues/9322
            // But keep the EVM version: https://github.com/ethereum/solidity/issues/15775
            let evm_version = project.settings.solc.evm_version;
            let version = self.solc_version().unwrap_or_else(|| Version::new(0, 8, 4));
            project.settings.solc.settings.sanitize(&version, SolcLanguage::Solidity);
            project.settings.solc.evm_version = evm_version;
        } else {
            project.settings.solc.optimizer.disable();
            project.settings.solc.optimizer.runs = None;
            project.settings.solc.optimizer.details = None;
            project.settings.solc.via_ir = None;
        }
    }

    /// Cleans the project.
    ///
    /// Returns a list of warning messages for any non-fatal cleanup failures. Cleanup is
    /// best-effort: all steps are attempted even if some fail.
    pub fn cleanup<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>>(
        &self,
        project: &Project<C, T>,
    ) -> Result<Vec<String>, SolcError> {
        let mut warnings = Vec::new();

        if let Err(err) = project.cleanup() {
            warnings.push(format!("failed to clean project artifacts: {err}"));
        }

        // Remove last test run failures file.
        if let Err(err) = fs::remove_file(&self.test_failures_file)
            && err.kind() != io::ErrorKind::NotFound
        {
            warnings.push(format!(
                "failed to remove test failures file {}: {err}",
                self.test_failures_file.display()
            ));
        }

        // Remove mutation test cache directory
        let _ = fs::remove_dir_all(project.root().join(&self.mutation_dir));

        // Remove fuzz and invariant cache directories.
        let mut remove_test_dir = |test_dir: &Option<PathBuf>| {
            if let Some(test_dir) = test_dir {
                let path = project.root().join(test_dir);
                if let Err(err) = fs::remove_dir_all(&path)
                    && err.kind() != io::ErrorKind::NotFound
                {
                    warnings.push(format!(
                        "failed to remove test cache directory {}: {err}",
                        path.display()
                    ));
                }
            }
        };
        remove_test_dir(&self.fuzz.failure_persist_dir);
        remove_test_dir(&self.fuzz.corpus.corpus_dir);
        remove_test_dir(&self.fuzz.corpus.frontier_dir);
        remove_test_dir(&self.invariant.corpus.corpus_dir);
        remove_test_dir(&self.invariant.failure_persist_dir);

        Ok(warnings)
    }

    /// Ensures that the configured version is installed if explicitly set
    ///
    /// If `solc` is [`SolcReq::Version`] then this will download and install the solc version if
    /// it's missing, unless the `offline` flag is enabled, in which case an error is thrown.
    ///
    /// If `solc` is [`SolcReq::Local`] then this will ensure that the path exists.
    fn ensure_solc(&self) -> Result<Option<Solc>, SolcError> {
        if let Some(solc) = &self.solc {
            let solc = match solc {
                SolcReq::Version(version) => {
                    if let Some(solc) = Solc::find_svm_installed_version(version)? {
                        solc
                    } else {
                        if self.offline {
                            return Err(SolcError::msg(format!(
                                "can't install missing solc {version} in offline mode"
                            )));
                        }
                        Solc::blocking_install(version)?
                    }
                }
                SolcReq::Local(solc) => {
                    if !solc.is_file() {
                        return Err(SolcError::msg(format!(
                            "`solc` {} does not exist",
                            solc.display()
                        )));
                    }
                    Solc::new(solc)?
                }
            };
            return Ok(Some(solc));
        }

        Ok(None)
    }

    /// Returns the Spec derived from the configured [EvmVersion]
    pub fn evm_spec_id<SPEC: FromEvmVersion>(&self) -> SPEC {
        self.hardfork.map(Into::into).unwrap_or_else(|| evm_spec_id(self.evm_version))
    }

    /// Returns whether the compiler version should be auto-detected
    ///
    /// Returns `false` if `solc_version` is explicitly set, otherwise returns the value of
    /// `auto_detect_solc`
    pub const fn is_auto_detect(&self) -> bool {
        if self.solc.is_some() {
            return false;
        }
        self.auto_detect_solc
    }

    /// Whether caching should be enabled for the given chain id
    pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
        !self.no_storage_caching
            && self.rpc_storage_caching.enable_for_chain_id(chain_id.into())
            && self.rpc_storage_caching.enable_for_endpoint(endpoint)
    }

    /// Returns the `ProjectPathsConfig` sub set of the config.
    ///
    /// **NOTE**: this uses the paths as they are and does __not__ modify them, see
    /// `[Self::sanitized]`
    ///
    /// # Example
    ///
    /// ```
    /// use foundry_compilers::solc::Solc;
    /// use foundry_config::Config;
    /// let config = Config::load_with_root(".")?.sanitized();
    /// let paths = config.project_paths::<Solc>();
    /// # Ok::<_, eyre::Error>(())
    /// ```
    pub fn project_paths<L>(&self) -> ProjectPathsConfig<L> {
        let mut builder = ProjectPathsConfig::builder()
            .cache(self.cache_path.join(SOLIDITY_FILES_CACHE_FILENAME))
            .sources(&self.src)
            .tests(&self.test)
            .scripts(&self.script)
            .artifacts(&self.out)
            .libs(self.libs.iter())
            .remappings(self.get_all_remappings())
            .allowed_path(&self.root)
            .allowed_paths(&self.libs)
            .allowed_paths(&self.allow_paths)
            .include_paths(&self.include_paths);

        if let Some(build_info_path) = &self.build_info_path {
            builder = builder.build_infos(build_info_path);
        }

        builder.build_with_root(&self.root)
    }

    /// Returns configuration for a compiler to use when setting up a [Project].
    pub fn solc_compiler(&self) -> Result<SolcCompiler, SolcError> {
        if let Some(solc) = self.ensure_solc()? {
            Ok(SolcCompiler::Specific(solc))
        } else {
            Ok(SolcCompiler::AutoDetect)
        }
    }

    /// Returns the solc version, if any.
    pub fn solc_version(&self) -> Option<Version> {
        self.solc.as_ref().and_then(|solc| solc.try_version().ok())
    }

    /// Returns configured [Vyper] compiler.
    pub fn vyper_compiler(&self) -> Result<Option<Vyper>, SolcError> {
        // Only instantiate Vyper if there are any Vyper files in the project.
        if !self.project_paths::<VyperLanguage>().has_input_files() {
            return Ok(None);
        }
        let vyper = if let Some(path) = &self.vyper.path {
            Some(Vyper::new(path)?)
        } else {
            Vyper::new("vyper").ok()
        };
        Ok(vyper)
    }

    /// Returns configuration for a compiler to use when setting up a [Project].
    pub fn compiler(&self) -> Result<MultiCompiler, SolcError> {
        Ok(MultiCompiler { solc: Some(self.solc_compiler()?), vyper: self.vyper_compiler()? })
    }

    /// Returns configured [MultiCompilerSettings].
    pub fn compiler_settings(&self) -> Result<MultiCompilerSettings, SolcError> {
        Ok(MultiCompilerSettings { solc: self.solc_settings()?, vyper: self.vyper_settings()? })
    }

    /// Returns all configured remappings.
    pub fn get_all_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
        self.remappings.iter().map(|m| m.clone().into())
    }

    /// Returns the configured rpc jwt secret
    ///
    /// Returns:
    ///    - The jwt secret, if configured
    ///
    /// # Example
    ///
    /// ```
    /// use foundry_config::Config;
    /// # fn t() {
    /// let config = Config::with_root("./");
    /// let rpc_jwt = config.get_rpc_jwt_secret().unwrap().unwrap();
    /// # }
    /// ```
    pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<'_, str>>, UnresolvedEnvVarError> {
        Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
    }

    /// Returns the configured rpc url
    ///
    /// Returns:
    ///    - the matching, resolved url of  `rpc_endpoints` if `eth_rpc_url` is an alias
    ///    - the `eth_rpc_url` as-is if it isn't an alias
    ///
    /// # Example
    ///
    /// ```
    /// use foundry_config::Config;
    /// # fn t() {
    /// let config = Config::with_root("./");
    /// let rpc_url = config.get_rpc_url().unwrap().unwrap();
    /// # }
    /// ```
    pub fn get_rpc_url(&self) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
        let maybe_alias = self.eth_rpc_url.as_deref()?;
        if let Some(alias) = self.get_rpc_url_with_alias(maybe_alias) {
            Some(alias)
        } else {
            Some(Ok(Cow::Borrowed(self.eth_rpc_url.as_deref()?)))
        }
    }

    /// Resolves the given alias to a matching rpc url
    ///
    /// # Returns
    ///
    /// In order of resolution:
    ///
    /// - the matching, resolved url of `rpc_endpoints` if `maybe_alias` is an alias
    /// - a mesc resolved url if `maybe_alias` is a known alias in mesc
    /// - `None` otherwise
    ///
    /// # Note on mesc
    ///
    /// The endpoint is queried for in mesc under the `foundry` profile, allowing users to customize
    /// endpoints for Foundry specifically.
    ///
    /// # Example
    ///
    /// ```
    /// use foundry_config::Config;
    /// # fn t() {
    /// let config = Config::with_root("./");
    /// let rpc_url = config.get_rpc_url_with_alias("mainnet").unwrap().unwrap();
    /// # }
    /// ```
    pub fn get_rpc_url_with_alias(
        &self,
        maybe_alias: &str,
    ) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
        let mut endpoints = self.rpc_endpoints.clone().resolved();
        if let Some(endpoint) = endpoints.remove(maybe_alias) {
            return Some(endpoint.url().map(Cow::Owned));
        }

        if let Some(mesc_url) = self.get_rpc_url_from_mesc(maybe_alias) {
            return Some(Ok(Cow::Owned(mesc_url)));
        }

        if let Some(builtin) = crate::endpoints::builtin_rpc_url(maybe_alias) {
            return Some(Ok(Cow::Borrowed(builtin)));
        }

        None
    }

    /// Attempts to resolve the URL for the given alias from [`mesc`](https://github.com/paradigmxyz/mesc)
    pub fn get_rpc_url_from_mesc(&self, maybe_alias: &str) -> Option<String> {
        // Note: mesc requires a MESC_PATH in the env, which the user can configure and is expected
        // to be part of the shell profile, default is ~/mesc.json
        let mesc_config = mesc::load::load_config_data()
            .inspect_err(|err| debug!(%err, "failed to load mesc config"))
            .ok()?;

        if let Ok(Some(endpoint)) =
            mesc::query::get_endpoint_by_query(&mesc_config, maybe_alias, Some("foundry"))
        {
            return Some(endpoint.url);
        }

        if maybe_alias.chars().all(|c| c.is_numeric()) {
            // try to lookup the mesc network by chain id if alias is numeric
            // This only succeeds if the chain id has a default:
            // "network_defaults": {
            //    "50104": "sophon_50104"
            // }
            if let Ok(Some(endpoint)) =
                mesc::query::get_endpoint_by_network(&mesc_config, maybe_alias, Some("foundry"))
            {
                return Some(endpoint.url);
            }
        }

        None
    }

    /// Returns the configured rpc, or the fallback url
    ///
    /// # Example
    ///
    /// ```
    /// use foundry_config::Config;
    /// # fn t() {
    /// let config = Config::with_root("./");
    /// let rpc_url = config.get_rpc_url_or("http://localhost:8545").unwrap();
    /// # }
    /// ```
    pub fn get_rpc_url_or<'a>(
        &'a self,
        fallback: impl Into<Cow<'a, str>>,
    ) -> Result<Cow<'a, str>, UnresolvedEnvVarError> {
        if let Some(url) = self.get_rpc_url() { url } else { Ok(fallback.into()) }
    }

    /// Returns the configured rpc or `"http://localhost:8545"` if no `eth_rpc_url` is set
    ///
    /// # Example
    ///
    /// ```
    /// use foundry_config::Config;
    /// # fn t() {
    /// let config = Config::with_root("./");
    /// let rpc_url = config.get_rpc_url_or_localhost_http().unwrap();
    /// # }
    /// ```
    pub fn get_rpc_url_or_localhost_http(&self) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
        self.get_rpc_url_or("http://localhost:8545")
    }

    /// Returns the `EtherscanConfig` to use, if any
    ///
    /// Returns
    ///  - the matching `ResolvedEtherscanConfig` of the `etherscan` table if `etherscan_api_key` is
    ///    an alias
    ///  - the matching `ResolvedEtherscanConfig` of the `etherscan` table if a `chain` is
    ///    configured. an alias
    ///  - the Mainnet  `ResolvedEtherscanConfig` if `etherscan_api_key` is set, `None` otherwise
    ///
    /// # Example
    ///
    /// ```
    /// use foundry_config::Config;
    /// # fn t() {
    /// let config = Config::with_root("./");
    /// let etherscan_config = config.get_etherscan_config().unwrap().unwrap();
    /// let client = etherscan_config.into_client().unwrap();
    /// # }
    /// ```
    pub fn get_etherscan_config(
        &self,
    ) -> Option<Result<ResolvedEtherscanConfig, EtherscanConfigError>> {
        self.get_etherscan_config_with_chain(None).transpose()
    }

    /// Same as [`Self::get_etherscan_config()`] but optionally updates the config with the given
    /// `chain`, and `etherscan_api_key`
    ///
    /// If not matching alias was found, then this will try to find the first entry in the table
    /// with a matching chain id. If an etherscan_api_key is already set it will take precedence
    /// over the chain's entry in the table.
    pub fn get_etherscan_config_with_chain(
        &self,
        chain: Option<Chain>,
    ) -> Result<Option<ResolvedEtherscanConfig>, EtherscanConfigError> {
        if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref())
            && self.etherscan.contains_key(maybe_alias)
        {
            return self.etherscan.clone().resolved().remove(maybe_alias).transpose();
        }

        // try to find by comparing chain IDs after resolving
        if let Some(res) = chain
            .or(self.chain)
            .and_then(|chain| self.etherscan.clone().resolved().find_chain(chain))
        {
            match (res, self.etherscan_api_key.as_ref()) {
                (Ok(mut config), Some(key)) => {
                    // we update the key, because if an etherscan_api_key is set, it should take
                    // precedence over the entry, since this is usually set via env var or CLI args.
                    config.key.clone_from(key);
                    return Ok(Some(config));
                }
                (Ok(config), None) => return Ok(Some(config)),
                (Err(err), None) => return Err(err),
                (Err(_), Some(_)) => {
                    // use the etherscan key as fallback
                }
            }
        }

        // etherscan fallback via API key
        if let Some(key) = self.etherscan_api_key.as_ref() {
            return Ok(ResolvedEtherscanConfig::create(
                key,
                chain.or(self.chain).unwrap_or_default(),
            ));
        }
        Ok(None)
    }

    /// Helper function to just get the API key
    ///
    /// Optionally updates the config with the given `chain`.
    ///
    /// See also [Self::get_etherscan_config_with_chain]
    #[expect(clippy::disallowed_macros)]
    pub fn get_etherscan_api_key(&self, chain: Option<Chain>) -> Option<String> {
        self.get_etherscan_config_with_chain(chain)
            .map_err(|e| {
                // `sh_warn!` is a circular dependency, preventing us from using it here.
                eprintln!(
                    "{}: failed getting etherscan config: {e}",
                    yansi::Paint::yellow("Warning"),
                );
            })
            .ok()
            .flatten()
            .map(|c| c.key)
    }

    /// Returns the remapping for the project's _src_ directory
    ///
    /// **Note:** this will add an additional `<src>/=<src path>` remapping here so imports that
    /// look like `import {Foo} from "src/Foo.sol";` are properly resolved.
    ///
    /// This is due the fact that `solc`'s VFS resolves [direct imports](https://docs.soliditylang.org/en/develop/path-resolution.html#direct-imports) that start with the source directory's name.
    pub fn get_source_dir_remapping(&self) -> Option<Remapping> {
        get_dir_remapping(&self.src)
    }

    /// Returns the remapping for the project's _test_ directory, but only if it exists
    pub fn get_test_dir_remapping(&self) -> Option<Remapping> {
        if self.root.join(&self.test).exists() { get_dir_remapping(&self.test) } else { None }
    }

    /// Returns the remapping for the project's _script_ directory, but only if it exists
    pub fn get_script_dir_remapping(&self) -> Option<Remapping> {
        if self.root.join(&self.script).exists() { get_dir_remapping(&self.script) } else { None }
    }

    /// Returns the `Optimizer` based on the configured settings
    ///
    /// Note: optimizer details can be set independently of `enabled`
    /// See also: <https://github.com/foundry-rs/foundry/issues/7689>
    /// and  <https://github.com/ethereum/solidity/blob/bbb7f58be026fdc51b0b4694a6f25c22a1425586/docs/using-the-compiler.rst?plain=1#L293-L294>
    pub fn optimizer(&self) -> Optimizer {
        Optimizer {
            enabled: self.optimizer,
            runs: self.optimizer_runs,
            // we always set the details because `enabled` is effectively a specific details profile
            // that can still be modified
            details: self.optimizer_details.clone(),
        }
    }

    /// returns the [`foundry_compilers::ConfigurableArtifacts`] for this config, that includes the
    /// `extra_output` fields
    pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts {
        let mut extra_output = self.extra_output.clone();

        // Sourcify verification requires solc metadata output. Since, it doesn't
        // affect the UX & performance of the compiler, output the metadata files
        // by default.
        // For more info see: <https://github.com/foundry-rs/foundry/issues/2795>
        // Metadata is not emitted as separate file because this breaks typechain support: <https://github.com/foundry-rs/foundry/issues/2969>
        if !extra_output.contains(&ContractOutputSelection::Metadata) {
            extra_output.push(ContractOutputSelection::Metadata);
        }

        ConfigurableArtifacts::new(extra_output, self.extra_output_files.iter().copied())
    }

    /// Parses all libraries in the form of
    /// `<file>:<lib>:<addr>`
    pub fn parsed_libraries(&self) -> Result<Libraries, SolcError> {
        Libraries::parse(&self.libraries)
    }

    /// Returns all libraries with applied remappings. Same as `self.solc_settings()?.libraries`.
    pub fn libraries_with_remappings(&self) -> Result<Libraries, SolcError> {
        let paths: ProjectPathsConfig = self.project_paths();
        Ok(self.parsed_libraries()?.apply(|libs| paths.apply_lib_remappings(libs)))
    }

    /// Returns the configured `solc` `Settings` that includes:
    /// - all libraries
    /// - the optimizer (including details, if configured)
    /// - evm version
    pub fn solc_settings(&self) -> Result<SolcSettings, SolcError> {
        // By default if no targets are specifically selected the model checker uses all targets.
        // This might be too much here, so only enable assertion checks.
        // If users wish to enable all options they need to do so explicitly.
        let mut model_checker = self.model_checker.clone();
        if let Some(model_checker_settings) = &mut model_checker
            && model_checker_settings.targets.is_none()
        {
            model_checker_settings.targets = Some(vec![ModelCheckerTarget::Assert]);
        }

        let mut settings = Settings {
            libraries: self.libraries_with_remappings()?,
            optimizer: self.optimizer(),
            evm_version: Some(self.evm_version),
            metadata: Some(SettingsMetadata {
                use_literal_content: Some(self.use_literal_content),
                bytecode_hash: Some(self.bytecode_hash),
                cbor_metadata: Some(self.cbor_metadata),
            }),
            debug: self.revert_strings.map(|revert_strings| DebuggingSettings {
                revert_strings: Some(revert_strings),
                // Not used.
                debug_info: Vec::new(),
            }),
            model_checker,
            // via_ssa_cfg implies via_ir only when via_ir is omitted, so we need to set both flags
            // to true if via_ssa_cfg is enabled.
            via_ir: Some(self.via_ir || self.via_ssa_cfg),
            via_ssa_cfg: Some(self.via_ssa_cfg),
            experimental: Some(self.experimental),
            // Not used.
            stop_after: None,
            // Set in project paths.
            remappings: Vec::new(),
            // Set with `with_extra_output` below.
            output_selection: Default::default(),
        }
        .with_extra_output(self.configured_artifacts_handler().output_selection());

        // We're keeping AST in `--build-info` for backwards compatibility with HardHat.
        if self.ast || self.build_info {
            settings = settings.with_ast();
        }

        let cli_settings =
            CliSettings { extra_args: self.extra_args.clone(), ..Default::default() };

        Ok(SolcSettings { settings, cli_settings })
    }

    /// Returns the configured [VyperSettings] that includes:
    /// - evm version
    pub fn vyper_settings(&self) -> Result<VyperSettings, SolcError> {
        // Let `opt_level` override `optimize` so child profiles can switch away from an
        // inherited optimization mode without sending both mutually exclusive settings to Vyper.
        let optimize = if self.vyper.opt_level.is_some() { None } else { self.vyper.optimize };

        Ok(VyperSettings {
            evm_version: Some(self.evm_version),
            optimize,
            opt_level: self.vyper.opt_level,
            bytecode_metadata: None,
            // TODO: We don't yet have a way to deserialize other outputs correctly, so request only
            // those for now. It should be enough to run tests and deploy contracts.
            output_selection: OutputSelection::common_output_selection([
                "abi".to_string(),
                "evm.bytecode".to_string(),
                "evm.deployedBytecode".to_string(),
            ]),
            search_paths: None,
            experimental_codegen: self.vyper.experimental_codegen,
            debug: self.vyper.debug,
            enable_decimals: self.vyper.enable_decimals,
            venom_experimental: self.vyper.venom_experimental,
            venom: self.vyper.venom.clone(),
        })
    }

    /// Returns the default figment
    ///
    /// The default figment reads from the following sources, in ascending
    /// priority order:
    ///
    ///   1. [`Config::default()`] (see [defaults](#defaults))
    ///   2. `foundry.toml` _or_ filename in `FOUNDRY_CONFIG` environment variable
    ///   3. `FOUNDRY_` prefixed environment variables
    ///
    /// The profile selected is the value set in the `FOUNDRY_PROFILE`
    /// environment variable. If it is not set, it defaults to `default`.
    ///
    /// # Example
    ///
    /// ```rust
    /// use foundry_config::Config;
    /// use serde::Deserialize;
    ///
    /// let my_config = Config::figment().extract::<Config>();
    /// ```
    pub fn figment() -> Figment {
        Self::default().into()
    }

    /// Returns the default figment enhanced with additional context extracted from the provided
    /// root, like remappings and directories.
    ///
    /// # Example
    ///
    /// ```rust
    /// use foundry_config::Config;
    /// use serde::Deserialize;
    ///
    /// let my_config = Config::figment_with_root(".").extract::<Config>();
    /// ```
    pub fn figment_with_root(root: impl AsRef<Path>) -> Figment {
        Self::with_root(root.as_ref()).into()
    }

    #[doc(hidden)]
    #[track_caller]
    pub fn figment_with_root_opt(root: Option<&Path>) -> Figment {
        let root = match root {
            Some(root) => root,
            None => &find_project_root(None).expect("could not determine project root"),
        };
        Self::figment_with_root(root)
    }

    /// Creates a new Config that adds additional context extracted from the provided root.
    ///
    /// # Example
    ///
    /// ```rust
    /// use foundry_config::Config;
    /// let my_config = Config::with_root(".");
    /// ```
    pub fn with_root(root: impl AsRef<Path>) -> Self {
        Self::_with_root(root.as_ref())
    }

    fn _with_root(root: &Path) -> Self {
        // autodetect paths
        let paths = ProjectPathsConfig::builder().build_with_root::<()>(root);
        let artifacts: PathBuf = paths.artifacts.file_name().unwrap().into();
        Self {
            root: paths.root,
            src: paths.sources.file_name().unwrap().into(),
            out: artifacts.clone(),
            libs: paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect(),
            fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]),
            ..Self::default()
        }
    }

    /// Returns the default config but with hardhat paths
    pub fn hardhat() -> Self {
        Self {
            src: "contracts".into(),
            out: "artifacts".into(),
            libs: vec!["node_modules".into()],
            ..Self::default()
        }
    }

    /// Extracts a basic subset of the config, used for initialisations.
    ///
    /// # Example
    ///
    /// ```rust
    /// use foundry_config::Config;
    /// let my_config = Config::with_root(".").into_basic();
    /// ```
    pub fn into_basic(self) -> BasicConfig {
        BasicConfig {
            profile: self.profile,
            src: self.src,
            out: self.out,
            libs: self.libs,
            remappings: self.remappings,
            network: self.networks.active_network_name().map(String::from),
        }
    }

    /// Updates the `foundry.toml` file for the given `root` based on the provided closure.
    ///
    /// **Note:** the closure will only be invoked if the `foundry.toml` file exists, See
    /// [Self::get_config_path()] and if the closure returns `true`.
    pub fn update_at<F>(root: &Path, f: F) -> eyre::Result<()>
    where
        F: FnOnce(&Self, &mut toml_edit::DocumentMut) -> bool,
    {
        let config = Self::load_with_root(root)?.sanitized();
        config.update(|doc| f(&config, doc))
    }

    /// Updates the `foundry.toml` file this `Config` ias based on with the provided closure.
    ///
    /// **Note:** the closure will only be invoked if the `foundry.toml` file exists, See
    /// [Self::get_config_path()] and if the closure returns `true`
    pub fn update<F>(&self, f: F) -> eyre::Result<()>
    where
        F: FnOnce(&mut toml_edit::DocumentMut) -> bool,
    {
        let file_path = self.get_config_path();
        if !file_path.exists() {
            return Ok(());
        }
        let contents = fs::read_to_string(&file_path)?;
        let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
        if f(&mut doc) {
            fs::write(file_path, doc.to_string())?;
        }
        Ok(())
    }

    /// Sets the `libs` entry inside a `foundry.toml` file but only if it exists
    ///
    /// # Errors
    ///
    /// An error if the `foundry.toml` could not be parsed.
    pub fn update_libs(&self) -> eyre::Result<()> {
        self.update(|doc| {
            let profile = self.profile.as_str().as_str();
            let root = &self.root;
            let libs: toml_edit::Value = self
                .libs
                .iter()
                .map(|path| {
                    let path =
                        if let Ok(relative) = path.strip_prefix(root) { relative } else { path };
                    toml_edit::Value::from(&*path.to_string_lossy())
                })
                .collect();
            let libs = toml_edit::value(libs);
            doc[Self::PROFILE_SECTION][profile]["libs"] = libs;
            true
        })
    }

    /// Serialize the config type as a String of TOML.
    ///
    /// This serializes to a table with the name of the profile
    ///
    /// ```toml
    /// [profile.default]
    /// src = "src"
    /// out = "out"
    /// libs = ["lib"]
    /// # ...
    /// ```
    pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
        // serializing to value first to prevent `ValueAfterTable` errors
        let mut value = toml::Value::try_from(self)?;
        // Config map always gets serialized as a table
        let value_table = value.as_table_mut().unwrap();
        // remove standalone sections from inner table
        let standalone_sections = Self::STANDALONE_SECTIONS
            .iter()
            .filter_map(|section| {
                let section = section.to_string();
                value_table.remove(&section).map(|value| (section, value))
            })
            .collect::<Vec<_>>();
        // wrap inner table in [profile.<profile>]
        let mut wrapping_table = [(
            Self::PROFILE_SECTION.into(),
            toml::Value::Table([(self.profile.to_string(), value)].into_iter().collect()),
        )]
        .into_iter()
        .collect::<toml::map::Map<_, _>>();
        // insert standalone sections
        for (section, value) in standalone_sections {
            wrapping_table.insert(section, value);
        }
        // stringify
        toml::to_string_pretty(&toml::Value::Table(wrapping_table))
    }

    /// Returns the path to the `foundry.toml` of this `Config`.
    pub fn get_config_path(&self) -> PathBuf {
        self.root.join(Self::FILE_NAME)
    }

    /// Returns the selected profile.
    ///
    /// If the `FOUNDRY_PROFILE` env variable is not set, this returns the `DEFAULT_PROFILE`.
    pub fn selected_profile() -> Profile {
        // Can't cache in tests because the env var can change.
        #[cfg(test)]
        {
            Self::force_selected_profile()
        }
        #[cfg(not(test))]
        {
            SELECTED_PROFILE.get_or_init(Self::force_selected_profile).clone()
        }
    }

    /// Sets the selected profile before it is initialized.
    ///
    /// Returns the previously selected profile if it was already initialized to a different value.
    pub fn try_set_selected_profile(profile: Profile) -> Result<(), Profile> {
        #[cfg(test)]
        {
            let _ = profile;
            Ok(())
        }
        #[cfg(not(test))]
        {
            match SELECTED_PROFILE.set(profile) {
                Ok(()) => Ok(()),
                Err(profile) if SELECTED_PROFILE.get() == Some(&profile) => Ok(()),
                Err(_) => Err(SELECTED_PROFILE.get().expect("profile initialized").clone()),
            }
        }
    }

    fn force_selected_profile() -> Profile {
        Profile::from_env_or("FOUNDRY_PROFILE", Self::DEFAULT_PROFILE)
    }

    /// Returns the path to foundry's global TOML file: `~/.foundry/foundry.toml`.
    pub fn foundry_dir_toml() -> Option<PathBuf> {
        Self::foundry_dir().map(|p| p.join(Self::FILE_NAME))
    }

    /// Returns the path to foundry's config dir: `~/.foundry/`.
    pub fn foundry_dir() -> Option<PathBuf> {
        dirs::home_dir().map(|p| p.join(Self::FOUNDRY_DIR_NAME))
    }

    /// Returns the path to foundry's cache dir: `~/.foundry/cache`.
    pub fn foundry_cache_dir() -> Option<PathBuf> {
        Self::foundry_dir().map(|p| p.join("cache"))
    }

    /// Returns the path to foundry rpc cache dir: `~/.foundry/cache/rpc`.
    pub fn foundry_rpc_cache_dir() -> Option<PathBuf> {
        Some(Self::foundry_cache_dir()?.join("rpc"))
    }
    /// Returns the path to foundry chain's cache dir: `~/.foundry/cache/rpc/<chain>`
    pub fn foundry_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
        Some(Self::foundry_rpc_cache_dir()?.join(chain_id.into().to_string()))
    }

    /// Returns the path to foundry's etherscan cache dir: `~/.foundry/cache/etherscan`.
    pub fn foundry_etherscan_cache_dir() -> Option<PathBuf> {
        Some(Self::foundry_cache_dir()?.join("etherscan"))
    }

    /// Returns the path to foundry's keystores dir: `~/.foundry/keystores`.
    pub fn foundry_keystores_dir() -> Option<PathBuf> {
        Some(Self::foundry_dir()?.join("keystores"))
    }

    /// Returns the path to foundry's etherscan cache dir for `chain_id`:
    /// `~/.foundry/cache/etherscan/<chain>`
    pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
        Some(Self::foundry_etherscan_cache_dir()?.join(chain_id.into().to_string()))
    }

    /// Returns the path to the cache dir of the `block` on the `chain`:
    /// `~/.foundry/cache/rpc/<chain>/<block>`
    pub fn foundry_block_cache_dir(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
        Some(Self::foundry_chain_cache_dir(chain_id)?.join(format!("{block}")))
    }

    /// Returns the path to the cache file of the `block` on the `chain`:
    /// `~/.foundry/cache/rpc/<chain>/<block>/storage.json`
    pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
        Some(Self::foundry_block_cache_dir(chain_id, block)?.join("storage.json"))
    }

    /// Returns the path to `foundry`'s data directory inside the user's data directory.
    ///
    /// | Platform | Value                                         | Example                                          |
    /// | -------  | --------------------------------------------- | ------------------------------------------------ |
    /// | Linux    | `$XDG_CONFIG_HOME` or `$HOME`/.config/foundry | /home/alice/.config/foundry                      |
    /// | macOS    | `$HOME`/Library/Application Support/foundry   | /Users/Alice/Library/Application Support/foundry |
    /// | Windows  | `{FOLDERID_RoamingAppData}/foundry`           | C:\Users\Alice\AppData\Roaming/foundry           |
    pub fn data_dir() -> eyre::Result<PathBuf> {
        let path = dirs::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
        std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
        Ok(path)
    }

    /// Returns the path to the `foundry.toml` file, the file is searched for in
    /// the current working directory and all parent directories until the root,
    /// and the first hit is used.
    ///
    /// If this search comes up empty, then it checks if a global `foundry.toml` exists at
    /// `~/.foundry/foundry.toml`, see [`Self::foundry_dir_toml`].
    pub fn find_config_file() -> Option<PathBuf> {
        fn find(path: &Path) -> Option<PathBuf> {
            if path.is_absolute() {
                return match path.is_file() {
                    true => Some(path.to_path_buf()),
                    false => None,
                };
            }
            let cwd = std::env::current_dir().ok()?;
            let mut cwd = cwd.as_path();
            loop {
                let file_path = cwd.join(path);
                if file_path.is_file() {
                    return Some(file_path);
                }
                cwd = cwd.parent()?;
            }
        }
        find(Env::var_or("FOUNDRY_CONFIG", Self::FILE_NAME).as_ref())
            .or_else(|| Self::foundry_dir_toml().filter(|p| p.exists()))
    }

    /// Clears the foundry cache.
    ///
    /// Returns warnings for any non-fatal deletion failures.
    pub fn clean_foundry_cache() -> eyre::Result<Vec<String>> {
        if let Some(cache_dir) = Self::foundry_cache_dir() {
            let path = cache_dir.as_path();
            if let Err(err) = fs::remove_dir_all(path)
                && err.kind() != io::ErrorKind::NotFound
            {
                return Ok(vec![format!(
                    "failed to remove foundry cache at {}: {err}",
                    path.display()
                )]);
            }
        } else {
            eyre::bail!("failed to get foundry_cache_dir");
        }

        Ok(vec![])
    }

    /// Clears the foundry cache for `chain`.
    ///
    /// Returns warnings for any non-fatal deletion failures.
    pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<Vec<String>> {
        if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
            let path = cache_dir.as_path();
            if let Err(err) = fs::remove_dir_all(path)
                && err.kind() != io::ErrorKind::NotFound
            {
                return Ok(vec![format!(
                    "failed to remove foundry cache for chain {chain} at {}: {err}",
                    path.display()
                )]);
            }
        } else {
            eyre::bail!("failed to get foundry_chain_cache_dir");
        }

        Ok(vec![])
    }

    /// Clears the foundry cache for `chain` and `block`.
    ///
    /// Returns warnings for any non-fatal deletion failures.
    pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<Vec<String>> {
        if let Some(cache_dir) = Self::foundry_block_cache_dir(chain, block) {
            let path = cache_dir.as_path();
            if let Err(err) = fs::remove_dir_all(path)
                && err.kind() != io::ErrorKind::NotFound
            {
                return Ok(vec![format!(
                    "failed to remove foundry cache for chain {chain} block {block} at {}: {err}",
                    path.display()
                )]);
            }
        } else {
            eyre::bail!("failed to get foundry_block_cache_dir");
        }

        Ok(vec![])
    }

    /// Clears the foundry etherscan cache.
    ///
    /// Returns warnings for any non-fatal deletion failures.
    pub fn clean_foundry_etherscan_cache() -> eyre::Result<Vec<String>> {
        if let Some(cache_dir) = Self::foundry_etherscan_cache_dir() {
            let path = cache_dir.as_path();
            if let Err(err) = fs::remove_dir_all(path)
                && err.kind() != io::ErrorKind::NotFound
            {
                return Ok(vec![format!(
                    "failed to remove foundry etherscan cache at {}: {err}",
                    path.display()
                )]);
            }
        } else {
            eyre::bail!("failed to get foundry_etherscan_cache_dir");
        }

        Ok(vec![])
    }

    /// Clears the foundry etherscan cache for `chain`.
    ///
    /// Returns warnings for any non-fatal deletion failures.
    pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<Vec<String>> {
        if let Some(cache_dir) = Self::foundry_etherscan_chain_cache_dir(chain) {
            let path = cache_dir.as_path();
            if let Err(err) = fs::remove_dir_all(path)
                && err.kind() != io::ErrorKind::NotFound
            {
                return Ok(vec![format!(
                    "failed to remove foundry etherscan cache for chain {chain} at {}: {err}",
                    path.display()
                )]);
            }
        } else {
            eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain);
        }

        Ok(vec![])
    }

    /// List the data in the foundry cache.
    pub fn list_foundry_cache() -> eyre::Result<Cache> {
        if let Some(cache_dir) = Self::foundry_rpc_cache_dir() {
            let mut cache = Cache { chains: vec![] };
            let Some(entries) = Self::ignore_not_found(cache_dir.read_dir())? else {
                return Ok(cache);
            };
            for entry in entries {
                let Some(entry) = Self::ignore_not_found(entry)? else {
                    continue;
                };
                let Some(metadata) = Self::ignore_not_found(fs::metadata(entry.path()))? else {
                    continue;
                };
                if !metadata.is_dir() {
                    continue;
                }
                if let Ok(chain) = Chain::from_str(&entry.file_name().to_string_lossy()) {
                    cache.chains.push(Self::list_foundry_chain_cache(chain)?);
                }
            }
            Ok(cache)
        } else {
            eyre::bail!("failed to get foundry_cache_dir");
        }
    }

    /// List the cached data for `chain`.
    pub fn list_foundry_chain_cache(chain: Chain) -> eyre::Result<ChainCache> {
        let block_explorer_data_size = match Self::foundry_etherscan_chain_cache_dir(chain) {
            Some(cache_dir) => Self::get_cached_block_explorer_data(&cache_dir)?,
            None => {
                warn!("failed to access foundry_etherscan_chain_cache_dir");
                0
            }
        };

        if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
            let blocks = Self::get_cached_blocks(&cache_dir)?;
            Ok(ChainCache {
                name: chain.to_string(),
                blocks,
                block_explorer: block_explorer_data_size,
            })
        } else {
            eyre::bail!("failed to get foundry_chain_cache_dir");
        }
    }

    /// The path provided to this function should point to a cached chain folder.
    fn get_cached_blocks(chain_path: &Path) -> eyre::Result<Vec<(String, u64)>> {
        let mut blocks = vec![];
        let Some(entries) = Self::ignore_not_found(chain_path.read_dir())? else {
            return Ok(blocks);
        };
        for block in entries {
            let Some(block) = Self::ignore_not_found(block)? else {
                continue;
            };
            if let Some(block) = Self::get_cached_block(block)? {
                blocks.push(block);
            }
        }
        Ok(blocks)
    }

    fn get_cached_block(block: fs::DirEntry) -> eyre::Result<Option<(String, u64)>> {
        let file_name = block.file_name();
        let Some(metadata) = Self::ignore_not_found(fs::symlink_metadata(block.path()))? else {
            return Ok(None);
        };
        let size = if metadata.is_dir() {
            let Some(cache_files) = Self::ignore_not_found(block.path().read_dir())? else {
                return Ok(None);
            };
            let mut size = 0;
            for cache_file in cache_files {
                let Some(cache_file) = Self::ignore_not_found(cache_file)? else {
                    continue;
                };
                size += Self::get_cache_file_size(cache_file)?.unwrap_or_default();
            }
            if size == 0 {
                return Ok(None);
            }
            size
        } else if metadata.is_file() && file_name.to_string_lossy().chars().all(char::is_numeric) {
            metadata.len()
        } else {
            return Ok(None);
        };
        Ok(Some((file_name.to_string_lossy().into_owned(), size)))
    }

    fn get_cache_file_size(cache_file: fs::DirEntry) -> eyre::Result<Option<u64>> {
        let Some(metadata) = Self::ignore_not_found(fs::symlink_metadata(cache_file.path()))?
        else {
            return Ok(None);
        };
        let cache_file_name = cache_file.file_name();
        let cache_file_name = cache_file_name.to_string_lossy();
        if !metadata.is_file()
            || (cache_file_name != "storage.json"
                && !cache_file_name
                    .strip_prefix("storage-")
                    .and_then(|name| name.strip_suffix(".json"))
                    .is_some_and(|hash| {
                        hash.len() == 64 && hash.bytes().all(|b| b.is_ascii_hexdigit())
                    }))
        {
            return Ok(None);
        }
        Ok(Some(metadata.len()))
    }

    /// The path provided to this function should point to the etherscan cache for a chain.
    fn get_cached_block_explorer_data(chain_path: &Path) -> eyre::Result<u64> {
        let Some(entries) = Self::ignore_not_found(fs::read_dir(chain_path))? else {
            return Ok(0);
        };
        Self::get_cached_dir_size(entries)
    }

    fn get_cached_dir_size(entries: fs::ReadDir) -> eyre::Result<u64> {
        let mut size = 0;
        for entry in entries {
            let Some(entry) = Self::ignore_not_found(entry)? else {
                continue;
            };
            size += Self::get_cached_entry_size(entry)?.unwrap_or_default();
        }
        Ok(size)
    }

    fn get_cached_entry_size(entry: fs::DirEntry) -> eyre::Result<Option<u64>> {
        let Some(metadata) = Self::ignore_not_found(fs::symlink_metadata(entry.path()))? else {
            return Ok(None);
        };
        if metadata.is_dir() {
            let Some(entries) = Self::ignore_not_found(fs::read_dir(entry.path()))? else {
                return Ok(None);
            };
            Ok(Some(Self::get_cached_dir_size(entries)?))
        } else {
            Ok(Some(metadata.len()))
        }
    }

    /// Treats cache entries removed during enumeration as absent.
    fn ignore_not_found<T>(result: io::Result<T>) -> eyre::Result<Option<T>> {
        match result {
            Ok(value) => Ok(Some(value)),
            Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
            Err(err) => Err(err.into()),
        }
    }

    fn merge_toml_provider(
        mut figment: Figment,
        toml_provider: impl Provider,
        profile: Profile,
    ) -> Figment {
        figment = figment.select(profile.clone());

        // add warnings
        figment = {
            let warnings = WarningsProvider::for_figment(&toml_provider, &figment);
            figment.merge(warnings)
        };

        // use [profile.<profile>] as [<profile>]
        let mut profiles = vec![Self::DEFAULT_PROFILE];
        if profile != Self::DEFAULT_PROFILE {
            profiles.push(profile.clone());
        }
        // Apply key fixes before selecting profiles, while standalone sections and profile names
        // are still distinguishable.
        let provider = ForcedSnakeCaseData(toml_provider).strict_select(profiles);
        let provider = &BackwardsCompatTomlProvider(provider);

        // merge the default profile as a base
        if profile != Self::DEFAULT_PROFILE {
            figment = figment.merge(provider.rename(Self::DEFAULT_PROFILE, profile.clone()));
        }
        // merge special keys into config
        for standalone_key in Self::STANDALONE_SECTIONS {
            if let Some((_, fallback)) =
                STANDALONE_FALLBACK_SECTIONS.iter().find(|(key, _)| standalone_key == key)
            {
                figment = figment.merge(
                    provider
                        .fallback(standalone_key, fallback)
                        .wrap(profile.clone(), standalone_key),
                );
            } else {
                figment = figment.merge(provider.wrap(profile.clone(), standalone_key));
            }
        }
        // merge the profile
        figment = figment.merge(provider);
        figment
    }

    /// Check if any defaults need to be normalized.
    ///
    /// This normalizes the default `evm_version` if a `solc` was provided in the config.
    ///
    /// See also <https://github.com/foundry-rs/foundry/issues/7014>
    fn normalize_defaults(&self, mut figment: Figment) -> Figment {
        if figment.contains("evm_version") {
            return figment;
        }

        // Normalize `evm_version` based on the provided solc version.
        if let Ok(solc) = figment.extract_inner::<SolcReq>("solc")
            && let Some(version) = solc
                .try_version()
                .ok()
                .and_then(|version| self.evm_version.normalize_version_solc(&version))
        {
            figment = figment.merge(("evm_version", version));
        }

        // Normalize `deny` based on the provided `deny_warnings` value.
        if figment.extract_inner::<bool>("deny_warnings").unwrap_or(false)
            && figment.extract_inner("deny") == Ok(DenyLevel::Never)
        {
            figment = figment.merge(("deny", DenyLevel::Warnings));
        }

        figment
    }
}

impl From<Config> for Figment {
    fn from(c: Config) -> Self {
        (&c).into()
    }
}
impl From<&Config> for Figment {
    fn from(c: &Config) -> Self {
        c.to_figment(FigmentProviders::All)
    }
}

/// Determines what providers should be used when loading the [`Figment`] for a [`Config`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FigmentProviders {
    /// Include all providers.
    #[default]
    All,
    /// Only include necessary providers that are useful for cast commands.
    ///
    /// This will exclude more expensive providers such as remappings.
    Cast,
    /// Only include necessary providers that are useful for anvil.
    ///
    /// This will exclude more expensive providers such as remappings.
    Anvil,
    /// Don't include any providers.
    None,
}

impl FigmentProviders {
    /// Returns true if all providers should be included.
    pub const fn is_all(&self) -> bool {
        matches!(self, Self::All)
    }

    /// Returns true if this is the cast preset.
    pub const fn is_cast(&self) -> bool {
        matches!(self, Self::Cast)
    }

    /// Returns true if this is the anvil preset.
    pub const fn is_anvil(&self) -> bool {
        matches!(self, Self::Anvil)
    }

    /// Returns true if no providers should be included.
    pub const fn is_none(&self) -> bool {
        matches!(self, Self::None)
    }
}

fn figment_value_is_configured(figment: &Figment, key: &str) -> bool {
    figment.find_metadata(key).is_some_and(|metadata| metadata.name.as_ref() != "Foundry Config")
}

/// Wrapper type for [`regex::Regex`] that implements [`PartialEq`] and [`serde`] traits.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(transparent)]
pub struct RegexWrapper {
    #[serde(with = "serde_regex")]
    inner: regex::Regex,
}

impl std::ops::Deref for RegexWrapper {
    type Target = regex::Regex;

    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl std::cmp::PartialEq for RegexWrapper {
    fn eq(&self, other: &Self) -> bool {
        self.as_str() == other.as_str()
    }
}

impl Eq for RegexWrapper {}

impl From<RegexWrapper> for regex::Regex {
    fn from(wrapper: RegexWrapper) -> Self {
        wrapper.inner
    }
}

impl From<regex::Regex> for RegexWrapper {
    fn from(re: Regex) -> Self {
        Self { inner: re }
    }
}

mod serde_regex {
    use regex::Regex;
    use serde::{Deserialize, Deserializer, Serializer};

    pub(crate) fn serialize<S>(value: &Regex, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(value.as_str())
    }

    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Regex, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        Regex::new(&s).map_err(serde::de::Error::custom)
    }
}

/// Ser/de `globset::Glob` explicitly to handle `Option<Glob>` properly
pub(crate) mod from_opt_glob {
    use serde::{Deserialize, Deserializer, Serializer};

    pub fn serialize<S>(value: &Option<globset::Glob>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match value {
            Some(glob) => serializer.serialize_str(glob.glob()),
            None => serializer.serialize_none(),
        }
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<globset::Glob>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s: Option<String> = Option::deserialize(deserializer)?;
        if let Some(s) = s {
            return Ok(Some(globset::Glob::new(&s).map_err(serde::de::Error::custom)?));
        }
        Ok(None)
    }
}

/// Parses a config profile
///
/// All `Profile` date is ignored by serde, however the `Config::to_string_pretty` includes it and
/// returns a toml table like
///
/// ```toml
/// #[profile.default]
/// src = "..."
/// ```
/// This ignores the `#[profile.default]` part in the toml
pub fn parse_with_profile<T: serde::de::DeserializeOwned>(
    s: &str,
) -> Result<Option<(Profile, T)>, Error> {
    let figment = Config::merge_toml_provider(
        Figment::new(),
        Toml::string(s).nested(),
        Config::DEFAULT_PROFILE,
    );
    if figment.profiles().any(|p| p == Config::DEFAULT_PROFILE) {
        Ok(Some((Config::DEFAULT_PROFILE, figment.select(Config::DEFAULT_PROFILE).extract()?)))
    } else {
        Ok(None)
    }
}

impl Provider for Config {
    fn metadata(&self) -> Metadata {
        Metadata::named("Foundry Config")
    }

    #[track_caller]
    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
        let mut data = Serialized::defaults(self).data()?;
        let root = Value::serialize(self.root.clone())?;
        let labels = Value::serialize(&self.labels)?;
        if let Some(entry) = data.get_mut(&Self::DEFAULT_PROFILE) {
            entry.insert("root".to_string(), root.clone());
            entry.insert("labels".to_string(), labels.clone());
            normalize_legacy_labels_in_profile(entry);
        }
        if let Some(entry) = data.get_mut(&self.profile) {
            entry.insert("root".to_string(), root);
            entry.insert("labels".to_string(), labels);
            normalize_legacy_labels_in_profile(entry);
        }
        Ok(data)
    }

    fn profile(&self) -> Option<Profile> {
        Some(self.profile.clone())
    }
}

impl Default for Config {
    fn default() -> Self {
        Self {
            profile: Self::DEFAULT_PROFILE,
            profiles: vec![Self::DEFAULT_PROFILE],
            fs_permissions: FsPermissions::new([PathPermission::read("out")]),
            isolate: true,
            root: root_default(),
            extends: None,
            src: "src".into(),
            test: "test".into(),
            script: "script".into(),
            out: "out".into(),
            libs: vec!["lib".into()],
            cache: true,
            dynamic_test_linking: true,
            cache_path: "cache".into(),
            broadcast: "broadcast".into(),
            snapshots: "snapshots".into(),
            gas_snapshot_check: false,
            gas_snapshot_emit: true,
            allow_paths: vec![],
            include_paths: vec![],
            force: false,
            evm_version: EvmVersion::Osaka,
            hardfork: None,
            gas_reports: vec!["*".to_string()],
            gas_reports_ignore: vec![],
            gas_reports_include_tests: false,
            solc: None,
            vyper: Default::default(),
            auto_detect_solc: true,
            offline: false,
            optimizer: None,
            optimizer_runs: None,
            optimizer_details: None,
            model_checker: None,
            extra_output: Default::default(),
            extra_output_files: Default::default(),
            names: false,
            sizes: false,
            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: "cache/test-failures".into(),
            mutation_dir: "cache/mutation".into(),
            threads: None,
            show_progress: false,
            fuzz: FuzzConfig::new("cache/fuzz".into()),
            invariant: InvariantConfig::new("cache/invariant".into()),
            symbolic: SymbolicConfig::default(),
            coverage: CoverageConfig::default(),
            mutation: MutationConfig::default(),
            tracing: TracingConfig::default(),
            always_use_create_2_factory: false,
            eip1559_fee_estimate: Eip1559FeeEstimatePreset::default(),
            ffi: false,
            live_logs: false,
            allow_internal_expect_revert: false,
            prompt_timeout: 120,
            sender: Self::DEFAULT_SENDER,
            tx_origin: Self::DEFAULT_SENDER,
            initial_balance: U256::from((1u128 << 96) - 1),
            block_number: U256::from(1),
            fork_block_number: None,
            chain: None,
            gas_limit: (1u64 << 30).into(), // ~1B
            code_size_limit: None,
            gas_price: None,
            block_base_fee_per_gas: 0,
            block_coinbase: Address::ZERO,
            block_timestamp: U256::from(1),
            block_difficulty: 0,
            block_prevrandao: Default::default(),
            block_gas_limit: None,
            disable_block_gas_limit: false,
            enable_tx_gas_limit: false,
            memory_limit: 1 << 27, // 2**27 = 128MiB = 134_217_728 bytes
            eth_rpc_url: None,
            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,
            verbosity: 0,
            remappings: vec![],
            auto_detect_remappings: true,
            libraries: vec![],
            ignored_error_codes: vec![
                SolidityErrorCode::SpdxLicenseNotProvided,
                SolidityErrorCode::ContractExceeds24576Bytes,
                SolidityErrorCode::ContractInitCodeSizeExceeds49152Bytes,
                SolidityErrorCode::TransientStorageUsed,
                SolidityErrorCode::TransferDeprecated,
                SolidityErrorCode::NatspecMemorySafeAssemblyDeprecated,
            ],
            ignored_error_codes_from: vec![],
            ignored_file_paths: vec![],
            deny: DenyLevel::Never,
            deny_warnings: false,
            via_ir: false,
            via_ssa_cfg: false,
            experimental: false,
            ast: false,
            rpc_storage_caching: Default::default(),
            rpc_endpoints: Default::default(),
            etherscan: Default::default(),
            no_storage_caching: false,
            no_rpc_rate_limit: false,
            use_literal_content: false,
            bytecode_hash: BytecodeHash::Ipfs,
            cbor_metadata: true,
            revert_strings: None,
            sparse_mode: false,
            build_info: false,
            build_info_path: None,
            fmt: Default::default(),
            lint: Default::default(),
            doc: Default::default(),
            bind_json: Default::default(),
            labels: Default::default(),
            unchecked_cheatcode_artifacts: false,
            create2_library_salt: Self::DEFAULT_CREATE2_LIBRARY_SALT,
            create2_deployer: Self::DEFAULT_CREATE2_DEPLOYER,
            skip: vec![],
            dependencies: Default::default(),
            soldeer: Default::default(),
            assertions_revert: true,
            legacy_assertions: false,
            warnings: vec![],
            extra_args: vec![],
            networks: Default::default(),
            transaction_timeout: 120,
            additional_compiler_profiles: Default::default(),
            compilation_restrictions: Default::default(),
            script_execution_protection: true,
            _non_exhaustive: (),
        }
    }
}

/// Wrapper for the config's `gas_limit` value necessary because toml-rs can't handle larger number
/// because integers are stored signed: <https://github.com/alexcrichton/toml-rs/issues/256>
///
/// Due to this limitation this type will be serialized/deserialized as String if it's larger than
/// `i64`
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct GasLimit(#[serde(deserialize_with = "crate::deserialize_u64_or_max")] pub u64);

impl From<u64> for GasLimit {
    fn from(gas: u64) -> Self {
        Self(gas)
    }
}

impl From<GasLimit> for u64 {
    fn from(gas: GasLimit) -> Self {
        gas.0
    }
}

impl Serialize for GasLimit {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        if self.0 == u64::MAX {
            serializer.serialize_str("max")
        } else if self.0 > i64::MAX as u64 {
            serializer.serialize_str(&self.0.to_string())
        } else {
            serializer.serialize_u64(self.0)
        }
    }
}

/// Variants for selecting the [`Solc`] instance
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SolcReq {
    /// Requires a specific solc version, that's either already installed (via `svm`) or will be
    /// auto installed (via `svm`)
    Version(Version),
    /// Path to an existing local solc installation
    Local(PathBuf),
}

impl SolcReq {
    /// Tries to get the solc version from the `SolcReq`
    ///
    /// If the `SolcReq` is a `Version` it will return the version, if it's a path to a binary it
    /// will try to get the version from the binary.
    fn try_version(&self) -> Result<Version, SolcError> {
        match self {
            Self::Version(version) => Ok(version.clone()),
            Self::Local(path) => Solc::new(path).map(|solc| solc.version),
        }
    }
}

impl<T: AsRef<str>> From<T> for SolcReq {
    fn from(s: T) -> Self {
        let s = s.as_ref();
        if let Ok(v) = Version::from_str(s) { Self::Version(v) } else { Self::Local(s.into()) }
    }
}

/// A subset of the foundry `Config`
/// used to initialize a `foundry.toml` file
///
/// # Example
///
/// ```rust
/// use foundry_config::{BasicConfig, Config};
/// use serde::Deserialize;
///
/// let my_config = Config::figment().extract::<BasicConfig>();
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BasicConfig {
    /// the profile tag: `[profile.default]`
    #[serde(skip)]
    pub profile: Profile,
    /// path of the source contracts dir, like `src` or `contracts`
    pub src: PathBuf,
    /// path to where artifacts shut be written to
    pub out: PathBuf,
    /// all library folders to include, `lib`, `node_modules`
    pub libs: Vec<PathBuf>,
    /// `Remappings` to use for this repo
    #[serde(
        default,
        skip_serializing_if = "Vec::is_empty",
        serialize_with = "remappings_serde::serialize"
    )]
    pub remappings: Vec<RelativeRemapping>,
    /// The active non-Ethereum network (e.g. `"tempo"`).
    #[serde(skip)]
    pub network: Option<String>,
}

impl BasicConfig {
    /// Serialize the config as a String of TOML.
    ///
    /// This serializes to a table with the name of the profile
    pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
        let mut profile_body = toml::Value::try_from(self)?;
        if let Some(ref network) = self.network
            && let toml::Value::Table(ref mut table) = profile_body
        {
            table.insert("network".to_string(), toml::Value::String(network.clone()));
        }

        let mut profile_section = toml::value::Table::new();
        profile_section.insert(self.profile.to_string(), profile_body);

        let mut document = toml::value::Table::new();
        document.insert("profile".to_string(), toml::Value::Table(profile_section));

        if self.network.as_deref() == Some("tempo") {
            let mut endpoints = toml::value::Table::new();
            endpoints.insert(
                "tempo".to_string(),
                toml::Value::String(crate::endpoints::TEMPO_RPC_URL.to_string()),
            );
            endpoints.insert(
                "moderato".to_string(),
                toml::Value::String(crate::endpoints::MODERATO_RPC_URL.to_string()),
            );
            document.insert("rpc_endpoints".to_string(), toml::Value::Table(endpoints));
        }

        let body = toml::to_string_pretty(&toml::Value::Table(document))?;
        Ok(format!(
            "{body}\n# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n"
        ))
    }
}

mod remappings_serde {
    use foundry_compilers::artifacts::remappings::RelativeRemapping;
    #[cfg(windows)]
    use path_slash::PathExt as _;
    use serde::{Serialize, Serializer};
    #[cfg(windows)]
    use std::path::Path;

    #[cfg(windows)]
    fn slash_context(context: &str) -> String {
        let has_trailing_separator =
            context.as_bytes().last().is_some_and(|c| *c == b'/' || *c == b'\\');
        let mut context = Path::new(context).to_slash_lossy().into_owned();
        if has_trailing_separator && !context.ends_with('/') {
            context.push('/');
        }
        context
    }

    pub fn serialize<S>(remappings: &[RelativeRemapping], serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        remappings
            .iter()
            .map(|remapping| {
                let Some(context) = &remapping.context else { return remapping.to_string() };
                let mut remapping = remapping.clone();
                remapping.context = None;
                #[cfg(windows)]
                let context = slash_context(context);
                #[cfg(not(windows))]
                let context = context.as_str();
                format!("{context}:{remapping}")
            })
            .collect::<Vec<_>>()
            .serialize(serializer)
    }
}

pub(crate) mod from_str_lowercase {
    use serde::{Deserialize, Deserializer, Serializer};
    use std::str::FromStr;

    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
    where
        T: std::fmt::Display,
        S: Serializer,
    {
        serializer.collect_str(&value.to_string().to_lowercase())
    }

    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
    where
        D: Deserializer<'de>,
        T: FromStr,
        T::Err: std::fmt::Display,
    {
        String::deserialize(deserializer)?.to_lowercase().parse().map_err(serde::de::Error::custom)
    }
}

fn canonic(path: impl Into<PathBuf>) -> PathBuf {
    let path = path.into();
    foundry_compilers::utils::canonicalize(&path).unwrap_or(path)
}

fn root_default() -> PathBuf {
    ".".into()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        cache::{CachedChains, CachedEndpoints},
        endpoints::RpcEndpointType,
        etherscan::ResolvedEtherscanConfigs,
        fmt::IndentStyle,
    };
    use NamedChain::Moonbeam;
    use endpoints::{RpcAuth, RpcEndpointConfig};
    use figment::error::Kind::InvalidType;
    use foundry_compilers::artifacts::{
        ModelCheckerEngine, YulDetails,
        vyper::{VyperOptimizationLevel, VyperOptimizationMode, VyperVenomSettings},
    };
    use foundry_evm_hardforks::{TempoHardfork, latest_active_tempo_hardfork};
    use similar_asserts::assert_eq;
    use soldeer_core::remappings::RemappingsLocation;
    use std::{
        fs::File,
        io::{self, Write},
        num::NonZeroUsize,
    };
    use tempfile::tempdir;

    // Helper function to clear `__warnings` in config, since it will be populated during loading
    // from file, causing testing problem when comparing to those created from `default()`, etc.
    fn clear_warning(config: &mut Config) {
        config.warnings = vec![];
    }

    fn mark_serialized_invariant_provenance(config: &mut Config) {
        config.invariant.corpus_random_sequence_weight_configured = true;
        config.invariant.workers_configured = true;
    }

    #[test]
    fn default_sender() {
        assert_eq!(Config::DEFAULT_SENDER, address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38"));
    }

    #[test]
    fn test_caching() {
        let mut config = Config::default();
        let chain_id = NamedChain::Mainnet;
        let url = "https://eth-mainnet.alchemyapi";
        assert!(config.enable_caching(url, chain_id));

        config.no_storage_caching = true;
        assert!(!config.enable_caching(url, chain_id));

        config.no_storage_caching = false;
        assert!(!config.enable_caching(url, NamedChain::Dev));
    }

    #[test]
    fn test_install_dir() {
        figment::Jail::expect_with(|jail| {
            let config = Config::load().unwrap();
            assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
            jail.create_file(
                "foundry.toml",
                r"
                [profile.default]
                libs = ['node_modules', 'lib']
            ",
            )?;
            let config = Config::load().unwrap();
            assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));

            jail.create_file(
                "foundry.toml",
                r"
                [profile.default]
                libs = ['custom', 'node_modules', 'lib']
            ",
            )?;
            let config = Config::load().unwrap();
            assert_eq!(config.install_lib_dir(), PathBuf::from("custom"));

            Ok(())
        });
    }

    #[test]
    fn test_figment_is_default() {
        figment::Jail::expect_with(|_| {
            let mut default: Config = Config::figment().extract()?;
            let default2 = Config::default();
            default.profile = default2.profile.clone();
            default.profiles = default2.profiles.clone();
            assert_eq!(default, default2);
            Ok(())
        });
    }

    #[test]
    fn figment_profiles() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [foo.baz]
                libs = ['node_modules', 'lib']

                [profile.default]
                libs = ['node_modules', 'lib']

                [profile.ci]
                libs = ['node_modules', 'lib']

                [profile.local]
                libs = ['node_modules', 'lib']
            ",
            )?;

            let config = crate::Config::load().unwrap();
            let expected: &[figment::Profile] = &["ci".into(), "default".into(), "local".into()];
            assert_eq!(config.profiles, expected);

            Ok(())
        });
    }

    #[test]
    fn test_default_round_trip() {
        figment::Jail::expect_with(|_| {
            let original = Config::figment();
            let roundtrip = Figment::from(Config::from_provider(&original).unwrap());
            for figment in &[original, roundtrip] {
                let config = Config::from_provider(figment).unwrap();
                assert_eq!(config, Config::default().normalized_optimizer_settings());
            }
            Ok(())
        });
    }

    #[test]
    fn ffi_env_disallowed() {
        figment::Jail::expect_with(|jail| {
            jail.set_env("FOUNDRY_FFI", "true");
            jail.set_env("FFI", "true");
            jail.set_env("DAPP_FFI", "true");
            let config = Config::load().unwrap();
            assert!(!config.ffi);

            Ok(())
        });
    }

    #[test]
    fn test_profile_env() {
        figment::Jail::expect_with(|jail| {
            jail.set_env("FOUNDRY_PROFILE", "default");
            let figment = Config::figment();
            assert_eq!(figment.profile(), "default");

            jail.set_env("FOUNDRY_PROFILE", "hardhat");
            let figment: Figment = Config::hardhat().into();
            assert_eq!(figment.profile(), "hardhat");

            jail.create_file(
                "foundry.toml",
                r"
                [profile.default]
                libs = ['lib']
                [profile.local]
                libs = ['modules']
            ",
            )?;
            jail.set_env("FOUNDRY_PROFILE", "local");
            let config = Config::load().unwrap();
            assert_eq!(config.libs, vec![PathBuf::from("modules")]);

            Ok(())
        });
    }

    #[test]
    fn test_default_test_path() {
        figment::Jail::expect_with(|_| {
            let config = Config::default();
            let paths_config = config.project_paths::<Solc>();
            assert_eq!(paths_config.tests, PathBuf::from(r"test"));
            Ok(())
        });
    }

    #[test]
    fn test_default_libs() {
        figment::Jail::expect_with(|jail| {
            let config = Config::load().unwrap();
            assert_eq!(config.libs, vec![PathBuf::from("lib")]);

            fs::create_dir_all(jail.directory().join("node_modules")).unwrap();
            let config = Config::load().unwrap();
            assert_eq!(config.libs, vec![PathBuf::from("node_modules")]);

            fs::create_dir_all(jail.directory().join("lib")).unwrap();
            let config = Config::load().unwrap();
            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);

            Ok(())
        });
    }

    #[test]
    fn test_inheritance_from_default_test_path() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                test = "defaulttest"
                src  = "defaultsrc"
                libs = ['lib', 'node_modules']

                [profile.custom]
                src = "customsrc"
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.src, PathBuf::from("defaultsrc"));
            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);

            jail.set_env("FOUNDRY_PROFILE", "custom");
            let config = Config::load().unwrap();
            assert_eq!(config.src, PathBuf::from("customsrc"));
            assert_eq!(config.test, PathBuf::from("defaulttest"));
            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);

            Ok(())
        });
    }

    #[test]
    fn test_custom_test_path() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                test = "mytest"
            "#,
            )?;

            let config = Config::load().unwrap();
            let paths_config = config.project_paths::<Solc>();
            assert_eq!(paths_config.tests, PathBuf::from(r"mytest"));
            Ok(())
        });
    }

    #[test]
    fn test_remappings() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "some-source"
                out = "some-out"
                cache = true
            "#,
            )?;
            let config = Config::load().unwrap();
            assert!(config.remappings.is_empty());

            jail.create_file(
                "remappings.txt",
                r"
                file-ds-test/=lib/ds-test/
                file-other/=lib/other/
            ",
            )?;

            let config = Config::load().unwrap();
            assert_eq!(
                config.remappings,
                vec![
                    Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
                    Remapping::from_str("file-other/=lib/other/").unwrap().into(),
                ],
            );

            jail.set_env("DAPP_REMAPPINGS", "ds-test=lib/ds-test/\nother/=lib/other/");
            let config = Config::load().unwrap();

            assert_eq!(
                config.remappings,
                vec![
                    // From environment (should have precedence over remapping.txt)
                    Remapping::from_str("ds-test=lib/ds-test/").unwrap().into(),
                    Remapping::from_str("other/=lib/other/").unwrap().into(),
                    // From remapping.txt (should have less precedence than remapping.txt)
                    Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
                    Remapping::from_str("file-other/=lib/other/").unwrap().into(),
                ],
            );

            Ok(())
        });
    }

    #[test]
    fn test_remappings_override() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "some-source"
                out = "some-out"
                cache = true
            "#,
            )?;
            let config = Config::load().unwrap();
            assert!(config.remappings.is_empty());

            jail.create_file(
                "remappings.txt",
                r"
                ds-test/=lib/ds-test/
                other/=lib/other/
            ",
            )?;

            let config = Config::load().unwrap();
            assert_eq!(
                config.remappings,
                vec![
                    Remapping::from_str("ds-test/=lib/ds-test/").unwrap().into(),
                    Remapping::from_str("other/=lib/other/").unwrap().into(),
                ],
            );

            jail.set_env("DAPP_REMAPPINGS", "ds-test/=lib/ds-test/src/\nenv-lib/=lib/env-lib/");
            let config = Config::load().unwrap();

            // Remappings should now be:
            // - ds-test from environment (lib/ds-test/src/)
            // - other from remappings.txt (lib/other/)
            // - env-lib from environment (lib/env-lib/)
            assert_eq!(
                config.remappings,
                vec![
                    Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap().into(),
                    Remapping::from_str("env-lib/=lib/env-lib/").unwrap().into(),
                    Remapping::from_str("other/=lib/other/").unwrap().into(),
                ],
            );

            // contains additional remapping to the source dir
            assert_eq!(
                config.get_all_remappings().collect::<Vec<_>>(),
                vec![
                    Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap(),
                    Remapping::from_str("env-lib/=lib/env-lib/").unwrap(),
                    Remapping::from_str("other/=lib/other/").unwrap(),
                ],
            );

            Ok(())
        });
    }

    #[test]
    fn test_can_update_libs() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                libs = ["node_modules"]
            "#,
            )?;

            let mut config = Config::load().unwrap();
            config.libs.push("libs".into());
            config.update_libs().unwrap();

            let config = Config::load().unwrap();
            assert_eq!(config.libs, vec![PathBuf::from("node_modules"), PathBuf::from("libs"),]);
            Ok(())
        });
    }

    #[test]
    fn test_large_gas_limit() {
        figment::Jail::expect_with(|jail| {
            let gas = u64::MAX;
            jail.create_file(
                "foundry.toml",
                &format!(
                    r#"
                [profile.default]
                gas_limit = "{gas}"
            "#
                ),
            )?;

            let config = Config::load().unwrap();
            assert_eq!(
                config,
                Config {
                    gas_limit: gas.into(),
                    ..Config::default().normalized_optimizer_settings()
                }
            );

            Ok(())
        });
    }

    #[test]
    #[should_panic]
    fn test_toml_file_parse_failure() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                eth_rpc_url = "https://example.com/
            "#,
            )?;

            let _config = Config::load().unwrap();

            Ok(())
        });
    }

    #[test]
    #[should_panic]
    fn test_toml_file_non_existing_config_var_failure() {
        figment::Jail::expect_with(|jail| {
            jail.set_env("FOUNDRY_CONFIG", "this config does not exist");

            let _config = Config::load().unwrap();

            Ok(())
        });
    }

    #[test]
    fn test_resolve_etherscan_with_chain() {
        figment::Jail::expect_with(|jail| {
            let env_key = "__BSC_ETHERSCAN_API_KEY";
            let env_value = "env value";
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]

                [etherscan]
                bsc = { key = "${__BSC_ETHERSCAN_API_KEY}", url = "https://api.bscscan.com/api" }
            "#,
            )?;

            let config = Config::load().unwrap();
            assert!(
                config
                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
                    .is_err()
            );

            unsafe {
                std::env::set_var(env_key, env_value);
            }

            assert_eq!(
                config
                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
                    .unwrap()
                    .unwrap()
                    .key,
                env_value
            );

            let mut with_key = config;
            with_key.etherscan_api_key = Some("via etherscan_api_key".to_string());

            assert_eq!(
                with_key
                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
                    .unwrap()
                    .unwrap()
                    .key,
                "via etherscan_api_key"
            );

            unsafe {
                std::env::remove_var(env_key);
            }
            Ok(())
        });
    }

    #[test]
    fn test_resolve_etherscan() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]

                [etherscan]
                mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
                moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}" }
            "#,
            )?;

            let config = Config::load().unwrap();

            assert!(config.etherscan.clone().resolved().has_unresolved());

            jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");

            let configs = config.etherscan.resolved();
            assert!(!configs.has_unresolved());

            let mb_urls = Moonbeam.etherscan_urls().unwrap();
            let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
            assert_eq!(
                configs,
                ResolvedEtherscanConfigs::new([
                    (
                        "mainnet",
                        ResolvedEtherscanConfig {
                            api_url: mainnet_urls.0.to_string(),
                            chain: Some(NamedChain::Mainnet.into()),
                            browser_url: Some(mainnet_urls.1.to_string()),
                            key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
                        }
                    ),
                    (
                        "moonbeam",
                        ResolvedEtherscanConfig {
                            api_url: mb_urls.0.to_string(),
                            chain: Some(Moonbeam.into()),
                            browser_url: Some(mb_urls.1.to_string()),
                            key: "123456789".to_string(),
                        }
                    ),
                ])
            );

            Ok(())
        });
    }

    #[test]
    fn test_resolve_etherscan_with_versions() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]

                [etherscan]
                mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN", api_version = "v2" }
                moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}", api_version = "v1" }
            "#,
            )?;

            let config = Config::load().unwrap();

            assert!(config.etherscan.clone().resolved().has_unresolved());

            jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");

            let configs = config.etherscan.resolved();
            assert!(!configs.has_unresolved());

            let mb_urls = Moonbeam.etherscan_urls().unwrap();
            let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
            assert_eq!(
                configs,
                ResolvedEtherscanConfigs::new([
                    (
                        "mainnet",
                        ResolvedEtherscanConfig {
                            api_url: mainnet_urls.0.to_string(),
                            chain: Some(NamedChain::Mainnet.into()),
                            browser_url: Some(mainnet_urls.1.to_string()),
                            key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
                        }
                    ),
                    (
                        "moonbeam",
                        ResolvedEtherscanConfig {
                            api_url: mb_urls.0.to_string(),
                            chain: Some(Moonbeam.into()),
                            browser_url: Some(mb_urls.1.to_string()),
                            key: "123456789".to_string(),
                        }
                    ),
                ])
            );

            Ok(())
        });
    }

    #[test]
    fn test_resolve_etherscan_chain_id() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                chain_id = "sepolia"

                [etherscan]
                sepolia = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
            "#,
            )?;

            let config = Config::load().unwrap();
            let etherscan = config.get_etherscan_config().unwrap().unwrap();
            assert_eq!(etherscan.chain, Some(NamedChain::Sepolia.into()));
            assert_eq!(etherscan.key, "FX42Z3BBJJEWXWGYV2X1CIPRSCN");

            Ok(())
        });
    }

    // any invalid entry invalidates whole [etherscan] sections
    #[test]
    fn test_resolve_etherscan_with_invalid_name() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [etherscan]
                mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
                an_invalid_name = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
            "#,
            )?;

            let config = Config::load().unwrap();
            let etherscan_config = config.get_etherscan_config();
            assert!(etherscan_config.is_none());

            Ok(())
        });
    }

    #[test]
    fn test_resolve_rpc_url() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                [rpc_endpoints]
                optimism = "https://example.com/"
                mainnet = "${_CONFIG_MAINNET}"
            "#,
            )?;
            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");

            let mut config = Config::load().unwrap();
            assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());

            config.eth_rpc_url = Some("mainnet".to_string());
            assert_eq!(
                "https://eth-mainnet.alchemyapi.io/v2/123455",
                config.get_rpc_url_or_localhost_http().unwrap()
            );

            config.eth_rpc_url = Some("optimism".to_string());
            assert_eq!("https://example.com/", config.get_rpc_url_or_localhost_http().unwrap());

            Ok(())
        })
    }

    #[test]
    fn test_resolve_rpc_url_if_etherscan_set() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                etherscan_api_key = "dummy"
                [rpc_endpoints]
                optimism = "https://example.com/"
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());

            Ok(())
        })
    }

    #[test]
    fn test_resolve_rpc_url_alias() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                [rpc_endpoints]
                polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_RESOLVE_RPC_ALIAS}"
            "#,
            )?;
            let mut config = Config::load().unwrap();
            config.eth_rpc_url = Some("polygonAmoy".to_string());
            assert!(config.get_rpc_url().unwrap().is_err());

            jail.set_env("_RESOLVE_RPC_ALIAS", "123455");

            let mut config = Config::load().unwrap();
            config.eth_rpc_url = Some("polygonAmoy".to_string());
            assert_eq!(
                "https://polygon-amoy.g.alchemy.com/v2/123455",
                config.get_rpc_url().unwrap().unwrap()
            );

            Ok(())
        })
    }

    #[test]
    fn test_resolve_rpc_aliases() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
               [profile.default]
               [etherscan]
               arbitrum_alias = { key = "${TEST_RESOLVE_RPC_ALIAS_ARBISCAN}" }
               [rpc_endpoints]
               arbitrum_alias = "https://arb-mainnet.g.alchemy.com/v2/${TEST_RESOLVE_RPC_ALIAS_ARB_ONE}"
            "#,
            )?;

            jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARB_ONE", "123455");
            jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARBISCAN", "123455");

            let config = Config::load().unwrap();

            let config = config.get_etherscan_config_with_chain(Some(NamedChain::Arbitrum.into()));
            assert!(config.is_err());
            assert_eq!(
                config.unwrap_err().to_string(),
                "At least one of `url` or `chain` must be present for Etherscan config with unknown alias `arbitrum_alias`"
            );

            Ok(())
        });
    }

    #[test]
    fn test_resolve_rpc_config() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [rpc_endpoints]
                optimism = "https://example.com/"
                mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000 }
            "#,
            )?;
            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");

            let config = Config::load().unwrap();
            assert_eq!(
                RpcEndpoints::new([
                    (
                        "optimism",
                        RpcEndpointType::String(RpcEndpointUrl::Url(
                            "https://example.com/".to_string()
                        ))
                    ),
                    (
                        "mainnet",
                        RpcEndpointType::Config(RpcEndpoint {
                            endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
                            extra_endpoints: vec![],
                            config: RpcEndpointConfig {
                                retries: Some(3),
                                retry_backoff: Some(1000),
                                compute_units_per_second: Some(1000),
                            },
                            auth: None,
                        })
                    ),
                ]),
                config.rpc_endpoints
            );

            let resolved = config.rpc_endpoints.resolved();
            assert_eq!(
                RpcEndpoints::new([
                    (
                        "optimism",
                        RpcEndpointType::String(RpcEndpointUrl::Url(
                            "https://example.com/".to_string()
                        ))
                    ),
                    (
                        "mainnet",
                        RpcEndpointType::Config(RpcEndpoint {
                            endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
                            extra_endpoints: vec![],
                            config: RpcEndpointConfig {
                                retries: Some(3),
                                retry_backoff: Some(1000),
                                compute_units_per_second: Some(1000),
                            },
                            auth: None,
                        })
                    ),
                ])
                .resolved(),
                resolved
            );
            Ok(())
        })
    }

    #[test]
    fn test_resolve_auth() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                eth_rpc_url = "optimism"
                [rpc_endpoints]
                optimism = "https://example.com/"
                mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000, auth = "Bearer ${_CONFIG_AUTH}" }
            "#,
            )?;

            let config = Config::load().unwrap();

            jail.set_env("_CONFIG_AUTH", "123456");
            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");

            assert_eq!(
                RpcEndpoints::new([
                    (
                        "optimism",
                        RpcEndpointType::String(RpcEndpointUrl::Url(
                            "https://example.com/".to_string()
                        ))
                    ),
                    (
                        "mainnet",
                        RpcEndpointType::Config(RpcEndpoint {
                            endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
                            extra_endpoints: vec![],
                            config: RpcEndpointConfig {
                                retries: Some(3),
                                retry_backoff: Some(1000),
                                compute_units_per_second: Some(1000)
                            },
                            auth: Some(RpcAuth::Env("Bearer ${_CONFIG_AUTH}".to_string())),
                        })
                    ),
                ]),
                config.rpc_endpoints
            );
            let resolved = config.rpc_endpoints.resolved();
            assert_eq!(
                RpcEndpoints::new([
                    (
                        "optimism",
                        RpcEndpointType::String(RpcEndpointUrl::Url(
                            "https://example.com/".to_string()
                        ))
                    ),
                    (
                        "mainnet",
                        RpcEndpointType::Config(RpcEndpoint {
                            endpoint: RpcEndpointUrl::Url(
                                "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
                            ),
                            extra_endpoints: vec![],
                            config: RpcEndpointConfig {
                                retries: Some(3),
                                retry_backoff: Some(1000),
                                compute_units_per_second: Some(1000)
                            },
                            auth: Some(RpcAuth::Raw("Bearer 123456".to_string())),
                        })
                    ),
                ])
                .resolved(),
                resolved
            );

            Ok(())
        });
    }

    #[test]
    fn test_resolve_endpoints() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                eth_rpc_url = "optimism"
                [rpc_endpoints]
                optimism = "https://example.com/"
                mainnet = "${_CONFIG_MAINNET}"
                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}"
                mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}/${_CONFIG_API_KEY2}"
            "#,
            )?;

            let config = Config::load().unwrap();

            assert_eq!(config.get_rpc_url().unwrap().unwrap(), "https://example.com/");

            assert!(config.rpc_endpoints.clone().resolved().has_unresolved());

            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
            jail.set_env("_CONFIG_API_KEY1", "123456");
            jail.set_env("_CONFIG_API_KEY2", "98765");

            let endpoints = config.rpc_endpoints.resolved();

            assert!(!endpoints.has_unresolved());

            assert_eq!(
                endpoints,
                RpcEndpoints::new([
                    ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
                    (
                        "mainnet",
                        RpcEndpointUrl::Url(
                            "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
                        )
                    ),
                    (
                        "mainnet_2",
                        RpcEndpointUrl::Url(
                            "https://eth-mainnet.alchemyapi.io/v2/123456".to_string()
                        )
                    ),
                    (
                        "mainnet_3",
                        RpcEndpointUrl::Url(
                            "https://eth-mainnet.alchemyapi.io/v2/123456/98765".to_string()
                        )
                    ),
                ])
                .resolved()
            );

            Ok(())
        });
    }

    #[test]
    fn test_extract_etherscan_config() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                etherscan_api_key = "optimism"

                [etherscan]
                optimism = { key = "https://etherscan-optimism.com/" }
                amoy = { key = "https://etherscan-amoy.com/" }
            "#,
            )?;

            let mut config = Config::load().unwrap();

            let optimism = config.get_etherscan_api_key(Some(NamedChain::Optimism.into()));
            assert_eq!(optimism, Some("https://etherscan-optimism.com/".to_string()));

            config.etherscan_api_key = Some("amoy".to_string());

            let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
            assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));

            Ok(())
        });
    }

    #[test]
    fn test_extract_etherscan_config_by_chain() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]

                [etherscan]
                amoy = { key = "https://etherscan-amoy.com/", chain = 80002 }
            "#,
            )?;

            let config = Config::load().unwrap();

            let amoy = config
                .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
                .unwrap()
                .unwrap();
            assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());

            Ok(())
        });
    }

    #[test]
    fn test_extract_etherscan_config_by_chain_with_url() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]

                [etherscan]
                amoy = { key = "https://etherscan-amoy.com/", chain = 80002 , url =  "https://verifier-url.com/"}
            "#,
            )?;

            let config = Config::load().unwrap();

            let amoy = config
                .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
                .unwrap()
                .unwrap();
            assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
            assert_eq!(amoy.api_url, "https://verifier-url.com/".to_string());

            Ok(())
        });
    }

    #[test]
    fn test_extract_etherscan_config_by_chain_and_alias() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                eth_rpc_url = "amoy"

                [etherscan]
                amoy = { key = "https://etherscan-amoy.com/" }

                [rpc_endpoints]
                amoy = "https://polygon-amoy.g.alchemy.com/v2/amoy"
            "#,
            )?;

            let config = Config::load().unwrap();

            let amoy = config.get_etherscan_config_with_chain(None).unwrap().unwrap();
            assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());

            let amoy_rpc = config.get_rpc_url().unwrap().unwrap();
            assert_eq!(amoy_rpc, "https://polygon-amoy.g.alchemy.com/v2/amoy");
            Ok(())
        });
    }

    #[test]
    fn test_toml_file() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "some-source"
                out = "some-out"
                cache = true
                eth_rpc_url = "https://example.com/"
                verbosity = 3
                remappings = ["ds-test=lib/ds-test/"]
                via_ir = true
                rpc_storage_caching = { chains = [1, "optimism", 999999], endpoints = "all"}
                use_literal_content = false
                bytecode_hash = "ipfs"
                cbor_metadata = true
                revert_strings = "strip"
                allow_paths = ["allow", "paths"]
                build_info_path = "build-info"
                always_use_create_2_factory = true

                [rpc_endpoints]
                optimism = "https://example.com/"
                mainnet = "${RPC_MAINNET}"
                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
                mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(
                config,
                Config {
                    src: "some-source".into(),
                    out: "some-out".into(),
                    cache: true,
                    eth_rpc_url: Some("https://example.com/".to_string()),
                    remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
                    verbosity: 3,
                    via_ir: true,
                    rpc_storage_caching: StorageCachingConfig {
                        chains: CachedChains::Chains(vec![
                            Chain::mainnet(),
                            Chain::optimism_mainnet(),
                            Chain::from_id(999999)
                        ]),
                        endpoints: CachedEndpoints::All,
                    },
                    use_literal_content: false,
                    bytecode_hash: BytecodeHash::Ipfs,
                    cbor_metadata: true,
                    revert_strings: Some(RevertStrings::Strip),
                    allow_paths: vec![PathBuf::from("allow"), PathBuf::from("paths")],
                    rpc_endpoints: RpcEndpoints::new([
                        ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
                        ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
                        (
                            "mainnet_2",
                            RpcEndpointUrl::Env(
                                "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
                            )
                        ),
                        (
                            "mainnet_3",
                            RpcEndpointUrl::Env(
                                "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
                                    .to_string()
                            )
                        ),
                    ]),
                    build_info_path: Some("build-info".into()),
                    always_use_create_2_factory: true,
                    ..Config::default().normalized_optimizer_settings()
                }
            );

            Ok(())
        });
    }

    #[test]
    fn test_load_remappings() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [profile.default]
                remappings = ['nested/=lib/nested/']
            ",
            )?;

            let config = Config::load_with_root(jail.directory()).unwrap();
            assert_eq!(
                config.remappings,
                vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
            );

            Ok(())
        });
    }

    #[test]
    fn test_load_full_toml() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                auto_detect_solc = true
                block_base_fee_per_gas = 0
                block_coinbase = '0x0000000000000000000000000000000000000000'
                block_difficulty = 0
                block_prevrandao = '0x0000000000000000000000000000000000000000000000000000000000000000'
                block_number = 1
                block_timestamp = 1
                use_literal_content = false
                bytecode_hash = 'ipfs'
                cbor_metadata = true
                cache = true
                cache_path = 'cache'
                evm_version = 'london'
                extra_output = []
                extra_output_files = []
                always_use_create_2_factory = false
                ffi = false
                force = false
                gas_limit = 9223372036854775807
                gas_price = 0
                gas_reports = ['*']
                ignored_error_codes = [1878]
                ignored_warnings_from = ["something"]
                deny = "never"
                initial_balance = '0xffffffffffffffffffffffff'
                libraries = []
                libs = ['lib']
                memory_limit = 134217728
                names = false
                no_storage_caching = false
                no_rpc_rate_limit = false
                offline = false
                optimizer = true
                optimizer_runs = 200
                out = 'out'
                remappings = ['nested/=lib/nested/']
                sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
                sizes = false
                sparse_mode = false
                src = 'src'
                test = 'test'
                tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
                verbosity = 0
                via_ir = false

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

                [rpc_endpoints]
                optimism = "https://example.com/"
                mainnet = "${RPC_MAINNET}"
                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"

                [fuzz]
                runs = 256
                seed = '0x3e8'
                max_test_rejects = 65536

                [invariant]
                runs = 256
                depth = 500
                workers = 1
                fail_on_revert = false
                call_override = false
                shrink_run_limit = 5000
            "#,
            )?;

            let config = Config::load_with_root(jail.directory()).unwrap();

            assert_eq!(config.ignored_file_paths, vec![PathBuf::from("something")]);
            assert_eq!(config.fuzz.seed, Some(U256::from(1000)));
            assert_eq!(
                config.remappings,
                vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
            );

            assert_eq!(
                config.rpc_endpoints,
                RpcEndpoints::new([
                    ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
                    ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
                    (
                        "mainnet_2",
                        RpcEndpointUrl::Env(
                            "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
                        )
                    ),
                ]),
            );

            Ok(())
        });
    }

    #[test]
    fn test_solc_req() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                solc_version = "0.8.12"
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));

            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                solc = "0.8.12"
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));

            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                solc = "path/to/local/solc"
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.solc, Some(SolcReq::Local("path/to/local/solc".into())));

            jail.set_env("FOUNDRY_SOLC_VERSION", "0.6.6");
            let config = Config::load().unwrap();
            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 6, 6))));
            Ok(())
        });
    }

    // ensures the newer `solc` takes precedence over `solc_version`
    #[test]
    fn test_backwards_solc_version() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [default]
                solc = "0.8.12"
                solc_version = "0.8.20"
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));

            Ok(())
        });

        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [default]
                solc_version = "0.8.20"
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 20))));

            Ok(())
        });
    }

    #[test]
    fn test_toml_casing_file() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "some-source"
                out = "some-out"
                cache = true
                eth-rpc-url = "https://example.com/"
                evm-version = "berlin"
                auto-detect-solc = false
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(
                config,
                Config {
                    src: "some-source".into(),
                    out: "some-out".into(),
                    cache: true,
                    eth_rpc_url: Some("https://example.com/".to_string()),
                    auto_detect_solc: false,
                    evm_version: EvmVersion::Berlin,
                    ..Config::default().normalized_optimizer_settings()
                }
            );

            Ok(())
        });
    }

    #[test]
    fn test_output_selection() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                extra_output = ["metadata", "ir-optimized"]
                extra_output_files = ["metadata"]
            "#,
            )?;

            let config = Config::load().unwrap();

            assert_eq!(
                config.extra_output,
                vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized]
            );
            assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]);

            Ok(())
        });
    }

    #[test]
    fn test_precedence() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "mysrc"
                out = "myout"
                verbosity = 3
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(
                config,
                Config {
                    src: "mysrc".into(),
                    out: "myout".into(),
                    verbosity: 3,
                    ..Config::default().normalized_optimizer_settings()
                }
            );

            jail.set_env("FOUNDRY_SRC", r"other-src");
            let config = Config::load().unwrap();
            assert_eq!(
                config,
                Config {
                    src: "other-src".into(),
                    out: "myout".into(),
                    verbosity: 3,
                    ..Config::default().normalized_optimizer_settings()
                }
            );

            jail.set_env("FOUNDRY_PROFILE", "foo");
            let val: Result<String, _> = Config::figment().extract_inner("profile");
            assert!(val.is_err());

            Ok(())
        });
    }

    #[test]
    fn test_extract_basic() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "mysrc"
                out = "myout"
                verbosity = 3
                evm_version = 'berlin'

                [profile.other]
                src = "other-src"
            "#,
            )?;
            let loaded = Config::load().unwrap();
            assert_eq!(loaded.evm_version, EvmVersion::Berlin);
            let base = loaded.into_basic();
            let default = Config::default();
            assert_eq!(
                base,
                BasicConfig {
                    profile: Config::DEFAULT_PROFILE,
                    src: "mysrc".into(),
                    out: "myout".into(),
                    libs: default.libs.clone(),
                    remappings: default.remappings.clone(),
                    network: None,
                }
            );
            jail.set_env("FOUNDRY_PROFILE", r"other");
            let base = Config::figment().extract::<BasicConfig>().unwrap();
            assert_eq!(
                base,
                BasicConfig {
                    profile: Config::DEFAULT_PROFILE,
                    src: "other-src".into(),
                    out: "myout".into(),
                    libs: default.libs.clone(),
                    remappings: default.remappings,
                    network: None,
                }
            );
            Ok(())
        });
    }

    #[test]
    #[should_panic]
    fn test_parse_invalid_fuzz_weight() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [fuzz]
                dictionary_weight = 101
            ",
            )?;
            let _config = Config::load().unwrap();
            Ok(())
        });
    }

    #[test]
    fn test_fallback_provider() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [fuzz]
                runs = 1
                include_storage = false
                dictionary_weight = 99

                [invariant]
                runs = 420

                [profile.ci.fuzz]
                dictionary_weight = 5

                [profile.ci.invariant]
                runs = 400
            ",
            )?;

            let invariant_default = InvariantConfig::default();
            let config = Config::load().unwrap();

            assert_ne!(config.invariant.runs, config.fuzz.runs);
            assert_eq!(config.invariant.runs, 420);

            assert_ne!(
                config.fuzz.dictionary.include_storage,
                invariant_default.dictionary.include_storage
            );
            assert_eq!(
                config.invariant.dictionary.include_storage,
                config.fuzz.dictionary.include_storage
            );

            assert_ne!(
                config.fuzz.dictionary.dictionary_weight,
                invariant_default.dictionary.dictionary_weight
            );
            assert_eq!(
                config.invariant.dictionary.dictionary_weight,
                config.fuzz.dictionary.dictionary_weight
            );

            jail.set_env("FOUNDRY_PROFILE", "ci");
            let ci_config = Config::load().unwrap();
            assert_eq!(ci_config.fuzz.runs, 1);
            assert_eq!(ci_config.invariant.runs, 400);
            assert_eq!(ci_config.fuzz.dictionary.dictionary_weight, 5);
            assert_eq!(
                ci_config.invariant.dictionary.dictionary_weight,
                config.fuzz.dictionary.dictionary_weight
            );

            Ok(())
        })
    }

    #[test]
    fn test_standalone_profile_sections() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [fuzz]
                runs = 100

                [invariant]
                runs = 120

                [symbolic]
                enabled = true
                max_paths = 12
                storage_layout = "generic"

                [profile.ci.fuzz]
                runs = 420

                [profile.ci.invariant]
                runs = 500

                [profile.ci.symbolic]
                max_paths = 34
                dump_smt = true
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.fuzz.runs, 100);
            assert_eq!(config.invariant.runs, 120);
            assert!(config.symbolic.enabled);
            assert_eq!(config.symbolic.max_paths, 12);
            assert_eq!(config.symbolic.storage_layout, SymbolicStorageLayout::Generic);
            assert!(!config.symbolic.dump_smt);

            jail.set_env("FOUNDRY_PROFILE", "ci");
            let config = Config::load().unwrap();
            assert_eq!(config.fuzz.runs, 420);
            assert_eq!(config.invariant.runs, 500);
            assert!(config.symbolic.enabled);
            assert_eq!(config.symbolic.max_paths, 34);
            assert_eq!(config.symbolic.storage_layout, SymbolicStorageLayout::Generic);
            assert!(config.symbolic.dump_smt);

            Ok(())
        });
    }

    #[test]
    fn can_handle_deviating_dapp_aliases() {
        figment::Jail::expect_with(|jail| {
            let addr = Address::ZERO;
            jail.set_env("DAPP_TEST_NUMBER", 1337);
            jail.set_env("DAPP_TEST_ADDRESS", format!("{addr:?}"));
            jail.set_env("DAPP_TEST_FUZZ_RUNS", 420);
            jail.set_env("DAPP_TEST_DEPTH", 20);
            jail.set_env("DAPP_FORK_BLOCK", 100);
            jail.set_env("DAPP_BUILD_OPTIMIZE_RUNS", 999);
            jail.set_env("DAPP_BUILD_OPTIMIZE", 0);

            let config = Config::load().unwrap();

            assert_eq!(config.block_number, U256::from(1337));
            assert_eq!(config.sender, addr);
            assert_eq!(config.fuzz.runs, 420);
            assert_eq!(config.invariant.depth, 20);
            assert_eq!(config.fork_block_number, Some(100));
            assert_eq!(config.optimizer_runs, Some(999));
            assert!(!config.optimizer.unwrap());

            Ok(())
        });
    }

    #[test]
    fn can_parse_libraries() {
        figment::Jail::expect_with(|jail| {
            jail.set_env(
                "DAPP_LIBRARIES",
                "[src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6]",
            );
            let config = Config::load().unwrap();
            assert_eq!(
                config.libraries,
                vec![
                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
                        .to_string()
                ]
            );

            jail.set_env(
                "DAPP_LIBRARIES",
                "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
            );
            let config = Config::load().unwrap();
            assert_eq!(
                config.libraries,
                vec![
                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
                        .to_string(),
                ]
            );

            jail.set_env(
                "DAPP_LIBRARIES",
                "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6,src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
            );
            let config = Config::load().unwrap();
            assert_eq!(
                config.libraries,
                vec![
                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
                        .to_string(),
                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
                        .to_string()
                ]
            );

            Ok(())
        });
    }

    #[test]
    fn test_parse_many_libraries() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [profile.default]
               libraries= [
                        './src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
                        './src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
                        './src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
                        './src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
                        './src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
                    ]
            ",
            )?;
            let config = Config::load().unwrap();

            let libs = config.parsed_libraries().unwrap().libs;

            similar_asserts::assert_eq!(
                libs,
                BTreeMap::from([
                    (
                        PathBuf::from("./src/SizeAuctionDiscount.sol"),
                        BTreeMap::from([
                            (
                                "Chainlink".to_string(),
                                "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
                            ),
                            (
                                "Math".to_string(),
                                "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
                            )
                        ])
                    ),
                    (
                        PathBuf::from("./src/SizeAuction.sol"),
                        BTreeMap::from([
                            (
                                "ChainlinkTWAP".to_string(),
                                "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
                            ),
                            (
                                "Math".to_string(),
                                "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
                            )
                        ])
                    ),
                    (
                        PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
                        BTreeMap::from([(
                            "ChainlinkTWAP".to_string(),
                            "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
                        )])
                    ),
                ])
            );

            Ok(())
        });
    }

    #[test]
    fn config_roundtrip() {
        figment::Jail::expect_with(|jail| {
            let default = Config::default().normalized_optimizer_settings();
            let basic = default.clone().into_basic();
            jail.create_file("foundry.toml", &basic.to_string_pretty().unwrap())?;

            let mut other = Config::load().unwrap();
            clear_warning(&mut other);
            assert_eq!(default, other);

            let other = other.into_basic();
            assert_eq!(basic, other);

            jail.create_file("foundry.toml", &default.to_string_pretty().unwrap())?;
            let mut other = Config::load().unwrap();
            clear_warning(&mut other);
            let mut serialized_default = default;
            mark_serialized_invariant_provenance(&mut serialized_default);
            assert_eq!(serialized_default, other);

            Ok(())
        });
    }

    #[test]
    fn config_serialization_preserves_context_directory_boundary() {
        let remapping = "lib/outer/:inner/=lib/outer/lib/inner/";
        let config = Config {
            remappings: vec![Remapping::from_str(remapping).unwrap().into()],
            ..Default::default()
        };

        let serialized = toml::Value::try_from(&config).unwrap();
        assert_eq!(serialized["remappings"][0].as_str(), Some(remapping));

        let serialized = toml::Value::try_from(config.into_basic()).unwrap();
        assert_eq!(serialized["remappings"][0].as_str(), Some(remapping));
    }

    #[cfg(windows)]
    #[test]
    fn config_serialization_preserves_verbatim_unc_context() {
        let remapping = r"\\?\UNC\server\share\project\:inner/=lib/inner/";
        let expected = r"\\?\UNC\server\share/project/:inner/=lib/inner/";
        let config = Config {
            remappings: vec![Remapping::from_str(remapping).unwrap().into()],
            ..Default::default()
        };

        let serialized = toml::Value::try_from(&config).unwrap();
        assert_eq!(serialized["remappings"][0].as_str(), Some(expected));

        let serialized = toml::Value::try_from(config.into_basic()).unwrap();
        assert_eq!(serialized["remappings"][0].as_str(), Some(expected));
    }

    #[test]
    fn test_fs_permissions() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                fs_permissions = [{ access = "read-write", path = "./"}]
            "#,
            )?;
            let loaded = Config::load().unwrap();

            assert_eq!(
                loaded.fs_permissions,
                FsPermissions::new(vec![PathPermission::read_write("./")])
            );

            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                fs_permissions = [{ access = "none", path = "./"}]
            "#,
            )?;
            let loaded = Config::load().unwrap();
            assert_eq!(loaded.fs_permissions, FsPermissions::new(vec![PathPermission::none("./")]));

            Ok(())
        });
    }

    #[test]
    fn test_optimizer_settings_basic() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [profile.default]
                optimizer = true

                [profile.default.optimizer_details]
                yul = false

                [profile.default.optimizer_details.yulDetails]
                stackAllocation = true
            ",
            )?;
            let mut loaded = Config::load().unwrap();
            clear_warning(&mut loaded);
            assert_eq!(
                loaded.optimizer_details,
                Some(OptimizerDetails {
                    yul: Some(false),
                    yul_details: Some(YulDetails {
                        stack_allocation: Some(true),
                        ..Default::default()
                    }),
                    ..Default::default()
                })
            );

            let s = loaded.to_string_pretty().unwrap();
            jail.create_file("foundry.toml", &s)?;
            mark_serialized_invariant_provenance(&mut loaded);

            let mut reloaded = Config::load().unwrap();
            clear_warning(&mut reloaded);
            assert_eq!(loaded, reloaded);

            Ok(())
        });
    }

    #[test]
    fn test_model_checker_settings_basic() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [profile.default]

                [profile.default.model_checker]
                contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
                engine = 'chc'
                targets = [ 'assert', 'outOfBounds' ]
                timeout = 10000
            ",
            )?;
            let mut loaded = Config::load().unwrap();
            clear_warning(&mut loaded);
            assert_eq!(
                loaded.model_checker,
                Some(ModelCheckerSettings {
                    contracts: BTreeMap::from([
                        ("a.sol".to_string(), vec!["A1".to_string(), "A2".to_string()]),
                        ("b.sol".to_string(), vec!["B1".to_string(), "B2".to_string()]),
                    ]),
                    engine: Some(ModelCheckerEngine::CHC),
                    targets: Some(vec![
                        ModelCheckerTarget::Assert,
                        ModelCheckerTarget::OutOfBounds
                    ]),
                    timeout: Some(10000),
                    invariants: None,
                    show_unproved: None,
                    div_mod_with_slacks: None,
                    solvers: None,
                    show_unsupported: None,
                    show_proved_safe: None,
                })
            );

            let s = loaded.to_string_pretty().unwrap();
            jail.create_file("foundry.toml", &s)?;
            mark_serialized_invariant_provenance(&mut loaded);

            let mut reloaded = Config::load().unwrap();
            clear_warning(&mut reloaded);
            assert_eq!(loaded, reloaded);

            Ok(())
        });
    }

    #[test]
    fn test_model_checker_settings_with_bool_flags() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [profile.default]

                [profile.default.model_checker]
                engine = 'chc'
                show_unproved = true
                show_unsupported = true
                show_proved_safe = false
                div_mod_with_slacks = true
            ",
            )?;
            let mut loaded = Config::load().unwrap();
            clear_warning(&mut loaded);

            let mc = loaded.model_checker.as_ref().unwrap();
            assert_eq!(mc.show_unproved, Some(true));
            assert_eq!(mc.show_unsupported, Some(true));
            assert_eq!(mc.show_proved_safe, Some(false));
            assert_eq!(mc.div_mod_with_slacks, Some(true));

            // Test round-trip: serialize and reload
            let s = loaded.to_string_pretty().unwrap();
            jail.create_file("foundry.toml", &s)?;

            let mut reloaded = Config::load().unwrap();
            clear_warning(&mut reloaded);

            let mc_reloaded = reloaded.model_checker.as_ref().unwrap();
            assert_eq!(mc_reloaded.show_unproved, Some(true));
            assert_eq!(mc_reloaded.show_unsupported, Some(true));
            assert_eq!(mc_reloaded.show_proved_safe, Some(false));
            assert_eq!(mc_reloaded.div_mod_with_slacks, Some(true));

            Ok(())
        });
    }

    #[test]
    fn test_model_checker_settings_relative_paths() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [profile.default]

                [profile.default.model_checker]
                contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
                engine = 'chc'
                targets = [ 'assert', 'outOfBounds' ]
                timeout = 10000
            ",
            )?;
            let loaded = Config::load().unwrap().sanitized();

            // NOTE(onbjerg): We have to canonicalize the path here using dunce because figment will
            // canonicalize the jail path using the standard library. The standard library *always*
            // transforms Windows paths to some weird extended format, which none of our code base
            // does.
            let dir = foundry_compilers::utils::canonicalize(jail.directory())
                .expect("Could not canonicalize jail path");
            assert_eq!(
                loaded.model_checker,
                Some(ModelCheckerSettings {
                    contracts: BTreeMap::from([
                        (
                            format!("{}", dir.join("a.sol").display()),
                            vec!["A1".to_string(), "A2".to_string()]
                        ),
                        (
                            format!("{}", dir.join("b.sol").display()),
                            vec!["B1".to_string(), "B2".to_string()]
                        ),
                    ]),
                    engine: Some(ModelCheckerEngine::CHC),
                    targets: Some(vec![
                        ModelCheckerTarget::Assert,
                        ModelCheckerTarget::OutOfBounds
                    ]),
                    timeout: Some(10000),
                    invariants: None,
                    show_unproved: None,
                    div_mod_with_slacks: None,
                    solvers: None,
                    show_unsupported: None,
                    show_proved_safe: None,
                })
            );

            Ok(())
        });
    }

    #[test]
    fn test_fmt_config() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [fmt]
                line_length = 100
                tab_width = 2
                bracket_spacing = true
                style = "space"
            "#,
            )?;
            let loaded = Config::load().unwrap().sanitized();
            assert_eq!(
                loaded.fmt,
                FormatterConfig {
                    line_length: 100,
                    tab_width: 2,
                    bracket_spacing: true,
                    style: IndentStyle::Space,
                    ..Default::default()
                }
            );

            Ok(())
        });
    }

    #[test]
    fn test_lint_config() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [lint]
                severity = ['high', 'medium']
                exclude_lints = ['incorrect-shift']
                ",
            )?;
            let loaded = Config::load().unwrap().sanitized();
            assert_eq!(
                loaded.lint,
                LinterConfig {
                    severity: vec![LintSeverity::High, LintSeverity::Med],
                    exclude_lints: vec!["incorrect-shift".into()],
                    ..Default::default()
                }
            );

            Ok(())
        });
    }

    #[test]
    fn test_invariant_config() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [invariant]
                runs = 512
                depth = 10
                min_depth = 2
                depth_mode = "random"
                workers = 4
                corpus_random_sequence_weight = 30
                payable_value_weight = 12
                mutation_weight_cmp = 7
            "#,
            )?;

            let loaded = Config::load().unwrap().sanitized();
            assert_eq!(
                loaded.invariant,
                InvariantConfig {
                    runs: 512,
                    depth: 10,
                    min_depth: 2,
                    depth_mode: InvariantDepthMode::Random,
                    workers: InvariantWorkers::Fixed(NonZeroUsize::new(4).unwrap()),
                    corpus: FuzzCorpusConfig {
                        corpus_random_sequence_weight: 30,
                        payable_value_weight: 12,
                        mutation_weights: FuzzCorpusMutationWeights {
                            mutation_weight_cmp: 7,
                            ..Default::default()
                        },
                        ..Default::default()
                    },
                    corpus_random_sequence_weight_configured: true,
                    workers_configured: true,
                    failure_persist_dir: Some(PathBuf::from("cache/invariant")),
                    ..Default::default()
                }
            );
            assert!(loaded.invariant.corpus_random_sequence_weight_configured);

            Ok(())
        });
    }

    #[test]
    fn test_invariant_corpus_random_sequence_weight_provenance() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [invariant]
                depth = 10
            "#,
            )?;

            let loaded = Config::load().unwrap();
            assert_eq!(
                loaded.invariant.corpus.corpus_random_sequence_weight,
                FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
            );
            assert!(!loaded.invariant.corpus_random_sequence_weight_configured);
            assert!(!loaded.invariant.workers_configured);

            jail.create_file(
                "foundry.toml",
                r#"
                [invariant]
                corpus_random_sequence_weight = 10
                workers = 4
            "#,
            )?;

            let loaded = Config::load().unwrap();
            assert_eq!(
                loaded.invariant.corpus.corpus_random_sequence_weight,
                FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
            );
            assert!(loaded.invariant.corpus_random_sequence_weight_configured);
            assert_eq!(
                loaded.invariant.workers,
                InvariantWorkers::Fixed(NonZeroUsize::new(4).unwrap())
            );
            assert!(loaded.invariant.workers_configured);

            Ok(())
        });
    }

    #[test]
    fn test_fuzz_corpus_random_sequence_weight_fallback_does_not_mark_invariant_configured() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [fuzz]
                corpus_random_sequence_weight = 10
            "#,
            )?;

            let loaded = Config::load().unwrap();
            assert_eq!(
                loaded.invariant.corpus.corpus_random_sequence_weight,
                FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
            );
            assert!(!loaded.invariant.corpus_random_sequence_weight_configured);

            Ok(())
        });
    }

    #[test]
    fn test_standalone_sections_env() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [fuzz]
                runs = 100

                [invariant]
                depth = 1
            ",
            )?;

            jail.set_env("FOUNDRY_FMT_LINE_LENGTH", "95");
            jail.set_env("FOUNDRY_FUZZ_DICTIONARY_WEIGHT", "99");
            jail.set_env("FOUNDRY_FUZZ_MAX_FUZZ_DICTIONARY_VALUES", "max");
            jail.set_env("FOUNDRY_INVARIANT_DEPTH", "5");
            jail.set_env("FOUNDRY_INVARIANT_MIN_DEPTH", "2");
            jail.set_env("FOUNDRY_INVARIANT_DEPTH_MODE", "random");
            jail.set_env("FOUNDRY_INVARIANT_WORKERS", "3");
            jail.set_env("FOUNDRY_INVARIANT_CORPUS_RANDOM_SEQUENCE_WEIGHT", "30");
            jail.set_env("FOUNDRY_INVARIANT_PAYABLE_VALUE_WEIGHT", "12");
            jail.set_env("FOUNDRY_INVARIANT_MUTATION_WEIGHT_CMP", "7");
            jail.set_env("FOUNDRY_SYMBOLIC_MAX_PATHS", "64");
            jail.set_env("FOUNDRY_SYMBOLIC_DUMP_SMT", "true");

            let config = Config::load().unwrap();
            assert_eq!(config.fmt.line_length, 95);
            assert_eq!(config.fuzz.dictionary.dictionary_weight, 99);
            assert_eq!(config.fuzz.dictionary.max_fuzz_dictionary_values, usize::MAX);
            assert_eq!(config.invariant.depth, 5);
            assert_eq!(config.invariant.min_depth, 2);
            assert_eq!(config.invariant.depth_mode, InvariantDepthMode::Random);
            assert_eq!(
                config.invariant.workers,
                InvariantWorkers::Fixed(NonZeroUsize::new(3).unwrap())
            );
            assert_eq!(config.invariant.corpus.corpus_random_sequence_weight, 30);
            assert!(config.invariant.corpus_random_sequence_weight_configured);
            assert_eq!(config.invariant.corpus.payable_value_weight, 12);
            assert_eq!(config.invariant.corpus.mutation_weights.mutation_weight_cmp, 7);
            assert_eq!(config.symbolic.max_paths, 64);
            assert!(config.symbolic.dump_smt);

            Ok(())
        });
    }

    #[test]
    fn test_invariant_workers_env_accepts_auto() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [invariant]
                workers = 3
            ",
            )?;

            jail.set_env("FOUNDRY_INVARIANT_WORKERS", "auto");

            let config = Config::load().unwrap();
            assert_eq!(config.invariant.workers, InvariantWorkers::Auto);

            Ok(())
        });
    }

    #[test]
    fn test_parse_with_profile() {
        let foundry_str = r"
            [profile.default]
            src = 'src'
            out = 'out'
            libs = ['lib']

            # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
        ";
        assert_eq!(
            parse_with_profile::<BasicConfig>(foundry_str).unwrap().unwrap(),
            (
                Config::DEFAULT_PROFILE,
                BasicConfig {
                    profile: Config::DEFAULT_PROFILE,
                    src: "src".into(),
                    out: "out".into(),
                    libs: vec!["lib".into()],
                    remappings: vec![],
                    network: None,
                }
            )
        );
    }

    #[test]
    fn test_implicit_profile_loads() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [default]
                src = 'my-src'
                out = 'my-out'
            ",
            )?;
            let loaded = Config::load().unwrap().sanitized();
            assert_eq!(loaded.src.file_name().unwrap(), "my-src");
            assert_eq!(loaded.out.file_name().unwrap(), "my-out");
            assert_eq!(
                loaded.warnings,
                vec![Warning::UnknownSection {
                    unknown_section: Profile::new("default"),
                    source: Some("foundry.toml".into())
                }]
            );

            Ok(())
        });
    }

    #[test]
    fn hardfork_overrides_spec_id() {
        let config = Config {
            hardfork: Some(FoundryHardfork::Tempo(TempoHardfork::T3)),
            ..Config::default()
        };

        assert_eq!(config.evm_spec_id::<TempoHardfork>(), TempoHardfork::T3);
    }

    #[test]
    fn solc_settings_include_experimental_setting() {
        let config = Config { experimental: true, ..Config::default() };
        let settings = config.solc_settings().unwrap();
        assert_eq!(settings.settings.experimental, Some(true));
        assert!(settings.cli_settings.extra_args.is_empty());

        let config = Config {
            experimental: false,
            extra_args: vec!["--experimental".to_string()],
            ..Config::default()
        };
        let settings = config.solc_settings().unwrap();
        assert_eq!(settings.settings.experimental, Some(false));
        assert_eq!(settings.cli_settings.extra_args, vec!["--experimental".to_string()]);
    }

    #[test]
    fn solc_settings_include_via_ssa_cfg_setting() {
        let config = Config { via_ssa_cfg: true, ..Config::default() };
        let settings = config.solc_settings().unwrap();
        assert_eq!(settings.settings.via_ssa_cfg, Some(true));
        assert!(settings.cli_settings.extra_args.is_empty());

        let config = Config {
            via_ssa_cfg: false,
            extra_args: vec!["--via-ssa-cfg".to_string()],
            ..Config::default()
        };
        let settings = config.solc_settings().unwrap();
        assert_eq!(settings.settings.via_ssa_cfg, Some(false));
        assert_eq!(settings.cli_settings.extra_args, vec!["--via-ssa-cfg".to_string()]);
    }

    #[test]
    fn tempo_network_defaults_to_latest_tempo_hardfork() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                network = "tempo"
            "#,
            )?;

            let config = Config::load().unwrap();
            assert!(config.networks.is_tempo());
            assert_eq!(config.evm_spec_id::<TempoHardfork>(), latest_active_tempo_hardfork());

            Ok(())
        });
    }

    #[test]
    fn tempo_hardfork_infers_tempo_network() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                hardfork = "tempo:T3"
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.hardfork, Some(FoundryHardfork::Tempo(TempoHardfork::T3)));
            assert!(config.networks.is_tempo());

            Ok(())
        });
    }

    #[test]
    fn hardfork_rejects_conflicting_network() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                tempo = true
                hardfork = "shanghai"
            "#,
            )?;

            let err = Config::load().unwrap_err();
            assert!(
                err.to_string()
                    .to_lowercase()
                    .contains("hardfork `shanghai` conflicts with network config `tempo`")
            );

            Ok(())
        });
    }

    #[test]
    fn test_etherscan_api_key() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [default]
            ",
            )?;
            jail.set_env("ETHERSCAN_API_KEY", "");
            let loaded = Config::load().unwrap().sanitized();
            assert!(loaded.etherscan_api_key.is_none());

            jail.set_env("ETHERSCAN_API_KEY", "DUMMY");
            let loaded = Config::load().unwrap().sanitized();
            assert_eq!(loaded.etherscan_api_key, Some("DUMMY".into()));

            Ok(())
        });
    }

    #[test]
    fn test_etherscan_api_key_figment() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [default]
                etherscan_api_key = 'DUMMY'
            ",
            )?;
            jail.set_env("ETHERSCAN_API_KEY", "ETHER");

            let figment = Config::figment_with_root(jail.directory())
                .merge(("etherscan_api_key", "USER_KEY"));

            let loaded = Config::from_provider(figment).unwrap();
            assert_eq!(loaded.etherscan_api_key, Some("USER_KEY".into()));

            Ok(())
        });
    }

    #[test]
    fn test_normalize_defaults() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [default]
                solc = '0.8.13'
            ",
            )?;

            let loaded = Config::load().unwrap().sanitized();
            assert_eq!(loaded.evm_version, EvmVersion::London);
            Ok(())
        });
    }

    // a test to print the config, mainly used to update the example config in the README
    #[expect(clippy::disallowed_macros)]
    #[test]
    #[ignore]
    fn print_config() {
        let config = Config {
            optimizer_details: Some(OptimizerDetails {
                peephole: None,
                inliner: None,
                jumpdest_remover: None,
                order_literals: None,
                deduplicate: None,
                cse: None,
                constant_optimizer: Some(true),
                yul: Some(true),
                yul_details: Some(YulDetails {
                    stack_allocation: None,
                    optimizer_steps: Some("dhfoDgvulfnTUtnIf".to_string()),
                }),
                simple_counter_for_loop_unchecked_increment: None,
            }),
            ..Default::default()
        };
        println!("{}", config.to_string_pretty().unwrap());
    }

    #[test]
    fn can_use_impl_figment_macro() {
        #[derive(Default, Serialize)]
        struct MyArgs {
            #[serde(skip_serializing_if = "Option::is_none")]
            root: Option<PathBuf>,
        }
        impl_figment_convert!(MyArgs);

        impl Provider for MyArgs {
            fn metadata(&self) -> Metadata {
                Metadata::default()
            }

            fn data(&self) -> Result<Map<Profile, Dict>, Error> {
                let value = Value::serialize(self)?;
                let error = InvalidType(value.to_actual(), "map".into());
                let dict = value.into_dict().ok_or(error)?;
                Ok(Map::from([(Config::selected_profile(), dict)]))
            }
        }

        let _figment: Figment = From::from(&MyArgs::default());

        #[derive(Default)]
        struct Outer {
            start: MyArgs,
            other: MyArgs,
            another: MyArgs,
        }
        impl_figment_convert!(Outer, start, other, another);

        let _figment: Figment = From::from(&Outer::default());
    }

    #[test]
    fn list_cached_blocks() -> eyre::Result<()> {
        fn fake_block_cache(chain_path: &Path, block_number: &str, size_bytes: usize) {
            let block_path = chain_path.join(block_number);
            fs::create_dir(block_path.as_path()).unwrap();
            let file_path = block_path.join("storage.json");
            let mut file = File::create(file_path).unwrap();
            writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
        }

        fn fake_endpoint_block_cache(
            chain_path: &Path,
            block_number: &str,
            endpoint: &str,
            size_bytes: usize,
        ) {
            let block_path = chain_path.join(block_number);
            let file_path = block_path.join(format!("storage-{endpoint}.json"));
            let mut file = File::create(file_path).unwrap();
            writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
        }

        fn fake_block_cache_block_path_as_file(
            chain_path: &Path,
            block_number: &str,
            size_bytes: usize,
        ) {
            let block_path = chain_path.join(block_number);
            let mut file = File::create(block_path).unwrap();
            writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
        }

        let chain_dir = tempdir()?;

        fake_block_cache(chain_dir.path(), "1", 100);
        fake_endpoint_block_cache(
            chain_dir.path(),
            "1",
            "0000000000000000000000000000000000000000000000000000000000000000",
            50,
        );
        fake_endpoint_block_cache(chain_dir.path(), "1", "backup", 75);
        fake_block_cache(chain_dir.path(), "2", 500);
        fake_block_cache_block_path_as_file(chain_dir.path(), "3", 900);
        // Pollution file that should not show up in the cached block
        let mut pol_file = File::create(chain_dir.path().join("pol.txt")).unwrap();
        writeln!(pol_file, "{}", [' '; 10].iter().collect::<String>()).unwrap();

        let result = Config::get_cached_blocks(chain_dir.path())?;

        assert_eq!(result.len(), 3);
        let block1 = &result.iter().find(|x| x.0 == "1").unwrap();
        let block2 = &result.iter().find(|x| x.0 == "2").unwrap();
        let block3 = &result.iter().find(|x| x.0 == "3").unwrap();

        assert_eq!(block1.0, "1");
        assert_eq!(block1.1, 150);
        assert_eq!(block2.0, "2");
        assert_eq!(block2.1, 500);
        assert_eq!(block3.0, "3");
        assert_eq!(block3.1, 900);

        chain_dir.close()?;
        Ok(())
    }

    #[test]
    fn list_cached_blocks_ignores_removed_entries() -> eyre::Result<()> {
        let chain_dir = tempdir()?;
        let block_path = chain_dir.path().join("1");
        fs::create_dir(&block_path)?;
        File::create(block_path.join("storage.json"))?;

        let block = fs::read_dir(chain_dir.path())?.next().unwrap()?;
        let cache_file = fs::read_dir(&block_path)?.next().unwrap()?;
        fs::remove_dir_all(block_path)?;

        assert!(Config::get_cached_block(block)?.is_none());
        assert!(Config::get_cache_file_size(cache_file)?.is_none());
        Ok(())
    }

    #[test]
    fn ignore_not_found_propagates_other_errors() {
        assert!(
            Config::ignore_not_found::<()>(Err(io::Error::from(io::ErrorKind::NotFound)))
                .unwrap()
                .is_none()
        );

        let err =
            Config::ignore_not_found::<()>(Err(io::Error::from(io::ErrorKind::PermissionDenied)))
                .unwrap_err();
        assert_eq!(
            err.downcast_ref::<io::Error>().unwrap().kind(),
            io::ErrorKind::PermissionDenied
        );
    }

    #[test]
    fn cache_listing_propagates_not_a_directory() -> eyre::Result<()> {
        let cache_file = tempfile::NamedTempFile::new()?;

        let err = Config::get_cached_blocks(cache_file.path()).unwrap_err();
        assert_eq!(err.downcast_ref::<io::Error>().unwrap().kind(), io::ErrorKind::NotADirectory);

        let err = Config::get_cached_block_explorer_data(cache_file.path()).unwrap_err();
        assert_eq!(err.downcast_ref::<io::Error>().unwrap().kind(), io::ErrorKind::NotADirectory);
        Ok(())
    }

    #[test]
    fn list_cached_blocks_uses_replaced_entry_type() -> eyre::Result<()> {
        let chain_dir = tempdir()?;
        let block_path = chain_dir.path().join("1");
        File::create(&block_path)?;
        let block = fs::read_dir(chain_dir.path())?.next().unwrap()?;

        fs::remove_file(&block_path)?;
        fs::create_dir(&block_path)?;
        fs::write(block_path.join("storage.json"), [0; 10])?;

        assert_eq!(Config::get_cached_block(block)?, Some(("1".to_string(), 10)));
        Ok(())
    }

    #[cfg(unix)]
    #[test]
    fn list_cached_blocks_ignores_symlinks() -> eyre::Result<()> {
        let chain_dir = tempdir()?;
        let target_dir = tempdir()?;
        fs::write(target_dir.path().join("storage.json"), [0; 10])?;
        std::os::unix::fs::symlink(target_dir.path(), chain_dir.path().join("1"))?;

        let block_path = chain_dir.path().join("2");
        fs::create_dir(&block_path)?;
        let target_file = tempfile::NamedTempFile::new()?;
        fs::write(target_file.path(), [0; 10])?;
        std::os::unix::fs::symlink(target_file.path(), block_path.join("storage.json"))?;

        assert!(Config::get_cached_blocks(chain_dir.path())?.is_empty());
        Ok(())
    }

    #[test]
    fn list_etherscan_cache_ignores_removed_entries() -> eyre::Result<()> {
        let cache_dir = tempdir()?;
        let sources_path = cache_dir.path().join("sources");
        fs::create_dir(&sources_path)?;
        File::create(sources_path.join("metadata.json"))?;

        let sources = fs::read_dir(cache_dir.path())?.next().unwrap()?;
        let metadata = fs::read_dir(&sources_path)?.next().unwrap()?;
        fs::remove_dir_all(sources_path)?;

        assert!(Config::get_cached_entry_size(sources)?.is_none());
        assert!(Config::get_cached_entry_size(metadata)?.is_none());
        Ok(())
    }

    #[test]
    fn list_etherscan_cache() -> eyre::Result<()> {
        fn fake_etherscan_cache(chain_path: &Path, address: &str, size_bytes: usize) {
            let metadata_path = chain_path.join("sources");
            let abi_path = chain_path.join("abi");
            let _ = fs::create_dir(metadata_path.as_path());
            let _ = fs::create_dir(abi_path.as_path());

            let metadata_file_path = metadata_path.join(address);
            let mut metadata_file = File::create(metadata_file_path).unwrap();
            writeln!(metadata_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
                .unwrap();

            let abi_file_path = abi_path.join(address);
            let mut abi_file = File::create(abi_file_path).unwrap();
            writeln!(abi_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
                .unwrap();
        }

        let chain_dir = tempdir()?;

        fake_etherscan_cache(chain_dir.path(), "1", 100);
        fake_etherscan_cache(chain_dir.path(), "2", 500);

        let result = Config::get_cached_block_explorer_data(chain_dir.path())?;

        assert_eq!(result, 600);

        chain_dir.close()?;
        Ok(())
    }

    #[test]
    fn test_parse_error_codes() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [default]
                ignored_error_codes = ["license", "unreachable", 1337]
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(
                config.ignored_error_codes,
                vec![
                    SolidityErrorCode::SpdxLicenseNotProvided,
                    SolidityErrorCode::Unreachable,
                    SolidityErrorCode::Other(1337)
                ]
            );

            Ok(())
        });
    }

    #[test]
    fn test_parse_file_paths() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [default]
                ignored_warnings_from = ["something"]
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.ignored_file_paths, vec![Path::new("something").to_path_buf()]);

            Ok(())
        });
    }

    #[test]
    fn test_parse_optimizer_settings() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r"
                [default]
                [profile.default.optimizer_details]
            ",
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.optimizer_details, Some(OptimizerDetails::default()));

            Ok(())
        });
    }

    #[test]
    fn test_parse_labels() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [labels]
                0x1F98431c8aD98523631AE4a59f267346ea31F984 = "Uniswap V3: Factory"
                0xC36442b4a4522E871399CD717aBDD847Ab11FE88 = "Uniswap V3: Positions NFT"
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(
                config.labels,
                AddressHashMap::from_iter(vec![
                    (
                        address!("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
                        "Uniswap V3: Factory".to_string()
                    ),
                    (
                        address!("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"),
                        "Uniswap V3: Positions NFT".to_string()
                    ),
                ])
            );
            assert_eq!(config.tracing.labels, config.labels);
            assert_eq!(
                config.warnings,
                vec![Warning::DeprecatedKey {
                    old: "[labels]".to_string(),
                    new: "[tracing.labels]".to_string(),
                }]
            );

            Ok(())
        });
    }

    #[test]
    fn test_parse_deprecated_profile_labels() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default.labels]
                0x0000000000000000000000000000000000000001 = "Alice"
            "#,
            )?;

            let config = Config::load().unwrap();
            let labels = AddressHashMap::from_iter(vec![(
                address!("0x0000000000000000000000000000000000000001"),
                "Alice".to_string(),
            )]);
            assert_eq!(config.labels, labels);
            assert_eq!(config.tracing.labels, labels);
            assert_eq!(
                config.warnings,
                vec![Warning::DeprecatedKey {
                    old: "labels".to_string(),
                    new: "tracing.labels".to_string(),
                }]
            );

            Ok(())
        });
    }

    #[test]
    fn test_deprecated_env_labels_use_tracing_section() {
        figment::Jail::expect_with(|jail| {
            jail.set_env(
                "FOUNDRY_LABELS",
                r#"{ "0x0000000000000000000000000000000000000001" = "Alice" }"#,
            );
            jail.set_env(
                "FOUNDRY_TRACING_LABELS",
                r#"{ "0x0000000000000000000000000000000000000001" = "Bob" }"#,
            );

            let config = Config::load().unwrap();
            assert_eq!(
                config.labels,
                AddressHashMap::from_iter([(
                    address!("0x0000000000000000000000000000000000000001"),
                    "Alice".to_string(),
                )])
            );
            assert_eq!(
                config.tracing.labels,
                AddressHashMap::from_iter([(
                    address!("0x0000000000000000000000000000000000000001"),
                    "Bob".to_string(),
                )])
            );

            Ok(())
        });
    }

    #[test]
    fn test_deprecated_labels_warn_for_inactive_profiles() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.ci.labels]
                0x0000000000000000000000000000000000000001 = "Alice"
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(
                config.warnings,
                vec![Warning::DeprecatedKey {
                    old: "labels".to_string(),
                    new: "tracing.labels".to_string(),
                }]
            );

            Ok(())
        });
    }

    #[test]
    fn test_tracing_serialization_keeps_global_verbosity() {
        let address = address!("0x0000000000000000000000000000000000000001");
        let labels = AddressHashMap::from_iter([(address, "Alice".to_string())]);
        let config = Config {
            tracing: TracingConfig { verbosity: 4, labels: labels.clone(), ..Default::default() },
            ..Default::default()
        };

        let serialized = toml::Value::try_from(&config).unwrap();
        let table = serialized.as_table().unwrap();
        assert_eq!(table["verbosity"].as_integer(), Some(0));
        assert!(!table.contains_key("labels"));
        assert_eq!(table["tracing"]["verbosity"].as_integer(), Some(4));

        let provided = Figment::from(&config).extract::<Config>().unwrap();
        assert_eq!(provided.verbosity, 0);
        assert!(provided.labels.is_empty());
        assert_eq!(provided.tracing.labels, labels);

        let merged = config.merge_inline_provider(("ffi", true)).unwrap();
        assert_eq!(merged.tracing.verbosity, 4);
        assert_eq!(merged.tracing.labels, config.tracing.labels);
    }

    #[test]
    fn test_legacy_programmatic_labels_survive_serialization() {
        let address = address!("0x0000000000000000000000000000000000000001");
        let labels = AddressHashMap::from_iter([(address, "Alice".to_string())]);
        let config = Config { labels: labels.clone(), ..Default::default() };

        let serialized = toml::Value::try_from(&config).unwrap();
        assert_eq!(
            serialized["labels"].as_table().unwrap().values().next().and_then(|v| v.as_str()),
            Some("Alice")
        );

        let provided = Config::from_provider(&config).unwrap();
        assert_eq!(provided.labels, labels);
        assert_eq!(provided.tracing.labels, labels);

        let merged = config.merge_inline_provider(("ffi", true)).unwrap();
        assert_eq!(merged.labels, labels);
        assert_eq!(merged.tracing.labels, labels);
    }

    #[test]
    fn test_parse_tracing_section() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                verbosity = 2

                [tracing]
                verbosity = 4
                disable_labels = true
                compact_labels = true
                trace_depth = 3
                decode_internal = true
                external_identification_timeout = 9

                [tracing.labels]
                0x0000000000000000000000000000000000000002 = "Bob"
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.verbosity, 2);
            assert_eq!(config.tracing.verbosity, 4);
            assert!(config.tracing.disable_labels);
            assert_eq!(config.tracing.trace_depth, Some(3));
            assert!(config.tracing.decode_internal);
            assert!(config.tracing.compact_labels);
            assert_eq!(config.tracing.external_identification_timeout, 9);
            let labels = AddressHashMap::from_iter(vec![(
                address!("0x0000000000000000000000000000000000000002"),
                "Bob".to_string(),
            )]);
            assert!(config.labels.is_empty());
            assert_eq!(config.tracing.labels, labels);
            assert!(config.warnings.is_empty());

            let serialized = config.to_string_pretty().unwrap();
            assert!(!serialized.contains("[labels]"));
            assert!(serialized.contains("[tracing.labels]"));

            jail.create_file("foundry.toml", &serialized)?;
            let reloaded = Config::load().unwrap();
            assert!(reloaded.labels.is_empty());
            assert_eq!(reloaded.tracing.labels, labels);
            assert!(reloaded.warnings.is_empty());

            Ok(())
        });
    }

    #[test]
    fn test_external_identification_timeout_env() {
        figment::Jail::expect_with(|jail| {
            jail.set_env("FOUNDRY_TRACING_EXTERNAL_IDENTIFICATION_TIMEOUT", "0");

            let config = Config::load().unwrap();

            assert_eq!(config.tracing.external_identification_timeout, 0);
            Ok(())
        });
    }

    #[test]
    fn test_global_and_tracing_verbosity_are_independent() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                verbosity = 4

                [tracing]
                disable_labels = true
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.verbosity, 4);
            assert_eq!(config.tracing.verbosity, 0);
            assert!(config.tracing.disable_labels);
            assert_eq!(config.tracing.external_identification_timeout, 5);

            Ok(())
        });
    }

    #[test]
    fn test_label_aliases_preserve_provider_precedence() {
        let address = address!("0x0000000000000000000000000000000000000001");
        let labels = |label: &str| AddressHashMap::from_iter([(address, label.to_string())]);
        let provider = |global: &str, local: &str| {
            let figment = Config::merge_toml_provider(
                Figment::from(Config::default()),
                Toml::string(global).nested(),
                Config::DEFAULT_PROFILE,
            );
            Config::merge_toml_provider(
                figment,
                Toml::string(local).nested(),
                Config::DEFAULT_PROFILE,
            )
        };

        let config = Config::from_provider(provider(
            r#"[tracing.labels]
            0x0000000000000000000000000000000000000001 = "global""#,
            r#"[labels]
            0x0000000000000000000000000000000000000001 = "local""#,
        ))
        .unwrap();
        assert_eq!(config.tracing.labels, labels("local"));

        let config = Config::from_provider(provider(
            r#"[labels]
            0x0000000000000000000000000000000000000001 = "global""#,
            r#"[tracing.labels]
            0x0000000000000000000000000000000000000001 = "local""#,
        ))
        .unwrap();
        assert_eq!(config.tracing.labels, labels("local"));
    }

    #[test]
    fn test_malformed_tracing_labels_are_not_replaced() {
        let provider = Toml::string(
            r#"
            [profile.default.labels]
            0x0000000000000000000000000000000000000001 = "legacy"

            [profile.default.tracing]
            labels = "invalid"
            "#,
        )
        .nested();

        assert!(Config::from_provider(provider).is_err());
    }

    #[test]
    fn test_parse_vyper() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [vyper]
                optimize = "O1"
                path = "/path/to/vyper"
                experimental_codegen = true
                venom_experimental = false
                debug = true
                enable_decimals = true

                [vyper.venom]
                disable_inlining = true
                disable_cse = true
                disable_sccp = false
                disable_load_elimination = true
                disable_dead_store_elimination = false
                disable_algebraic_optimization = true
                disable_branch_optimization = false
                disable_assert_elimination = true
                disable_mem2var = false
                disable_simplify_cfg = true
                disable_remove_unused_variables = false
                inline_threshold = 15
            "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(
                config.vyper,
                VyperConfig {
                    optimize: Some(VyperOptimizationMode::O1),
                    opt_level: None,
                    path: Some("/path/to/vyper".into()),
                    experimental_codegen: Some(true),
                    venom_experimental: Some(false),
                    debug: Some(true),
                    enable_decimals: Some(true),
                    venom: Some(VyperVenomSettings {
                        disable_inlining: Some(true),
                        disable_cse: Some(true),
                        disable_sccp: Some(false),
                        disable_load_elimination: Some(true),
                        disable_dead_store_elimination: Some(false),
                        disable_algebraic_optimization: Some(true),
                        disable_branch_optimization: Some(false),
                        disable_assert_elimination: Some(true),
                        disable_mem2var: Some(false),
                        disable_simplify_cfg: Some(true),
                        disable_remove_unused_variables: Some(false),
                        inline_threshold: Some(15),
                    }),
                }
            );

            Ok(())
        });
    }

    #[test]
    fn test_vyper_settings_include_extended_config() {
        let config = Config {
            vyper: VyperConfig {
                opt_level: Some(VyperOptimizationLevel::O3),
                experimental_codegen: Some(true),
                venom_experimental: Some(false),
                debug: Some(true),
                enable_decimals: Some(true),
                venom: Some(VyperVenomSettings {
                    disable_cse: Some(true),
                    inline_threshold: Some(15),
                    ..Default::default()
                }),
                ..Default::default()
            },
            ..Config::default().normalized_optimizer_settings()
        };

        let settings = config.vyper_settings().unwrap();
        assert_eq!(settings.optimize, None);
        assert_eq!(settings.opt_level, Some(VyperOptimizationLevel::O3));
        assert_eq!(settings.experimental_codegen, Some(true));
        assert_eq!(settings.venom_experimental, Some(false));
        assert_eq!(settings.debug, Some(true));
        assert_eq!(settings.enable_decimals, Some(true));
        assert_eq!(
            settings.venom,
            Some(VyperVenomSettings {
                disable_cse: Some(true),
                inline_threshold: Some(15),
                ..Default::default()
            })
        );
    }

    #[test]
    fn vyper_opt_level_overrides_optimize() {
        let config = Config {
            vyper: VyperConfig {
                optimize: Some(VyperOptimizationMode::Gas),
                opt_level: Some(VyperOptimizationLevel::O3),
                ..Default::default()
            },
            ..Config::default().normalized_optimizer_settings()
        };

        let settings = config.vyper_settings().unwrap();
        assert_eq!(settings.optimize, None);
        assert_eq!(settings.opt_level, Some(VyperOptimizationLevel::O3));
    }

    #[test]
    fn test_parse_soldeer() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [soldeer]
                remappings_generate = true
                remappings_regenerate = false
                remappings_version = true
                remappings_prefix = "@"
                remappings_location = "txt"
                recursive_deps = true
            "#,
            )?;

            let config = Config::load().unwrap();

            assert_eq!(
                config.soldeer,
                Some(SoldeerConfig {
                    remappings_generate: true,
                    remappings_regenerate: false,
                    remappings_version: true,
                    remappings_prefix: "@".to_string(),
                    remappings_location: RemappingsLocation::Txt,
                    recursive_deps: true,
                })
            );

            Ok(())
        });
    }

    // <https://github.com/foundry-rs/foundry/issues/10926>
    #[test]
    fn test_resolve_mesc_by_chain_id() {
        let s = r#"{
    "mesc_version": "0.2.1",
    "default_endpoint": null,
    "endpoints": {
        "sophon_50104": {
            "name": "sophon_50104",
            "url": "https://rpc.sophon.xyz",
            "chain_id": "50104",
            "endpoint_metadata": {}
        }
    },
    "network_defaults": {
    },
    "network_names": {},
    "profiles": {
        "foundry": {
            "name": "foundry",
            "default_endpoint": "local_ethereum",
            "network_defaults": {
                "50104": "sophon_50104"
            },
            "profile_metadata": {},
            "use_mesc": true
        }
    },
    "global_metadata": {}
}"#;

        let config = serde_json::from_str(s).unwrap();
        let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
            .unwrap()
            .unwrap();
        assert_eq!(endpoint.url, "https://rpc.sophon.xyz");

        let s = r#"{
    "mesc_version": "0.2.1",
    "default_endpoint": null,
    "endpoints": {
        "sophon_50104": {
            "name": "sophon_50104",
            "url": "https://rpc.sophon.xyz",
            "chain_id": "50104",
            "endpoint_metadata": {}
        }
    },
    "network_defaults": {
        "50104": "sophon_50104"
    },
    "network_names": {},
    "profiles": {},
    "global_metadata": {}
}"#;

        let config = serde_json::from_str(s).unwrap();
        let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
            .unwrap()
            .unwrap();
        assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
    }

    #[test]
    fn test_get_etherscan_config_with_unknown_chain() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [etherscan]
                mainnet = { chain = 3658348, key = "api-key"}
            "#,
            )?;
            let config = Config::load().unwrap();
            let unknown_chain = Chain::from_id(3658348);
            let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
            assert!(result.is_err());
            let error_msg = result.unwrap_err().to_string();
            assert!(error_msg.contains("No known Etherscan API URL for chain `3658348`"));
            assert!(error_msg.contains("Specify a `url`"));
            assert!(error_msg.contains("Verify the chain `3658348` is correct"));

            Ok(())
        });
    }

    #[test]
    fn test_get_etherscan_config_with_existing_chain_and_url() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [etherscan]
                mainnet = { chain = 1, key = "api-key" }
            "#,
            )?;
            let config = Config::load().unwrap();
            let unknown_chain = Chain::from_id(1);
            let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
            assert!(result.is_ok());
            Ok(())
        });
    }

    #[test]
    fn test_can_inherit_a_base_toml() {
        figment::Jail::expect_with(|jail| {
            // Create base config file with optimizer_runs = 800
            jail.create_file(
                "base-config.toml",
                r#"
                    [profile.default]
                    optimizer_runs = 800

                    [invariant]
                    runs = 1000

                    [rpc_endpoints]
                    mainnet = "https://example.com"
                    optimism = "https://example-2.com/"
                    "#,
            )?;

            // Create local config that inherits from base-config.toml
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base-config.toml"

                    [invariant]
                    runs = 333
                    depth = 15

                    [rpc_endpoints]
                    mainnet = "https://test.xyz/rpc"
                    "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.extends, Some(Extends::Path("base-config.toml".to_string())));

            // optimizer_runs should be inherited from base-config.toml
            assert_eq!(config.optimizer_runs, Some(800));

            // invariant settings should be overridden by local config
            assert_eq!(config.invariant.runs, 333);
            assert_eq!(config.invariant.depth, 15);

            // rpc_endpoints.mainnet should be overridden by local config
            // optimism should be inherited from base config
            let endpoints = config.rpc_endpoints.resolved();
            assert!(
                endpoints.get("mainnet").unwrap().url().unwrap().contains("https://test.xyz/rpc")
            );
            assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("example-2.com"));

            Ok(())
        });
    }

    #[test]
    fn inherited_symbolic_sections_preserve_source_precedence() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default.symbolic]
                    max_paths = 10
                    depth = 100
                "#,
            )?;
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"

                    [symbolic]
                    max_paths = 20
                "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.symbolic.max_paths, 20);
            assert_eq!(config.symbolic.depth, Some(100));

            jail.create_file(
                "base.toml",
                r#"
                    [symbolic]
                    max_paths = 30
                    depth = 200
                "#,
            )?;
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"

                    [profile.default.symbolic]
                    max_paths = 40
                "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.symbolic.max_paths, 40);
            assert_eq!(config.symbolic.depth, Some(200));

            Ok(())
        });
    }

    #[test]
    fn inherited_symbolic_sections_detect_effective_collisions() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default.symbolic]
                    max_paths = 10
                "#,
            )?;
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = { path = "base.toml", strategy = "no-collision" }

                    [symbolic]
                    max_paths = 20
                "#,
            )?;

            let err = Config::load().unwrap_err().to_string();
            assert!(err.contains("Key collision detected"), "unexpected error: {err}");
            assert!(err.contains("symbolic"), "unexpected error: {err}");

            Ok(())
        });
    }

    #[test]
    fn inherited_label_aliases_preserve_source_precedence() {
        figment::Jail::expect_with(|jail| {
            let address = address!("0x0000000000000000000000000000000000000001");

            jail.create_file(
                "base.toml",
                r#"
                    [profile.default.tracing.labels]
                    0x0000000000000000000000000000000000000001 = "base"
                "#,
            )?;
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"

                    [profile.default.labels]
                    0x0000000000000000000000000000000000000001 = "local"
                "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.tracing.labels.get(&address).map(String::as_str), Some("local"));

            jail.create_file(
                "base.toml",
                r#"
                    [profile.default.labels]
                    0x0000000000000000000000000000000000000001 = "base"
                "#,
            )?;
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"

                    [profile.default.tracing.labels]
                    0x0000000000000000000000000000000000000001 = "local"
                "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.tracing.labels.get(&address).map(String::as_str), Some("local"));

            jail.create_file(
                "base.toml",
                r#"
                    [profile.default.tracing.labels]
                    0x0000000000000000000000000000000000000001 = "base"
                "#,
            )?;
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"

                    [labels]
                    0x0000000000000000000000000000000000000001 = "local"
                "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.tracing.labels.get(&address).map(String::as_str), Some("local"));
            assert_eq!(
                config.warnings,
                vec![Warning::DeprecatedKey {
                    old: "[labels]".to_string(),
                    new: "[tracing.labels]".to_string(),
                }]
            );

            jail.create_file(
                "base.toml",
                r#"
                    [labels]
                    0x0000000000000000000000000000000000000001 = "base"
                "#,
            )?;
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"

                    [profile.default.tracing.labels]
                    0x0000000000000000000000000000000000000001 = "local"
                "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.tracing.labels.get(&address).map(String::as_str), Some("local"));

            Ok(())
        });
    }

    #[test]
    fn inherited_label_aliases_detect_effective_collisions() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default.tracing.labels]
                    0x0000000000000000000000000000000000000001 = "base"
                "#,
            )?;
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = { path = "base.toml", strategy = "no-collision" }

                    [labels]
                    0x0000000000000000000000000000000000000001 = "local"
                "#,
            )?;

            let err = Config::load().unwrap_err().to_string();
            assert_eq!(
                err,
                "failed to extract foundry config:\n\
                 foundry config error: Key collision detected in profile 'default' when extending \
                 'base.toml'. Conflicting keys: [\"tracing\"]. Use 'extends.strategy' or \
                 'extends_strategy' to specify how to handle conflicts.\n"
            );

            Ok(())
        });
    }

    #[test]
    fn inherited_fuzz_section_remains_invariant_fallback() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "base.toml",
                r#"
                    [fuzz]
                    include_storage = false
                    dictionary_weight = 99
                "#,
            )?;
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"

                    [invariant]
                    runs = 420
                "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.invariant.runs, 420);
            assert!(!config.invariant.dictionary.include_storage);
            assert_eq!(config.invariant.dictionary.dictionary_weight, 99);

            Ok(())
        });
    }

    #[test]
    fn test_inheritance_validation() {
        figment::Jail::expect_with(|jail| {
            // Test 1: Base file with 'extends' should fail
            jail.create_file(
                "base-with-inherit.toml",
                r#"
                    [profile.default]
                    extends = "another.toml"
                    optimizer_runs = 800
                    "#,
            )?;

            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base-with-inherit.toml"
                    "#,
            )?;

            // Should fail because base file has 'extends'
            let result = Config::load();
            assert!(result.is_err());
            assert!(result.unwrap_err().to_string().contains("Nested inheritance is not allowed"));

            // Test 2: Circular reference should fail
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "foundry.toml"
                    "#,
            )?;

            let result = Config::load();
            assert!(result.is_err());
            assert!(result.unwrap_err().to_string().contains("cannot inherit from itself"));

            // Test 3: Non-existent base file should fail
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "non-existent.toml"
                    "#,
            )?;

            let result = Config::load();
            assert!(result.is_err());
            let err_msg = result.unwrap_err().to_string();
            assert!(
                err_msg.contains("does not exist")
                    || err_msg.contains("Failed to resolve inherited config path"),
                "Error message: {err_msg}"
            );

            Ok(())
        });
    }

    #[test]
    fn test_complex_inheritance_merging() {
        figment::Jail::expect_with(|jail| {
            // Create a comprehensive base config
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default]
                    optimizer = true
                    optimizer_runs = 1000
                    via_ir = false
                    solc = "0.8.19"

                    [invariant]
                    runs = 500
                    depth = 100

                    [fuzz]
                    runs = 256
                    seed = "0x123"

                    [rpc_endpoints]
                    mainnet = "https://base-mainnet.com"
                    optimism = "https://base-optimism.com"
                    arbitrum = "https://base-arbitrum.com"
                    "#,
            )?;

            // Create local config that overrides some values
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"
                    optimizer_runs = 200  # Override
                    via_ir = true        # Override
                    # optimizer and solc are inherited

                    [invariant]
                    runs = 333  # Override
                    # depth is inherited

                    # fuzz section is fully inherited

                    [rpc_endpoints]
                    mainnet = "https://local-mainnet.com"  # Override
                    # optimism and arbitrum are inherited
                    polygon = "https://local-polygon.com"  # New
                    "#,
            )?;

            let config = Config::load().unwrap();

            // Check profile.default values
            assert_eq!(config.optimizer, Some(true));
            assert_eq!(config.optimizer_runs, Some(200));
            assert_eq!(config.via_ir, true);
            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));

            // Check invariant section
            assert_eq!(config.invariant.runs, 333);
            assert_eq!(config.invariant.depth, 100);

            // Check fuzz section (fully inherited)
            assert_eq!(config.fuzz.runs, 256);
            assert_eq!(config.fuzz.seed, Some(U256::from(0x123)));

            // Check rpc_endpoints
            let endpoints = config.rpc_endpoints.resolved();
            assert!(endpoints.get("mainnet").unwrap().url().unwrap().contains("local-mainnet"));
            assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("base-optimism"));
            assert!(endpoints.get("arbitrum").unwrap().url().unwrap().contains("base-arbitrum"));
            assert!(endpoints.get("polygon").unwrap().url().unwrap().contains("local-polygon"));

            Ok(())
        });
    }

    #[test]
    fn test_inheritance_with_different_profiles() {
        figment::Jail::expect_with(|jail| {
            // Create base config with multiple profiles
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default]
                    optimizer = true
                    optimizer_runs = 200

                    [profile.ci]
                    optimizer = true
                    optimizer_runs = 10000
                    via_ir = true

                    [profile.dev]
                    optimizer = false
                    "#,
            )?;

            // Local config inherits from base - only for default profile
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"
                    verbosity = 3

                    [profile.ci]
                    optimizer_runs = 5000  # This doesn't inherit from base.toml's ci profile
                    "#,
            )?;

            // Test default profile
            let config = Config::load().unwrap();
            assert_eq!(config.optimizer, Some(true));
            assert_eq!(config.optimizer_runs, Some(200));
            assert_eq!(config.verbosity, 3);

            // Test CI profile (NO 'extends', so doesn't inherit from base)
            jail.set_env("FOUNDRY_PROFILE", "ci");
            let config = Config::load().unwrap();
            assert_eq!(config.optimizer_runs, Some(5000));
            assert_eq!(config.optimizer, Some(true));
            // via_ir is not set in local ci profile and there's no 'extends', so default
            assert_eq!(config.via_ir, false);

            Ok(())
        });
    }

    #[test]
    fn test_inheritance_with_env_vars() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default]
                    optimizer_runs = 500
                    sender = "0x0000000000000000000000000000000000000001"
                    verbosity = 1
                    "#,
            )?;

            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"
                    verbosity = 2
                    "#,
            )?;

            // Environment variables should override both base and local values
            jail.set_env("FOUNDRY_OPTIMIZER_RUNS", "999");
            jail.set_env("FOUNDRY_VERBOSITY", "4");

            let config = Config::load().unwrap();
            assert_eq!(config.optimizer_runs, Some(999));
            assert_eq!(config.verbosity, 4);
            assert_eq!(
                config.sender,
                "0x0000000000000000000000000000000000000001"
                    .parse::<alloy_primitives::Address>()
                    .unwrap()
            );

            Ok(())
        });
    }

    #[test]
    fn test_inheritance_with_subdirectories() {
        figment::Jail::expect_with(|jail| {
            // Create base config in a subdirectory
            jail.create_dir("configs")?;
            jail.create_file(
                "configs/base.toml",
                r#"
                    [profile.default]
                    optimizer_runs = 800
                    src = "contracts"
                    "#,
            )?;

            // Reference it with relative path
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "configs/base.toml"
                    test = "tests"
                    "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.optimizer_runs, Some(800));
            assert_eq!(config.src, PathBuf::from("contracts"));
            assert_eq!(config.test, PathBuf::from("tests"));

            // Test with parent directory reference
            jail.create_dir("project")?;
            jail.create_file(
                "shared-base.toml",
                r#"
                    [profile.default]
                    optimizer_runs = 1500
                    "#,
            )?;

            jail.create_file(
                "project/foundry.toml",
                r#"
                    [profile.default]
                    extends = "../shared-base.toml"
                    "#,
            )?;

            std::env::set_current_dir(jail.directory().join("project")).unwrap();
            let config = Config::load().unwrap();
            assert_eq!(config.optimizer_runs, Some(1500));

            Ok(())
        });
    }

    #[test]
    fn test_inheritance_with_empty_files() {
        figment::Jail::expect_with(|jail| {
            // Empty base file
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default]
                    "#,
            )?;

            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"
                    optimizer_runs = 300
                    "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.optimizer_runs, Some(300));

            // Empty local file (only 'extends')
            jail.create_file(
                "base2.toml",
                r#"
                    [profile.default]
                    optimizer_runs = 400
                    via_ir = true
                    "#,
            )?;

            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base2.toml"
                    "#,
            )?;

            let config = Config::load().unwrap();
            assert_eq!(config.optimizer_runs, Some(400));
            assert!(config.via_ir);

            Ok(())
        });
    }

    #[test]
    fn test_inheritance_array_and_table_merging() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default]
                    libs = ["lib", "node_modules"]
                    ignored_error_codes = [5667, 1878]
                    extra_output = ["metadata", "ir"]

                    [profile.default.model_checker]
                    engine = "chc"
                    timeout = 10000
                    targets = ["assert"]

                    [profile.default.optimizer_details]
                    peephole = true
                    inliner = true
                    "#,
            )?;

            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"
                    libs = ["custom-lib"]  # Concatenates with base array
                    ignored_error_codes = [2018]  # Concatenates with base array

                    [profile.default.model_checker]
                    timeout = 5000  # Overrides base value
                    # engine and targets are inherited

                    [profile.default.optimizer_details]
                    jumpdest_remover = true  # Adds new field
                    # peephole and inliner are inherited
                    "#,
            )?;

            let config = Config::load().unwrap();

            // Arrays are now concatenated with admerge (base + local)
            assert_eq!(
                config.libs,
                vec![
                    PathBuf::from("lib"),
                    PathBuf::from("node_modules"),
                    PathBuf::from("custom-lib")
                ]
            );
            assert_eq!(
                config.ignored_error_codes,
                vec![
                    SolidityErrorCode::UnusedFunctionParameter, // 5667 from base.toml
                    SolidityErrorCode::SpdxLicenseNotProvided,  // 1878 from base.toml
                    SolidityErrorCode::FunctionStateMutabilityCanBeRestricted  // 2018 from local
                ]
            );

            // Tables are deep-merged
            assert_eq!(config.model_checker.as_ref().unwrap().timeout, Some(5000));
            assert_eq!(
                config.model_checker.as_ref().unwrap().engine,
                Some(ModelCheckerEngine::CHC)
            );
            assert_eq!(
                config.model_checker.as_ref().unwrap().targets,
                Some(vec![ModelCheckerTarget::Assert])
            );

            // optimizer_details table is actually merged, not replaced
            assert_eq!(config.optimizer_details.as_ref().unwrap().peephole, Some(true));
            assert_eq!(config.optimizer_details.as_ref().unwrap().inliner, Some(true));
            assert_eq!(config.optimizer_details.as_ref().unwrap().jumpdest_remover, None);

            Ok(())
        });
    }

    #[test]
    fn test_inheritance_with_special_sections() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default]
                    # Base file should not have 'extends' to avoid nested inheritance

                    [labels]
                    "0x0000000000000000000000000000000000000001" = "Alice"
                    "0x0000000000000000000000000000000000000002" = "Bob"

                    [[profile.default.fs_permissions]]
                    access = "read"
                    path = "./src"

                    [[profile.default.fs_permissions]]
                    access = "read-write"
                    path = "./cache"
                    "#,
            )?;

            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"

                    [labels]
                    "0x0000000000000000000000000000000000000002" = "Bob Updated"
                    "0x0000000000000000000000000000000000000003" = "Charlie"

                    [[profile.default.fs_permissions]]
                    access = "read"
                    path = "./test"
                    "#,
            )?;

            let config = Config::load().unwrap();

            // Labels should be merged
            assert_eq!(
                config.labels.get(
                    &"0x0000000000000000000000000000000000000001"
                        .parse::<alloy_primitives::Address>()
                        .unwrap()
                ),
                Some(&"Alice".to_string())
            );
            assert_eq!(
                config.labels.get(
                    &"0x0000000000000000000000000000000000000002"
                        .parse::<alloy_primitives::Address>()
                        .unwrap()
                ),
                Some(&"Bob Updated".to_string())
            );
            assert_eq!(
                config.labels.get(
                    &"0x0000000000000000000000000000000000000003"
                        .parse::<alloy_primitives::Address>()
                        .unwrap()
                ),
                Some(&"Charlie".to_string())
            );

            // fs_permissions array is now concatenated with addmerge (base + local)
            assert_eq!(config.fs_permissions.permissions.len(), 3); // 2 from base + 1 from local
            // Check that all permissions are present
            assert!(
                config
                    .fs_permissions
                    .permissions
                    .iter()
                    .any(|p| p.path.to_str().unwrap() == "./src")
            );
            assert!(
                config
                    .fs_permissions
                    .permissions
                    .iter()
                    .any(|p| p.path.to_str().unwrap() == "./cache")
            );
            assert!(
                config
                    .fs_permissions
                    .permissions
                    .iter()
                    .any(|p| p.path.to_str().unwrap() == "./test")
            );

            Ok(())
        });
    }

    #[test]
    fn test_inheritance_with_compilation_settings() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default]
                    solc = "0.8.19"
                    evm_version = "paris"
                    via_ir = false
                    optimizer = true
                    optimizer_runs = 200

                    [profile.default.optimizer_details]
                    peephole = true
                    inliner = false
                    jumpdest_remover = true
                    order_literals = false
                    deduplicate = true
                    cse = true
                    constant_optimizer = true
                    yul = true

                    [profile.default.optimizer_details.yul_details]
                    stack_allocation = true
                    optimizer_steps = "dhfoDgvulfnTUtnIf"
                    "#,
            )?;

            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"
                    evm_version = "shanghai"  # Override
                    optimizer_runs = 1000  # Override

                    [profile.default.optimizer_details]
                    inliner = true  # Override
                    # Rest inherited
                    "#,
            )?;

            let config = Config::load().unwrap();

            // Check compilation settings
            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
            assert_eq!(config.evm_version, EvmVersion::Shanghai);
            assert_eq!(config.via_ir, false);
            assert_eq!(config.optimizer, Some(true));
            assert_eq!(config.optimizer_runs, Some(1000));

            // Check optimizer details - the table is actually merged
            let details = config.optimizer_details.as_ref().unwrap();
            assert_eq!(details.peephole, Some(true));
            assert_eq!(details.inliner, Some(true));
            assert_eq!(details.jumpdest_remover, None);
            assert_eq!(details.order_literals, None);
            assert_eq!(details.deduplicate, Some(true));
            assert_eq!(details.cse, Some(true));
            assert_eq!(details.constant_optimizer, None);
            assert_eq!(details.yul, Some(true));

            // Check yul details - inherited from base
            if let Some(yul_details) = details.yul_details.as_ref() {
                assert_eq!(yul_details.stack_allocation, Some(true));
                assert_eq!(yul_details.optimizer_steps, Some("dhfoDgvulfnTUtnIf".to_string()));
            }

            Ok(())
        });
    }

    #[test]
    fn test_inheritance_with_remappings() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default]
                    remappings = [
                        "forge-std/=lib/forge-std/src/",
                        "@openzeppelin/=lib/openzeppelin-contracts/",
                        "ds-test/=lib/ds-test/src/"
                    ]
                    auto_detect_remappings = false
                    "#,
            )?;

            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"
                    remappings = [
                        "@custom/=lib/custom/",
                        "ds-test/=lib/forge-std/lib/ds-test/src/"  # Note: This will be added alongside base remappings
                    ]
                    "#,
            )?;

            let config = Config::load().unwrap();

            // Remappings array is now concatenated with admerge (base + local)
            assert!(config.remappings.iter().any(|r| r.to_string().contains("@custom/")));
            assert!(config.remappings.iter().any(|r| r.to_string().contains("ds-test/")));
            assert!(config.remappings.iter().any(|r| r.to_string().contains("forge-std/")));
            assert!(config.remappings.iter().any(|r| r.to_string().contains("@openzeppelin/")));

            // auto_detect_remappings should be inherited
            assert!(!config.auto_detect_remappings);

            Ok(())
        });
    }

    #[test]
    fn test_inheritance_with_multiple_profiles_and_single_file() {
        figment::Jail::expect_with(|jail| {
            // Create base config with prod and test profiles
            jail.create_file(
                "base.toml",
                r#"
                    [profile.prod]
                    optimizer = true
                    optimizer_runs = 10000
                    via_ir = true

                    [profile.test]
                    optimizer = false

                    [profile.test.fuzz]
                    runs = 100
                    "#,
            )?;

            // Local config inherits from base for prod profile
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.prod]
                    extends = "base.toml"
                    evm_version = "shanghai"  # Additional setting

                    [profile.test]
                    extends = "base.toml"

                    [profile.test.fuzz]
                    runs = 500  # Override
                    "#,
            )?;

            // Test prod profile
            jail.set_env("FOUNDRY_PROFILE", "prod");
            let config = Config::load().unwrap();
            assert_eq!(config.optimizer, Some(true));
            assert_eq!(config.optimizer_runs, Some(10000));
            assert_eq!(config.via_ir, true);
            assert_eq!(config.evm_version, EvmVersion::Shanghai);

            // Test test profile
            jail.set_env("FOUNDRY_PROFILE", "test");
            let config = Config::load().unwrap();
            assert_eq!(config.optimizer, Some(false));
            assert_eq!(config.fuzz.runs, 500);

            Ok(())
        });
    }

    #[test]
    fn test_inheritance_with_multiple_profiles_and_files() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "prod.toml",
                r#"
                    [profile.prod]
                    optimizer = true
                    optimizer_runs = 20000
                    gas_limit = 50000000
                    "#,
            )?;
            jail.create_file(
                "dev.toml",
                r#"
                    [profile.dev]
                    optimizer = true
                    optimizer_runs = 333
                    gas_limit = 555555
                    "#,
            )?;

            // Local config with only both profiles
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.dev]
                    extends = "dev.toml"
                    sender = "0x0000000000000000000000000000000000000001"

                    [profile.prod]
                    extends = "prod.toml"
                    sender = "0x0000000000000000000000000000000000000002"
                    "#,
            )?;

            // Test that prod profile correctly inherits even without a default profile
            jail.set_env("FOUNDRY_PROFILE", "dev");
            let config = Config::load().unwrap();
            assert_eq!(config.optimizer, Some(true));
            assert_eq!(config.optimizer_runs, Some(333));
            assert_eq!(config.gas_limit, 555555.into());
            assert_eq!(
                config.sender,
                "0x0000000000000000000000000000000000000001"
                    .parse::<alloy_primitives::Address>()
                    .unwrap()
            );

            // Test that prod profile correctly inherits even without a default profile
            jail.set_env("FOUNDRY_PROFILE", "prod");
            let config = Config::load().unwrap();
            assert_eq!(config.optimizer, Some(true));
            assert_eq!(config.optimizer_runs, Some(20000));
            assert_eq!(config.gas_limit, 50000000.into());
            assert_eq!(
                config.sender,
                "0x0000000000000000000000000000000000000002"
                    .parse::<alloy_primitives::Address>()
                    .unwrap()
            );

            Ok(())
        });
    }

    #[test]
    fn test_extends_strategy_extend_arrays() {
        figment::Jail::expect_with(|jail| {
            // Create base config with arrays
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default]
                    libs = ["lib", "node_modules"]
                    ignored_error_codes = [5667, 1878]
                    optimizer_runs = 200
                    "#,
            )?;

            // Local config extends with extend-arrays strategy (concatenates arrays)
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"
                    libs = ["mylib", "customlib"]
                    ignored_error_codes = [1234]
                    optimizer_runs = 500
                    "#,
            )?;

            let config = Config::load().unwrap();

            // Arrays should be concatenated (base + local)
            assert_eq!(config.libs.len(), 4);
            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
            assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
            assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
            assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));

            assert_eq!(config.ignored_error_codes.len(), 3);
            assert!(
                config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
            ); // 5667
            assert!(
                config.ignored_error_codes.contains(&SolidityErrorCode::SpdxLicenseNotProvided)
            ); // 1878
            assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); // 1234 - generic

            // Non-array values should be replaced
            assert_eq!(config.optimizer_runs, Some(500));

            Ok(())
        });
    }

    #[test]
    fn test_extends_strategy_replace_arrays() {
        figment::Jail::expect_with(|jail| {
            // Create base config with arrays
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default]
                    libs = ["lib", "node_modules"]
                    ignored_error_codes = [5667, 1878]
                    optimizer_runs = 200
                    "#,
            )?;

            // Local config extends with replace-arrays strategy (replaces arrays entirely)
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = { path = "base.toml", strategy = "replace-arrays" }
                    libs = ["mylib", "customlib"]
                    ignored_error_codes = [1234]
                    optimizer_runs = 500
                    "#,
            )?;

            let config = Config::load().unwrap();

            // Arrays should be replaced entirely (only local values)
            assert_eq!(config.libs.len(), 2);
            assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
            assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
            assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
            assert!(!config.libs.iter().any(|l| l.to_str() == Some("node_modules")));

            assert_eq!(config.ignored_error_codes.len(), 1);
            assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); // 1234
            assert!(
                !config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
            ); // 5667

            // Non-array values should be replaced
            assert_eq!(config.optimizer_runs, Some(500));

            Ok(())
        });
    }

    #[test]
    fn test_extends_strategy_no_collision_success() {
        figment::Jail::expect_with(|jail| {
            // Create base config
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default]
                    optimizer = true
                    optimizer_runs = 200
                    src = "src"
                    "#,
            )?;

            // Local config extends with no-collision strategy and no conflicts
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = { path = "base.toml", strategy = "no-collision" }
                    test = "tests"
                    libs = ["lib"]
                    "#,
            )?;

            let config = Config::load().unwrap();

            // Values from base should be present
            assert_eq!(config.optimizer, Some(true));
            assert_eq!(config.optimizer_runs, Some(200));
            assert_eq!(config.src, PathBuf::from("src"));

            // Values from local should be present
            assert_eq!(config.test, PathBuf::from("tests"));
            assert_eq!(config.libs.len(), 1);
            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));

            Ok(())
        });
    }

    #[test]
    fn test_extends_strategy_no_collision_error() {
        figment::Jail::expect_with(|jail| {
            // Create base config
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default]
                    optimizer = true
                    optimizer_runs = 200
                    libs = ["lib", "node_modules"]
                    "#,
            )?;

            // Local config extends with no-collision strategy but has conflicts
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = { path = "base.toml", strategy = "no-collision" }
                    optimizer_runs = 500
                    libs = ["mylib"]
                    "#,
            )?;

            // Loading should fail due to key collision
            let result = Config::load();

            if let Ok(config) = result {
                panic!(
                    "Expected error but got config with optimizer_runs: {:?}, libs: {:?}",
                    config.optimizer_runs, config.libs
                );
            }

            let err = result.unwrap_err();
            let err_str = err.to_string();
            assert!(
                err_str.contains("Key collision detected") || err_str.contains("collision"),
                "Error message doesn't mention collision: {err_str}"
            );

            Ok(())
        });
    }

    #[test]
    fn test_extends_both_syntaxes() {
        figment::Jail::expect_with(|jail| {
            // Create base config
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default]
                    libs = ["lib"]
                    optimizer = true
                    "#,
            )?;

            // Test 1: Simple string syntax (should use default extend-arrays)
            jail.create_file(
                "foundry_string.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"
                    libs = ["custom"]
                    "#,
            )?;

            // Test 2: Object syntax with explicit strategy
            jail.create_file(
                "foundry_object.toml",
                r#"
                    [profile.default]
                    extends = { path = "base.toml", strategy = "replace-arrays" }
                    libs = ["custom"]
                    "#,
            )?;

            // Test string syntax (default extend-arrays)
            jail.set_env("FOUNDRY_CONFIG", "foundry_string.toml");
            let config = Config::load().unwrap();
            assert_eq!(config.libs.len(), 2); // Should concatenate
            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
            assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));

            // Test object syntax (replace-arrays)
            jail.set_env("FOUNDRY_CONFIG", "foundry_object.toml");
            let config = Config::load().unwrap();
            assert_eq!(config.libs.len(), 1); // Should replace
            assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
            assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));

            Ok(())
        });
    }

    #[test]
    fn test_extends_strategy_default_is_extend_arrays() {
        figment::Jail::expect_with(|jail| {
            // Create base config
            jail.create_file(
                "base.toml",
                r#"
                    [profile.default]
                    libs = ["lib", "node_modules"]
                    optimizer = true
                    "#,
            )?;

            // Local config extends without specifying strategy (should default to extend-arrays)
            jail.create_file(
                "foundry.toml",
                r#"
                    [profile.default]
                    extends = "base.toml"
                    libs = ["custom"]
                    optimizer = false
                    "#,
            )?;

            // Should work with default extend-arrays strategy
            let config = Config::load().unwrap();

            // Arrays should be concatenated by default
            assert_eq!(config.libs.len(), 3);
            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
            assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
            assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));

            // Non-array values should be replaced
            assert_eq!(config.optimizer, Some(false));

            Ok(())
        });
    }

    #[test]
    fn test_deprecated_deny_warnings_is_handled() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                deny_warnings = true
                "#,
            )?;
            let config = Config::load().unwrap();

            // Assert that the deprecated flag is correctly interpreted
            assert_eq!(config.deny, DenyLevel::Warnings);
            Ok(())
        });
    }

    #[test]
    fn warns_on_deprecated_keys_in_inactive_profiles() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "src"

                [profile.ci]
                deny_warnings = true
                "#,
            )?;

            let cfg = Config::load().unwrap();
            assert!(
                cfg.warnings.iter().any(|w| matches!(
                    w,
                    crate::Warning::DeprecatedKey { old, new }
                    if old == "deny_warnings" && new == "deny = warnings"
                )),
                "Expected deprecated key warning for inactive profile, got: {:?}",
                cfg.warnings
            );
            Ok(())
        });
    }

    #[test]
    fn warns_on_deprecated_profile_names() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.cancun]
                "#,
            )?;

            let cfg = Config::load().unwrap();
            assert!(
                cfg.warnings.iter().any(|w| matches!(
                    w,
                    crate::Warning::DeprecatedKey { old, new }
                    if old == "cancun" && new == "evm_version = Cancun"
                )),
                "Expected deprecated profile-name warning, got: {:?}",
                cfg.warnings
            );
            Ok(())
        });
    }

    #[test]
    fn warns_on_unknown_keys_in_profile() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                unknown_key_xyz = 123
                "#,
            )?;

            let cfg = Config::load().unwrap();
            assert!(cfg.warnings.iter().any(
                |w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "unknown_key_xyz")
            ));
            Ok(())
        });
    }

    #[test]
    fn no_unknown_key_warning_for_network_field() {
        // Regression test: `network` is a flattened `Option` field of `NetworkConfigs`. It must
        // not trigger an unknown-key warning, regardless of whether it is set.
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                network = "tempo"
                "#,
            )?;

            let cfg = Config::load().unwrap();
            assert!(
                !cfg.warnings.iter().any(
                    |w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "network")
                ),
                "did not expect UnknownKey warning for `network`, got: {:?}",
                cfg.warnings
            );
            Ok(())
        });
    }

    #[test]
    fn no_unknown_key_warning_for_legacy_tempo_alias() {
        // Regression test: the legacy `tempo = true` alias must keep working without warnings.
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                tempo = true
                "#,
            )?;

            let cfg = Config::load().unwrap();
            assert!(
                !cfg.warnings
                    .iter()
                    .any(|w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "tempo")),
                "did not expect UnknownKey warning for `tempo`, got: {:?}",
                cfg.warnings
            );
            Ok(())
        });
    }

    #[test]
    fn fails_on_ambiguous_version_in_compilation_restrictions() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "src"

                [[profile.default.compilation_restrictions]]
                paths = "src/*.sol"
                version = "0.8.11"
                "#,
            )?;

            let err = Config::load().expect_err("expected bare version to fail");
            let err_msg = err.to_string();
            assert!(
                err_msg.contains("Invalid version format '0.8.11'")
                    && err_msg.contains("Bare version numbers are ambiguous"),
                "Expected error about ambiguous version, got: {err_msg}"
            );

            Ok(())
        });
    }

    #[test]
    fn accepts_explicit_version_requirements() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "src"

                [[profile.default.compilation_restrictions]]
                paths = "src/*.sol"
                version = "=0.8.11"

                [[profile.default.compilation_restrictions]]
                paths = "test/*.sol"
                version = ">=0.8.11"
                "#,
            )?;

            let config = Config::load().expect("should accept explicit version requirements");
            assert_eq!(config.compilation_restrictions.len(), 2);

            Ok(())
        });
    }

    #[test]
    fn warns_on_unknown_keys_in_all_config_sections() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "src"
                unknown_profile_key = "should_warn"

                # Standalone sections with unknown keys
                [fmt]
                line_length = 120
                unknown_fmt_key = "should_warn"

                [lint]
                severity = ["high"]
                unknown_lint_key = "should_warn"

                [doc]
                out = "docs"
                unknown_doc_key = "should_warn"

                [fuzz]
                runs = 256
                unknown_fuzz_key = "should_warn"

                [invariant]
                runs = 256
                unknown_invariant_key = "should_warn"

                [symbolic]
                enabled = true
                depth = 128
                unknown_symbolic_key = "should_warn"

                [mutation]
                unknown_mutation_key = "should_warn"

                [vyper]
                unknown_vyper_key = "should_warn"

                [bind_json]
                out = "bindings.sol"
                unknown_bind_json_key = "should_warn"

                # Nested profile sections with unknown keys
                [profile.default.fmt]
                line_length = 100
                unknown_nested_fmt_key = "should_warn"

                [profile.default.lint]
                severity = ["low"]
                unknown_nested_lint_key = "should_warn"

                [profile.default.doc]
                out = "documentation"
                unknown_nested_doc_key = "should_warn"

                [profile.default.fuzz]
                runs = 512
                unknown_nested_fuzz_key = "should_warn"

                [profile.default.invariant]
                runs = 512
                unknown_nested_invariant_key = "should_warn"

                [profile.default.symbolic]
                max_paths = 512
                unknown_nested_symbolic_key = "should_warn"

                [profile.default.mutation]
                unknown_nested_mutation_key = "should_warn"

                [profile.default.vyper]
                unknown_nested_vyper_key = "should_warn"

                [profile.default.bind_json]
                out = "nested_bindings.sol"
                unknown_nested_bind_json_key = "should_warn"

                # Array sections with unknown keys
                [[profile.default.compilation_restrictions]]
                paths = "src/*.sol"
                unknown_compilation_key = "should_warn"

                [[profile.default.additional_compiler_profiles]]
                name = "via-ir"
                via_ir = true
                unknown_compiler_profile_key = "should_warn"
                "#,
            )?;

            let cfg = Config::load().unwrap();

            // Expected warnings for profile-level unknown key
            assert!(
                cfg.warnings.iter().any(|w| matches!(
                    w,
                    crate::Warning::UnknownKey { key, .. } if key == "unknown_profile_key"
                )),
                "Expected warning for 'unknown_profile_key' in profile, got: {:?}",
                cfg.warnings
            );

            // Expected warnings for standalone sections
            let standalone_expected = [
                ("unknown_fmt_key", "fmt"),
                ("unknown_lint_key", "lint"),
                ("unknown_doc_key", "doc"),
                ("unknown_fuzz_key", "fuzz"),
                ("unknown_invariant_key", "invariant"),
                ("unknown_symbolic_key", "symbolic"),
                ("unknown_mutation_key", "mutation"),
                ("unknown_vyper_key", "vyper"),
                ("unknown_bind_json_key", "bind_json"),
            ];

            for (expected_key, expected_section) in standalone_expected {
                assert!(
                    cfg.warnings.iter().any(|w| matches!(
                        w,
                        crate::Warning::UnknownSectionKey { key, section, .. }
                        if key == expected_key && section == expected_section
                    )),
                    "Expected warning for '{}' in standalone section '{}', got: {:?}",
                    expected_key,
                    expected_section,
                    cfg.warnings
                );
            }

            // Expected warnings for nested profile sections
            let nested_expected = [
                ("unknown_nested_fmt_key", "fmt"),
                ("unknown_nested_lint_key", "lint"),
                ("unknown_nested_doc_key", "doc"),
                ("unknown_nested_fuzz_key", "fuzz"),
                ("unknown_nested_invariant_key", "invariant"),
                ("unknown_nested_symbolic_key", "symbolic"),
                ("unknown_nested_mutation_key", "mutation"),
                ("unknown_nested_vyper_key", "vyper"),
                ("unknown_nested_bind_json_key", "bind_json"),
            ];

            for (expected_key, expected_section) in nested_expected {
                assert!(
                    cfg.warnings.iter().any(|w| matches!(
                        w,
                        crate::Warning::UnknownSectionKey { key, section, .. }
                        if key == expected_key && section == expected_section
                    )),
                    "Expected warning for '{}' in nested section '{}', got: {:?}",
                    expected_key,
                    expected_section,
                    cfg.warnings
                );
            }

            // Expected warnings for array item sections
            let array_expected = [
                ("unknown_compilation_key", "compilation_restrictions"),
                ("unknown_compiler_profile_key", "additional_compiler_profiles"),
            ];

            for (expected_key, expected_section) in array_expected {
                assert!(
                    cfg.warnings.iter().any(|w| matches!(
                        w,
                        crate::Warning::UnknownSectionKey { key, section, .. }
                        if key == expected_key && section == expected_section
                    )),
                    "Expected warning for '{}' in array section '{}', got: {:?}",
                    expected_key,
                    expected_section,
                    cfg.warnings
                );
            }

            // Verify total count of unknown key warnings
            let unknown_key_warnings: Vec<_> = cfg
                .warnings
                .iter()
                .filter(|w| {
                    matches!(w, crate::Warning::UnknownKey { .. })
                        || matches!(w, crate::Warning::UnknownSectionKey { .. })
                })
                .collect();

            // 1 profile key + 9 standalone + 9 nested + 2 array = 21 total
            assert_eq!(
                unknown_key_warnings.len(),
                21,
                "Expected 21 unknown key warnings (1 profile + 9 standalone + 9 nested + 2 array), got {}: {:?}",
                unknown_key_warnings.len(),
                unknown_key_warnings
            );

            Ok(())
        });
    }

    #[test]
    fn warns_on_unknown_keys_in_extended_config() {
        figment::Jail::expect_with(|jail| {
            // Create base config with unknown keys
            jail.create_file(
                "base.toml",
                r#"
                [profile.default]
                optimizer_runs = 800
                unknown_base_profile_key = "should_warn"

                [lint]
                severity = ["high"]
                unknown_base_lint_key = "should_warn"

                [fmt]
                line_length = 100
                unknown_base_fmt_key = "should_warn"
                "#,
            )?;

            // Create local config that extends base with its own unknown keys
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                extends = "base.toml"
                src = "src"
                unknown_local_profile_key = "should_warn"

                [lint]
                unknown_local_lint_key = "should_warn"

                [fuzz]
                runs = 512
                unknown_local_fuzz_key = "should_warn"

                [[profile.default.compilation_restrictions]]
                paths = "src/*.sol"
                unknown_local_restriction_key = "should_warn"
                "#,
            )?;

            let cfg = Config::load().unwrap();

            // Verify base config values are inherited
            assert_eq!(cfg.optimizer_runs, Some(800));

            // Unknown keys from both base and local configs should be detected.
            // Note: Due to how figment merges configs before validation, the source
            // will show the local config file for all warnings. This is a known
            // limitation - proper source attribution for extended configs would
            // require validating each file before the merge.

            // Verify all expected unknown keys are detected
            let expected_unknown_keys = ["unknown_base_profile_key", "unknown_local_profile_key"];
            for expected_key in expected_unknown_keys {
                assert!(
                    cfg.warnings.iter().any(|w| matches!(
                        w,
                        crate::Warning::UnknownKey { key, .. } if key == expected_key
                    )),
                    "Expected warning for '{}', got: {:?}",
                    expected_key,
                    cfg.warnings
                );
            }

            let expected_section_keys = [
                ("unknown_base_lint_key", "lint"),
                ("unknown_base_fmt_key", "fmt"),
                ("unknown_local_lint_key", "lint"),
                ("unknown_local_fuzz_key", "fuzz"),
                ("unknown_local_restriction_key", "compilation_restrictions"),
            ];
            for (expected_key, expected_section) in expected_section_keys {
                assert!(
                    cfg.warnings.iter().any(|w| matches!(
                        w,
                        crate::Warning::UnknownSectionKey { key, section, .. }
                        if key == expected_key && section == expected_section
                    )),
                    "Expected warning for '{}' in section '{}', got: {:?}",
                    expected_key,
                    expected_section,
                    cfg.warnings
                );
            }

            // Verify total: 2 profile keys + 5 section keys = 7 warnings
            let unknown_warnings: Vec<_> = cfg
                .warnings
                .iter()
                .filter(|w| {
                    matches!(w, crate::Warning::UnknownKey { .. })
                        || matches!(w, crate::Warning::UnknownSectionKey { .. })
                })
                .collect();
            assert_eq!(
                unknown_warnings.len(),
                7,
                "Expected 7 unknown key warnings, got {}: {:?}",
                unknown_warnings.len(),
                unknown_warnings
            );

            Ok(())
        });
    }

    // Test for issue #12844: FOUNDRY_PROFILE=nonexistent should warn and fall back to default.
    #[test]
    fn warns_on_unknown_profile() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "src"
                "#,
            )?;

            jail.set_env("FOUNDRY_PROFILE", "nonexistent");
            let cfg = Config::load().expect("expected unknown profile to fall back to default");
            assert_eq!(cfg.profile, Config::DEFAULT_PROFILE);
            assert!(
                cfg.warnings.iter().any(|w| matches!(
                    w,
                    crate::Warning::UnknownProfile { profile } if profile == "nonexistent"
                )),
                "Expected UnknownProfile warning, got: {:?}",
                cfg.warnings
            );

            Ok(())
        });
    }

    // Test for issue #13316: vyper config keys should not trigger unknown key warnings
    #[test]
    fn no_false_warnings_for_vyper_config_keys() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "src"

                [vyper]
                optimize = "O1"
                path = "/usr/bin/vyper"
                experimental_codegen = true
                venom_experimental = false
                debug = true
                enable_decimals = true

                [vyper.venom]
                disable_cse = true
                inline_threshold = 15
                "#,
            )?;

            let cfg = Config::load().unwrap();
            // None of the valid vyper keys should trigger warnings
            let vyper_warnings: Vec<_> = cfg
                .warnings
                .iter()
                .filter(|w| {
                    matches!(
                        w,
                        crate::Warning::UnknownSectionKey { section, .. } if section == "vyper"
                    )
                })
                .collect();

            assert!(
                vyper_warnings.is_empty(),
                "Valid vyper keys should not trigger warnings, got: {vyper_warnings:?}"
            );

            Ok(())
        });
    }

    // Test for issue #13316: vyper config in profile should not trigger false warnings
    #[test]
    fn no_false_warnings_for_nested_vyper_config_keys() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "src"

                [profile.default.vyper]
                opt_level = "s"
                path = "/opt/vyper/bin/vyper"
                experimental_codegen = false
                debug = true
                enable_decimals = true
                venom = { disable_sccp = true, disable_mem2var = false }
                "#,
            )?;

            let cfg = Config::load().unwrap();
            // None of the valid vyper keys should trigger warnings
            let vyper_warnings: Vec<_> = cfg
                .warnings
                .iter()
                .filter(|w| {
                    matches!(
                        w,
                        crate::Warning::UnknownSectionKey { section, .. } if section == "vyper"
                    )
                })
                .collect();

            assert!(
                vyper_warnings.is_empty(),
                "Valid nested vyper keys should not trigger warnings, got: {vyper_warnings:?}"
            );

            Ok(())
        });
    }

    // Test for issue #13316: inline vyper config format should not trigger false warnings
    // This matches the exact format used in https://github.com/pcaversaccio/snekmate
    #[test]
    fn no_false_warnings_for_inline_vyper_config() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "src"
                vyper = { optimize = "gas", debug = true }

                [profile.default-venom]
                vyper = { opt_level = "O2", experimental_codegen = true, venom = { disable_cse = true } }

                [profile.ci-venom]
                vyper = { venom_experimental = true, enable_decimals = true }
                "#,
            )?;

            let cfg = Config::load().unwrap();
            let vyper_warnings: Vec<_> = cfg
                .warnings
                .iter()
                .filter(|w| {
                    matches!(
                        w,
                        crate::Warning::UnknownSectionKey { section, .. } if section == "vyper"
                    )
                })
                .collect();

            assert!(
                vyper_warnings.is_empty(),
                "Valid inline vyper config should not trigger warnings, got: {vyper_warnings:?}"
            );

            Ok(())
        });
    }

    // Test for issue #13316: unknown vyper keys should still warn
    #[test]
    fn warns_on_unknown_vyper_keys() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "src"

                [vyper]
                optimize = "gas"
                unknown_vyper_option = true
                "#,
            )?;

            let cfg = Config::load().unwrap();
            assert!(
                cfg.warnings.iter().any(|w| matches!(
                    w,
                    crate::Warning::UnknownSectionKey { key, section, .. }
                    if key == "unknown_vyper_option" && section == "vyper"
                )),
                "Unknown vyper key should trigger warning, got: {:?}",
                cfg.warnings
            );

            Ok(())
        });
    }

    // Test for issue #12844: known profile should work
    #[test]
    fn succeeds_on_known_profile() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "src"

                [profile.ci]
                src = "src"
                fuzz = { runs = 10000 }
                "#,
            )?;

            jail.set_env("FOUNDRY_PROFILE", "ci");
            let config = Config::load().expect("known profile should work");
            assert_eq!(config.profile.as_str(), "ci");
            assert_eq!(config.fuzz.runs, 10000);

            Ok(())
        });
    }

    // Test for issue #12963: nested lib configs should fallback to default profile
    // when they don't define the requested profile
    #[test]
    fn nested_lib_config_falls_back_to_default_profile() {
        figment::Jail::expect_with(|jail| {
            // Create a lib directory with only default profile
            let lib_path = jail.directory().join("lib/mylib");
            std::fs::create_dir_all(&lib_path).unwrap();
            jail.create_file(
                "lib/mylib/foundry.toml",
                r#"
                [profile.default]
                src = "contracts"
                "#,
            )?;

            // Set a profile that doesn't exist in the lib
            jail.set_env("FOUNDRY_PROFILE", "ci");

            // load_with_root_and_fallback should succeed and fall back to default
            let config = Config::load_with_root_and_fallback(&lib_path)
                .expect("lib config should load with fallback");
            assert_eq!(config.profile, Config::DEFAULT_PROFILE);
            assert_eq!(config.src.as_os_str(), "contracts");

            Ok(())
        });
    }

    // Test for issue #12963: nested lib configs should use requested profile if it exists
    #[test]
    fn nested_lib_config_uses_profile_if_exists() {
        figment::Jail::expect_with(|jail| {
            // Create a lib directory with both default and ci profiles
            let lib_path = jail.directory().join("lib/mylib");
            std::fs::create_dir_all(&lib_path).unwrap();
            jail.create_file(
                "lib/mylib/foundry.toml",
                r#"
                [profile.default]
                src = "contracts"

                [profile.ci]
                src = "contracts"
                fuzz = { runs = 5000 }
                "#,
            )?;

            // Set a profile that exists in the lib
            jail.set_env("FOUNDRY_PROFILE", "ci");

            // load_with_root_and_fallback should use the ci profile
            let config = Config::load_with_root_and_fallback(&lib_path)
                .expect("lib config should load with profile");
            assert_eq!(config.profile.as_str(), "ci");
            assert_eq!(config.fuzz.runs, 5000);

            Ok(())
        });
    }

    // Test for issue #13170: profile names with hyphens should work correctly
    #[test]
    fn succeeds_on_hyphenated_profile_name() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "src"

                [profile.ci-venom]
                src = "src"
                fuzz = { runs = 7500 }

                [profile.default-venom]
                src = "src"
                fuzz = { runs = 8000 }
                "#,
            )?;

            // Test ci-venom profile
            jail.set_env("FOUNDRY_PROFILE", "ci-venom");
            let config = Config::load().expect("hyphenated profile should work");
            assert_eq!(config.profile.as_str(), "ci-venom");
            assert_eq!(config.fuzz.runs, 7500);

            // Test default-venom profile
            jail.set_env("FOUNDRY_PROFILE", "default-venom");
            let config = Config::load().expect("hyphenated profile should work");
            assert_eq!(config.profile.as_str(), "default-venom");
            assert_eq!(config.fuzz.runs, 8000);

            // Verify the profiles list contains hyphenated names
            assert!(
                config.profiles.iter().any(|p| p.as_str() == "ci-venom"),
                "profiles should contain 'ci-venom', got: {:?}",
                config.profiles
            );
            assert!(
                config.profiles.iter().any(|p| p.as_str() == "default-venom"),
                "profiles should contain 'default-venom', got: {:?}",
                config.profiles
            );

            Ok(())
        });
    }

    #[test]
    fn standalone_section_name_can_be_used_as_profile_name() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.symbolic]
                eth-rpc-url = "https://example.com/"
                "#,
            )?;
            jail.set_env("FOUNDRY_PROFILE", "symbolic");

            let config = Config::load().unwrap();
            assert_eq!(config.profile.as_str(), "symbolic");
            assert_eq!(config.eth_rpc_url.as_deref(), Some("https://example.com/"));

            Ok(())
        });
    }

    // Test for issue #13170: hyphenated profile with nested config keys
    #[test]
    fn hyphenated_profile_with_nested_sections() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default]
                src = "src"

                [profile.ci-venom]
                src = "src"
                optimizer_runs = 500

                [profile.ci-venom.fuzz]
                runs = 10000
                max_test_rejects = 350000

                [profile.ci-venom.invariant]
                runs = 375
                depth = 500
                "#,
            )?;

            jail.set_env("FOUNDRY_PROFILE", "ci-venom");
            let config =
                Config::load().expect("hyphenated profile with nested sections should work");
            assert_eq!(config.profile.as_str(), "ci-venom");
            assert_eq!(config.optimizer_runs, Some(500));
            assert_eq!(config.fuzz.runs, 10000);
            assert_eq!(config.fuzz.max_test_rejects, 350000);
            assert_eq!(config.invariant.runs, 375);
            assert_eq!(config.invariant.depth, 500);

            Ok(())
        });
    }

    #[test]
    fn coverage_section_in_profile() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default.coverage]
                report = ["summary", "lcov"]
                lcov_version = "2.2.0"
                ir_minimum = true
                report_file = "out/lcov.info"
                include_libs = true
                exclude_tests = true
                skip_files = ["test/**", "src/mocks/**"]
                "#,
            )?;
            let config = Config::load_with_root(jail.directory()).unwrap();
            assert_eq!(
                config.coverage.report,
                vec![CoverageReportKind::Summary, CoverageReportKind::Lcov]
            );
            assert_eq!(config.coverage.lcov_version, semver::Version::new(2, 2, 0));
            assert!(config.coverage.ir_minimum);
            assert_eq!(
                config.coverage.report_file.as_deref(),
                Some(std::path::Path::new("out/lcov.info"))
            );
            assert!(config.coverage.include_libs);
            assert!(config.coverage.exclude_tests);
            assert_eq!(
                config.coverage.skip_files,
                vec!["test/**".to_string(), "src/mocks/**".to_string()]
            );
            Ok(())
        });
    }

    #[test]
    fn coverage_standalone_section_falls_back_to_default_profile() {
        figment::Jail::expect_with(|jail| {
            // Standalone `[coverage]` should populate the active profile,
            // matching how `[fuzz]` / `[invariant]` work.
            jail.create_file(
                "foundry.toml",
                r#"
                [coverage]
                skip_files = ["script/**"]
                exclude_tests = true
                "#,
            )?;
            let config = Config::load_with_root(jail.directory()).unwrap();
            assert_eq!(config.coverage.skip_files, vec!["script/**".to_string()]);
            assert!(config.coverage.exclude_tests);
            // Untouched fields keep their defaults.
            assert_eq!(config.coverage.report, vec![CoverageReportKind::Summary]);
            assert_eq!(config.coverage.lcov_version, semver::Version::new(1, 0, 0));
            Ok(())
        });
    }

    #[test]
    fn coverage_per_profile_overrides_default() {
        figment::Jail::expect_with(|jail| {
            jail.create_file(
                "foundry.toml",
                r#"
                [profile.default.coverage]
                skip_files = ["script/**"]

                [profile.ci.coverage]
                skip_files = ["test/**", "lib/**"]
                exclude_tests = true
                "#,
            )?;

            // Default profile sees the default-profile coverage block.
            let config = Config::load_with_root(jail.directory()).unwrap();
            assert_eq!(config.coverage.skip_files, vec!["script/**".to_string()]);
            assert!(!config.coverage.exclude_tests);

            // CI profile sees its own override.
            jail.set_env("FOUNDRY_PROFILE", "ci");
            let config = Config::load_with_root(jail.directory()).unwrap();
            assert_eq!(
                config.coverage.skip_files,
                vec!["test/**".to_string(), "lib/**".to_string()]
            );
            assert!(config.coverage.exclude_tests);
            Ok(())
        });
    }
}
