// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

/// @notice ERC-20 test double that attempts a configured arbitrary callback
/// during transfer/transferFrom while still completing an exact transfer.
contract CallbackERC20 {
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    address public callbackTarget;
    bytes public callbackData;
    bool public callbackEnabled;
    bool public callbackAttempted;
    bool public callbackSucceeded;

    error InsufficientBalance();
    error InsufficientAllowance();
    error CallbackCallFailed();

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

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

    function configureCallback(address target, bytes calldata data) external {
        callbackTarget = target;
        callbackData = data;
        callbackEnabled = true;
        callbackAttempted = false;
        callbackSucceeded = false;
    }

    function disableCallback() external {
        callbackEnabled = false;
    }

    /// @notice Lets a test make the call with this token as msg.sender.
    function invoke(address target, bytes calldata data) external returns (bytes memory result) {
        (bool ok, bytes memory ret) = target.call(data);
        if (!ok) revert CallbackCallFailed();
        return ret;
    }

    /// @notice Calls a reward claim with this token as the beneficiary.
    function claimReward(address rewards, address asset) external {
        (bool ok,) = rewards.call(abi.encodeWithSignature("claimReward(address)", asset));
        if (!ok) revert CallbackCallFailed();
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        _attemptCallback();
        _move(msg.sender, to, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external returns (bool) {
        uint256 permitted = allowance[from][msg.sender];
        if (permitted != type(uint256).max) {
            if (permitted < amount) revert InsufficientAllowance();
            allowance[from][msg.sender] = permitted - amount;
        }
        _attemptCallback();
        _move(from, to, amount);
        return true;
    }

    function _attemptCallback() private {
        if (!callbackEnabled || callbackAttempted) return;
        callbackAttempted = true;
        (callbackSucceeded,) = callbackTarget.call(callbackData);
    }

    function _move(address from, address to, uint256 amount) private {
        if (balanceOf[from] < amount) revert InsufficientBalance();
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
    }
}
