use super::*;

forgetest!(invariant_after_invariant, |prj, cmd| {
    prj.insert_vm();
    prj.insert_ds_test();

    prj.add_test(
        "InvariantAfterInvariant.t.sol",
        r#"
import { DSTest as Test } from "src/test.sol";

struct FuzzSelector {
    address addr;
    bytes4[] selectors;
}

contract AfterInvariantHandler {
    uint256 public count;

    function inc() external {
        count += 1;
    }
}

contract InvariantAfterInvariantTest is Test {
    AfterInvariantHandler handler;

    function setUp() public {
        handler = new AfterInvariantHandler();
    }

    function targetSelectors() public returns (FuzzSelector[] memory) {
        FuzzSelector[] memory targets = new FuzzSelector[](1);
        bytes4[] memory selectors = new bytes4[](1);
        selectors[0] = handler.inc.selector;
        targets[0] = FuzzSelector(address(handler), selectors);
        return targets;
    }

    function afterInvariant() public {
        require(handler.count() < 10, "afterInvariant failure");
    }

    /// forge-config: default.invariant.runs = 1
    /// forge-config: default.invariant.depth = 11
    function invariant_after_invariant_failure() public view {
        require(handler.count() < 20, "invariant after invariant failure");
    }

    /// forge-config: default.invariant.runs = 1
    /// forge-config: default.invariant.depth = 11
    function invariant_failure() public view {
        require(handler.count() < 9, "invariant failure");
    }

    /// forge-config: default.invariant.runs = 1
    /// forge-config: default.invariant.depth = 5
    function invariant_success() public view {
        require(handler.count() < 11, "invariant should not fail");
    }
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 3 tests for test/InvariantAfterInvariant.t.sol:InvariantAfterInvariantTest
[FAIL: afterInvariant failure]
	[SEQUENCE]
 invariant_after_invariant_failure() ([RUNS])

[STATS]

[FAIL: invariant failure]
	[SEQUENCE]
 invariant_failure() ([RUNS])

[STATS]

[PASS] invariant_success() ([RUNS])

[STATS]

Suite result: FAILED. 1 passed; 2 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 1 tests passed, 2 failed, 0 skipped (3 total tests)

Failing tests:
Encountered 2 failing tests in test/InvariantAfterInvariant.t.sol:InvariantAfterInvariantTest
[FAIL: afterInvariant failure]
	[SEQUENCE]
 invariant_after_invariant_failure() ([RUNS])
[FAIL: invariant failure]
	[SEQUENCE]
 invariant_failure() ([RUNS])

Encountered a total of 2 failing tests, 1 tests succeeded

Tip: Run `forge test --rerun` to retry only the 2 failed tests

[SEED] (use `--fuzz-seed` to reproduce)

"#]]);
});

forgetest_init!(invariant_assume, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.runs = 1;
        config.invariant.depth = 10;
        // Should not treat vm.assume as revert.
        config.invariant.fail_on_revert = true;
    });

    prj.add_test(
        "InvariantAssume.t.sol",
        r#"
import "forge-std/Test.sol";

contract Handler is Test {
    uint256 public count;
    function doSomething(uint256 param) public {
        vm.assume(param == 0);
        count++;
    }
}

contract InvariantAssume is Test {
    Handler handler;

    function setUp() public {
        handler = new Handler();
    }

    function invariant_dummy() public {}
}
"#,
    );

    assert_invariant(cmd.args(["test"])).success().stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!

Ran 1 test for test/InvariantAssume.t.sol:InvariantAssume
[PASS] invariant_dummy() ([RUNS])

[STATS]

Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests)

"#]]);

    // Test that max_assume_rejects is respected.
    prj.update_config(|config| {
        config.invariant.max_assume_rejects = 1;
    });

    assert_invariant(&mut cmd).failure().stdout_eq(str![[r#"
No files changed, compilation skipped

Ran 1 test for test/InvariantAssume.t.sol:InvariantAssume
[FAIL: `vm.assume` rejected too many inputs (1 allowed)] invariant_dummy() ([RUNS])

[STATS]

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/InvariantAssume.t.sol:InvariantAssume
[FAIL: `vm.assume` rejected too many inputs (1 allowed)] invariant_dummy() ([RUNS])

Encountered a total of 1 failing tests, 0 tests succeeded

Tip: Run `forge test --rerun` to retry only the 1 failed test

[SEED] (use `--fuzz-seed` to reproduce)

"#]]);
});

// https://github.com/foundry-rs/foundry/issues/5868
forgetest!(invariant_calldata_dictionary, |prj, cmd| {
    prj.insert_utils();
    prj.update_config(|config| {
        config.fuzz.seed = Some(U256::from(1));
        config.invariant.runs = 1000;
        config.invariant.depth = 20;
    });

    prj.add_test(
        "InvariantCalldataDictionary.t.sol",
        r#"
import "./utils/Test.sol";

struct FuzzSelector {
    address addr;
    bytes4[] selectors;
}

contract Owned {
    address public owner;
    address private ownerCandidate;

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    modifier onlyOwnerCandidate() {
        require(msg.sender == ownerCandidate);
        _;
    }

    function transferOwnership(address candidate) external onlyOwner {
        ownerCandidate = candidate;
    }

    function acceptOwnership() external onlyOwnerCandidate {
        owner = ownerCandidate;
    }
}

contract Handler is Test {
    Owned owned;

    constructor(Owned _owned) {
        owned = _owned;
    }

    function transferOwnership(address sender, address candidate) external {
        vm.assume(sender != address(0));
        vm.prank(sender);
        owned.transferOwnership(candidate);
    }

    function acceptOwnership(address sender) external {
        vm.assume(sender != address(0));
        vm.prank(sender);
        owned.acceptOwnership();
    }
}

contract InvariantCalldataDictionary is Test {
    address owner;
    Owned owned;
    Handler handler;
    address[] actors;

    function setUp() public {
        owner = address(this);
        owned = new Owned();
        handler = new Handler(owned);
        actors.push(owner);
        actors.push(address(777));
    }

    function targetSelectors() public returns (FuzzSelector[] memory) {
        FuzzSelector[] memory targets = new FuzzSelector[](1);
        bytes4[] memory selectors = new bytes4[](2);
        selectors[0] = handler.transferOwnership.selector;
        selectors[1] = handler.acceptOwnership.selector;
        targets[0] = FuzzSelector(address(handler), selectors);
        return targets;
    }

    function fixtureSender() external returns (address[] memory) {
        return actors;
    }

    function fixtureCandidate() external returns (address[] memory) {
        return actors;
    }

    function invariant_owner_never_changes() public {
        assertEq(owned.owner(), owner);
    }
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantCalldataDictionary.t.sol:InvariantCalldataDictionary
[FAIL: assertion failed]
	[SEQUENCE]
 invariant_owner_never_changes() ([RUNS])

...

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/InvariantCalldataDictionary.t.sol:InvariantCalldataDictionary
[FAIL: assertion failed]
	[SEQUENCE]
 invariant_owner_never_changes() ([RUNS])

Encountered a total of 1 failing tests, 0 tests succeeded

Tip: Run `forge test --rerun` to retry only the 1 failed test

[SEED] (use `--fuzz-seed` to reproduce)

"#]]);
});

forgetest_init!(invariant_custom_error, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.depth = 10;
        config.invariant.fail_on_revert = true;
    });

    prj.add_test(
        "InvariantCustomError.t.sol",
        r#"
import "forge-std/Test.sol";

contract ContractWithCustomError {
    error InvariantCustomError(uint256, string);

    function revertWithInvariantCustomError() external {
        revert InvariantCustomError(111, "custom");
    }
}

contract Handler is Test {
    ContractWithCustomError target;

    constructor() {
        target = new ContractWithCustomError();
    }

    function revertTarget() external {
        target.revertWithInvariantCustomError();
    }
}

contract InvariantCustomError is Test {
    Handler handler;

    function setUp() external {
        handler = new Handler();
    }

    function invariant_decode_error() public {}
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantCustomError.t.sol:InvariantCustomError
[FAIL: InvariantCustomError(111, "custom")]
	[SEQUENCE]
 invariant_decode_error() ([RUNS])

[STATS]

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/InvariantCustomError.t.sol:InvariantCustomError
[FAIL: InvariantCustomError(111, "custom")]
	[SEQUENCE]
 invariant_decode_error() ([RUNS])

Encountered a total of 1 failing tests, 0 tests succeeded

Tip: Run `forge test --rerun` to retry only the 1 failed test

[SEED] (use `--fuzz-seed` to reproduce)

"#]]);
});

forgetest_init!(invariant_excluded_senders, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.depth = 10;
        config.invariant.fail_on_revert = true;
    });

    prj.add_test(
        "InvariantExcludedSenders.t.sol",
        r#"
import "forge-std/Test.sol";

contract InvariantSenders {
    uint256 public count;
    function checkSender() external {
        require(msg.sender != 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D, "sender cannot be cheatcode address");
        require(msg.sender != 0x000000000000000000636F6e736F6c652e6c6f67, "sender cannot be console address");
        require(msg.sender != 0x4e59b44847b379578588920cA78FbF26c0B4956C, "sender cannot be CREATE2 deployer");
        count++;
    }
}

contract InvariantExcludedSendersTest is Test {
    InvariantSenders target;

    function setUp() public {
        target = new InvariantSenders();
    }

    function invariant_check_sender() public view {}
}
"#,
    );

    assert_invariant(cmd.args(["test"])).success().stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!

Ran 1 test for test/InvariantExcludedSenders.t.sol:InvariantExcludedSendersTest
[PASS] invariant_check_sender() ([RUNS])

[STATS]

Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests)

"#]]);
});

forgetest_init!(invariant_fixtures, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.runs = 1;
        config.invariant.depth = 100;
        // disable literals to test fixtures
        config.invariant.dictionary.max_fuzz_dictionary_literals = 0;
        config.fuzz.dictionary.max_fuzz_dictionary_literals = 0;
    });

    prj.add_test(
        "InvariantFixtures.t.sol",
        r#"
import "forge-std/Test.sol";

contract Target {
    bool ownerFound;
    bool amountFound;
    bool magicFound;
    bool keyFound;
    bool backupFound;
    bool extraStringFound;

    function fuzzWithFixtures(
        address owner_,
        uint256 _amount,
        int32 magic,
        bytes32 key,
        bytes memory backup,
        string memory extra
    ) external {
        if (owner_ == address(0x6B175474E89094C44Da98b954EedeAC495271d0F)) {
            ownerFound = true;
        }
        if (_amount == 1122334455) amountFound = true;
        if (magic == -777) magicFound = true;
        if (key == "abcd1234") keyFound = true;
        if (keccak256(backup) == keccak256("qwerty1234")) backupFound = true;
        if (keccak256(abi.encodePacked(extra)) == keccak256(abi.encodePacked("112233aabbccdd"))) {
            extraStringFound = true;
        }
    }

    function isCompromised() public view returns (bool) {
        return ownerFound && amountFound && magicFound && keyFound && backupFound && extraStringFound;
    }
}

/// Try to compromise target contract by finding all accepted values using fixtures.
contract InvariantFixtures is Test {
    Target target;
    address[] public fixture_owner_ = [address(0x6B175474E89094C44Da98b954EedeAC495271d0F)];
    uint256[] public fixture_amount = [1, 2, 1122334455];

    function setUp() public {
        target = new Target();
    }

    function fixtureMagic() external returns (int32[2] memory) {
        int32[2] memory magic;
        magic[0] = -777;
        magic[1] = 777;
        return magic;
    }

    function fixtureKey() external pure returns (bytes32[] memory) {
        bytes32[] memory keyFixture = new bytes32[](1);
        keyFixture[0] = "abcd1234";
        return keyFixture;
    }

    function fixtureBackup() external pure returns (bytes[] memory) {
        bytes[] memory backupFixture = new bytes[](1);
        backupFixture[0] = "qwerty1234";
        return backupFixture;
    }

    function fixtureExtra() external pure returns (string[] memory) {
        string[] memory extraFixture = new string[](1);
        extraFixture[0] = "112233aabbccdd";
        return extraFixture;
    }

    function invariant_target_not_compromised() public {
        assertEq(target.isCompromised(), false);
    }
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantFixtures.t.sol:InvariantFixtures
[FAIL: assertion failed: true != false]
	[SEQUENCE]
 invariant_target_not_compromised() ([RUNS])

[STATS]

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/InvariantFixtures.t.sol:InvariantFixtures
[FAIL: assertion failed: true != false]
	[SEQUENCE]
 invariant_target_not_compromised() ([RUNS])

Encountered a total of 1 failing tests, 0 tests succeeded

Tip: Run `forge test --rerun` to retry only the 1 failed test

[SEED] (use `--fuzz-seed` to reproduce)

"#]]);
});

forgetest_init!(invariant_breaks_without_fixtures, |prj, cmd| {
    prj.update_config(|config| {
        config.fuzz.seed = Some(U256::from(1));
        config.invariant.runs = 1;
        config.invariant.depth = 100;
    });

    prj.add_test(
        "InvariantLiterals.t.sol",
        r#"
import "forge-std/Test.sol";

contract Target {
    bool ownerFound;
    bool amountFound;
    bool magicFound;
    bool keyFound;
    bool backupFound;
    bool extraStringFound;

    function fuzzWithoutFixtures(
        address owner_,
        uint256 _amount,
        int32 magic,
        bytes32 key,
        bytes memory backup,
        string memory extra
    ) external {
        if (owner_ == address(0x6B175474E89094C44Da98b954EedeAC495271d0F)) {
            ownerFound = true;
        }
        if (_amount == 1122334455) amountFound = true;
        if (magic == -777) magicFound = true;
        if (key == "abcd1234") keyFound = true;
        if (keccak256(backup) == keccak256("qwerty1234")) backupFound = true;
        if (keccak256(abi.encodePacked(extra)) == keccak256(abi.encodePacked("112233aabbccdd"))) {
            extraStringFound = true;
        }
    }

    function isCompromised() public view returns (bool) {
        return ownerFound && amountFound && magicFound && keyFound && backupFound && extraStringFound;
    }
}

/// Try to compromise target contract by finding all accepted values without using fixtures.
contract InvariantLiterals is Test {
    Target target;

    function setUp() public {
        target = new Target();
    }

    function invariant_target_not_compromised() public {
        assertEq(target.isCompromised(), false);
    }
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantLiterals.t.sol:InvariantLiterals
[FAIL: assertion failed: true != false]
	[SEQUENCE]
 invariant_target_not_compromised() ([RUNS])

[STATS]

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/InvariantLiterals.t.sol:InvariantLiterals
[FAIL: assertion failed: true != false]
	[SEQUENCE]
 invariant_target_not_compromised() ([RUNS])

Encountered a total of 1 failing tests, 0 tests succeeded

Tip: Run `forge test --rerun` to retry only the 1 failed test

[SEED] (use `--fuzz-seed` to reproduce)

"#]]);
});

forgetest!(invariant_handler_failure, |prj, cmd| {
    prj.insert_utils();
    prj.update_config(|config| {
        config.invariant.fail_on_revert = true;
        config.invariant.runs = 1;
        config.invariant.depth = 10;
    });

    prj.add_test(
        "InvariantHandlerFailure.t.sol",
        r#"
import "./utils/Test.sol";

struct FuzzSelector {
    address addr;
    bytes4[] selectors;
}

contract Handler is Test {
    function doSomething() public {
        require(false, "failed on revert");
    }
}

contract InvariantHandlerFailure is Test {
    bytes4[] internal selectors;

    Handler handler;

    function targetSelectors() public returns (FuzzSelector[] memory) {
        FuzzSelector[] memory targets = new FuzzSelector[](1);
        bytes4[] memory selectors = new bytes4[](1);
        selectors[0] = handler.doSomething.selector;
        targets[0] = FuzzSelector(address(handler), selectors);
        return targets;
    }

    function setUp() public {
        handler = new Handler();
    }

    function statefulFuzz_BrokenInvariant() public {}
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantHandlerFailure.t.sol:InvariantHandlerFailure
[FAIL: failed on revert]
	[SEQUENCE]
 statefulFuzz_BrokenInvariant() ([RUNS])

[STATS]

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/InvariantHandlerFailure.t.sol:InvariantHandlerFailure
[FAIL: failed on revert]
	[SEQUENCE]
 statefulFuzz_BrokenInvariant() ([RUNS])

Encountered a total of 1 failing tests, 0 tests succeeded

Tip: Run `forge test --rerun` to retry only the 1 failed test

[SEED] (use `--fuzz-seed` to reproduce)

"#]]);
});

// Here we test that the fuzz engine can include a contract created during the fuzz
// in its fuzz dictionary and eventually break the invariant.
// Specifically, can Judas, a created contract from Jesus, break Jesus contract
// by revealing his identity.
forgetest_init!(
    #[cfg_attr(windows, ignore = "for some reason there's different rng")]
    invariant_inner_contract,
    |prj, cmd| {
        prj.update_config(|config| {
            config.invariant.depth = 10;
        });

        prj.add_test(
            "InvariantInnerContract.t.sol",
            r#"
import "forge-std/Test.sol";

contract Jesus {
    address fren;
    bool public identity_revealed;

    function create_fren() public {
        fren = address(new Judas());
    }

    function kiss() public {
        require(msg.sender == fren);
        identity_revealed = true;
    }
}

contract Judas {
    Jesus jesus;

    constructor() {
        jesus = Jesus(msg.sender);
    }

    function betray() public {
        jesus.kiss();
    }
}

contract InvariantInnerContract is Test {
    Jesus jesus;

    function setUp() public {
        jesus = new Jesus();
    }

    function invariantHideJesus() public {
        require(jesus.identity_revealed() == false, "jesus betrayed");
    }
}
"#,
        );

        assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantInnerContract.t.sol:InvariantInnerContract
[FAIL: jesus betrayed]
	[SEQUENCE]
 invariantHideJesus() ([RUNS])

[STATS]

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/InvariantInnerContract.t.sol:InvariantInnerContract
[FAIL: jesus betrayed]
	[SEQUENCE]
 invariantHideJesus() ([RUNS])

Encountered a total of 1 failing tests, 0 tests succeeded

Tip: Run `forge test --rerun` to retry only the 1 failed test

[SEED] (use `--fuzz-seed` to reproduce)

"#]]);

        // `fuzz_seed` at 119 makes this sequence shrinkable from 4 to 2.
        prj.update_config(|config| {
            config.fuzz.seed = Some(U256::from(119u32));
            // Disable persisted failures for rerunning the test.
            config.invariant.failure_persist_dir = Some(
                config
                    .invariant
                    .failure_persist_dir
                    .as_ref()
                    .unwrap()
                    .parent()
                    .unwrap()
                    .join("persistence2"),
            );
        });
        assert_invariant(&mut cmd).failure().stdout_eq(str![[r#"
No files changed, compilation skipped

Ran 1 test for test/InvariantInnerContract.t.sol:InvariantInnerContract
[FAIL: jesus betrayed]
	[SEQUENCE]
 invariantHideJesus() ([RUNS])
...
"#]]);
    }
);

// https://github.com/foundry-rs/foundry/issues/7219
forgetest!(invariant_preserve_state, |prj, cmd| {
    prj.insert_utils();
    prj.update_config(|config| {
        config.invariant.depth = 10;
        config.invariant.fail_on_revert = true;
    });

    prj.add_test(
        "InvariantPreserveState.t.sol",
        r#"
import "./utils/Test.sol";

struct FuzzSelector {
    address addr;
    bytes4[] selectors;
}

contract Handler is Test {
    function thisFunctionReverts() external {
        if (block.number < 10) {} else {
            revert();
        }
    }

    function advanceTime(uint256 blocks) external {
        blocks = blocks % 10;
        vm.roll(block.number + blocks);
        vm.warp(block.timestamp + blocks * 12);
    }
}

contract InvariantPreserveState is Test {
    Handler handler;

    function setUp() public {
        handler = new Handler();
    }

    function targetSelectors() public returns (FuzzSelector[] memory) {
        FuzzSelector[] memory targets = new FuzzSelector[](1);
        bytes4[] memory selectors = new bytes4[](2);
        selectors[0] = handler.thisFunctionReverts.selector;
        selectors[1] = handler.advanceTime.selector;
        targets[0] = FuzzSelector(address(handler), selectors);
        return targets;
    }

    function invariant_preserve_state() public {
        assertTrue(true);
    }
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantPreserveState.t.sol:InvariantPreserveState
[FAIL: EvmError: Revert]
	[SEQUENCE]
 invariant_preserve_state() ([RUNS])

[STATS]

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/InvariantPreserveState.t.sol:InvariantPreserveState
[FAIL: EvmError: Revert]
	[SEQUENCE]
 invariant_preserve_state() ([RUNS])

Encountered a total of 1 failing tests, 0 tests succeeded

Tip: Run `forge test --rerun` to retry only the 1 failed test

[SEED] (use `--fuzz-seed` to reproduce)

"#]]);
});

// add code so contract is accounted as valid sender
// see https://github.com/foundry-rs/foundry/issues/4245
forgetest!(invariant_reentrancy, |prj, cmd| {
    prj.insert_utils();
    prj.update_config(|config| {
        config.invariant.depth = 10;
        config.invariant.fail_on_revert = false;
        config.invariant.call_override = true;
    });

    prj.add_test(
        "InvariantReentrancy.t.sol",
        r#"
import "./utils/Test.sol";

contract Malicious {
    function world() public {
        payable(msg.sender).call("");
    }
}

contract Vulnerable {
    bool public open_door = false;
    bool public stolen = false;
    Malicious mal;

    constructor(address _mal) {
        mal = Malicious(_mal);
    }

    function hello() public {
        open_door = true;
        mal.world();
        open_door = false;
    }

    function backdoor() public {
        require(open_door, "");
        stolen = true;
    }
}

contract InvariantReentrancy is Test {
    Vulnerable vuln;
    Malicious mal;

    function setUp() public {
        mal = new Malicious();
        vuln = new Vulnerable(address(mal));
    }

    // do not include `mal` in identified contracts
    // see https://github.com/foundry-rs/foundry/issues/4245
    function targetContracts() public view returns (address[] memory) {
        address[] memory targets = new address[](1);
        targets[0] = address(vuln);
        return targets;
    }

    function invariantNotStolen() public {
        require(vuln.stolen() == false, "stolen");
    }
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantReentrancy.t.sol:InvariantReentrancy
[FAIL: stolen]
	[SEQUENCE]
 invariantNotStolen() ([RUNS])

[STATS]

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/InvariantReentrancy.t.sol:InvariantReentrancy
[FAIL: stolen]
	[SEQUENCE]
 invariantNotStolen() ([RUNS])

Encountered a total of 1 failing tests, 0 tests succeeded

Tip: Run `forge test --rerun` to retry only the 1 failed test

[SEED] (use `--fuzz-seed` to reproduce)

"#]]);
});

// Tests that call_override detects the classic DAO-style reentrancy vulnerability
// in EtherStore where balances are updated AFTER the external call.
forgetest!(invariant_reentrancy_ether_store, |prj, cmd| {
    prj.insert_utils();
    prj.update_config(|config| {
        config.invariant.depth = 15;
        config.invariant.fail_on_revert = false;
        config.invariant.call_override = true;
    });

    prj.add_test(
        "InvariantReentrancyEtherStore.t.sol",
        r#"
import "./utils/Test.sol";

struct FuzzSelector {
    address addr;
    bytes4[] selectors;
}

// Classic reentrancy-vulnerable contract
contract EtherStore {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
        uint256 bal = balances[msg.sender];
        require(bal > 0);
        // BUG: External call before state update
        (bool sent,) = msg.sender.call{value: bal}("");
        require(sent, "Failed to send Ether");
        balances[msg.sender] = 0;
    }
}

contract InvariantReentrancyEtherStore is Test {
    EtherStore store;
    address attacker;

    function setUp() public {
        store = new EtherStore();
        attacker = address(0x1337);

        vm.deal(address(this), 10 ether);
        store.deposit{value: 5 ether}();

        // Attacker gets 2 ether, deposits 1 ether, keeps 1 ether in wallet.
        // After withdrawing their deposit, attacker wallet balance cannot exceed 2 ether.
        vm.deal(attacker, 2 ether);
        vm.prank(attacker);
        store.deposit{value: 1 ether}();
    }

    function targetContracts() public view returns (address[] memory) {
        address[] memory targets = new address[](1);
        targets[0] = address(store);
        return targets;
    }

    function targetSenders() public view returns (address[] memory) {
        address[] memory senders = new address[](1);
        senders[0] = attacker;
        return senders;
    }

    function targetSelectors() public view returns (FuzzSelector[] memory) {
        bytes4[] memory selectors = new bytes4[](1);
        selectors[0] = EtherStore.withdraw.selector;
        FuzzSelector[] memory targets = new FuzzSelector[](1);
        targets[0] = FuzzSelector(address(store), selectors);
        return targets;
    }

    // Attacker should never have more than 2 ether (1 kept + 1 withdrawn)
    function invariantSolvency() public view {
        require(attacker.balance <= 2 ether, "reentrancy: attacker wallet > 2 ether");
    }
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
...
Ran 1 test for test/InvariantReentrancyEtherStore.t.sol:InvariantReentrancyEtherStore
[FAIL: reentrancy: attacker wallet > 2 ether]
	[SEQUENCE]
 invariantSolvency() ([RUNS])

[STATS]

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/InvariantReentrancyEtherStore.t.sol:InvariantReentrancyEtherStore
[FAIL: reentrancy: attacker wallet > 2 ether]
	[SEQUENCE]
 invariantSolvency() ([RUNS])

Encountered a total of 1 failing tests, 0 tests succeeded

Tip: Run `forge test --rerun` to retry only the 1 failed test

[SEED] (use `--fuzz-seed` to reproduce)

"#]]);
});

forgetest_init!(invariant_roll_fork, |prj, cmd| {
    prj.add_rpc_endpoints();
    prj.update_config(|config| {
        config.fuzz.seed = Some(U256::from(119u32));
        config.invariant.shrink_run_limit = 0;
    });

    prj.add_test(
        "InvariantRollFork.t.sol",
        r#"
import "forge-std/Test.sol";

interface IERC20 {
    function totalSupply() external view returns (uint256 supply);
}

contract RollForkHandler is Test {
    uint256 public totalSupply;

    function work() external {
        vm.rollFork(block.number + 1);
        totalSupply = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F).totalSupply();
    }
}

contract InvariantRollForkBlockTest is Test {
    RollForkHandler forkHandler;

    function setUp() public {
        vm.createSelectFork("mainnet", 19812632);
        forkHandler = new RollForkHandler();
    }

    /// forge-config: default.invariant.runs = 2
    /// forge-config: default.invariant.depth = 4
    function invariant_fork_handler_block() public view {
        require(block.number < 19812634, "too many blocks mined");
    }
}

contract InvariantRollForkStateTest is Test {
    RollForkHandler forkHandler;

    function setUp() public {
        vm.createSelectFork("mainnet", 19812632);
        forkHandler = new RollForkHandler();
    }

    /// forge-config: default.invariant.runs = 1
    function invariant_fork_handler_state() public view {
        require(forkHandler.totalSupply() < 3254378807384273078310283461, "wrong supply");
    }
}
"#,
    );

    assert_invariant(cmd.args(["test", "-j1"])).failure().stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!

Ran 1 test for test/InvariantRollFork.t.sol:InvariantRollForkBlockTest
[FAIL: too many blocks mined]
	[SEQUENCE]
 invariant_fork_handler_block() ([RUNS])

[STATS]

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]

Ran 1 test for test/InvariantRollFork.t.sol:InvariantRollForkStateTest
[FAIL: wrong supply]
	[SEQUENCE]
 invariant_fork_handler_state() ([RUNS])

[STATS]

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]

Ran 2 test suites [ELAPSED]: 0 tests passed, 2 failed, 0 skipped (2 total tests)

Failing tests:
Encountered 1 failing test in test/InvariantRollFork.t.sol:InvariantRollForkBlockTest
[FAIL: too many blocks mined]
	[SEQUENCE]
 invariant_fork_handler_block() ([RUNS])

Encountered 1 failing test in test/InvariantRollFork.t.sol:InvariantRollForkStateTest
[FAIL: wrong supply]
	[SEQUENCE]
 invariant_fork_handler_state() ([RUNS])

Encountered a total of 2 failing tests, 0 tests succeeded

Tip: Run `forge test --rerun` to retry only the 2 failed tests

[SEED] (use `--fuzz-seed` to reproduce)

"#]]);
});

forgetest_init!(invariant_scrape_values, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.depth = 10;
        config.fuzz.seed = Some(U256::from(100u32));
    });

    prj.add_test(
        "InvariantScrapeValues.t.sol",
        r#"
import "forge-std/Test.sol";

contract FindFromReturnValue {
    bool public found = false;

    function seed() public returns (int256) {
        int256 mystery = 13337;
        return (1337 + mystery);
    }

    function find(int256 i) public {
        int256 mystery = 13337;
        if (i == 1337 + mystery) {
            found = true;
        }
    }
}

contract FindFromReturnValueTest is Test {
    FindFromReturnValue target;

    function setUp() public {
        target = new FindFromReturnValue();
    }

    /// forge-config: default.invariant.runs = 50
    /// forge-config: default.invariant.depth = 300
    /// forge-config: default.invariant.fail-on-revert = true
    function invariant_value_not_found() public view {
        require(!target.found(), "value from return found");
    }
}

contract FindFromLogValue {
    event FindFromLog(int256 indexed mystery, bytes32 rand);

    bool public found = false;

    function seed() public {
        int256 mystery = 13337;
        emit FindFromLog(1337 + mystery, keccak256(abi.encodePacked("mystery")));
    }

    function find(int256 i) public {
        int256 mystery = 13337;
        if (i == 1337 + mystery) {
            found = true;
        }
    }
}

contract FindFromLogValueTest is Test {
    FindFromLogValue target;

    function setUp() public {
        target = new FindFromLogValue();
    }

    /// forge-config: default.invariant.runs = 50
    /// forge-config: default.invariant.depth = 300
    /// forge-config: default.invariant.fail-on-revert = true
    function invariant_value_not_found() public view {
        require(!target.found(), "value from logs found");
    }
}
"#,
    );

    assert_invariant(cmd.args(["test", "-j1"])).failure().stdout_eq(str![[r#"
...
Ran 2 test suites [ELAPSED]: 0 tests passed, 2 failed, 0 skipped (2 total tests)

Failing tests:
Encountered 1 failing test in test/InvariantScrapeValues.t.sol:FindFromLogValueTest
[FAIL: value from logs found]
	[SEQUENCE]
 invariant_value_not_found() ([RUNS])

Encountered 1 failing test in test/InvariantScrapeValues.t.sol:FindFromReturnValueTest
[FAIL: value from return found]
	[SEQUENCE]
 invariant_value_not_found() ([RUNS])

Encountered a total of 2 failing tests, 0 tests succeeded

Tip: Run `forge test --rerun` to retry only the 2 failed tests

[SEED] (use `--fuzz-seed` to reproduce)

"#]]);
});

forgetest_init!(invariant_sequence_no_reverts, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.depth = 15;
        config.invariant.fail_on_revert = false;
        config.invariant.corpus.corpus_random_sequence_weight = 10;
        // Use original counterexample to test sequence len.
        config.invariant.shrink_run_limit = 0;
    });

    prj.add_test(
        "InvariantSequenceNoReverts.t.sol",
        r#"
import "forge-std/Test.sol";

contract SequenceNoReverts {
    uint256 public count;

    function work(uint256 x) public {
        x;
        count++;
    }
}

contract SequenceNoRevertsTest is Test {
    SequenceNoReverts target;

    function setUp() public {
        target = new SequenceNoReverts();
    }

    function invariant_no_reverts() public view {
        require(target.count() < 10, "condition met");
    }
}
"#,
    );

    // ensure original counterexample len is 10 (even without shrinking)
    cmd.args(["test"]).assert_failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantSequenceNoReverts.t.sol:SequenceNoRevertsTest
[FAIL: condition met]
	[Sequence] (original: 10, shrunk: 10)
...
 invariant_no_reverts() ([..])
...
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests)
...
"#]]);
});

forgetest_init!(
    #[cfg_attr(windows, ignore = "for some reason there's different rng")]
    invariant_shrink_big_sequence,
    |prj, cmd| {
        prj.update_config(|config| {
            config.fuzz.seed = Some(U256::from(119u32));
            config.invariant.runs = 1;
            config.invariant.depth = 1000;
            config.invariant.shrink_run_limit = 425;
        });

        prj.add_test(
            "InvariantShrinkBigSequence.t.sol",
            r#"
import "forge-std/Test.sol";

contract ShrinkBigSequence {
    uint256 cond;

    function work(uint256 x) public {
        if (x % 2 != 0 && x < 9000) {
            cond++;
        }
    }

    function checkCond() public view {
        require(cond < 77, "condition met");
    }
}

contract ShrinkBigSequenceTest is Test {
    ShrinkBigSequence target;

    function setUp() public {
        target = new ShrinkBigSequence();
    }

    function invariant_shrink_big_sequence() public view {
        target.checkCond();
    }
}
"#,
        );

        // ensure shrinks to same sequence of 77
        cmd.args(["test"]).assert_failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantShrinkBigSequence.t.sol:ShrinkBigSequenceTest
[FAIL: condition met]
	[Sequence] (original: [..], shrunk: 77)
...
"#]]);
        cmd.assert_failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantShrinkBigSequence.t.sol:ShrinkBigSequenceTest
[FAIL: condition met]
	[Sequence] (original: [..], shrunk: 77)
...
"#]]);
    }
);

forgetest_init!(invariant_shrink_fail_on_revert, |prj, cmd| {
    prj.update_config(|config| {
        config.fuzz.seed = Some(U256::from(119u32));
        config.invariant.fail_on_revert = true;
        config.invariant.runs = 1;
        config.invariant.depth = 200;
    });

    prj.add_test(
        "InvariantShrinkFailOnRevert.t.sol",
        r#"
import "forge-std/Test.sol";

contract ShrinkFailOnRevert {
    uint256 cond;

    function work(uint256 x) public {
        if (x % 2 != 0 && x < 9000) {
            cond++;
        }
        require(cond < 10, "condition met");
    }
}

contract ShrinkFailOnRevertTest is Test {
    ShrinkFailOnRevert target;

    function setUp() public {
        target = new ShrinkFailOnRevert();
    }

    function invariant_shrink_fail_on_revert() public view {}
}
"#,
    );

    cmd.args(["test"]).assert_failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantShrinkFailOnRevert.t.sol:ShrinkFailOnRevertTest
[FAIL: condition met]
	[Sequence] (original: [..], shrunk: 10)
...
"#]]);
});

forgetest_init!(invariant_fail_on_assert_panic, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.runs = 1;
        config.invariant.depth = 10;
        config.invariant.fail_on_revert = false;
    });

    prj.add_test(
        "InvariantFailOnAssertPanic.t.sol",
        r#"
import "forge-std/Test.sol";

contract AssertHandler {
    uint256 public calls;

    function alwaysAssert() external {
        calls++;
        assert(false);
    }
}

contract InvariantFailOnAssertPanic is Test {
    AssertHandler handler;

    function setUp() public {
        handler = new AssertHandler();
        targetContract(address(handler));
    }

    function invariant_fail_on_assert_panic() public view {}
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantFailOnAssertPanic.t.sol:InvariantFailOnAssertPanic
Assertion Tests: 1 assertion bug(s) found
[FAIL: panic: assertion failed (0x01)][..]
...
 invariant_fail_on_assert_panic() ([RUNS])
...
"#]]);
});

forgetest_init!(invariant_ignore_assert_panic_when_flag_off, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.runs = 1;
        config.invariant.depth = 10;
        config.invariant.fail_on_revert = false;
    });

    prj.add_test(
        "InvariantIgnoreAssertWhenFlagOff.t.sol",
        r#"
import "forge-std/Test.sol";

contract AssertHandler {
    uint256 public calls;

    function alwaysAssert() external {
        calls++;
        assert(false);
    }
}

contract InvariantIgnoreAssertWhenFlagOff is Test {
    AssertHandler handler;

    function setUp() public {
        handler = new AssertHandler();
        targetContract(address(handler));
    }

    function invariant_assert_discarded() public view {}
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantIgnoreAssertWhenFlagOff.t.sol:InvariantIgnoreAssertWhenFlagOff
Assertion Tests: 1 assertion bug(s) found
[FAIL: panic: assertion failed (0x01)][..]
...
 invariant_assert_discarded() ([RUNS])
...
"#]]);
});

forgetest_init!(invariant_fail_on_assert_ignores_non_assert_panic, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.runs = 1;
        config.invariant.depth = 10;
        config.invariant.fail_on_revert = false;
    });

    prj.add_test(
        "InvariantIgnoreNonAssertPanic.t.sol",
        r#"
import "forge-std/Test.sol";

contract OverflowHandler {
    uint256 public calls;

    function alwaysOverflow() external {
        calls++;
        uint256 x = type(uint256).max;
        x = x + 1;
    }
}

contract InvariantIgnoreNonAssertPanic is Test {
    OverflowHandler handler;

    function setUp() public {
        handler = new OverflowHandler();
        targetContract(address(handler));
    }

    function invariant_non_assert_panic_discarded() public view {}
}
"#,
    );

    assert_invariant(cmd.args(["test"])).success().stdout_eq(str![[r#"
...
[PASS] invariant_non_assert_panic_discarded() ([RUNS])
...
"#]]);
});

forgetest_init!(invariant_fail_on_assert_ignores_require_revert, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.runs = 1;
        config.invariant.depth = 10;
        config.invariant.fail_on_revert = false;
    });

    prj.add_test(
        "InvariantIgnoreRequireRevert.t.sol",
        r#"
import "forge-std/Test.sol";

contract RequireHandler {
    uint256 public calls;

    function alwaysRequire() external {
        calls++;
        require(false, "require failed");
    }
}

contract InvariantIgnoreRequireRevert is Test {
    RequireHandler handler;

    function setUp() public {
        handler = new RequireHandler();
        targetContract(address(handler));
    }

    function invariant_require_revert_discarded() public view {}
}
"#,
    );

    assert_invariant(cmd.args(["test"])).success().stdout_eq(str![[r#"
...
[PASS] invariant_require_revert_discarded() ([RUNS])
...
"#]]);
});

forgetest_init!(invariant_replay_fail_on_assert, |prj, cmd| {
    prj.update_config(|config| {
        config.fuzz.seed = Some(U256::from(119u32));
        config.invariant.fail_on_revert = false;
        config.invariant.runs = 1;
        config.invariant.depth = 200;
    });

    prj.add_test(
        "InvariantReplayFailOnAssert.t.sol",
        r#"
import "forge-std/Test.sol";

contract ReplayAssertHandler {
    uint256 public calls;

    function alwaysAssert() external {
        calls++;
        assert(false);
    }
}

contract ReplayFailOnAssertTest is Test {
    ReplayAssertHandler handler;

    function setUp() public {
        handler = new ReplayAssertHandler();
        targetContract(address(handler));
    }

    function invariant_replay_fail_on_assert() public view {}
}
"#,
    );

    cmd.args(["test"]).assert_failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantReplayFailOnAssert.t.sol:ReplayFailOnAssertTest
Assertion Tests: 1 assertion bug(s) found
[FAIL: panic: assertion failed (0x01)][..]
...
"#]]);

    cmd.assert_failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantReplayFailOnAssert.t.sol:ReplayFailOnAssertTest
Assertion Tests: 1 assertion bug(s) found
[FAIL: panic: assertion failed (0x01)][..]
...
"#]]);
});

forgetest_init!(invariant_fail_on_vm_assert_revert, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.runs = 1;
        config.invariant.depth = 10;
        config.invariant.fail_on_revert = false;
    });

    prj.add_test(
        "InvariantFailOnVmAssertRevert.t.sol",
        r#"
import "forge-std/Test.sol";

contract VmAssertHandler {
    Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
    uint256 public calls;

    function alwaysVmAssert() external {
        calls++;
        vm.assertEq(uint256(1), uint256(2));
    }
}

contract InvariantFailOnVmAssertRevert is Test {
    VmAssertHandler handler;

    function setUp() public {
        handler = new VmAssertHandler();
        targetContract(address(handler));
    }

    function invariant_fail_on_vm_assert_revert() public view {}
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantFailOnVmAssertRevert.t.sol:InvariantFailOnVmAssertRevert
Assertion Tests: 1 assertion bug(s) found
[FAIL: assertion failed: 1 != 2][..]
...
 invariant_fail_on_vm_assert_revert() ([RUNS])
...
"#]]);
});

forgetest_init!(invariant_ignore_vm_assert_when_flag_off, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.runs = 1;
        config.invariant.depth = 10;
        config.invariant.fail_on_revert = false;
    });

    prj.add_test(
        "InvariantIgnoreVmAssertWhenFlagOff.t.sol",
        r#"
import "forge-std/Test.sol";

contract VmAssertHandler {
    Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
    uint256 public calls;

    function alwaysVmAssert() external {
        calls++;
        vm.assertEq(uint256(1), uint256(2));
    }
}

contract InvariantIgnoreVmAssertWhenFlagOff is Test {
    VmAssertHandler handler;

    function setUp() public {
        handler = new VmAssertHandler();
        targetContract(address(handler));
    }

    function invariant_vm_assert_discarded() public view {}
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantIgnoreVmAssertWhenFlagOff.t.sol:InvariantIgnoreVmAssertWhenFlagOff
Assertion Tests: 1 assertion bug(s) found
[FAIL: assertion failed: 1 != 2][..]
...
 invariant_vm_assert_discarded() ([RUNS])
...
"#]]);
});

forgetest_init!(invariant_fail_on_vm_assert_global_flag, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.runs = 1;
        config.invariant.depth = 10;
        config.invariant.fail_on_revert = false;
        config.assertions_revert = false;
    });

    prj.add_test(
        "InvariantFailOnVmAssertGlobalFlag.t.sol",
        r#"
import "forge-std/Test.sol";

contract VmAssertHandler {
    Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
    uint256 public calls;

    function alwaysVmAssert() external {
        calls++;
        vm.assertEq(uint256(1), uint256(2));
    }
}

contract InvariantFailOnVmAssertGlobalFlag is Test {
    VmAssertHandler handler;

    function setUp() public {
        handler = new VmAssertHandler();
        targetContract(address(handler));
    }

    function invariant_fail_on_vm_assert_global_flag() public view {}
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantFailOnVmAssertGlobalFlag.t.sol:InvariantFailOnVmAssertGlobalFlag
Assertion Tests: 1 assertion bug(s) found
[FAIL: assertion failed][..]
...
 invariant_fail_on_vm_assert_global_flag() ([RUNS])
...
"#]]);
});

forgetest_init!(invariant_ignore_vm_assert_global_flag_when_flag_off, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.runs = 1;
        config.invariant.depth = 10;
        config.invariant.fail_on_revert = false;
        config.assertions_revert = false;
    });

    prj.add_test(
        "InvariantIgnoreVmAssertGlobalFlagWhenFlagOff.t.sol",
        r#"
import "forge-std/Test.sol";

contract VmAssertHandler {
    Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
    uint256 public calls;

    function alwaysVmAssert() external {
        calls++;
        vm.assertEq(uint256(1), uint256(2));
    }
}

contract InvariantIgnoreVmAssertGlobalFlagWhenFlagOff is Test {
    VmAssertHandler handler;

    function setUp() public {
        handler = new VmAssertHandler();
        targetContract(address(handler));
    }

    function invariant_vm_assert_global_flag_discarded() public view {}
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantIgnoreVmAssertGlobalFlagWhenFlagOff.t.sol:InvariantIgnoreVmAssertGlobalFlagWhenFlagOff
Assertion Tests: 1 assertion bug(s) found
[FAIL: assertion failed][..]
...
 invariant_vm_assert_global_flag_discarded() ([RUNS])
...
"#]]);
});

forgetest_init!(invariant_shrink_with_assert, |prj, cmd| {
    prj.update_config(|config| {
        config.fuzz.seed = Some(U256::from(100u32));
        config.invariant.runs = 1;
        config.invariant.depth = 15;
    });

    prj.add_test(
        "InvariantShrinkWithAssert.t.sol",
        r#"
import "forge-std/Test.sol";

contract Counter {
    uint256 public number;

    function increment() public {
        number++;
    }

    function decrement() public {
        number--;
    }
}

contract InvariantShrinkWithAssert is Test {
    Counter public counter;

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

    function invariant_with_assert() public {
        assertTrue(counter.number() < 2, "wrong counter assert");
    }

    function invariant_with_require() public {
        require(counter.number() < 2, "wrong counter require");
    }
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantShrinkWithAssert.t.sol:InvariantShrinkWithAssert
[FAIL: wrong counter assert] invariant_with_assert
	[SEQUENCE]
...
[FAIL: wrong counter require] invariant_with_require
	[SEQUENCE]

InvariantShrinkWithAssert invariants: 2/2 invariants broken
...
[FAIL: wrong counter assert] invariant_with_assert
[FAIL: wrong counter require] invariant_with_require
...
 InvariantShrinkWithAssert invariants ([RUNS])
...
"#]]);
});

forgetest_init!(invariant_replay_keeps_assertion_failure_from_invariant_function, |prj, cmd| {
    prj.update_config(|config| {
        config.fuzz.seed = Some(U256::from(100u32));
        config.invariant.runs = 1;
        config.invariant.depth = 15;
    });

    prj.add_test(
        "InvariantReplayKeepsInvariantAssertion.t.sol",
        r#"
import "forge-std/Test.sol";

contract Counter {
    uint256 public number;

    function increment() public {
        number++;
    }

    function decrement() public {
        number--;
    }
}

contract InvariantReplayKeepsInvariantAssertion is Test {
    Counter public counter;

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

    function invariant_with_assert() public view {
        assertTrue(counter.number() < 2, "wrong counter assert");
    }
}
"#,
    );

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

Ran 1 test for test/InvariantReplayKeepsInvariantAssertion.t.sol:InvariantReplayKeepsInvariantAssertion
[FAIL: wrong counter assert]
...
 invariant_with_assert() ([..])
...
"#]]);

    cmd.assert_failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantReplayKeepsInvariantAssertion.t.sol:InvariantReplayKeepsInvariantAssertion
[FAIL: wrong counter assert]
	[Sequence] (original: 2, shrunk: 2)
...
 invariant_with_assert() ([..])
...
"#]]);
});

forgetest_init!(invariant_replay_keeps_assertion_failure_from_after_invariant, |prj, cmd| {
    prj.update_config(|config| {
        config.fuzz.seed = Some(U256::from(119u32));
        config.invariant.runs = 1;
        config.invariant.depth = 2;
    });

    prj.add_test(
        "InvariantReplayKeepsAfterInvariantAssertion.t.sol",
        r#"
import "forge-std/Test.sol";

contract AfterInvariantAssertHandler {
    uint256 public count;

    function inc() external {
        count += 1;
    }
}

contract InvariantReplayKeepsAfterInvariantAssertion is Test {
    AfterInvariantAssertHandler handler;

    function setUp() public {
        handler = new AfterInvariantAssertHandler();
        targetContract(address(handler));
    }

    function afterInvariant() public view {
        assertTrue(handler.count() < 2, "afterInvariant assertion");
    }

    function invariant_success() public view {
        require(handler.count() < 10, "invariant should not fail");
    }
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!

Ran 1 test for test/InvariantReplayKeepsAfterInvariantAssertion.t.sol:InvariantReplayKeepsAfterInvariantAssertion
[FAIL: afterInvariant assertion]
	[SEQUENCE]
 invariant_success() ([RUNS])
...
"#]]);

    assert_invariant(&mut cmd).failure().stdout_eq(str![[r#"
No files changed, compilation skipped

Ran 1 test for test/InvariantReplayKeepsAfterInvariantAssertion.t.sol:InvariantReplayKeepsAfterInvariantAssertion
[FAIL: afterInvariant assertion]
	[SEQUENCE]
 invariant_success() ([RUNS])
...
"#]]);
});

forgetest_init!(invariant_replay_persists_initial_after_invariant_failure, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.runs = 1;
        config.invariant.depth = 2;
    });

    prj.add_test(
        "InvariantReplayInitialAfterInvariantFailure.t.sol",
        r#"
import "forge-std/Test.sol";

contract InitialAfterInvariantHandler {
    uint256 public count;

    function inc() external {
        count += 1;
    }
}

contract InvariantReplayInitialAfterInvariantFailure is Test {
    InitialAfterInvariantHandler handler;

    function setUp() public {
        handler = new InitialAfterInvariantHandler();
        handler.inc();
        handler.inc();
        targetContract(address(handler));
    }

    function afterInvariant() public view {
        assertTrue(handler.count() < 2, "afterInvariant initial assertion");
    }

    function invariant_success() public view {
        require(handler.count() < 10, "invariant should not fail");
    }
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!

Ran 1 test for test/InvariantReplayInitialAfterInvariantFailure.t.sol:InvariantReplayInitialAfterInvariantFailure
[FAIL: afterInvariant initial assertion]
	[SEQUENCE]
 invariant_success() ([RUNS])
...
"#]]);

    cmd.forge_fuse().args(["test", "--rerun"]).assert_failure().stdout_eq(str![[r#"
No files changed, compilation skipped

Ran 1 test for test/InvariantReplayInitialAfterInvariantFailure.t.sol:InvariantReplayInitialAfterInvariantFailure
[FAIL: afterInvariant initial assertion]
	[Sequence] (original: 1, shrunk: 1)
		sender=[..] addr=[..] calldata=inc() args=[]
 invariant_success() (runs: 1, calls: 1, reverts: 0)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/InvariantReplayInitialAfterInvariantFailure.t.sol:InvariantReplayInitialAfterInvariantFailure
[FAIL: afterInvariant initial assertion]
	[Sequence] (original: 1, shrunk: 1)
		sender=[..] addr=[..] calldata=inc() args=[]
 invariant_success() (runs: 1, calls: 1, reverts: 0)
...
"#]]);
});

forgetest_init!(invariant_test1, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.depth = 10;
    });

    prj.add_test(
        "InvariantTest1.t.sol",
        r#"
import "forge-std/Test.sol";

contract InvariantBreaker {
    bool public flag0 = true;
    bool public flag1 = true;

    function set0(int256 val) public returns (bool) {
        if (val % 100 == 0) {
            flag0 = false;
        }
        return flag0;
    }

    function set1(int256 val) public returns (bool) {
        if (val % 10 == 0 && !flag0) {
            flag1 = false;
        }
        return flag1;
    }
}

contract InvariantTest is Test {
    InvariantBreaker inv;

    function setUp() public {
        inv = new InvariantBreaker();
    }

    function invariant_neverFalse() public {
        require(inv.flag1(), "false");
    }

    function statefulFuzz_neverFalseWithInvariantAlias() public {
        require(inv.flag1(), "false");
    }
}
"#,
    );

    assert_invariant(cmd.args(["test"])).failure().stdout_eq(str![[r#"
...
Ran 1 test for test/InvariantTest1.t.sol:InvariantTest
[FAIL: false] invariant_neverFalse
	[SEQUENCE]

[FAIL: false] statefulFuzz_neverFalseWithInvariantAlias
	[SEQUENCE]

InvariantTest invariants: 2/2 invariants broken
[FAIL: false] invariant_neverFalse
[FAIL: false] statefulFuzz_neverFalseWithInvariantAlias
2 invariant failure(s) persisted to cache/invariant/failures/InvariantTest — rerun to shrink
 InvariantTest invariants ([RUNS])

[STATS]

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/InvariantTest1.t.sol:InvariantTest
[FAIL: false] invariant_neverFalse
	[SEQUENCE]

[FAIL: false] statefulFuzz_neverFalseWithInvariantAlias
	[SEQUENCE]

InvariantTest invariants: 2/2 invariants broken
[FAIL: false] invariant_neverFalse
[FAIL: false] statefulFuzz_neverFalseWithInvariantAlias
2 invariant failure(s) persisted to cache/invariant/failures/InvariantTest — rerun to shrink
 InvariantTest invariants ([RUNS])

Encountered a total of 1 failing tests, 0 tests succeeded

Tip: Run `forge test --rerun` to retry only the 1 failed test

[SEED] (use `--fuzz-seed` to reproduce)

"#]]);
});

forgetest_init!(invariant_warp_and_roll, |prj, cmd| {
    prj.update_config(|config| {
        config.fuzz.seed = Some(U256::from(119u32));
        config.invariant.max_time_delay = Some(604800);
        config.invariant.max_block_delay = Some(60480);
        config.invariant.shrink_run_limit = 0;
    });

    prj.add_test(
        "InvariantWarpAndRoll.t.sol",
        r#"
import "forge-std/Test.sol";

contract Counter {
    uint256 public number;

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

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

contract InvariantWarpAndRoll {
    Counter public counter;

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

    function invariant_warp() public view {
        require(block.timestamp < 500000, "max timestamp");
    }

    /// forge-config: default.invariant.show_solidity = true
    function invariant_roll() public view {
        require(block.number < 200000, "max block");
    }
}
"#,
    );

    assert_invariant(cmd.args(["test", "--mt", "invariant_warp"])).failure().stdout_eq(str![[r#"
...
[FAIL: max timestamp]
	[SEQUENCE]
 invariant_warp() ([RUNS])
...

"#]]);

    assert_invariant(cmd.forge_fuse().args(["test", "--mt", "invariant_roll"]))
        .failure()
        .stdout_eq(str![[r#"
...
[FAIL: max block]
	[SEQUENCE]
 invariant_roll() ([RUNS])
...

"#]]);

    // Test that time and block advance in target contract as well.
    prj.update_config(|config| {
        config.invariant.fail_on_revert = true;
    });
    prj.add_test(
        "HandlerWarpAndRoll.t.sol",
        r#"
import "forge-std/Test.sol";

contract Counter {
    uint256 public number;
    function setNumber(uint256 newNumber) public {
        require(block.number < 200000, "max block");
        number = newNumber;
    }

    function increment() public {
        require(block.timestamp < 500000, "max timestamp");
        number++;
    }
}

contract HandlerWarpAndRoll {
    Counter public counter;

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

    function invariant_handler() public view {
    }
}
"#,
    );

    assert_invariant(cmd.forge_fuse().args(["test", "--mt", "invariant_handler"]))
        .failure()
        .stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!

Ran 1 test for test/HandlerWarpAndRoll.t.sol:HandlerWarpAndRoll
[FAIL: max timestamp]
	[SEQUENCE]
 invariant_handler() ([RUNS])

...

"#]]);
});

// Test that state is preserved across calls during invariant replay.
// Regression test for commit 0584a581b which changed replay_run to use execute_tx
// (which uses call_raw) instead of transact_raw, but forgot to add the commit() call.
forgetest_init!(invariant_replay_state_preserved, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.runs = 1;
        config.invariant.depth = 5;
    });

    prj.add_test(
        "InvariantReplayState.t.sol",
        r#"
import "forge-std/Test.sol";

contract Handler is Test {
    uint256 public counter;

    function increment(uint256 amount) public {
        uint256 before = counter;
        counter += 1;
        console.log("before:", before, "after:", counter);
    }
}

contract InvariantReplayStateTest is Test {
    Handler handler;

    function setUp() public {
        handler = new Handler();
        targetContract(address(handler));
    }

    function invariant_counter_increases() public view {
        assertTrue(true);
    }
}
"#,
    );

    // With -vvv we see logs from replay. The "before" value of each call should
    // match the "after" value from the previous call, proving state persists.
    cmd.args(["test", "-vvv"]).assert_success().stdout_eq(str![[r#"
...
[PASS] invariant_counter_increases() (runs: 1, calls: 5, reverts: 0)
...
Logs:
  before: 0 after: 1
  before: 1 after: 2
  before: 2 after: 3
  before: 3 after: 4
  before: 4 after: 5
...
"#]]);
});

// Test optimization mode for invariant testing.
// When an invariant function returns int256, it becomes an optimization target.
// The fuzzer maximizes the return value instead of checking for failures.
forgetest!(invariant_optimization_mode, |prj, cmd| {
    prj.insert_vm();
    prj.insert_ds_test();

    prj.add_test(
        "InvariantOptimize.t.sol",
        r#"
import { DSTest as Test } from "src/test.sol";

struct FuzzSelector {
    address addr;
    bytes4[] selectors;
}

contract OptimizationHandler {
    int256 public value;

    // Each call adds exactly 10 to value
    function increment() external {
        value += 10;
    }
}

contract InvariantOptimizeTest is Test {
    OptimizationHandler handler;

    function setUp() public {
        handler = new OptimizationHandler();
    }

    function targetSelectors() public returns (FuzzSelector[] memory) {
        FuzzSelector[] memory targets = new FuzzSelector[](1);
        bytes4[] memory selectors = new bytes4[](1);
        selectors[0] = handler.increment.selector;
        targets[0] = FuzzSelector(address(handler), selectors);
        return targets;
    }

    /// forge-config: default.invariant.runs = 1
    /// forge-config: default.invariant.depth = 5
    /// @notice Optimization mode: returns int256 to maximize.
    /// With depth=5 and only increment(), max value should be 50.
    function invariant_optimize_value() public view returns (int256) {
        return handler.value();
    }
}
"#,
    );

    // Optimization mode: best value shown first, should reach 50 (5 calls * 10 each)
    // Shows [Best sequence] with calls in output
    cmd.args(["test", "-vvv"]).assert_success().stdout_eq(str![[r#"
...
[PASS]
	[Best sequence] [..]
[..]calldata=increment()[..]
[..]calldata=increment()[..]
[..]calldata=increment()[..]
[..]calldata=increment()[..]
[..]calldata=increment()[..]
 invariant_optimize_value() (best: 50, runs: 1, calls: 5)
...
"#]]);
});

// Test that optimization mode works with negative values (finding max of negative range).
forgetest!(invariant_optimization_negative_values, |prj, cmd| {
    prj.insert_vm();
    prj.insert_ds_test();

    prj.add_test(
        "InvariantOptimizeNegative.t.sol",
        r#"
import { DSTest as Test } from "src/test.sol";

struct FuzzSelector {
    address addr;
    bytes4[] selectors;
}

contract NegativeHandler {
    int256 public value = -100;

    // Each call adds exactly 25 to value
    function increase() external {
        value += 25;
    }
}

contract InvariantOptimizeNegativeTest is Test {
    NegativeHandler handler;

    function setUp() public {
        handler = new NegativeHandler();
    }

    function targetSelectors() public returns (FuzzSelector[] memory) {
        FuzzSelector[] memory targets = new FuzzSelector[](1);
        bytes4[] memory selectors = new bytes4[](1);
        selectors[0] = handler.increase.selector;
        targets[0] = FuzzSelector(address(handler), selectors);
        return targets;
    }

    /// forge-config: default.invariant.runs = 1
    /// forge-config: default.invariant.depth = 4
    /// Starting at -100, 4 calls of +25 each = -100 + 100 = 0
    function invariant_optimize_negative() public view returns (int256) {
        return handler.value();
    }
}
"#,
    );

    // Optimization should reach 0: starting at -100, 4 calls * +25 = 0
    // Shows [Best sequence] with calls in output
    cmd.args(["test", "-vvv"]).assert_success().stdout_eq(str![[r#"
...
[PASS]
	[Best sequence] [..]
[..]calldata=increase()[..]
[..]calldata=increase()[..]
[..]calldata=increase()[..]
[..]calldata=increase()[..]
 invariant_optimize_negative() (best: 0, runs: 1, calls: 4)
...
"#]]);
});

// Test that optimization mode:
// 1. Evaluates at every prefix regardless of check_interval (finds true max, not just last-call
//    value)
// 2. Persists the best value across runs via corpus directory
forgetest_init!(invariant_optimization_check_interval_and_persistence, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.runs = 1;
        config.invariant.depth = 5;
        config.invariant.check_interval = 0;
        config.invariant.corpus.corpus_dir = Some("opt_corpus".into());
    });
    prj.add_test(
        "InvariantOptimize.t.sol",
        r#"
import {Test} from "forge-std/Test.sol";

contract PeakHandler {
    int256 public value;

    // Value peaks at call 3, then drops.
    function step() external {
        value++;
        if (value > 3) {
            value = -100;
        }
    }
}

contract InvariantOptimizeTest is Test {
    PeakHandler handler;

    function setUp() public {
        handler = new PeakHandler();
        targetContract(address(handler));
    }

    /// forge-config: default.invariant.runs = 1
    /// forge-config: default.invariant.depth = 5
    /// forge-config: default.invariant.check_interval = 0
    function invariant_optimize_peak() public view returns (int256) {
        return handler.value();
    }
}
"#,
    );

    // Run 1: best should be 3 (prefix len 3), NOT -100 (depth 5).
    // Validates check_interval=0 doesn't skip optimization sampling.
    cmd.args(["test", "-vvv"]).assert_success().stdout_eq(str![[r#"
...
[PASS]
	[Best sequence] [..]
[..]calldata=step()[..]
[..]calldata=step()[..]
[..]calldata=step()[..]
 invariant_optimize_peak() (best: 3, runs: 1, calls: 5)
...
"#]]);

    // Run 2: persisted best should survive across runs.
    cmd.forge_fuse().args(["test", "-vvv"]).assert_success().stdout_eq(str![[r#"
...
 invariant_optimize_peak() (best: 3, runs: 1, calls: 5)
...
"#]]);
});

forgetest_init!(invariant_zero_delays_are_disabled, |prj, cmd| {
    prj.add_test(
        "InvariantZeroDelay.t.sol",
        r#"
contract InvariantZeroDelay {
    Target target;

    function setUp() public {
        target = new Target();
    }

    /// forge-config: default.invariant.runs = 1
    /// forge-config: default.invariant.depth = 1
    /// forge-config: default.invariant.max_time_delay = 0
    function invariant_zeroTimeDelay() public pure {}

    /// forge-config: default.invariant.runs = 1
    /// forge-config: default.invariant.depth = 1
    /// forge-config: default.invariant.max_block_delay = 0
    function invariant_zeroBlockDelay() public pure {}
}

contract Target {
    function touch() public {}
}
"#,
    );

    cmd.args(["test", "--mt", "invariant_zeroTimeDelay"]).assert_success();
    cmd.forge_fuse().args(["test", "--mt", "invariant_zeroBlockDelay"]).assert_success();
});

// Test optimization mode with time-dependent logic using warp and fixed seed for reproducibility.
// This test ensures warp values are correctly accumulated during shrinking.
forgetest_init!(invariant_optimization_with_warp, |prj, cmd| {
    prj.add_test(
        "InvariantOptimizeWarp.t.sol",
        r#"
import {Test} from "forge-std/Test.sol";

contract InvariantOptimizeWarpTest is Test {
    int256 public maxValue;

    function setUp() public {
        targetContract(address(this));
    }

    // Simulates time-dependent pricing with warp. Higher timestamp = higher value.
    function updateValue(uint256 multiplier) public {
        if (multiplier == 0 || multiplier > 100) revert();
        // Value depends on block.timestamp which can be warped
        int256 newValue = int256(block.timestamp * multiplier / 1000);
        if (newValue > maxValue) {
            maxValue = newValue;
        }
    }

    /// forge-config: default.invariant.runs = 10
    /// forge-config: default.invariant.depth = 15
    /// forge-config: default.invariant.max_time_delay = 604800
    function invariant_optimize_max_value() public view returns (int256) {
        return maxValue;
    }
}
"#,
    );

    // Use fixed seed for deterministic output. The optimizer finds sequences that
    // maximize value through time manipulation (warp). Shrinking reduces to 1 call.
    cmd.args(["test", "-vvv", "--fuzz-seed", "12345"]).assert_success().stdout_eq(str![[r#"
...
[PASS]
	[Best sequence] [..]
[..]calldata=updateValue(uint256) args=[100]
 invariant_optimize_max_value() (best: [..], runs: 10, calls: 150)
...
"#]]);
});

// Regression test for delay-aware shrinking in check mode.
// Removed calls may still contribute warp/roll, so the final shrunk sequence must preserve those
// values in the remaining call.
forgetest_init!(invariant_shrink_preserves_warp_roll, |prj, cmd| {
    prj.add_test(
        "InvariantRollWarpShrink.t.sol",
        r#"
import "forge-std/Test.sol";

contract Roll {
    uint256 public number;

    function increment() public {
        require(block.number > 50000, "wrong block");
        number++;
    }
}

contract Warp {
    uint256 public number;

    function increment() public {
        require(block.timestamp > 500000, "wrong timestamp");
        number++;
    }
}

contract InvariantRoll is Test {
    Roll public roll;

    function setUp() public {
        roll = new Roll();
    }

    /// forge-config: default.fuzz.seed = "119"
    /// forge-config: default.invariant.max_block_delay = 60480
    /// forge-config: default.invariant.show_solidity = true
    function invariant_roll() public view {
        require(roll.number() == 0, "number is not zero");
    }
}

contract InvariantWarp is Test {
    Warp public warp;

    function setUp() public {
        warp = new Warp();
    }

    /// forge-config: default.fuzz.seed = "119"
    /// forge-config: default.invariant.max_time_delay = 604800
    /// forge-config: default.invariant.show_solidity = true
    function invariant_warp() public view {
        require(warp.number() == 0, "max time");
    }
}
"#,
    );

    cmd.args(["test", "--mt", "invariant_roll"]).assert_failure().stdout_eq(str![[r#"
...
[FAIL: number is not zero]
	[Sequence] (original: 3, shrunk: 1)
		vm.roll(block.number + [..]);
		vm.prank([..]);
		Roll(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f).increment();
 invariant_roll() (runs: 1, calls: 3, reverts: 2)
...

"#]]);

    cmd.forge_fuse().args(["test", "--mt", "invariant_warp"]).assert_failure().stdout_eq(str![[
        r#"
...
[FAIL: max time]
	[Sequence] (original: [..], shrunk: 1)
		vm.warp(block.timestamp + [..]);
		vm.prank([..]);
		Warp(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f).increment();
 invariant_warp() (runs: 1, calls: [..], reverts: [..])
...

"#
    ]]);
});

// Test that invariant fuzzer generates random msg.value for payable functions.
// Based on the example from https://github.com/foundry-rs/foundry/pull/8644
forgetest_init!(invariant_msg_value, |prj, cmd| {
    prj.update_config(|config| {
        config.fuzz.seed = Some(U256::from(42u32));
        config.invariant.runs = 200;
        config.invariant.depth = 20;
    });

    prj.add_test(
        "InvariantMsgValue.t.sol",
        r#"
import "forge-std/Test.sol";

contract ValueTarget {
    bool public valueReceived;

    // Payable function that tracks if any value was received
    function deposit() external payable {
        if (msg.value > 0) {
            valueReceived = true;
        }
    }
}

contract InvariantMsgValue is Test {
    ValueTarget target;
    address sender1;
    address sender2;

    function setUp() public {
        target = new ValueTarget();
        // Create and fund specific senders
        sender1 = makeAddr("sender1");
        sender2 = makeAddr("sender2");
        vm.deal(sender1, 1000 ether);
        vm.deal(sender2, 1000 ether);
        // Target only these funded senders
        targetSender(sender1);
        targetSender(sender2);
        // Target only the ValueTarget contract
        targetContract(address(target));
    }

    function invariant_value_never_received() public view {
        require(!target.valueReceived(), "Value was received");
    }
}
"#,
    );

    // The knob should be able to disable payable value generation completely.
    cmd.forge_fuse()
        .args([
            "test",
            "--mt",
            "invariant_value_never_received",
            "--invariant-payable-value-weight",
            "0",
        ])
        .assert_success();
    cmd.forge_fuse().arg("clean").assert_success();

    // The default invariant weight should fail because the fuzzer generates msg.value > 0 for
    // payable functions.
    // First check regular output format shows value=X
    cmd.forge_fuse()
        .args(["test", "--mt", "invariant_value_never_received"])
        .assert_failure()
        .stdout_eq(str![[r#"
...
[FAIL: Value was received]
	[Sequence] (original: [..], shrunk: 1)
		sender=[..] addr=[test/InvariantMsgValue.t.sol:ValueTarget][..] value=[..] calldata=deposit() args=[]
...
"#]]);

    // Now check solidity output format shows proper {value: X} syntax
    cmd.forge_fuse().arg("clean").assert_success();
    prj.update_config(|config| {
        config.invariant.show_solidity = true;
    });
    cmd.forge_fuse()
        .args(["test", "--mt", "invariant_value_never_received"])
        .assert_failure()
        .stdout_eq(str![[r#"
...
[FAIL: Value was received]
	[Sequence] (original: [..], shrunk: 1)
		vm.prank([..]);
		ValueTarget([..]).deposit{value: [..]}();
...
"#]]);
});

forgetest_init!(invariant_call_override_skips_storage_hook_callbacks, |prj, cmd| {
    prj.update_config(|config| {
        config.invariant.runs = 16;
        config.invariant.depth = 8;
        config.invariant.call_override = true;
        config.invariant.fail_on_revert = true;
    });

    prj.add_test(
        "InvariantStorageHooks.t.sol",
        r#"
import "forge-std/Test.sol";

interface StorageHookVm {
    function registerSstoreHook(address target, bytes4 callback) external;
}

contract StorageHookTarget {
    uint256 public value;

    function store(uint256 newValue) external {
        value = newValue;
    }
}

contract StorageHookHelper {
    function copy(uint256 value) external pure returns (uint256) {
        if (value == 7) {
            return 7;
        }
        return value;
    }
}

contract StorageHookHandler {
    StorageHookVm constant hookVm =
        StorageHookVm(address(uint160(uint256(keccak256("hevm cheat code")))));

    StorageHookTarget public immutable target;
    StorageHookHelper public immutable helper;
    uint256 public ghostValue;

    constructor(StorageHookTarget target_) {
        target = target_;
        helper = new StorageHookHelper();
        hookVm.registerSstoreHook(address(target_), StorageHookHandler.onStore.selector);
    }

    function mutate(uint256 newValue) external {
        target.store(newValue);
    }

    function onStore(address, bytes32, bytes32, bytes32 newValue) external {
        require(msg.sender == address(hookVm), "only storage hook");
        ghostValue = helper.copy(uint256(newValue));
    }
}

contract InvariantStorageHooks is Test {
    StorageHookTarget target;
    StorageHookHandler handler;

    function setUp() public {
        target = new StorageHookTarget();
        handler = new StorageHookHandler(target);
    }

    function invariant_hook_tracks_target() public view {
        assertEq(handler.ghostValue(), target.value());
    }
}
"#,
    );

    assert_invariant(cmd.args(["test"])).success();
});
