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

import { Test } from "forge-std/Test.sol";
import { RewardProgramV3 } from "../../contracts/bonds/RewardProgramV3.sol";
import { TaskEscrowV3 } from "../../contracts/escrow/TaskEscrowV3.sol";
import { MutualAidPoolV3 } from "../../contracts/mutualaid/MutualAidPoolV3.sol";
import { VaultfireBIPVaultV3 } from "../../contracts/vault/VaultfireBIPVaultV3.sol";
import { CallbackERC20 } from "../mocks/CallbackERC20.sol";

/// @notice Confirms that malicious ERC-20 callbacks cannot interleave any
/// asset-accounting state transition or consume a pull credit twice.
contract CallbackReentrancyV3Test is Test {
    address internal constant OWNER = address(0xBEEF);
    address internal constant ALICE = address(0xA11CE);
    address internal constant BIDDER = address(0xB1D);
    address internal constant TREASURY = address(0xCAFE);

    function test_taskCreationReservesStateAndRejectsCallbackBid() external {
        TaskEscrowV3 escrow = new TaskEscrowV3(OWNER, TREASURY, 0, 10 ether, 20 ether, 1 days);
        CallbackERC20 token = new CallbackERC20();
        token.mint(ALICE, 5 ether);
        vm.prank(ALICE);
        token.approve(address(escrow), type(uint256).max);

        token.configureCallback(
            address(token),
            abi.encodeCall(CallbackERC20.invoke, (address(escrow), abi.encodeCall(escrow.placeBid, (1))))
        );
        vm.prank(ALICE);
        uint256 taskId = escrow.createTaskERC20(
            address(token),
            5 ether,
            uint64(block.timestamp + 1 days),
            uint64(block.timestamp + 2 days),
            uint64(block.timestamp + 3 days)
        );

        assertEq(taskId, 1);
        assertTrue(token.callbackAttempted());
        assertFalse(token.callbackSucceeded());
        assertFalse(escrow.isBidder(taskId, address(token)));
        assertEq(escrow.liabilities(address(token)), 5 ether);
        assertEq(token.balanceOf(address(escrow)), 5 ether);
    }

    function test_taskPullWithdrawalRejectsCallbackTaskCreation() external {
        TaskEscrowV3 escrow = new TaskEscrowV3(OWNER, TREASURY, 0, 10 ether, 20 ether, 1 days);
        CallbackERC20 token = new CallbackERC20();
        token.mint(ALICE, 5 ether);
        vm.startPrank(ALICE);
        token.approve(address(escrow), type(uint256).max);
        uint256 taskId = escrow.createTaskERC20(
            address(token),
            5 ether,
            uint64(block.timestamp + 1 days),
            uint64(block.timestamp + 2 days),
            uint64(block.timestamp + 3 days)
        );
        vm.stopPrank();
        vm.prank(BIDDER);
        escrow.placeBid(taskId);
        vm.prank(ALICE);
        escrow.acceptBid(taskId, BIDDER);
        vm.prank(BIDDER);
        escrow.submitWork(taskId);
        vm.prank(ALICE);
        escrow.approve(taskId);

        token.configureCallback(
            address(token),
            abi.encodeCall(
                CallbackERC20.invoke,
                (
                    address(escrow),
                    abi.encodeCall(
                        escrow.createTaskERC20,
                        (
                            address(token),
                            1,
                            uint64(block.timestamp + 1 days),
                            uint64(block.timestamp + 2 days),
                            uint64(block.timestamp + 3 days)
                        )
                    )
                )
            )
        );
        vm.prank(BIDDER);
        escrow.withdrawCredit(address(token));

        assertTrue(token.callbackAttempted());
        assertFalse(token.callbackSucceeded());
        assertEq(token.balanceOf(BIDDER), 5 ether);
        assertEq(escrow.liabilities(address(token)), 0);
    }

    function test_rewardFundingRejectsSponsorCallbackCancellation() external {
        RewardProgramV3 rewards = new RewardProgramV3(OWNER);
        vm.prank(OWNER);
        rewards.setAuthorizedIssuer(address(this));
        CallbackERC20 token = new CallbackERC20();
        uint256 programId = _tokenCreateProgram(token, rewards, 5 ether);
        token.mint(ALICE, 5 ether);
        vm.prank(ALICE);
        token.approve(address(rewards), type(uint256).max);

        token.configureCallback(
            address(token),
            abi.encodeCall(CallbackERC20.invoke, (address(rewards), abi.encodeCall(rewards.cancelProgram, (programId))))
        );
        vm.prank(ALICE);
        rewards.fundERC20(programId, 5 ether);

        assertTrue(token.callbackAttempted());
        assertFalse(token.callbackSucceeded());
        rewards.commit(programId, 1, 5 ether);
        assertEq(rewards.liabilities(address(token)), 5 ether);
        assertEq(token.balanceOf(address(rewards)), 5 ether);
    }

    function test_rewardClaimRejectsCallbackClaimReplay() external {
        RewardProgramV3 rewards = new RewardProgramV3(OWNER);
        vm.prank(OWNER);
        rewards.setAuthorizedIssuer(address(this));
        CallbackERC20 token = new CallbackERC20();
        uint256 programId = _tokenCreateProgram(token, rewards, 5 ether);
        token.mint(ALICE, 5 ether);
        vm.startPrank(ALICE);
        token.approve(address(rewards), type(uint256).max);
        rewards.fundERC20(programId, 5 ether);
        vm.stopPrank();
        rewards.commit(programId, 1, 5 ether);
        rewards.issue(programId, 1, address(token), 5 ether);

        token.configureCallback(
            address(token), abi.encodeCall(CallbackERC20.claimReward, (address(rewards), address(token)))
        );
        token.claimReward(address(rewards), address(token));

        assertTrue(token.callbackAttempted());
        assertFalse(token.callbackSucceeded());
        assertEq(token.balanceOf(address(token)), 5 ether);
        assertEq(rewards.claimableRewards(address(token), address(token)), 0);
        assertEq(rewards.liabilities(address(token)), 0);
    }

    function test_mutualAidPayoutRejectsOwnerCallbackReplay() external {
        CallbackERC20 token = new CallbackERC20();
        MutualAidPoolV3 pool = new MutualAidPoolV3(address(token), address(token), 5 ether, 10 ether);
        token.mint(ALICE, 10 ether);
        vm.startPrank(ALICE);
        token.approve(address(pool), type(uint256).max);
        pool.contributeERC20(10 ether);
        vm.stopPrank();

        bytes memory pay = abi.encodeCall(pool.pay, (ALICE, 1 ether, keccak256("aid")));
        token.configureCallback(address(token), abi.encodeCall(CallbackERC20.invoke, (address(pool), pay)));
        token.invoke(address(pool), pay);

        assertTrue(token.callbackAttempted());
        assertFalse(token.callbackSucceeded());
        assertEq(pool.totalPaid(), 1 ether);
        assertEq(pool.liabilities(), 9 ether);
        assertEq(token.balanceOf(ALICE), 1 ether);
    }

    function test_vaultDepositAndWithdrawRejectTokenCallbackEntrypoints() external {
        CallbackERC20 token = new CallbackERC20();
        VaultfireBIPVaultV3 vault = new VaultfireBIPVaultV3(address(token), OWNER, 100 ether);
        token.mint(ALICE, 10 ether);
        vm.prank(ALICE);
        token.approve(address(vault), type(uint256).max);

        token.configureCallback(
            address(token),
            abi.encodeCall(
                CallbackERC20.invoke, (address(vault), abi.encodeCall(vault.deposit, (1 ether, address(token))))
            )
        );
        vm.prank(ALICE);
        vault.deposit(10 ether, ALICE);
        assertTrue(token.callbackAttempted());
        assertFalse(token.callbackSucceeded());
        assertEq(vault.totalAssets(), 10 ether);

        token.configureCallback(
            address(token),
            abi.encodeCall(
                CallbackERC20.invoke,
                (address(vault), abi.encodeCall(vault.withdraw, (1 ether, address(token), address(token))))
            )
        );
        vm.prank(ALICE);
        vault.withdraw(10 ether, address(token), ALICE);
        assertTrue(token.callbackAttempted());
        assertFalse(token.callbackSucceeded());
        assertEq(vault.totalAssets(), 0);
        assertEq(token.balanceOf(address(token)), 10 ether);
    }

    function _tokenCreateProgram(CallbackERC20 token, RewardProgramV3 rewards, uint256 cap)
        private
        returns (uint256 programId)
    {
        bytes memory result = token.invoke(
            address(rewards),
            abi.encodeCall(
                rewards.createProgram,
                (
                    address(token),
                    cap,
                    uint64(block.timestamp),
                    uint64(block.timestamp + 7 days),
                    bytes32(0),
                    bytes32(0),
                    bytes32(0),
                    bytes32(0)
                )
            )
        );
        programId = abi.decode(result, (uint256));
    }
}
