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

import { Test } from "forge-std/Test.sol";
import { RewardProgramV3 } from "../../contracts/bonds/RewardProgramV3.sol";
import { FeeOnTransferMockERC20 } from "../mocks/FeeOnTransferMockERC20.sol";
import { MockERC20 } from "../mocks/MockERC20.sol";
import { ForceNativeDonation, NoDeliveryMockERC20, OutboundFeeMockERC20 } from "../mocks/MaliciousERC20s.sol";

contract RewardProgramV3AssetAccountingTest is Test {
    address internal constant OWNER = address(0xBEEF);
    address internal constant SPONSOR = address(0x5A0B50);
    address internal constant BENEFICIARY = address(0xA11CE);

    RewardProgramV3 internal rewards;

    function setUp() public {
        rewards = new RewardProgramV3(OWNER);
        vm.prank(OWNER);
        rewards.setAuthorizedIssuer(address(this));
    }

    function testFuzz_erc20FundingAndClaimUseExactBalanceDeltas(uint96 rawAmount) external {
        uint256 amount = bound(uint256(rawAmount), 2, 1e24);
        MockERC20 token = new MockERC20();
        uint256 id = _createProgram(address(token), amount);
        token.mint(SPONSOR, amount);
        vm.startPrank(SPONSOR);
        token.approve(address(rewards), amount);
        rewards.fundERC20(id, amount);
        vm.stopPrank();

        assertEq(rewards.assets(address(token)), amount);
        assertEq(rewards.liabilities(address(token)), amount);
        assertTrue(rewards.isSolvent(address(token)));
        rewards.commit(id, 1, amount);
        rewards.issue(id, 1, BENEFICIARY, amount);
        assertEq(rewards.claimableRewards(address(token), BENEFICIARY), amount);

        vm.prank(BENEFICIARY);
        rewards.claimReward(address(token));
        assertEq(token.balanceOf(BENEFICIARY), amount);
        assertEq(rewards.assets(address(token)), 0);
        assertEq(rewards.liabilities(address(token)), 0);
    }

    function test_feeAndNoDeliveryTokensCannotCreateRewardLiabilities() external {
        FeeOnTransferMockERC20 fee = new FeeOnTransferMockERC20();
        uint256 feeProgram = _createProgram(address(fee), 10 ether);
        fee.mint(SPONSOR, 10 ether);
        vm.startPrank(SPONSOR);
        fee.approve(address(rewards), 10 ether);
        vm.expectRevert(RewardProgramV3.UnsupportedAssetTransfer.selector);
        rewards.fundERC20(feeProgram, 10 ether);
        vm.stopPrank();

        NoDeliveryMockERC20 noDelivery = new NoDeliveryMockERC20();
        uint256 noDeliveryProgram = _createProgram(address(noDelivery), 10 ether);
        noDelivery.mint(SPONSOR, 10 ether);
        vm.startPrank(SPONSOR);
        noDelivery.approve(address(rewards), 10 ether);
        vm.expectRevert(RewardProgramV3.UnsupportedAssetTransfer.selector);
        rewards.fundERC20(noDeliveryProgram, 10 ether);
        vm.stopPrank();

        assertEq(rewards.liabilities(address(fee)), 0);
        assertEq(rewards.liabilities(address(noDelivery)), 0);
    }

    function test_outboundFeeTokenCannotBurnARewardClaim() external {
        OutboundFeeMockERC20 fee = new OutboundFeeMockERC20();
        uint256 id = _createProgram(address(fee), 10 ether);
        fee.mint(SPONSOR, 10 ether);
        vm.startPrank(SPONSOR);
        fee.approve(address(rewards), 10 ether);
        rewards.fundERC20(id, 10 ether);
        vm.stopPrank();
        rewards.commit(id, 1, 10 ether);
        rewards.issue(id, 1, BENEFICIARY, 10 ether);

        vm.prank(BENEFICIARY);
        vm.expectRevert(RewardProgramV3.UnsupportedAssetTransfer.selector);
        rewards.claimReward(address(fee));
        assertEq(rewards.claimableRewards(address(fee), BENEFICIARY), 10 ether);
        assertEq(rewards.liabilities(address(fee)), 10 ether);
        assertTrue(rewards.isSolvent(address(fee)));
    }

    function test_forcedNativeDonationIsReportedOnlyAsSurplus() external {
        uint256 id = _createProgram(address(0), 1 ether);
        vm.deal(SPONSOR, 1 ether);
        vm.prank(SPONSOR);
        rewards.fundNative{ value: 1 ether }(id);

        ForceNativeDonation donor = new ForceNativeDonation{ value: 2 ether }();
        donor.force(payable(address(rewards)));
        assertEq(rewards.assets(address(0)), 3 ether);
        assertEq(rewards.liabilities(address(0)), 1 ether);
        assertEq(rewards.surplus(address(0)), 2 ether);
        assertTrue(rewards.isSolvent(address(0)));
    }

    function _createProgram(address asset, uint256 maxLiability) private returns (uint256) {
        vm.prank(SPONSOR);
        return rewards.createProgram(
            asset,
            maxLiability,
            uint64(block.timestamp),
            uint64(block.timestamp + 7 days),
            bytes32(0),
            bytes32(0),
            bytes32(0),
            bytes32(0)
        );
    }
}
