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

// TEST-ONLY mocks for the paymaster-stack audit suite.
// NEVER deploy to chain 2800. These exist only so the hardhat suite can drive
// the real paymaster sources through accept/reject/pricing/quota/gas paths.

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../AereStakeQuotaPaymaster.sol";
import "../paymaster/AereEntryPoint.sol";

/// AereOracle-shaped mock: getPrice(bytes32) -> (uint128 price1e8, uint64 ts, uint256 contributors).
contract MockAereOracle {
    mapping(bytes32 => uint128) public px;
    mapping(bytes32 => bool) public boom;

    function set(bytes32 k, uint128 p) external { px[k] = p; }
    function setRevert(bytes32 k, bool b) external { boom[k] = b; }

    function getPrice(bytes32 k) external view returns (uint128, uint64, uint256) {
        require(!boom[k], "oracle: revert");
        return (px[k], uint64(block.timestamp), 1);
    }
}

/// AereStaking-shaped mock that DOES implement balanceOf (unlike the real one),
/// so the quota-math tests can drive stakedOf via the delegated path.
contract MockStaking {
    mapping(address => uint256) public bal;
    bool public boom;

    function setBal(address u, uint256 v) external { bal[u] = v; }
    function setBoom(bool b) external { boom = b; }

    function balanceOf(address u) external view returns (uint256) {
        require(!boom, "staking: boom");
        return bal[u];
    }
}

/// Faithful clone of AereLockedStaking's storage layout + getLock signature
/// (returns the Lock struct in the SAME field order as the real contract),
/// plus a batch `seed` so the OOG gas test can create many locks cheaply.
contract MockLockedStaking {
    struct Lock {
        uint256 amount;
        uint64 startedAt;
        uint64 unlockAt;
        uint8 tier;
        bool withdrawn;
    }
    mapping(address => Lock[]) private _locks;
    bool public boom;

    function setBoom(bool b) external { boom = b; }

    function seed(address u, uint256 n, uint256 amount, uint64 unlockAt) external {
        for (uint256 i = 0; i < n; i++) {
            _locks[u].push(Lock(amount, uint64(block.timestamp), unlockAt, 0, false));
        }
    }

    function lockCount(address u) external view returns (uint256) {
        require(!boom, "locked: boom");
        return _locks[u].length;
    }

    function getLock(address u, uint256 id) external view returns (Lock memory) {
        return _locks[u][id];
    }
}

/// Locked-staking mock whose getLock returns the fields in the ORDER the
/// paymaster's IAereLockedStaking interface EXPECTS: (tier, amount, startTime,
/// unlockTime, withdrawn). Used to (a) prove the field order is the root cause of
/// the revert, and (b) measure the loop gas in the hypothetical "interface fixed"
/// world where the loop actually executes.
contract MockLockedStakingMatched {
    struct L { uint8 tier; uint256 amount; uint64 startTime; uint64 unlockTime; bool withdrawn; }
    mapping(address => L[]) private _locks;
    bool public boom;

    function setBoom(bool b) external { boom = b; }

    function seed(address u, uint256 n, uint256 amount, uint64 unlockTime) external {
        for (uint256 i = 0; i < n; i++) {
            _locks[u].push(L(0, amount, uint64(block.timestamp), unlockTime, false));
        }
    }

    function lockCount(address u) external view returns (uint256) {
        require(!boom, "matched: boom");
        return _locks[u].length;
    }

    function getLock(address u, uint256 id)
        external
        view
        returns (uint8 tier, uint256 amount, uint64 startTime, uint64 unlockTime, bool withdrawn)
    {
        L memory l = _locks[u][id];
        return (l.tier, l.amount, l.startTime, l.unlockTime, l.withdrawn);
    }
}

/// ERC-20 whose transferFrom returns false (no revert) when armed, to exercise
/// AereTokenPaymaster's `require(...transferFrom..., "TokenPM: pull failed")`.
contract MockERC20FalseTransferFrom is ERC20 {
    uint8 private immutable _dec;
    bool public lie;

    constructor(string memory n, string memory s, uint8 d) ERC20(n, s) { _dec = d; }
    function decimals() public view override returns (uint8) { return _dec; }
    function mint(address to, uint256 amt) external { _mint(to, amt); }
    function setLie(bool b) external { lie = b; }

    function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
        if (lie) return false;
        return super.transferFrom(from, to, amount);
    }
}

/// Measures the gas consumed by a staticcall to stakedOf(), read via callStatic.
contract StakeGasProbe {
    function measure(address pm, address user) external view returns (uint256 gasUsed, uint256 staked) {
        uint256 g0 = gasleft();
        staked = AereStakeQuotaPaymaster(payable(pm)).stakedOf(user);
        gasUsed = g0 - gasleft();
    }
}

/// Attempts to re-enter AereEntryPoint.withdrawTo during the value callback,
/// to prove the nonReentrant guard blocks reentrancy.
contract ReentrantWithdrawer {
    AereEntryPoint public immutable ep;
    bool public tried;

    constructor(address _ep) { ep = AereEntryPoint(payable(_ep)); }

    function fund() external payable { ep.depositTo{ value: msg.value }(address(this)); }

    function attack(uint256 amount) external { ep.withdrawTo(payable(address(this)), amount); }

    receive() external payable {
        if (!tried && address(ep).balance >= 1) {
            tried = true;
            ep.withdrawTo(payable(address(this)), 1); // must revert (nonReentrant)
        }
    }
}
