// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

/**
 * @title CoverageProbes
 * @notice TEST-ONLY helpers for the round-2 branch-coverage suite. None of these
 *         is deployed to any network; they exist purely so that failure branches
 *         which cannot be driven by an EOA become reachable from Hardhat.
 *
 *         They are excluded from coverage instrumentation (see .solcover.js) so
 *         the measured per-file table stays apples-to-apples with the baseline.
 */

/// @notice Refuses every plain AERE transfer (no receive, no fallback), but can
///         still ORIGINATE calls via `exec`. This is what makes the
///         `(bool ok, ) = x.call{value: v}(""); require(ok)` FALSE branches
///         reachable: an EOA recipient always accepts value, a contract with no
///         receive() does not.
contract EtherRejector {
    /// Forward an arbitrary call so this contract can be the msg.sender that
    /// triggers a payout back to itself (which then fails).
    function exec(address target, bytes calldata data) external returns (bytes memory) {
        (bool ok, bytes memory ret) = target.call(data);
        if (!ok) {
            assembly {
                revert(add(ret, 0x20), mload(ret))
            }
        }
        return ret;
    }

    /// Same, with value attached (used to register/delegate from a contract).
    function execValue(address target, bytes calldata data, uint256 value) external payable returns (bytes memory) {
        (bool ok, bytes memory ret) = target.call{ value: value }(data);
        if (!ok) {
            assembly {
                revert(add(ret, 0x20), mload(ret))
            }
        }
        return ret;
    }

    /// Accept an ERC-721 so this contract can hold a fee-stream NFT.
    function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
        return this.onERC721Received.selector;
    }

    // Deliberately NO receive() and NO fallback(): plain value transfers revert.
}

/// @notice `owner()` returns the zero address, exercising the
///         `o != address(0)` FALSE side of AereFeeMonetizationV2.controlsContract
///         (a target with an owner() that answers, but answers zero).
contract ZeroOwnerProbe {
    function owner() external pure returns (address) {
        return address(0);
    }
}

/// @notice Has code but no `owner()` at all, so the staticcall hits the fallback
///         and the try/catch CATCH arm in controlsContract is taken.
contract NoOwnerProbe {
    uint256 public marker = 1;
}

/// @notice Accepts AERE and, on the FIRST value transfer after being armed,
///         re-enters a target with stored calldata. The re-entry is made with a
///         low-level call so the ReentrancyGuard's revert is CAUGHT here and the
///         outer, legitimate call still completes — the same shape as
///         MockReentrantBountyToken, which is what lets the blocked branch be
///         observed instead of unwinding the whole transaction.
contract ReentrantEtherProbe {
    address public target;
    bytes public payload;
    bool public armed;
    bool public reentryAttempted;
    bool public reentryBlocked;

    function arm(address _target, bytes calldata _payload) external {
        target = _target;
        payload = _payload;
        armed = true;
        reentryAttempted = false;
        reentryBlocked = false;
    }

    /// Originate a call (so this contract can be the delegator / NFT holder).
    function exec(address to, bytes calldata data) external returns (bytes memory) {
        (bool ok, bytes memory ret) = to.call(data);
        if (!ok) {
            assembly {
                revert(add(ret, 0x20), mload(ret))
            }
        }
        return ret;
    }

    function execValue(address to, bytes calldata data, uint256 value) external payable returns (bytes memory) {
        (bool ok, bytes memory ret) = to.call{ value: value }(data);
        if (!ok) {
            assembly {
                revert(add(ret, 0x20), mload(ret))
            }
        }
        return ret;
    }

    function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
        return this.onERC721Received.selector;
    }

    function _tryReenter() internal {
        if (!armed || target == address(0)) return;
        armed = false; // one-shot, or the guard would be re-tested forever
        reentryAttempted = true;
        (bool ok, ) = target.call(payload);
        reentryBlocked = !ok;
    }

    receive() external payable {
        _tryReenter();
    }
}

/// @notice ERC20 whose transferFrom re-enters a target once. AereSink.flush pulls
///         with transferFrom BEFORE it does anything else, so this is the hook
///         that reaches flush's own ReentrancyGuard.
contract ReentrantFlushToken {
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    address public target;
    bytes public payload;
    bool public armed;
    bool public reentryAttempted;
    bool public reentryBlocked;

    function mint(address to, uint256 amt) external {
        balanceOf[to] += amt;
    }

    function arm(address _target, bytes calldata _payload) external {
        target = _target;
        payload = _payload;
        armed = true;
    }

    function approve(address spender, uint256 amt) external returns (bool) {
        allowance[msg.sender][spender] = amt;
        return true;
    }

    function _tryReenter() internal {
        if (!armed || target == address(0)) return;
        armed = false;
        reentryAttempted = true;
        (bool ok, ) = target.call(payload);
        reentryBlocked = !ok;
    }

    function transfer(address to, uint256 amt) external returns (bool) {
        balanceOf[msg.sender] -= amt;
        balanceOf[to] += amt;
        return true;
    }

    function transferFrom(address from, address to, uint256 amt) external returns (bool) {
        _tryReenter();
        balanceOf[from] -= amt;
        balanceOf[to] += amt;
        return true;
    }
}

/// @notice ERC20 whose transfer REVERTS outright (rather than returning false).
///         AereSink._safeTransfer checks `!ok || (ret.length > 0 && !decode)`;
///         the return-false token drives the SECOND leg, and only a token that
///         actually reverts drives the FIRST (`!ok`).
contract RevertingTransferToken {
    mapping(address => uint256) public balanceOf;

    function mint(address to, uint256 amt) external {
        balanceOf[to] += amt;
    }

    function transfer(address, uint256) external pure returns (bool) {
        revert("token: transfer disabled");
    }

    function transferFrom(address, address, uint256) external pure returns (bool) {
        revert("token: transferFrom disabled");
    }

    function approve(address, uint256) external pure returns (bool) {
        return true;
    }
}

/// @notice V2-style router whose swap reverts with EMPTY revert data.
///         AereSink's catch block does `err.length > 0 ? string(err) : "swap-failed"`.
///         A router that reverts with a reason string drives the TRUE side; only
///         a bare `revert()` (zero-length returndata) drives the FALSE side.
contract EmptyRevertRouter {
    function swapExactTokensForTokens(
        uint256,
        uint256,
        address[] calldata,
        address,
        uint256
    ) external pure returns (uint256[] memory) {
        revert();
    }
}

/// @notice Price oracle that reverts for ONE designated asset only.
///         AereSink._computeOracleFloor has TWO nested try/catch arms; the shared
///         MockPriceOracle can only revert globally, which reaches the OUTER
///         catch. Reverting for the AERE leg alone is the only way to reach the
///         INNER catch.
contract SelectiveRevertOracle {
    mapping(address => uint256) public prices;
    mapping(address => bool) public revertsFor;

    function setPrice(address asset, uint256 price) external {
        prices[asset] = price;
    }

    function setRevertFor(address asset, bool b) external {
        revertsFor[asset] = b;
    }

    function getPrice(address asset) external view returns (uint256) {
        require(!revertsFor[asset], "oracle: asset paused");
        return prices[asset];
    }
}
