use forge_lint::{
    linter::Lint,
    sol::{self, SolLint},
};
use foundry_config::{
    DenyLevel, LintSeverity, LinterConfig, SolidityErrorCode, lint::LintSpecificConfig,
};

mod geiger;

const CONTRACT: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

struct _PascalCaseInfo { uint256 a; }
uint256 constant screaming_snake_case_info = 0;

contract ContractWithLints {
    uint256 VARIABLE_MIXED_CASE_INFO;

    function incorrectShiftHigh() public pure {
        uint256 result = 8;
        assembly { result := shr(result, 8) }
    }
    function divideBeforeMultiplyMedium() public {
        (1 / 2) * 3;
    }
    function unoptimizedHashGas(uint256 a, uint256 b) public view {
        keccak256(abi.encodePacked(a, b));
    }
    function FUNCTION_MIXED_CASE_INFO() public { VARIABLE_MIXED_CASE_INFO = 1; }
}
"#;

const OTHER_CONTRACT: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// forge-lint: disable-next-line
import { ContractWithLints } from "./ContractWithLints.sol";

contract OtherContractWithLints {
    function functionMIXEDCaseInfo() public { uint256 x = 1; }
}
"#;

const ONLY_IMPORTS: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// forge-lint: disable-next-line
import { ContractWithLints } from "./ContractWithLints.sol";

import { _PascalCaseInfo } from "./ContractWithLints.sol";
import "./ContractWithLints.sol";

contract Dummy {
    bool foo;
}
"#;

const DEFAULT_INFO_LINTS_IMPORT: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract UnusedSymbol {}
"#;

const DEFAULT_INFO_LINTS: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { UnusedSymbol } from "./DefaultInfoLintsImport.sol";

contract DefaultInfoLints {
    function BAD_CASE() public { uint256 x = 1; }
}
"#;

const COUNTER_A: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract CounterA {
    uint256 public CounterA_Fail_Lint;
}
"#;

const COUNTER_B: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract CounterB {
    uint256 public CounterB_Fail_Lint;
}
"#;

const COUNTER_WITH_CONST: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

uint256 constant MAX = 1000000;

contract Counter {
    uint256 public number;

    function setNumber(uint256 newNumber) public {
        number = newNumber;
    }

    function increment() public {
        number++;
    }
}
"#;

const COUNTER_TEST_WITH_CONST: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import { Counter, MAX } from "../src/Counter.sol";

contract CounterTest {
  Counter public counter;

  function setUp() public {
    counter = new Counter();
  }

  function testFuzz_setNumber(uint256[MAX] calldata numbers) public {
    for (uint256 i = 0; i < numbers.length; ++i) {
      counter.setNumber(numbers[i]);
    }
  }
}
"#;

const MULTI_CONTRACT_FILE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IToken {
    function transfer(address to, uint256 amount) external returns (bool);
}

library MathLib {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        return a + b;
    }
}

contract FirstContract {
    uint256 public value;
    
    function setValue(uint256 _value) public {
        value = _value;
    }
}

abstract contract BaseContract {
    function baseFunction() public virtual;
}

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
}

contract SecondContract {
    address public owner;
    
    constructor() {
        owner = msg.sender;
    }
}

library StringLib {
    function toUpperCase(string memory str) internal pure returns (string memory) {
        return str;
    }
}

abstract contract AbstractStorage {
    mapping(address => uint256) internal balances;
    
    function getBalance(address account) public view virtual returns (uint256);
}

interface Token {
    function transfer(address to, uint256 amount) external returns (bool);
}
"#;

const SOLO_INTERFACES: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface ERC20 {
    function balanceOf(address account) external view returns (uint256);
}

interface IToken {
    function transfer(address to, uint256 amount) external returns (bool);
}
"#;

forgetest!(lint_does_not_write_artifacts, |prj, cmd| {
    use std::fs;

    prj.add_source("LintTarget", "contract LintTarget {}");
    let artifact = prj.root().join("out/LintTarget.sol/LintTarget.json");

    cmd.arg("lint").assert_success();
    assert!(!artifact.exists());

    fs::create_dir_all(artifact.parent().unwrap()).unwrap();
    fs::write(&artifact, b"sentinel").unwrap();

    cmd.forge_fuse().arg("lint").assert_success();
    assert_eq!(fs::read(artifact).unwrap(), b"sentinel");
});

forgetest!(can_use_config, |prj, cmd| {
    prj.add_source("ContractWithLints", CONTRACT);
    prj.add_source("OtherContractWithLints", OTHER_CONTRACT);

    // Check config for `severity` and `exclude`
    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![LintSeverity::High, LintSeverity::Med],
            exclude_lints: vec!["incorrect-shift".into()],
            ignore: vec![],
            lint_on_build: true,
            ..Default::default()
        };
    });
    cmd.arg("lint").assert_success().stderr_eq(str![[r#"
warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision
   [FILE]:16:9
   │
16 │         (1 / 2) * 3;
   │         ━━━━━━━━━━━
   │
   ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply


"#]]);
});

forgetest!(can_use_config_ignore, |prj, cmd| {
    prj.add_source("ContractWithLints", CONTRACT);
    prj.add_source("OtherContract", OTHER_CONTRACT);

    // Check config for `ignore`
    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![],
            exclude_lints: vec![],
            ignore: vec!["src/ContractWithLints.sol".into()],
            lint_on_build: true,
            ..Default::default()
        };
    });
    cmd.arg("lint").assert_success().stderr_eq(str![[r#"
note[mixed-case-function]: function names should use mixedCase
  [FILE]:9:14
  │
9 │     function functionMIXEDCaseInfo() public { uint256 x = 1; }
  │              ━━━━━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCaseInfo`
  │
  ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function


"#]]);

    // Check config again, ignoring all files
    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![],
            exclude_lints: vec![],
            ignore: vec!["src/ContractWithLints.sol".into(), "src/OtherContract.sol".into()],
            lint_on_build: true,
            ..Default::default()
        };
    });
    cmd.forge_fuse().arg("lint").assert_success().stderr_eq(str![[r#"
nothing to lint

"#]]);
});

forgetest!(default_lint_severity_includes_info, |prj, cmd| {
    prj.add_source("DefaultInfoLintsImport", DEFAULT_INFO_LINTS_IMPORT);
    prj.add_source("DefaultInfoLints", DEFAULT_INFO_LINTS);

    cmd.arg("lint").assert_success().stderr_eq(str![[r#"
note[mixed-case-function]: function names should use mixedCase
  [FILE]:8:14
  │
8 │     function BAD_CASE() public { uint256 x = 1; }
  │              ━━━━━━━━ help: consider using: `badCase`
  │
  ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function

note[unused-import]: unused imports should be removed
  [FILE]:5:10
  │
5 │ import { UnusedSymbol } from "./DefaultInfoLintsImport.sol";
  │          ━━━━━━━━━━━━
  │
  ╰ help: https://getfoundry.sh/forge/linting/unused-import


"#]]);
});

forgetest!(can_use_config_mixed_case_exception, |prj, cmd| {
    prj.add_source("ContractWithLints", CONTRACT);
    prj.add_source("OtherContract", OTHER_CONTRACT);

    // Check config for `ignore`
    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![],
            exclude_lints: vec![],
            ignore: vec!["src/ContractWithLints.sol".into()],
            lint_on_build: true,
            lint_specific: LintSpecificConfig {
                mixed_case_exceptions: vec!["MIXED".to_string()],
                ..Default::default()
            },
        };
    });
    cmd.arg("lint").assert_success().stderr_eq(str![[""]]);
});

forgetest!(multi_contract_file_no_exceptions, |prj, cmd| {
    prj.add_source("MixedFile", MULTI_CONTRACT_FILE);

    // Without exceptions, should flag all 8 contract-like items
    prj.update_config(|config| {
        config.lint = LinterConfig {
            lint_on_build: true,
            severity: vec![
                LintSeverity::High,
                LintSeverity::Med,
                LintSeverity::Low,
                LintSeverity::Info,
            ],
            ..Default::default()
        };
    });

    let output = cmd.arg("lint").assert_success();
    let stderr = String::from_utf8_lossy(&output.get_output().stderr);

    // Should see 9 instances of multi-contract-file lint
    assert_eq!(stderr.matches("note[multi-contract-file]").count(), 9);
    assert!(stderr.contains("IToken"));
    assert!(stderr.contains("IERC20"));
    assert!(stderr.contains("Token"));
    assert!(stderr.contains("MathLib"));
    assert!(stderr.contains("StringLib"));
    assert!(stderr.contains("BaseContract"));
    assert!(stderr.contains("AbstractStorage"));
    assert!(stderr.contains("FirstContract"));
    assert!(stderr.contains("SecondContract"));
});

forgetest!(multi_contract_file_interface_exception, |prj, cmd| {
    use foundry_config::lint::ContractException;

    prj.add_source("MixedFile", MULTI_CONTRACT_FILE);

    // With interface exception, should flag 6 items
    prj.update_config(|config| {
        config.lint = LinterConfig {
            lint_on_build: true,
            severity: vec![
                LintSeverity::High,
                LintSeverity::Med,
                LintSeverity::Low,
                LintSeverity::Info,
            ],
            exclude_lints: vec!["interface-naming".into()],
            lint_specific: LintSpecificConfig {
                multi_contract_file_exceptions: vec![ContractException::Interface],
                ..Default::default()
            },
            ..Default::default()
        };
    });

    let output = cmd.arg("lint").assert_success();
    let stderr = String::from_utf8_lossy(&output.get_output().stderr);

    // Should see 6 instances (3 interfaces excluded: IToken, IERC20, Token)
    assert_eq!(stderr.matches("note[multi-contract-file]").count(), 6);
    assert!(!stderr.contains("IToken"));
    assert!(!stderr.contains("IERC20"));
    assert!(!stderr.contains("Token"));
    assert!(stderr.contains("MathLib"));
    assert!(stderr.contains("FirstContract"));
});

forgetest!(multi_contract_file_library_exception, |prj, cmd| {
    use foundry_config::lint::ContractException;

    prj.add_source("MixedFile", MULTI_CONTRACT_FILE);

    // With library exception, should flag 7 items
    prj.update_config(|config| {
        config.lint = LinterConfig {
            lint_on_build: true,
            severity: vec![
                LintSeverity::High,
                LintSeverity::Med,
                LintSeverity::Low,
                LintSeverity::Info,
            ],
            lint_specific: LintSpecificConfig {
                multi_contract_file_exceptions: vec![ContractException::Library],
                ..Default::default()
            },
            ..Default::default()
        };
    });

    let output = cmd.arg("lint").assert_success();
    let stderr = String::from_utf8_lossy(&output.get_output().stderr);

    // Should see 7 instances (2 libraries excluded)
    assert_eq!(stderr.matches("note[multi-contract-file]").count(), 7);
    assert!(stderr.contains("IToken"));
    assert!(!stderr.contains("MathLib"));
    assert!(!stderr.contains("StringLib"));
    assert!(stderr.contains("FirstContract"));
});

forgetest!(multi_contract_file_abstract_exception, |prj, cmd| {
    use foundry_config::lint::ContractException;

    prj.add_source("MixedFile", MULTI_CONTRACT_FILE);

    // With abstract contract exception, should flag 7 items
    prj.update_config(|config| {
        config.lint = LinterConfig {
            lint_on_build: true,
            severity: vec![
                LintSeverity::High,
                LintSeverity::Med,
                LintSeverity::Low,
                LintSeverity::Info,
            ],
            lint_specific: LintSpecificConfig {
                multi_contract_file_exceptions: vec![ContractException::AbstractContract],
                ..Default::default()
            },
            ..Default::default()
        };
    });

    let output = cmd.arg("lint").assert_success();
    let stderr = String::from_utf8_lossy(&output.get_output().stderr);

    // Should see 7 instances (2 abstract contracts excluded)
    assert_eq!(stderr.matches("note[multi-contract-file]").count(), 7);
    assert!(stderr.contains("IToken"));
    assert!(stderr.contains("MathLib"));
    assert!(stderr.contains("FirstContract"));
    assert!(!stderr.contains("BaseContract"));
    assert!(!stderr.contains("AbstractStorage"));
});

forgetest!(multi_contract_file_multiple_exceptions, |prj, cmd| {
    use foundry_config::lint::ContractException;

    prj.add_source("MixedFile", MULTI_CONTRACT_FILE);

    // With interface + library exceptions, should flag 4 items
    prj.update_config(|config| {
        config.lint = LinterConfig {
            lint_on_build: true,
            severity: vec![
                LintSeverity::High,
                LintSeverity::Med,
                LintSeverity::Low,
                LintSeverity::Info,
            ],
            exclude_lints: vec!["interface-naming".into()],
            lint_specific: LintSpecificConfig {
                multi_contract_file_exceptions: vec![
                    ContractException::Interface,
                    ContractException::Library,
                ],
                ..Default::default()
            },
            ..Default::default()
        };
    });

    let output = cmd.arg("lint").assert_success();
    let stderr = String::from_utf8_lossy(&output.get_output().stderr);

    // Should see 4 instances (3 interfaces + 2 libraries excluded)
    assert_eq!(stderr.matches("note[multi-contract-file]").count(), 4);
    assert!(!stderr.contains("IToken"));
    assert!(!stderr.contains("IERC20"));
    assert!(!stderr.contains("Token"));
    assert!(!stderr.contains("MathLib"));
    assert!(stderr.contains("BaseContract"));
    assert!(stderr.contains("FirstContract"));
});

forgetest!(multi_contract_file_all_exceptions, |prj, cmd| {
    use foundry_config::lint::ContractException;

    prj.add_source("MixedFile", MULTI_CONTRACT_FILE);

    // With all exceptions, should still flag 2 regular contracts
    prj.update_config(|config| {
        config.lint = LinterConfig {
            lint_on_build: true,
            severity: vec![
                LintSeverity::High,
                LintSeverity::Med,
                LintSeverity::Low,
                LintSeverity::Info,
            ],
            lint_specific: LintSpecificConfig {
                multi_contract_file_exceptions: vec![
                    ContractException::Interface,
                    ContractException::Library,
                    ContractException::AbstractContract,
                ],
                ..Default::default()
            },
            ..Default::default()
        };
    });

    let output = cmd.arg("lint").assert_success();
    let stderr = String::from_utf8_lossy(&output.get_output().stderr);

    // Should see 2 instances (only the 2 regular contracts)
    assert_eq!(stderr.matches("note[multi-contract-file]").count(), 2);
    assert!(!stderr.contains("IToken"));
    assert!(!stderr.contains("MathLib"));
    assert!(!stderr.contains("BaseContract"));
    assert!(stderr.contains("FirstContract"));
    assert!(stderr.contains("SecondContract"));
});

forgetest!(multi_contract_file_invalid_toml_value, |prj, cmd| {
    use std::fs;

    prj.add_source("Simple", "contract Simple {}");

    // Write invalid TOML config with invalid enum value
    let config_path = prj.root().join("foundry.toml");
    let invalid_config = r#"
[profile.default]
src = "src"
out = "out"
libs = ["lib"]

[profile.default.lint.lint_specific]
multi_contract_file_exceptions = ["interface", "bad_contract_type", "library"]
"#;

    fs::write(&config_path, invalid_config).unwrap();

    // Should fail with deserialization error
    let output = cmd.arg("lint").assert_failure();
    let stderr = String::from_utf8_lossy(&output.get_output().stderr);

    // Assert specific error message for invalid enum variant
    assert!(stderr.contains("unknown variant"));
    assert!(stderr.contains("expected `one of `interface`, `library`, `abstract_contract`"));
});

forgetest!(multi_contract_file_valid_toml_values, |prj, cmd| {
    use std::fs;

    prj.add_source("MixedFile", MULTI_CONTRACT_FILE);

    // Write valid TOML config with all valid enum values
    let config_path = prj.root().join("foundry.toml");
    let valid_config = r#"
[profile.default]
src = "src"
out = "out"
libs = ["lib"]

[profile.default.lint]
lint_on_build = true
severity = ["high", "medium", "low", "info"]

[profile.default.lint.lint_specific]
multi_contract_file_exceptions = ["interface", "library", "abstract_contract"]
"#;

    fs::write(&config_path, valid_config).unwrap();

    // Should succeed and only flag the 2 regular contracts
    let output = cmd.arg("lint").assert_success();
    let stderr = String::from_utf8_lossy(&output.get_output().stderr);

    assert_eq!(stderr.matches("note[multi-contract-file]").count(), 2);
    assert!(stderr.contains("FirstContract"));
    assert!(stderr.contains("SecondContract"));
});

forgetest!(interface_naming_fails_for_non_prefixed, |prj, cmd| {
    prj.add_source("MixedFile", MULTI_CONTRACT_FILE);

    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![],
            exclude_lints: vec!["multi-contract-file".into()],
            ignore: vec![],
            lint_on_build: true,
            ..Default::default()
        };
    });

    let output = cmd.arg("lint").assert_success();
    let stderr = String::from_utf8_lossy(&output.get_output().stderr);

    // Should flag only the interface that doesn't start with 'I': Token
    assert_eq!(stderr.matches("note[interface-naming]").count(), 1);
    assert!(stderr.contains("Token"));
});

forgetest!(interface_file_naming_fails_for_non_prefixed_file, |prj, cmd| {
    prj.add_source("SoloInterfaces", SOLO_INTERFACES);

    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![],
            exclude_lints: vec!["multi-contract-file".into()],
            ignore: vec![],
            lint_on_build: true,
            ..Default::default()
        };
    });

    let output = cmd.arg("lint").assert_success();
    let stderr = String::from_utf8_lossy(&output.get_output().stderr);

    // File name "SoloInterfaces" doesn't start with 'I', so interface-file-naming should trigger
    assert_eq!(stderr.matches("note[interface-file-naming]").count(), 1);
    // ERC20 is not prefixed with 'I', so interface-naming should trigger
    assert_eq!(stderr.matches("note[interface-naming]").count(), 1);
    assert!(stderr.contains("ERC20"));
});

forgetest!(can_override_config_severity, |prj, cmd| {
    prj.add_source("ContractWithLints", CONTRACT);
    prj.add_source("OtherContractWithLints", OTHER_CONTRACT);

    // Override severity
    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![LintSeverity::High, LintSeverity::Med],
            exclude_lints: vec![],
            ignore: vec!["src/ContractWithLints.sol".into()],
            lint_on_build: true,
            ..Default::default()
        };
    });
    cmd.arg("lint").args(["--severity", "info"]).assert_success().stderr_eq(str![[r#"
note[mixed-case-function]: function names should use mixedCase
  [FILE]:9:14
  │
9 │     function functionMIXEDCaseInfo() public { uint256 x = 1; }
  │              ━━━━━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCaseInfo`
  │
  ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function


"#]]);
});

forgetest!(can_override_config_path, |prj, cmd| {
    prj.add_source("ContractWithLints", CONTRACT);
    prj.add_source("OtherContractWithLints", OTHER_CONTRACT);

    // Override excluded files
    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![LintSeverity::High, LintSeverity::Med],
            exclude_lints: vec!["incorrect-shift".into()],
            ignore: vec!["src/ContractWithLints.sol".into()],
            lint_on_build: true,
            ..Default::default()
        };
    });
    cmd.arg("lint").arg("src/ContractWithLints.sol").assert_success().stderr_eq(str![[r#"
warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision
   [FILE]:16:9
   │
16 │         (1 / 2) * 3;
   │         ━━━━━━━━━━━
   │
   ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply


"#]]);
});

forgetest!(can_override_config_lint, |prj, cmd| {
    prj.add_source("ContractWithLints", CONTRACT);
    prj.add_source("OtherContractWithLints", OTHER_CONTRACT);

    // Override excluded lints
    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![LintSeverity::High, LintSeverity::Med],
            exclude_lints: vec!["incorrect-shift".into()],
            ignore: vec![],
            lint_on_build: true,
            ..Default::default()
        };
    });
    cmd.arg("lint").args(["--only-lint", "incorrect-shift"]).assert_success().stderr_eq(str![[
        r#"
warning[incorrect-shift]: the order of args in a shift operation is incorrect
   [FILE]:13:30
   │
13 │         assembly { result := shr(result, 8) }
   │                              ━━━━━━━━━━━━━━
   │
   ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift


"#
    ]]);
});

forgetest!(build_runs_linter_by_default, |prj, cmd| {
    prj.add_source("ContractWithLints", CONTRACT);

    // Configure linter to show only medium severity lints
    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![LintSeverity::Med],
            exclude_lints: vec!["incorrect-shift".into()],
            ignore: vec![],
            lint_on_build: true,
            ..Default::default()
        };
    });

    // Run forge build and expect linting output before compilation
    cmd.arg("build").assert_success().stderr_eq(str![[r#"
warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision
   [FILE]:16:9
   │
16 │         (1 / 2) * 3;
   │         ━━━━━━━━━━━
   │
   ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply


"#]]).stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful with warnings:
Warning (6133): Statement has no effect.
  [FILE]:16:9:
   |
16 |         (1 / 2) * 3;
   |         ^^^^^^^^^^^

Warning (2018): Function state mutability can be restricted to pure
  [FILE]:15:5:
   |
15 |     function divideBeforeMultiplyMedium() public {
   |     ^ (Relevant source part starts here and spans across multiple lines).

Warning (2018): Function state mutability can be restricted to pure
  [FILE]:18:5:
   |
18 |     function unoptimizedHashGas(uint256 a, uint256 b) public view {
   |     ^ (Relevant source part starts here and spans across multiple lines).


"#]]);
});

forgetest!(build_respects_quiet_flag_for_linting, |prj, cmd| {
    prj.add_source("ContractWithLints", CONTRACT);

    // Configure linter to show medium severity lints
    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![LintSeverity::Med],
            exclude_lints: vec!["incorrect-shift".into()],
            ignore: vec![],
            lint_on_build: true,
            ..Default::default()
        };
    });

    // Run forge build with --quiet flag - should not show linting output
    cmd.arg("build").arg("--quiet").assert_success().stderr_eq(str![[""]]).stdout_eq(str![[""]]);
});

forgetest!(build_with_json_uses_json_linter_output, |prj, cmd| {
    prj.add_source("ContractWithLints", CONTRACT);

    // Configure linter to show medium severity lints
    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![LintSeverity::Med],
            exclude_lints: vec!["incorrect-shift".into()],
            ignore: vec![],
            lint_on_build: true,
            ..Default::default()
        };
    });

    // Run forge build with --json flag - should use JSON formatter for linting
    let output = cmd.arg("build").arg("--json").assert_success();

    // Should contain JSON linting output
    let stderr = String::from_utf8_lossy(&output.get_output().stderr);
    assert!(stderr.contains("\"code\""));
    assert!(stderr.contains("divide-before-multiply"));

    // Should also contain JSON compilation output
    let stdout = String::from_utf8_lossy(&output.get_output().stdout);
    assert!(stdout.contains("\"errors\""));
    assert!(stdout.contains("\"sources\""));
});

forgetest!(build_respects_lint_on_build_false, |prj, cmd| {
    prj.add_source("ContractWithLints", CONTRACT);

    // Configure linter with medium severity lints but disable lint_on_build
    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![LintSeverity::Med],
            exclude_lints: vec!["incorrect-shift".into()],
            ignore: vec![],
            lint_on_build: false,
            ..Default::default()
        };
    });

    // Run forge build - should NOT show linting output because lint_on_build is false
    cmd.arg("build").assert_success().stderr_eq(str![[""]]).stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful with warnings:
Warning (6133): Statement has no effect.
  [FILE]:16:9:
   |
16 |         (1 / 2) * 3;
   |         ^^^^^^^^^^^

Warning (2018): Function state mutability can be restricted to pure
  [FILE]:15:5:
   |
15 |     function divideBeforeMultiplyMedium() public {
   |     ^ (Relevant source part starts here and spans across multiple lines).

Warning (2018): Function state mutability can be restricted to pure
  [FILE]:18:5:
   |
18 |     function unoptimizedHashGas(uint256 a, uint256 b) public view {
   |     ^ (Relevant source part starts here and spans across multiple lines).


"#]]);
});

// Lint diagnostics produced during `forge build` must stream to stderr through the emitter
// installed inside `SolidityLinter::lint`.
forgetest!(build_emits_lint_diagnostics, |prj, cmd| {
    prj.add_source("CounterAWithLints", COUNTER_A);

    prj.update_config(|config| {
        config.lint.severity = vec![LintSeverity::Info];
    });

    cmd.arg("build").assert_success().stderr_eq(str![[r#"
note[mixed-case-variable]: mutable variables should use mixedCase
  [FILE]:6:20
  │
6 │     uint256 public CounterA_Fail_Lint;
  │                    ━━━━━━━━━━━━━━━━━━ help: consider using: `counterAFailLint`
  │
  ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable


"#]]);
});

forgetest!(build_no_lint_flag_skips_lint, |prj, cmd| {
    prj.add_source("ContractWithLints", CONTRACT);

    // Configure linter with medium severity lints and ensure lint_on_build is enabled
    // so the only thing skipping the lint step is the `--no-lint` flag.
    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![LintSeverity::Med],
            exclude_lints: vec!["incorrect-shift".into()],
            ignore: vec![],
            lint_on_build: true,
            ..Default::default()
        };
    });

    cmd.args(["build", "--no-lint"]).assert_success().stderr_eq(str![[r#""#]]).stdout_eq(str![[
        r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful with warnings:
Warning (6133): Statement has no effect.
  [FILE]:16:9:
   |
16 |         (1 / 2) * 3;
   |         ^^^^^^^^^^^

Warning (2018): Function state mutability can be restricted to pure
  [FILE]:15:5:
   |
15 |     function divideBeforeMultiplyMedium() public {
   |     ^ (Relevant source part starts here and spans across multiple lines).

Warning (2018): Function state mutability can be restricted to pure
  [FILE]:18:5:
   |
18 |     function unoptimizedHashGas(uint256 a, uint256 b) public view {
   |     ^ (Relevant source part starts here and spans across multiple lines).


"#
    ]]);
});

// Denied lint diagnostics are expected failures and must not be presented as internal errors.
forgetest!(build_denied_lints_do_not_emit_internal_failure_notice, |prj, cmd| {
    prj.add_source("CounterAWithLints", COUNTER_A);

    prj.update_config(|config| {
        config.lint.severity = vec![LintSeverity::Info];
        config.deny = DenyLevel::Notes;
    });

    cmd.arg("build").assert_failure().stderr_eq(str![[r#"
note[mixed-case-variable]: mutable variables should use mixedCase
  [FILE]:6:20
  │
6 │     uint256 public CounterA_Fail_Lint;
  │                    ━━━━━━━━━━━━━━━━━━ help: consider using: `counterAFailLint`
  │
  ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable

Error: post-build lint step failed

Context:
- aborting due to 1 linter note(s)

"#]]);
});

// Solar currently rejects enum `@param` tags while solc accepts them. This recoverable frontend
// diagnostic must not prevent type-dependent late lints from running.
forgetest!(build_lints_after_recoverable_solar_diagnostic, |prj, cmd| {
    prj.add_source(
        "RecoverableSolarDiagnostic",
        r#"
/// @param First The first choice.
enum Choice { First }

contract RecoverableSolarDiagnostic {
    function chainId() public view returns (uint64) {
        return uint64(block.chainid);
    }
}
"#,
    );

    prj.update_config(|config| {
        config.lint.severity = vec![LintSeverity::Med];
        config.deny = DenyLevel::Warnings;
    });

    cmd.arg("build").assert_failure().stderr_eq(str![[r#"
warning[unsafe-typecast]: typecasts that can truncate values should be checked
  [FILE]:9:16
  │
9 │         return uint64(block.chainid);
  │                ━━━━━━━━━━━━━━━━━━━━━
  │
  ├ note: consider disabling this lint if you're certain the cast is safe
  │ [..]
  │       // casting to 'uint64' is safe because [explain why]
  │       // forge-lint: disable-next-line(unsafe-typecast)
  │ [..]
  │ [..]
  ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast

Error: post-build lint step failed

Context:
- aborting due to 1 linter warning(s)

"#]]);
});

// Same setup as above, but `--no-lint` skips the lint step so the failure notice never fires.
forgetest!(build_no_lint_flag_does_not_emit_lint_failure_notice, |prj, cmd| {
    prj.add_source("CounterAWithLints", COUNTER_A);

    prj.update_config(|config| {
        config.lint.severity = vec![LintSeverity::Info];
        config.deny = DenyLevel::Notes;
    });

    cmd.args(["build", "--no-lint"]).assert_success().stderr_eq(str![[r#""#]]);
});

forgetest!(can_process_inline_config_regardless_of_input_order, |prj, cmd| {
    prj.add_source("ContractWithLints", CONTRACT);
    prj.add_source("OtherContractWithLints", OTHER_CONTRACT);
    cmd.arg("lint").assert_success();

    prj.add_source("OtherContractWithLints", OTHER_CONTRACT);
    prj.add_source("ContractWithLints", CONTRACT);
    cmd.arg("lint").assert_success();
});

// <https://github.com/foundry-rs/foundry/issues/11080>
forgetest!(can_use_only_lint_with_multilint_passes, |prj, cmd| {
    prj.add_source("ContractWithLints", CONTRACT);
    prj.add_source("OnlyImports", ONLY_IMPORTS);
    cmd.arg("lint").args(["--only-lint", "unused-import"]).assert_success().stderr_eq(str![[r#"
note[unused-import]: unused imports should be removed
  [FILE]:8:10
  │
8 │ import { _PascalCaseInfo } from "./ContractWithLints.sol";
  │          ━━━━━━━━━━━━━━━
  │
  ╰ help: https://getfoundry.sh/forge/linting/unused-import


"#]]);
});

// <https://github.com/foundry-rs/foundry/issues/11234>
forgetest!(can_lint_only_built_files, |prj, cmd| {
    prj.add_source("CounterAWithLints", COUNTER_A);
    prj.add_source("CounterBWithLints", COUNTER_B);

    prj.update_config(|config| {
        config.lint.severity = vec![LintSeverity::Info];
    });

    // Both contracts should be linted on build. Redact contract as order is not guaranteed.
    cmd.forge_fuse().args(["build"]).assert_success().stderr_eq(str![[r#"
note[mixed-case-variable]: mutable variables should use mixedCase
...
note[mixed-case-variable]: mutable variables should use mixedCase
...
"#]]);

    // Only contract CounterBWithLints that we build should be linted.
    let args = ["build", "src/CounterBWithLints.sol"];
    cmd.forge_fuse().args(args).assert_success().stderr_eq(str![[r#"
note[mixed-case-variable]: mutable variables should use mixedCase
  [FILE]:6:20
  │
6 │     uint256 public CounterB_Fail_Lint;
  │                    ━━━━━━━━━━━━━━━━━━ help: consider using: `counterBFailLint`
  │
  ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable


"#]]);
});

// <https://github.com/foundry-rs/foundry/issues/11392>
forgetest!(can_lint_param_constants, |prj, cmd| {
    prj.add_source("Counter", COUNTER_WITH_CONST);
    prj.add_test("CounterTest", COUNTER_TEST_WITH_CONST);

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

"#]]);
});

// <https://github.com/foundry-rs/foundry/issues/11460>
forgetest!(lint_json_output_no_ansi_escape_codes, |prj, cmd| {
    prj.add_source(
        "UnwrappedModifierTest",
        r#"
        // SPDX-License-Identifier: MIT
        pragma solidity ^0.8.0;

        contract UnwrappedModifierTest {
            mapping(address => bool) isOwner;

            modifier onlyOwner() {
                require(isOwner[msg.sender], "Not owner");
                require(msg.sender != address(0), "Zero address");
                _;
            }

            function doSomething() public onlyOwner {}
        }
            "#,
    );

    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![LintSeverity::CodeSize],
            exclude_lints: vec![],
            ignore: vec![],
            lint_on_build: true,
            ..Default::default()
        };
    });

    // should produce clean JSON without ANSI escape sequences (for the url nor the snippets)
    cmd.arg("lint")
        .arg("--json")
        .assert_json_stdout_with_status(
            true,
            str![[r#"
{
  "$message_type": "diagnostic",
  "message": "wrap modifier logic to reduce code size",
  "code": {
    "code": "unwrapped-modifier-logic",
    "explanation": null
  },
  "level": "note",
  "spans": [
    {
      "file_name": "src/UnwrappedModifierTest.sol",
      "byte_start": 174,
      "byte_end": 355,
      "line_start": 8,
      "line_end": 12,
      "column_start": 13,
      "column_end": 14,
      "is_primary": true,
      "text": [
        {
          "text": "            modifier onlyOwner() {",
          "highlight_start": 13,
          "highlight_end": 35
        },
        {
          "text": "                require(isOwner[msg.sender], \"Not owner\");",
          "highlight_start": 1,
          "highlight_end": 59
        },
        {
          "text": "                require(msg.sender != address(0), \"Zero address\");",
          "highlight_start": 1,
          "highlight_end": 67
        },
        {
          "text": "                _;",
          "highlight_start": 1,
          "highlight_end": 19
        },
        {
          "text": "            }",
          "highlight_start": 1,
          "highlight_end": 14
        }
      ],
      "label": null,
      "suggested_replacement": null,
      "suggestion_applicability": null,
      "expansion": null
    }
  ],
  "children": [
    {
      "message": "https://getfoundry.sh/forge/linting/unwrapped-modifier-logic",
      "code": null,
      "level": "help",
      "spans": [],
      "children": [],
      "rendered": null
    },
    {
      "message": "wrap modifier logic to reduce code size",
      "code": null,
      "level": "help",
      "spans": [
        {
          "file_name": "src/UnwrappedModifierTest.sol",
          "byte_start": 174,
          "byte_end": 355,
          "line_start": 8,
          "line_end": 12,
          "column_start": 13,
          "column_end": 14,
          "is_primary": true,
          "text": [
            {
              "text": "            modifier onlyOwner() {",
              "highlight_start": 13,
              "highlight_end": 35
            },
            {
              "text": "                require(isOwner[msg.sender], \"Not owner\");",
              "highlight_start": 1,
              "highlight_end": 59
            },
            {
              "text": "                require(msg.sender != address(0), \"Zero address\");",
              "highlight_start": 1,
              "highlight_end": 67
            },
            {
              "text": "                _;",
              "highlight_start": 1,
              "highlight_end": 19
            },
            {
              "text": "            }",
              "highlight_start": 1,
              "highlight_end": 14
            }
          ],
          "label": null,
          "suggested_replacement": "modifier onlyOwner() {\n                _onlyOwner();\n                _;\n            }\n\n            function _onlyOwner() internal {\n                require(isOwner[msg.sender], \"Not owner\");\n                require(msg.sender != address(0), \"Zero address\");\n            }",
          "suggestion_applicability": "MachineApplicable",
          "expansion": null
        }
      ],
      "children": [],
      "rendered": null
    }
  ],
  "rendered": "note[unwrapped-modifier-logic]: wrap modifier logic to reduce code size\n\nhelp: wrap modifier logic to reduce code size\n 9 +                 _onlyOwner();\n10 +                 _;\n11 +             }\n12 + \n13 +             function _onlyOwner() internal {\n14 +                 require(isOwner[msg.sender], \"Not owner\");\n15 +                 require(msg.sender != address(0), \"Zero address\");\n16 +             }\n   ╭▸ src/UnwrappedModifierTest.sol:8:13\n   │\n 8 │ ┏             modifier onlyOwner() {\n 9 │ ┃                 require(isOwner[msg.sender], \"Not owner\");\n10 │ ┃                 require(msg.sender != address(0), \"Zero address\");\n11 │ ┃                 _;\n12 │ ┃             }\n   │ ┗━━━━━━━━━━━━━┛\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic\n   ╭╴\n 8 ±             modifier onlyOwner() {\n   ╰╴\n"
}
"#]],
        )
        .stderr_eq("");
});

forgetest!(lint_json_compiler_error, |prj, cmd| {
    prj.add_source(
        "Broken",
        r#"pragma solidity ^0.8.30;

contract Broken {
    function f() public {
        uint256 value = missing;
    }
}
"#,
    );

    cmd.args(["lint", "--json"])
        .assert_json_stdout_with_status(false, str![[r#"
{
  "$message_type": "diagnostic",
  "message": "unresolved symbol `missing`",
  "code": null,
  "level": "error",
  "spans": [
    {
      "file_name": "src/Broken.sol",
      "byte_start": 140,
      "byte_end": 147,
      "line_start": 6,
      "line_end": 6,
      "column_start": 25,
      "column_end": 32,
      "is_primary": true,
      "text": [
        {
          "text": "        uint256 value = missing;",
          "highlight_start": 25,
          "highlight_end": 32
        }
      ],
      "label": null,
      "suggested_replacement": null,
      "suggestion_applicability": null,
      "expansion": null
    }
  ],
  "children": [],
  "rendered": "error: unresolved symbol `missing`\n  ╭▸ src/Broken.sol:6:25\n  │\n6 │         uint256 value = missing;\n  ╰╴                        ━━━━━━━\n\n"
}
"#]])
        .stderr_eq("");
});

forgetest!(can_fail_on_lints, |prj, cmd| {
    prj.add_source("ContractWithLints", CONTRACT);

    // -- LINT ALL SEVERITIES [OUTPUT: WARN + NOTE] ----------------------------

    cmd.forge_fuse().arg("lint").assert_success(); // DenyLevel::Never (default)

    prj.update_config(|config| {
        config.deny = DenyLevel::Warnings;
    });
    cmd.forge_fuse().arg("lint").assert_failure();

    prj.update_config(|config| {
        config.deny = DenyLevel::Notes;
    });
    cmd.forge_fuse().arg("lint").assert_failure();

    // cmd flags can override config
    prj.update_config(|config| {
        config.deny = DenyLevel::Never;
    });
    cmd.forge_fuse().args(["lint", "--deny warnings"]).assert_failure();
    cmd.forge_fuse().args(["lint", "--deny notes"]).assert_failure();

    // usage of `--deny-warnings` flag works, but emits a warning
    cmd.forge_fuse().args(["lint", "--deny-warnings"]).assert_failure().stderr_eq(str![[r#"
Warning: `--deny-warnings` is being deprecated in favor of `--deny warnings`.
...

"#]]);

    // usage of `deny_warnings` config works, but emits a warning
    prj.create_file(
        "foundry.toml",
        r#"
[profile.default]
deny_warnings = true
"#,
    );
    cmd.forge_fuse().arg("lint").assert_failure().stderr_eq(str![[r#"
Warning: Key `deny_warnings` is being deprecated in favor of `deny = warnings`. It will be removed in future versions.
...

"#]]);

    // -- ONLY LINT LOW SEVERITIES [OUTPUT: NOTE] ------------------------------

    prj.update_config(|config| {
        config.deny_warnings = false;
        config.deny = DenyLevel::Never;
        config.lint.severity = vec![LintSeverity::Info, LintSeverity::Gas, LintSeverity::CodeSize];
    });
    cmd.forge_fuse().arg("lint").assert_success();

    prj.update_config(|config| {
        config.deny = DenyLevel::Warnings;
    });
    cmd.forge_fuse().arg("lint").assert_success();

    prj.update_config(|config| {
        config.deny = DenyLevel::Notes;
    });
    cmd.forge_fuse().arg("lint").assert_failure();

    // cmd flags can override config
    prj.update_config(|config| {
        config.deny = DenyLevel::Never;
    });
    cmd.forge_fuse().args(["lint", "--deny notes"]).assert_failure();
});

// ------------------------------------------------------------------------------------------------

#[tokio::test]
async fn ensure_lint_rule_docs() {
    let client = reqwest::Client::new();
    let mut failures = Vec::new();

    for lint in registered_lints() {
        let url = lint.help();
        let response = match client.get(url).send().await {
            Ok(response) => response,
            Err(err) => {
                failures.push(format!("{} ({url}) could not be fetched: {err}", lint.id()));
                continue;
            }
        };

        if !response.status().is_success() {
            failures.push(format!("{} ({url}) returned HTTP {}", lint.id(), response.status()));
            continue;
        }

        let content = match response.text().await {
            Ok(content) => content.to_lowercase(),
            Err(err) => {
                failures
                    .push(format!("{} ({url}) response body could not be read: {err}", lint.id()));
                continue;
            }
        };

        let selector = lint.id().to_lowercase();
        let selector_with_space = selector.replace('-', " ");
        if !content.contains(&selector) && !content.contains(&selector_with_space) {
            failures.push(format!("{} ({url}) did not mention the lint id", lint.id()));
        }
    }

    if !failures.is_empty() {
        let mut msg = String::from(
            "Foundry Book lint validation failed. The following lint pages are missing or invalid:\n",
        );
        for failure in failures {
            msg.push_str(&format!("  - {failure}\n"));
        }
        msg.push_str("Please open a PR: https://github.com/foundry-rs/book");
        panic!("{msg}");
    }
}

#[test]
fn ensure_no_privileged_lint_id() {
    for lint in registered_lints() {
        assert_ne!(lint.id(), "all", "lint-id 'all' is reserved. Please use a different id");
    }
}

fn registered_lints() -> impl Iterator<Item = &'static SolLint> {
    sol::high::REGISTERED_LINTS
        .iter()
        .chain(sol::med::REGISTERED_LINTS)
        .chain(sol::low::REGISTERED_LINTS)
        .chain(sol::info::REGISTERED_LINTS)
        .chain(sol::gas::REGISTERED_LINTS)
        .chain(sol::codesize::REGISTERED_LINTS)
}

// <https://github.com/foundry-rs/foundry/issues/13107>
forgetest!(dependency_warnings_do_not_affect_lint_exit_code, |prj, cmd| {
    // Library with code that triggers a solc warning (unused local variable)
    const LIB_WITH_WARNING: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

library LibWithWarning {
    function foo() internal pure returns (uint256) {
        uint256 unusedVar = 42;
        return 1;
    }
}
"#;

    // Clean contract that imports the library but has no lint issues
    const CLEAN_CONTRACT: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { LibWithWarning } from "../lib/LibWithWarning.sol";

contract CleanContract {
    function bar() public pure returns (uint256) {
        return LibWithWarning.foo();
    }
}
"#;

    prj.add_lib("LibWithWarning", LIB_WITH_WARNING);
    prj.add_source("CleanContract", CLEAN_CONTRACT);

    // Ignore the solc warning so compilation succeeds, but it still gets counted in diagnostics
    prj.update_config(|config| {
        config.ignored_error_codes = vec![SolidityErrorCode::UnusedLocalVariable];
    });

    // Clear cache to force recompilation during lint
    prj.clear_cache();

    // Lint with deny = notes via CLI flag.
    // Should succeed because the linter only counts lint diagnostics, not build-phase warnings.
    cmd.args(["lint", "-D", "notes"]).assert_success();
});

forgetest!(skips_linting_for_old_solidity_versions, |prj, cmd| {
    const OLD_CONTRACT: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;

contract OldContract {
    uint256 VARIABLE_MIXED_CASE_INFO;

    function FUNCTION_MIXED_CASE_INFO() public {}
}
"#;

    // Add a contract with Solidity 0.7.x which has lint issues
    prj.add_source("OldContract", OLD_CONTRACT);
    prj.update_config(|config| {
        config.lint = LinterConfig {
            severity: vec![],
            exclude_lints: vec![],
            ignore: vec![],
            lint_on_build: true,
            ..Default::default()
        };
    });

    // Run forge build - should SUCCEED without linting
    cmd.arg("build").assert_success().stderr_eq(str![[
        r#"Warning: unable to lint. Solar only supports Solidity versions >=0.8.0

"#
    ]]);

    // Run forge lint - should FAIL
    cmd.forge_fuse().arg("lint").assert_failure().stderr_eq(str![[
        r#"Error: unable to lint. Solar only supports Solidity versions >=0.8.0

"#
    ]]);
});

const PRAGMA_INCONSISTENT_ALPHA: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Alpha {}
"#;

const PRAGMA_INCONSISTENT_BETA: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

contract Beta {}
"#;

forgetest!(pragma_inconsistent_cross_file, |prj, cmd| {
    prj.add_source("Alpha", PRAGMA_INCONSISTENT_ALPHA);
    prj.add_source("Beta", PRAGMA_INCONSISTENT_BETA);

    cmd.arg("lint").args(["--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(str![
        [r#"
note[pragma-inconsistent]: 2 different Solidity pragma version requirements are used: 0.8.20, ^0.8.20
  [FILE]:3:1
  │
3 │ pragma solidity ^0.8.20;
  │ ━━━━━━━━━━━━━━━━━━━━━━━━
  │
  ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent


"#]
    ]);
});

const PRAGMA_EXACT_A: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

contract A {}
"#;

const PRAGMA_EXACT_B: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

contract B {}
"#;

const PRAGMA_EXACT_C: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

contract C {}
"#;

const PRAGMA_CARET_A: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract A {}
"#;

const PRAGMA_CARET_B: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract B {}
"#;

const PRAGMA_CARET_C: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract C {}
"#;

const NO_PRAGMA_C: &str = r#"
// SPDX-License-Identifier: MIT

contract C {}
"#;

// Multiple files all using the exact same pragma must NOT warn.
forgetest!(pragma_inconsistent_consistent_exact_no_warning, |prj, cmd| {
    prj.add_source("A", PRAGMA_EXACT_A);
    prj.add_source("B", PRAGMA_EXACT_B);
    prj.add_source("C", PRAGMA_EXACT_C);

    cmd.arg("lint")
        .args(["--only-lint", "pragma-inconsistent"])
        .assert_success()
        .stderr_eq(str![[r#""#]]);
});

// Multiple files all using the exact same caret pragma must NOT warn.
forgetest!(pragma_inconsistent_consistent_caret_no_warning, |prj, cmd| {
    prj.add_source("A", PRAGMA_CARET_A);
    prj.add_source("B", PRAGMA_CARET_B);

    cmd.arg("lint")
        .args(["--only-lint", "pragma-inconsistent"])
        .assert_success()
        .stderr_eq(str![[r#""#]]);
});

// A single file in the project cannot conflict with itself.
forgetest!(pragma_inconsistent_single_file_no_warning, |prj, cmd| {
    prj.add_source("A", PRAGMA_CARET_A);

    cmd.arg("lint")
        .args(["--only-lint", "pragma-inconsistent"])
        .assert_success()
        .stderr_eq(str![[r#""#]]);
});

// Even files that share a requirement still emit when ANY other variant exists.
// Two files with `0.8.20` plus one file with `^0.8.20` => 3 emits total.
forgetest!(pragma_inconsistent_duplicates_among_conflict, |prj, cmd| {
    prj.add_source("A", PRAGMA_EXACT_A);
    prj.add_source("B", PRAGMA_EXACT_B);
    prj.add_source("C", PRAGMA_CARET_C);

    cmd.arg("lint").args(["--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(str![
        [r#"
note[pragma-inconsistent]: 2 different Solidity pragma version requirements are used: 0.8.20, ^0.8.20
  [FILE]:3:1
  │
3 │ pragma solidity 0.8.20;
  │ ━━━━━━━━━━━━━━━━━━━━━━━
  │
  ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent


"#]
    ]);
});

// Files without a `pragma solidity` directive must not affect the conflict computation.
// Note: `add_raw_source` is used here to bypass the helper that would otherwise inject a default
// `pragma solidity =<SOLC_VERSION>;` for files that omit one.
forgetest!(pragma_inconsistent_files_without_pragma, |prj, cmd| {
    prj.add_raw_source("A", PRAGMA_EXACT_A);
    prj.add_raw_source("B", PRAGMA_CARET_B);
    // C has no pragma at all; should be ignored by the cross-file check.
    prj.add_raw_source("C", NO_PRAGMA_C);

    cmd.arg("lint").args(["--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(str![
        [r#"
note[pragma-inconsistent]: 2 different Solidity pragma version requirements are used: 0.8.20, ^0.8.20
  [FILE]:3:1
  │
3 │ pragma solidity 0.8.20;
  │ ━━━━━━━━━━━━━━━━━━━━━━━
  │
  ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent


"#]
    ]);
});
