// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;

import "utils/Test.sol";

contract Reverter {
    error CustomError();

    function revertWithMessage(string memory message) public pure {
        revert(message);
    }

    function doNotRevert() public pure {}

    function panic() public pure returns (uint256) {
        return uint256(100) - uint256(101);
    }

    function revertWithCustomError() public pure {
        revert CustomError();
    }

    function nestedRevert(Reverter inner, string memory message) public pure {
        inner.revertWithMessage(message);
    }

    function callThenRevert(Dummy dummy, string memory message) public pure {
        dummy.callMe();
        revert(message);
    }

    function callThenNoRevert(Dummy dummy) public pure {
        dummy.callMe();
    }

    function revertWithoutReason() public pure {
        revert();
    }
}

contract ConstructorReverter {
    constructor(string memory message) {
        revert(message);
    }
}

/// Used to ensure that the dummy data from `vm.expectRevert`
/// is large enough to decode big structs.
///
/// The struct is based on issue #2454
struct LargeDummyStruct {
    address a;
    uint256 b;
    bool c;
    address d;
    address e;
    string f;
    address[8] g;
    address h;
    uint256 i;
}

contract Dummy {
    function callMe() public pure returns (string memory) {
        return "thanks for calling";
    }

    function largeReturnType() public pure returns (LargeDummyStruct memory) {
        revert("reverted with large return type");
    }
}

contract ExpectRevertTest is Test {
    function shouldRevert() internal {
        revert();
    }

    function testExpectRevertString() public {
        Reverter reverter = new Reverter();
        vm.expectRevert("revert");
        reverter.revertWithMessage("revert");
    }

    function testExpectRevertWithEncodedErrorPrefix() public {
        Reverter reverter = new Reverter();
        vm.expectRevert(abi.encodeWithSignature("Error(string)", "my revert reason"));
        reverter.revertWithMessage("my revert reason");

        vm.expectRevert(abi.encodeWithSignature("Error(string)", "A"));
        reverter.revertWithMessage("A");

        vm.expectRevert(abi.encodeWithSignature("Error(string)", "revert: A"));
        reverter.revertWithMessage("revert: A");
    }

    function testExpectRevertConstructor() public {
        vm.expectRevert("constructor revert");
        new ConstructorReverter("constructor revert");
    }

    function testExpectRevertBuiltin() public {
        Reverter reverter = new Reverter();
        vm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x11));
        reverter.panic();
    }

    function testExpectRevertCustomError() public {
        Reverter reverter = new Reverter();
        vm.expectRevert(abi.encodePacked(Reverter.CustomError.selector));
        reverter.revertWithCustomError();
    }

    function testExpectRevertNested() public {
        Reverter reverter = new Reverter();
        Reverter inner = new Reverter();
        vm.expectRevert("nested revert");
        reverter.nestedRevert(inner, "nested revert");
    }

    function testExpectRevertCallsThenReverts() public {
        Reverter reverter = new Reverter();
        Dummy dummy = new Dummy();
        vm.expectRevert("called a function and then reverted");
        reverter.callThenRevert(dummy, "called a function and then reverted");
    }

    function testDummyReturnDataForBigType() public {
        Dummy dummy = new Dummy();
        vm.expectRevert("reverted with large return type");
        dummy.largeReturnType();
    }

    function testExpectRevertNoReason() public {
        Reverter reverter = new Reverter();
        vm.expectRevert(bytes(""));
        reverter.revertWithoutReason();
    }

    function testExpectRevertAnyRevert() public {
        vm.expectRevert();
        new ConstructorReverter("hello this is a revert message");

        Reverter reverter = new Reverter();
        vm.expectRevert();
        reverter.revertWithMessage("this is also a revert message");

        vm.expectRevert();
        reverter.panic();

        vm.expectRevert();
        reverter.revertWithCustomError();

        Reverter reverter2 = new Reverter();
        vm.expectRevert();
        reverter.nestedRevert(reverter2, "this too is a revert message");

        Dummy dummy = new Dummy();
        vm.expectRevert();
        reverter.callThenRevert(dummy, "revert message 4 i ran out of synonims for also");

        vm.expectRevert();
        reverter.revertWithoutReason();
    }

    function testexpectCheatcodeRevert() public {
        vm._expectCheatcodeRevert('JSON value at ".a" is not an object');
        vm.parseJsonKeys('{"a": "b"}', ".a");
    }
}

contract AContract {
    BContract bContract;
    CContract cContract;

    constructor(BContract _bContract, CContract _cContract) {
        bContract = _bContract;
        cContract = _cContract;
    }

    function callAndRevert() public pure {
        require(1 > 2, "Reverted by AContract");
    }

    function callAndRevertInBContract() public {
        bContract.callAndRevert();
    }

    function callAndRevertInCContract() public {
        cContract.callAndRevert();
    }

    function callAndRevertInCContractThroughBContract() public {
        bContract.callAndRevertInCContract();
    }

    function createDContract() public {
        new DContract();
    }

    function createDContractThroughBContract() public {
        bContract.createDContract();
    }

    function createDContractThroughCContract() public {
        cContract.createDContract();
    }

    function doNotRevert() public {}
}

contract BContract {
    CContract cContract;

    constructor(CContract _cContract) {
        cContract = _cContract;
    }

    function callAndRevert() public pure {
        require(1 > 2, "Reverted by BContract");
    }

    function callAndRevertInCContract() public {
        this.doNotRevert();
        cContract.doNotRevert();
        cContract.callAndRevert();
    }

    function createDContract() public {
        this.doNotRevert();
        cContract.doNotRevert();
        new DContract();
    }

    function createDContractThroughCContract() public {
        this.doNotRevert();
        cContract.doNotRevert();
        cContract.createDContract();
    }

    function doNotRevert() public {}
}

contract CContract {
    error CContractError(string reason);

    function callAndRevert() public pure {
        revert CContractError("Reverted by CContract");
    }

    function createDContract() public {
        new DContract();
    }

    function doNotRevert() public {}
}

contract DContract {
    constructor() {
        require(1 > 2, "Reverted by DContract");
    }
}

contract ExpectRevertWithReverterTest is Test {
    error CContractError(string reason);

    AContract aContract;
    BContract bContract;
    CContract cContract;

    function setUp() public {
        cContract = new CContract();
        bContract = new BContract(cContract);
        aContract = new AContract(bContract, cContract);
    }

    function testExpectRevertsWithReverter() public {
        // Test expect revert with reverter at first call.
        vm.expectRevert(address(aContract));
        aContract.callAndRevert();
        // Test expect revert with reverter at second subcall.
        vm.expectRevert(address(bContract));
        aContract.callAndRevertInBContract();
        // Test expect revert with partial data match and reverter at third subcall.
        vm.expectPartialRevert(CContractError.selector, address(cContract));
        aContract.callAndRevertInCContractThroughBContract();
        // Test expect revert with exact data match and reverter at second subcall.
        vm.expectRevert(abi.encodeWithSelector(CContractError.selector, "Reverted by CContract"), address(cContract));
        aContract.callAndRevertInCContract();
    }

    // The reverter for a CREATE that reverts is the would-be deployed address.
    function testExpectRevertsWithReverterInConstructor() public {
        address expected;

        expected = vm.computeCreateAddress(address(cContract), vm.getNonce(address(cContract)));
        vm.expectRevert(abi.encodePacked("Reverted by DContract"), expected);
        cContract.createDContract();

        expected = vm.computeCreateAddress(address(bContract), vm.getNonce(address(bContract)));
        vm.expectRevert(expected);
        bContract.createDContract();
        expected = vm.computeCreateAddress(address(cContract), vm.getNonce(address(cContract)));
        vm.expectRevert(expected);
        bContract.createDContractThroughCContract();

        expected = vm.computeCreateAddress(address(aContract), vm.getNonce(address(aContract)));
        vm.expectRevert(expected);
        aContract.createDContract();
        expected = vm.computeCreateAddress(address(bContract), vm.getNonce(address(bContract)));
        vm.expectRevert(expected);
        aContract.createDContractThroughBContract();
        expected = vm.computeCreateAddress(address(cContract), vm.getNonce(address(cContract)));
        vm.expectRevert(expected);
        aContract.createDContractThroughCContract();
    }

    // <https://github.com/foundry-rs/foundry/issues/14613>
    // Regression: when the next operation is a top-level CREATE whose constructor
    // reverts directly, the reverter address argument must be enforced (it used to
    // be silently ignored). The matched reverter is the would-be-deployed address.
    function testExpectRevertsWithReverterTopLevelCreate() public {
        address expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this)));
        vm.expectRevert(expected);
        new DContract();

        expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this)));
        vm.expectRevert(abi.encodePacked("Reverted by DContract"), expected);
        new DContract();
    }

    // <https://github.com/foundry-rs/foundry/issues/14613>
    // Nested CREATE chain: innermost failed deployment wins.
    function testExpectRevertsWithReverterNestedCreate() public {
        address outer = vm.computeCreateAddress(address(this), vm.getNonce(address(this)));
        // Contracts start at nonce 1; the inner DContract is outer's first creation.
        address innerReverter = vm.computeCreateAddress(outer, 1);
        vm.expectRevert(innerReverter);
        new NestedDContractCreator();
    }

    // <https://github.com/foundry-rs/foundry/issues/14613>
    // Regression: `expectPartialRevert(bytes4, address)` overload must enforce
    // the reverter address argument when matching a top-level CREATE revert.
    function testExpectPartialRevertWithReverterTopLevelCreate() public {
        address expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this)));
        // `Reverted by DContract` triggers Solidity's `Error(string)` selector.
        vm.expectPartialRevert(bytes4(keccak256("Error(string)")), expected);
        new DContract();
    }

    // <https://github.com/foundry-rs/foundry/issues/14613>
    // Regression: `expectRevert(bytes4, address)` (exact 4-byte selector + reverter)
    // overload must enforce the reverter address argument for a top-level CREATE.
    function testExpectRevertWithBytes4SelectorAndReverterTopLevelCreate() public {
        address expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this)));
        vm.expectRevert(DCustomErrorContract.CustomError.selector, expected);
        new DCustomErrorContract();
    }

    // <https://github.com/foundry-rs/foundry/issues/14613>
    // Regression: `expectRevert(address, uint64)` count-bearing overload must
    // exercise the `count > 1` branch in `create_end`. Use CREATE2 with the same
    // salt so both deploys would resolve to the same would-be address (each
    // constructor reverts so no contract is ever actually placed there).
    function testExpectRevertsWithReverterCountTopLevelCreate2() public {
        bytes32 salt = bytes32(uint256(0x42));
        address expected = vm.computeCreate2Address(salt, keccak256(type(DContract).creationCode), address(this));
        vm.expectRevert(expected, 2);
        new DContract{salt: salt}();
        new DContract{salt: salt}();
    }

    // <https://github.com/foundry-rs/foundry/issues/14613>
    // Regression: CREATE2 deploys must also enforce the reverter address argument.
    function testExpectRevertsWithReverterTopLevelCreate2() public {
        bytes32 salt = bytes32(uint256(0xC0FFEE));
        address expected = vm.computeCreate2Address(salt, keccak256(type(DContract).creationCode), address(this));
        vm.expectRevert(expected);
        new DContract{salt: salt}();
    }

    // <https://github.com/foundry-rs/foundry/issues/14613>
    // Regression: `count > 1` with a nested CREATE chain must still report the
    // innermost reverter, not the outer frame. Uses fixed outer+inner CREATE2 salts
    // so both iterations resolve to the same would-be addresses.
    function testExpectRevertsWithReverterCountNestedCreate2() public {
        bytes32 outerSalt = bytes32(uint256(0xBEEF));
        bytes32 innerSalt = NESTED_DCONTRACT_CREATOR2_INNER_SALT;
        address outer =
            vm.computeCreate2Address(outerSalt, keccak256(type(NestedDContractCreator2).creationCode), address(this));
        address inner = vm.computeCreate2Address(innerSalt, keccak256(type(DContract).creationCode), outer);

        vm.expectRevert(inner, 2);
        new NestedDContractCreator2{salt: outerSalt}();
        new NestedDContractCreator2{salt: outerSalt}();
    }
}

// Used by `testExpectRevertsWithReverterNestedCreate`: a contract whose constructor
// directly creates another contract that reverts.
contract NestedDContractCreator {
    constructor() {
        new DContract();
    }
}

// File-level constant shared between NestedDContractCreator2 and its test.
bytes32 constant NESTED_DCONTRACT_CREATOR2_INNER_SALT = bytes32(uint256(0xDEAD));

// Used by `testExpectRevertsWithReverterCountNestedCreate2`: fixed inner salt so
// both CREATE2 iterations produce the same inner would-be address.
contract NestedDContractCreator2 {
    constructor() {
        new DContract{salt: NESTED_DCONTRACT_CREATOR2_INNER_SALT}();
    }
}

// Used by `testExpectRevertWithBytes4SelectorAndReverterTopLevelCreate`: constructor
// reverts with a parameter-less custom error so the full revert data is exactly the
// 4-byte selector.
contract DCustomErrorContract {
    error CustomError();

    constructor() {
        revert CustomError();
    }
}

contract ExpectRevertCount is Test {
    function testRevertCountAny() public {
        uint64 count = 3;
        Reverter reverter = new Reverter();
        vm.expectRevert(count);
        reverter.revertWithMessage("revert");
        reverter.revertWithMessage("revert2");
        reverter.revertWithMessage("revert3");

        vm.expectRevert("revert");
        reverter.revertWithMessage("revert");
    }

    function testNoRevert() public {
        uint64 count = 0;
        Reverter reverter = new Reverter();
        vm.expectRevert(count);
        reverter.doNotRevert();
    }

    function testRevertCountSpecific() public {
        uint64 count = 2;
        Reverter reverter = new Reverter();
        vm.expectRevert("revert", count);
        reverter.revertWithMessage("revert");
        reverter.revertWithMessage("revert");
    }

    function testNoRevertSpecific() public {
        uint64 count = 0;
        Reverter reverter = new Reverter();
        vm.expectRevert("revert", count);
        reverter.doNotRevert();
    }

    function testRevertCountWithConstructor() public {
        uint64 count = 1;
        vm.expectRevert("constructor revert", count);
        new ConstructorReverter("constructor revert");
    }

    function testNoRevertWithConstructor() public {
        uint64 count = 0;
        vm.expectRevert("constructor revert", count);
        new CContract();
    }

    function testRevertCountNestedSpecific() public {
        uint64 count = 2;
        Reverter reverter = new Reverter();
        Reverter inner = new Reverter();

        vm.expectRevert("nested revert", count);
        reverter.revertWithMessage("nested revert");
        reverter.nestedRevert(inner, "nested revert");

        vm.expectRevert("nested revert", count);
        reverter.nestedRevert(inner, "nested revert");
        reverter.nestedRevert(inner, "nested revert");
    }

    function testRevertCountCallsThenReverts() public {
        uint64 count = 2;
        Reverter reverter = new Reverter();
        Dummy dummy = new Dummy();

        vm.expectRevert("called a function and then reverted", count);
        reverter.callThenRevert(dummy, "called a function and then reverted");
        reverter.callThenRevert(dummy, "called a function and then reverted");
    }

    function testNoRevertCall() public {
        uint64 count = 0;
        Reverter reverter = new Reverter();
        Dummy dummy = new Dummy();

        vm.expectRevert("called a function and then reverted", count);
        reverter.callThenNoRevert(dummy);
    }
}

contract ExpectRevertCountWithReverter is Test {
    function testRevertCountWithReverter() public {
        uint64 count = 2;
        Reverter reverter = new Reverter();
        vm.expectRevert(address(reverter), count);
        reverter.revertWithMessage("revert");
        reverter.revertWithMessage("revert");
    }

    function testNoRevertWithReverter() public {
        uint64 count = 0;
        Reverter reverter = new Reverter();
        vm.expectRevert(address(reverter), count);
        reverter.doNotRevert();
    }

    function testReverterCountWithData() public {
        uint64 count = 2;
        Reverter reverter = new Reverter();
        vm.expectRevert("revert", address(reverter), count);
        reverter.revertWithMessage("revert");
        reverter.revertWithMessage("revert");
    }
}

contract ExpectRevertPrecompileTest is Test {
    /// Test that vm.expectRevert works when the next external call targets a
    /// precompile address directly. Precompile calls don't create an interpreter
    /// frame (no `initialize_interp`), so depth tracking must account for them.
    function testExpectRevertDirectPrecompileCall() public {
        // BLAKE2F precompile (0x09) expects exactly 213 bytes of input.
        // Calling it with invalid input reverts.
        vm.expectRevert();
        address(0x09).call(hex"00");
    }
}

contract ExpectRevertWithErrorTest is Test {
    /// Ref: <https://github.com/foundry-rs/foundry/issues/12511>
    function test_f() external {
        bytes memory v = abi.encodeWithSignature("Error(string)", "");
        vm.expectRevert(v);
        this.f(v);

        bytes memory v1 = abi.encodeWithSignature("Error(string)", unicode"🙀");
        vm.expectRevert(v1);
        this.f(v1);
    }

    function f(bytes memory v) external pure {
        assembly {
            revert(add(v, 32), mload(v))
        }
    }
}
