pragma solidity 0.8.20;

import {Test} from "forge-std/Test.sol";

import {Fatso} from "../../src/FatsoERC20.sol";
import {Stinky} from "../../src/StinkyERC20.sol";
import {Ghost} from "../../src/GhstERC20.sol";
import {GhostAuthority} from "../../src/GhostAuthority.sol";
import {GhostDistributor} from "../../src/StakingDistributor.sol";
import {GhostTreasury} from "../../src/Treasury.sol";
import {GhostStaking} from "../../src/Staking.sol";
import {ERC20Mock} from "../../src/mocks/ERC20Mock.sol";
import {GhostBondingCalculator} from "../../src/StandardBondingCalculator.sol";

import {ITreasury} from "../../src/interfaces/ITreasury.sol";
import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";

contract RebaseBatcher {
    address public immutable STAKING;

    constructor(address _staking) {
        STAKING = _staking;
    }

    function multipleRebases(uint256 times) external returns (uint256) {
        return _rebase(times);
    }

    function _rebase(uint256 times) internal returns (uint256) {
        if (times == 0) return 0;

        (bool success, ) = STAKING.call(abi.encodeWithSignature("rebase()"));
        require(success, "Batch rebase failed");

        return 1 + _rebase(times - 1);
    }
}

contract StakingTest is Test {
    using SafeERC20 for Stinky;

    address constant INITIALIZER       = 0x0000000000000000000000000000000000000001;
    address constant GOVERNOR          = 0x0000000000000000000000000000000000000003;
    address constant GUARDIAN          = 0x0000000000000000000000000000000000000004;
    address constant POLICY            = 0x0000000000000000000000000000000000000005;
    address constant VAULT             = 0x0000000000000000000000000000000000000006;
    address constant ALICE             = 0x0000000000000000000000000000000000000007;
    address constant BOB               = 0x0000000000000000000000000000000000000008;

    uint48 public constant EPOCH_LENGTH = 2200;
    uint48 public constant EPOCH_NUMBER = 1;
    uint48 public constant EPOCH_END_TIME = 1337;

    uint256 constant public INITIAL_INDEX = 10819917194513808e56;
    uint256 constant public TOTAL_INITIAL_SUPPLY = 5000000000000000;

    Fatso ftso;
    Stinky stnk;
    Ghost ghst;
    GhostStaking staking;
    GhostTreasury treasury;
    GhostAuthority authority;
    GhostBondingCalculator calculator;

    uint256 public constant AMOUNT = 69;
    uint256 public constant BIG_AMOUNT = AMOUNT * 1e9;

    event Staked(
        address sender,
        address receiver,
        uint256 amount,
        bool isRebase,
        bool isClaim
    );

    event Unstaked(
        address sender,
        address receiver,
        uint256 amount,
        bool isTrigger,
        bool isRebase
    );

    event DistributorSet(address distributor);
    event WarmupSet(uint256 warmup);
    event Ghosted(bytes32 indexed receiver, uint256 indexed amount);

    function setUp() public {
        vm.startPrank(INITIALIZER);
        authority = new GhostAuthority(
            GOVERNOR,
            GUARDIAN,
            POLICY,
            VAULT
        );
        ftso = new Fatso(address(authority), "Fatso", "FTSO");
        stnk = new Stinky(INITIAL_INDEX, "Stinky", "STNK");
        ghst = new Ghost(address(stnk), "Ghost", "GHST");
        staking = new GhostStaking(
            address(ftso),
            address(stnk),
            address(ghst),
            EPOCH_LENGTH,
            EPOCH_NUMBER,
            EPOCH_END_TIME,
            address(authority),
            0
        );
        treasury = new GhostTreasury(address(ftso), 69, address(authority));
        stnk.initialize(address(staking), address(treasury), address(ghst));
        ghst.initialize(address(staking));
        calculator = new GhostBondingCalculator(address(ftso), 1, 1);
        vm.stopPrank();
        vm.roll(block.number + 1);
    }

    function test_correctAfterConstruction() public view {
        assertEq(staking.stnk(), address(stnk));
        assertEq(staking.ftso(), address(ftso));
        assertEq(staking.ghst(), address(ghst));
        (uint48 length, uint48 number, uint48 end, uint256 distribute) = staking.epoch();
        assertEq(length, EPOCH_LENGTH);
        assertEq(number, EPOCH_NUMBER);
        assertEq(end, EPOCH_END_TIME);
        assertEq(distribute, 0);
    }

    function test_GOVERNORCouldSetDistributor(address maybeDistributor) public {
        vm.assume(maybeDistributor != address(0));
        assertEq(staking.distributor(), address(0));
        vm.prank(GOVERNOR);
        staking.setDistributor(maybeDistributor);
        assertEq(staking.distributor(), maybeDistributor);
    }

    function test_emitsDistributorSetEvent(address maybeDistributor) public {
        vm.assume(maybeDistributor != address(0));
        vm.expectEmit(true, true, true, false, address(staking));
        emit DistributorSet(maybeDistributor);
        vm.prank(GOVERNOR);
        staking.setDistributor(maybeDistributor);
    }

    function test_arbitraryAddressCouldNotAddDistributor(address someone, address maybeDistributor) public {
        vm.assume(maybeDistributor != address(0) && someone != GOVERNOR);
        vm.expectRevert();
        vm.prank(someone);
        staking.setDistributor(maybeDistributor);
    }

    function test_GOVERNORCouldSetWarmupPeriod(uint48 maybeWarmupPeriod) public {
        vm.assume(maybeWarmupPeriod > 0);
        assertEq(staking.warmupPeriod(), 0);
        vm.prank(GOVERNOR);
        staking.setWarmupPeriod(maybeWarmupPeriod);
        assertEq(staking.warmupPeriod(), maybeWarmupPeriod);
    }

    function test_emitsWarmupEvent(uint48 maybeWarmupPeriod) public {
        vm.expectEmit(true, true, true, false, address(staking));
        emit WarmupSet(maybeWarmupPeriod);
        vm.prank(GOVERNOR);
        staking.setWarmupPeriod(maybeWarmupPeriod);
    }

    function test_arbitraryAddressCouldNotSetWarmupPeriod(address someone, uint48 maybeWarmupPeriod) public {
        vm.assume(maybeWarmupPeriod > 0 && someone != GOVERNOR);
        vm.expectRevert();
        vm.prank(someone);
        staking.setWarmupPeriod(maybeWarmupPeriod);
    }

    function test_stake_addAmountToTheWarmupWhenClaimIsFalse() public {
        _mintAndApprove(ALICE, AMOUNT);

        vm.prank(ALICE);
        uint256 rebased = staking.stake(AMOUNT, ALICE, false, false);
        assertEq(ftso.balanceOf(ALICE), 0);
        assertApproxEqAbs(staking.supplyInWarmup(), rebased, 1);

        (uint256 deposit, uint256 payout, uint48 expiry, bool lock) = staking.warmupInfo(ALICE);
        assertEq(deposit, AMOUNT);
        assertEq(payout, ghst.balanceTo(AMOUNT));
        assertEq(expiry, 1);
        assertEq(lock, false);
    }

    function test_stake_emitsStakedEvent() public {
        _mintAndApprove(ALICE, AMOUNT);

        vm.expectEmit(true, true, true, false, address(staking));
        emit Staked(ALICE, ALICE, AMOUNT, true, true);

        vm.prank(ALICE);
        staking.stake(AMOUNT, ALICE, true, true);
    }

    function test_stake_exchangesFatsoToStinkyWhenClaimIsTrueAndRebasingIsTrue() public {
        _mintAndApprove(ALICE, AMOUNT);

        vm.prank(ALICE);
        uint256 rebasedAmount = staking.stake(AMOUNT, ALICE, true, true);

        assertEq(ftso.balanceOf(ALICE), 0);
        assertEq(stnk.balanceOf(ALICE), rebasedAmount);
    }

    function test_stake_exchangesFatsoForNewlyMintedGhostWhenClaimIsTrueAndRebasingIsFalse() public {
        _mintAndApprove(ALICE, AMOUNT);

        vm.prank(ALICE);
        uint256 rebasedAmount = staking.stake(AMOUNT, ALICE, false, true);

        assertEq(ftso.balanceOf(ALICE), 0);
        assertEq(ghst.balanceOf(ALICE), rebasedAmount);
    }

    function test_stake_addAmountToWarmupWhenClaimIsTrueAndWarmupGtZero() public {
        _mintAndApprove(ALICE, AMOUNT);
        vm.prank(GOVERNOR);
        staking.setWarmupPeriod(1);

        vm.prank(ALICE);
        uint256 rebased = staking.stake(AMOUNT, ALICE, false, true);

        assertEq(ftso.balanceOf(ALICE), 0);
        assertApproxEqAbs(staking.supplyInWarmup(), rebased, 1);

        (uint256 deposit, uint256 payout, uint48 expiry, bool lock) = staking.warmupInfo(ALICE);
        assertEq(deposit, AMOUNT);
        assertEq(payout, ghst.balanceTo(AMOUNT));
        assertEq(expiry, 2);
        assertEq(lock, false);
    }

    function test_stake_allowsSelfDepositWhenNotLocked() public {
        _mintAndApprove(ALICE, AMOUNT);

        vm.prank(ALICE);
        staking.stake(AMOUNT, BOB, false, false);

        assertEq(ftso.balanceOf(ALICE), 0);
    }

    function test_stake_disablesExternalDepositsWhenLocked() public {
        _mintAndApprove(ALICE, AMOUNT);
        vm.prank(BOB);
        staking.toggleLock();

        vm.expectRevert();
        vm.prank(ALICE);
        staking.stake(AMOUNT, BOB, false, false);

        assertEq(ftso.balanceOf(ALICE), AMOUNT);
    }

    function test_claim_transferStinkyWhenRebasingIsTrue() public {
        _prepareAndRoll(ALICE, AMOUNT, false, false);

        assertEq(stnk.balanceOf(ALICE), 0);
        vm.prank(ALICE);
        uint256 rebased = staking.claim(ALICE, true);
        assertEq(stnk.balanceOf(ALICE), rebased);
        assertEq(rebased > 0, true);
    }

    function test_claim_mintsGhostWhenRebasingIsFalse() public {
        _prepareAndRoll(ALICE, AMOUNT, false, false);

        assertEq(ghst.balanceOf(ALICE), 0);
        vm.prank(ALICE);
        uint256 rebased = staking.claim(ALICE, false);
        assertEq(ghst.balanceOf(ALICE), rebased);
        assertEq(rebased > 0, true);
    }

    function test_claim_preventsExternalClaimsWhenLocked() public {
        _prepareAndRoll(ALICE, AMOUNT, false, false);

        assertEq(ghst.balanceOf(ALICE), 0);
        vm.prank(ALICE);
        staking.toggleLock();

        vm.expectRevert();
        vm.prank(BOB);
        staking.claim(ALICE, false);
        assertEq(ghst.balanceOf(ALICE), 0);
    }

    function test_claim_allowExternalClaimsWhenNotLocked() public {
        _prepareAndRoll(ALICE, AMOUNT, false, false);

        assertEq(ghst.balanceOf(ALICE), 0);
        vm.prank(BOB);
        uint256 rebased = staking.claim(ALICE, false);

        assertEq(ghst.balanceOf(ALICE), rebased);
        assertEq(rebased > 0, true);
    }

    function test_claim_doesNothingWhenThereIsNothingToClaim() public {
        assertEq(ghst.balanceOf(ALICE), 0);
        vm.prank(ALICE);
        uint256 rebased = staking.claim(ALICE, false);
        assertEq(ghst.balanceOf(ALICE), 0);
        assertEq(rebased, 0);
    }

    function test_claim_doesNothingWhenWarmupIsntOver() public {
        _mintAndApprove(ALICE, AMOUNT);
        vm.prank(GOVERNOR);
        staking.setWarmupPeriod(1337);
        vm.prank(ALICE);
        staking.stake(AMOUNT, ALICE, false, false);

        assertEq(stnk.balanceOf(ALICE), 0);
        vm.prank(ALICE);
        uint256 rebased = staking.claim(ALICE, true);
        assertEq(stnk.balanceOf(ALICE), 0);
        assertEq(rebased, 0);
    }

    function test_forefeit_removesStakeFromWarmupAndReturnsFatso() public {
        _prepareAndRoll(ALICE, AMOUNT, false, false);

        assertEq(ftso.balanceOf(ALICE), 0);
        vm.prank(ALICE);
        uint256 deposit = staking.forfeit();

        assertEq(ftso.balanceOf(ALICE), deposit);
        assertEq(staking.supplyInWarmup(), 0);

        (uint256 depositInWarmup, uint256 shares, uint48 expiry,) = staking.warmupInfo(ALICE);
        assertEq(depositInWarmup, 0);
        assertEq(shares, 0);
        assertEq(expiry, 0);
    }

    function test_forefeit_transfersZeroIfThereIsNoBalanceInWarmup() public {
        vm.prank(ALICE);
        uint256 deposit = staking.forfeit();
        assertEq(deposit, 0);
        assertEq(ftso.balanceOf(ALICE), 0);
        assertEq(staking.supplyInWarmup(), 0);
    }

    function test_unstake_emitsUnstakedEvent() public {
        _prepareAndRoll(ALICE, AMOUNT, true, true);
        uint256 aliceBalance = stnk.balanceOf(ALICE);
        vm.startPrank(ALICE);
        stnk.approve(address(staking), aliceBalance);

        vm.expectEmit(true, true, true, false, address(staking));
        emit Unstaked(ALICE, ALICE, aliceBalance, false, true);

        staking.unstake(aliceBalance, ALICE, false, true);
        vm.stopPrank();
    }

    function test_unstake_canRedeemStinkyToFatso() public {
        _prepareAndRoll(ALICE, AMOUNT, true, true);
        uint256 aliceBalance = stnk.balanceOf(ALICE);
        vm.startPrank(ALICE);
        stnk.approve(address(staking), aliceBalance);
        staking.unstake(aliceBalance, ALICE, false, true);
        vm.stopPrank();
        assertEq(stnk.balanceOf(ALICE), 0);
        assertEq(ftso.balanceOf(ALICE), AMOUNT);
    }

    function test_unstake_canRedeemGhostToFatso() public {
        _prepareAndRoll(ALICE, AMOUNT, false, true);
        uint256 aliceBalance = ghst.balanceOf(ALICE);
        vm.startPrank(ALICE);
        ghst.approve(address(staking), aliceBalance);
        vm.roll(block.number + 69);
        staking.unstake(aliceBalance, ALICE, false, false);
        vm.stopPrank();
        assertEq(ghst.balanceOf(ALICE), 0);
        assertEq(ftso.balanceOf(ALICE), ghst.balanceFrom(aliceBalance));
    }

    function test_wrap_convertsStinkyIntoGhost() public {
        _prepareAndRoll(ALICE, AMOUNT, true, true);
        uint256 aliceBalance = stnk.balanceOf(ALICE);
        assertEq(ghst.balanceOf(ALICE), 0);

        vm.startPrank(ALICE);
        stnk.approve(address(staking), aliceBalance);
        uint256 newBalance = staking.wrap(ALICE, aliceBalance);
        vm.stopPrank();

        assertEq(stnk.balanceOf(ALICE), 0);
        assertEq(ghst.balanceOf(ALICE), newBalance);
    }

    function test_wrap_convertsGhostToStinky() public {
        _prepareAndRoll(ALICE, AMOUNT, false, true);
        uint256 aliceBalance = ghst.balanceOf(ALICE);
        assertEq(stnk.balanceOf(ALICE), 0);

        vm.roll(block.number + 69);
        vm.startPrank(ALICE);
        ghst.approve(address(staking), aliceBalance);
        uint256 newBalance = staking.unwrap(ALICE, aliceBalance);
        vm.stopPrank();

        assertEq(ghst.balanceOf(ALICE), 0);
        assertEq(stnk.balanceOf(ALICE), newBalance);
    }

    function test_rebase_doesNothingIfTheBlockIsBeforeTheEpochEndBlock() public {
        (, uint48 numberBefore, uint48 endBefore, uint256 distributeBefore) = staking.epoch();
        assertEq(endBefore > block.timestamp, true);
        staking.rebase();
        (, uint48 numberAfter, uint48 endAfter, uint256 distributeAfter) = staking.epoch();
        assertEq(distributeAfter, distributeBefore);
        assertEq(endAfter, endBefore);
        assertEq(numberAfter, numberBefore);
    }

    function test_rebase_incrementsEpochNumberAndCallRebase() public {
        (uint48 length, uint48 numberBefore, uint48 endBefore,) = staking.epoch();
        assertEq(endBefore > block.timestamp, true);
        vm.warp(endBefore);
        staking.rebase();
        (, uint48 numberAfter, uint48 endAfter,) = staking.epoch();

        assertEq(endAfter, endBefore + length);
        assertEq(numberAfter, numberBefore + 1);
    }

    function test_rebase_whenTheFatsoBalanceEqualsToStinkySupplyZeroDistribution() public {
        (uint48 length, uint48 numberBefore, uint48 endBefore, uint256 distributeBefore) = staking.epoch();
        assertEq(endBefore > block.timestamp, true);

        vm.prank(address(staking));
        stnk.safeTransfer(ALICE, AMOUNT);
        vm.prank(VAULT);
        ftso.mint(address(staking), AMOUNT);

        assertEq(stnk.balanceOf(ALICE), ftso.balanceOf(address(staking)));

        vm.warp(endBefore);
        staking.rebase();

        (, uint48 numberAfter, uint48 endAfter, uint256 distributeAfter) = staking.epoch();
        assertEq(endAfter, endBefore + length);
        assertEq(numberAfter, numberBefore + 1);
        assertEq(distributeAfter, distributeBefore);
    }

    function test_rebase_distributeDifferenceBetweenStakedAndTotalSupply() public {
        (uint48 length, uint48 numberBefore, uint48 endBefore,) = staking.epoch();
        assertEq(endBefore > block.timestamp, true);

        vm.prank(address(staking));
        stnk.safeTransfer(ALICE, AMOUNT);
        vm.prank(VAULT);
        ftso.mint(address(staking), AMOUNT * 2);

        vm.warp(endBefore);
        staking.rebase();

        (, uint48 numberAfter, uint48 endAfter, uint256 distributeAfter) = staking.epoch();
        assertEq(endAfter, endBefore + length);
        assertEq(numberAfter, numberBefore + 1);
        assertEq(distributeAfter, AMOUNT);
    }

    function test_rebase_callDistributorIfSet() public {
        (uint48 length, uint48 numberBefore, uint48 endBefore,) = staking.epoch();
        assertEq(endBefore > block.timestamp, true);
        uint256 extendedAmount = AMOUNT * 1e18;

        GhostDistributor distributor = new GhostDistributor(
            address(treasury),
            address(ftso),
            address(staking),
            address(authority),
            1e4 // 1%
        );
        ERC20Mock reserve = new ERC20Mock("Reserve Token", "RET");

        vm.startPrank(GOVERNOR);
        authority.pushVault(address(treasury));
        staking.setDistributor(address(distributor));
        treasury.enable(ITreasury.STATUS.REWARDMANAGER, address(distributor), address(0));
        treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, ALICE, address(0));
        treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, BOB, address(0));
        treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator));
        vm.stopPrank();

        vm.startPrank(BOB);
        reserve.mint(BOB, extendedAmount);
        reserve.approve(address(treasury), extendedAmount);
        uint256 zeroProfit = treasury.tokenValue(address(reserve), extendedAmount);
        treasury.deposit(address(reserve), extendedAmount, zeroProfit);
        vm.stopPrank();

        vm.startPrank(ALICE);
        assertEq(ftso.balanceOf(address(staking)), 0);
        reserve.mint(ALICE, extendedAmount);
        reserve.approve(address(treasury), extendedAmount);
        uint256 send = treasury.deposit(address(reserve), extendedAmount, 0);
        assertEq(ftso.balanceOf(ALICE), send);
        assertEq(reserve.balanceOf(address(treasury)), extendedAmount * 2);
        ftso.approve(address(staking), send);
        staking.stake(send, ALICE, true, true);
        assertEq(ftso.balanceOf(address(staking)), send);
        assertEq(stnk.balanceOf(ALICE), send);
        vm.stopPrank();

        assertEq(treasury.excessReserves(), send);
        assertEq(treasury.totalReserves(), send * 2);

        skip(endBefore);
        uint256 extraToStaking = send * 1e4 / 1e6;
        vm.prank(address(staking));
        staking.rebase();
        assertEq(ftso.balanceOf(address(staking)), send + extraToStaking);

        (, uint48 numberAfter, uint48 endAfter, uint256 distributeAfter) = staking.epoch();
        assertEq(endAfter, endBefore + length);
        assertEq(numberAfter, numberBefore + 1);
        assertEq(distributeAfter, extraToStaking);
    }

    function test_correctStakeAmountDuringRebase() public {
        uint256 extendedAmount = AMOUNT * 1e18;
        uint256 bounty = 1;
        GhostDistributor distributor = new GhostDistributor(
            address(treasury),
            address(ftso),
            address(staking),
            address(authority),
            1e6
        );
        ERC20Mock reserve = new ERC20Mock("Reserve Token", "RET");

        vm.startPrank(GOVERNOR);
        authority.pushVault(address(treasury));
        staking.setDistributor(address(distributor));
        distributor.setBounty(bounty);
        treasury.enable(ITreasury.STATUS.REWARDMANAGER, address(distributor), address(0));
        treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, ALICE, address(0));
        treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator));
        vm.stopPrank();

        vm.startPrank(ALICE);
        reserve.mint(ALICE, extendedAmount);
        reserve.approve(address(treasury), type(uint256).max);

        uint256 profit = treasury.tokenValue(address(reserve), extendedAmount) - AMOUNT;
        treasury.deposit(address(reserve), extendedAmount, profit);
        vm.stopPrank();

        (,, uint48 end,) = staking.epoch();
        skip(end);
        vm.roll(block.number + 1);

        vm.startPrank(ALICE);
        ftso.approve(address(staking), type(uint256).max);
        staking.stake(AMOUNT, ALICE, true, true);
        vm.stopPrank();
        vm.roll(block.number + 1);

        uint256 postBounty = AMOUNT + bounty;

        assertEq(stnk.balanceOf(ALICE), postBounty);
        assertEq(ftso.balanceOf(address(staking)), postBounty);

        skip(end + EPOCH_LENGTH);
        staking.rebase();

        (,,, uint256 distribute) = staking.epoch();
        assertEq(distribute, postBounty);
        assertEq(stnk.balanceOf(ALICE), postBounty);
        assertEq(ftso.balanceOf(address(staking)), 2 * postBounty + bounty);
    }

    function test_arbitraryAddressCouldNotAddGatekeeper(address someone, address maybeGatekeeper) public {
        vm.assume(maybeGatekeeper != address(0) && someone != GOVERNOR);
        vm.expectRevert();
        vm.prank(someone);
        staking.setDistributor(maybeGatekeeper);
    }

    function test_GOVERNORCouldSetGatekeeper() public {
        address previousGatekeeper = staking.gatekeeper();
        vm.prank(GOVERNOR);
        staking.setGatekeeperAddress(420);
        assert(staking.gatekeeper() != previousGatekeeper);
    }

    function test_couldNotGhostTokensIfNoGhst() public {
        assertEq(staking.ghostedSupply(), 0);
        vm.expectRevert();
        vm.prank(ALICE);
        staking.ghost(bytes32(abi.encodePacked(ALICE)), AMOUNT);
        assertEq(staking.ghostedSupply(), 0);
    }

    function test_correctlyGhostTokens() public {
        _prepareAndRoll(ALICE, BIG_AMOUNT, true, true);
        uint256 aliceBalance = stnk.balanceOf(ALICE);

        vm.startPrank(ALICE);
        stnk.approve(address(staking), aliceBalance);
        uint256 ghstBalance = staking.wrap(ALICE, aliceBalance);
        vm.stopPrank();

        assertEq(staking.ghostedSupply(), 0);
        assertEq(stnk.circulatingSupply(), BIG_AMOUNT - 1); // precision fix
        assertEq(ghst.totalSupply(), ghstBalance);

        vm.prank(ALICE);
        staking.ghost(bytes32(abi.encodePacked(ALICE)), ghstBalance);

        assertEq(staking.ghostedSupply(), ghstBalance);
        assertEq(stnk.circulatingSupply(), BIG_AMOUNT - 1); // precision fix
        assertEq(ghst.totalSupply(), 0);
    }

    function test_ghostTokensEmitsEvent() public {
        _prepareAndRoll(ALICE, BIG_AMOUNT, true, true);
        uint256 aliceBalance = stnk.balanceOf(ALICE);

        vm.startPrank(ALICE);
        stnk.approve(address(staking), aliceBalance);
        uint256 ghstBalance = staking.wrap(ALICE, aliceBalance);
        vm.stopPrank();

        bytes32 receiver = bytes32(abi.encodePacked(ALICE));
        vm.expectEmit(true, true, true, false, staking.gatekeeper());
        emit Ghosted(receiver, ghstBalance);

        vm.prank(ALICE);
        staking.ghost(receiver, ghstBalance);
    }

    function test_breakoutLogicWorks() public {
        uint256 initialIndex = staking.index();
        bytes32 receiver = bytes32(abi.encodePacked(ALICE));
        uint256 rebased = _prepareAndRoll(ALICE, BIG_AMOUNT, false, false);

        (uint256 deposit, uint256 payout,,) = staking.warmupInfo(ALICE);
        assertEq(deposit, rebased);
        assertEq(payout, ghst.balanceTo(rebased));
        assertEq(staking.supplyInWarmup(), ghst.balanceFrom(payout));

        vm.expectRevert();
        vm.prank(BOB);
        staking.breakout(receiver, payout);

        uint256 requestedPayout = 1;
        uint256 expectedPayout = 0;
        while (expectedPayout < payout / 2) {
            uint256 pseudoRandom = uint256(keccak256(abi.encodePacked("salt", requestedPayout, expectedPayout)));
            uint256 range = (payout * 3) / 100;
            requestedPayout = (pseudoRandom % range) + 1;

            vm.expectEmit(true, true, true, false, staking.gatekeeper());
            emit Ghosted(receiver, requestedPayout);

            vm.prank(ALICE);
            staking.breakout(receiver, requestedPayout);
            expectedPayout += requestedPayout;

            (,uint256 tmpPayout,,) = staking.warmupInfo(ALICE);
            assertEq(tmpPayout, payout - expectedPayout);
            assertEq(staking.supplyInWarmup(), ghst.balanceFrom(payout - expectedPayout));

            (,, uint48 end,) = staking.epoch();
            skip(end);
            staking.rebase();
        }

        (uint256 newDeposit, uint256 newPayout,,) = staking.warmupInfo(ALICE);
        assertEq(newPayout, payout - expectedPayout);
        assertEq(staking.supplyInWarmup(), ghst.balanceFrom(payout - expectedPayout));
        assertEq(ftso.balanceOf(ALICE), 0);

        vm.prank(ALICE);
        staking.forfeit();

        uint256 restoredPayout = expectedPayout * initialIndex / 1e18;
        assertLt(ftso.balanceOf(ALICE) + restoredPayout, BIG_AMOUNT);
        assertEq(newDeposit, ftso.balanceOf(ALICE));
    }

    function test_revertDuringReentrancyRebase() public {
        RebaseBatcher rebaseBatcher = new RebaseBatcher(address(staking));
        uint256 startBlock = block.number;
        uint256 passedRebases = 69;
        skip((1 + passedRebases) * EPOCH_LENGTH);

        (, uint48 prevNumber,,) = staking.epoch();
        rebaseBatcher.multipleRebases(passedRebases);
        (, uint48 currNumber,,) = staking.epoch();
        assertEq(currNumber, prevNumber + 1);

        uint256 i;
        for (; i < passedRebases;) {
            vm.roll(block.number + 1);
            (,uint48 number,,) = staking.epoch();
            rebaseBatcher.multipleRebases(1);
            (,currNumber,,) = staking.epoch();
            assertEq(currNumber, number + 1);
            unchecked { ++i; }
        }

        assertEq(currNumber, startBlock + passedRebases);
    }

    function _mintAndApprove(address who, uint256 value) internal {
        vm.prank(VAULT);
        ftso.mint(who, value);
        vm.prank(who);
        ftso.approve(address(staking), value);
    }

    function _prepareAndRoll(address who, uint256 value, bool rebase, bool claim) internal returns (uint256) {
        _mintAndApprove(who, value);

        (,, uint48 end,) = staking.epoch();
        skip(end);

        vm.prank(who);
        uint256 rebased = staking.stake(value, who, rebase, claim);

        (,, uint48 expiry,) = staking.warmupInfo(ALICE);
        vm.roll(expiry);
        return rebased;
    }
}
