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

import "forge-std/Test.sol";
import { VaultfireBIPVaultV3 } from "../../contracts/vault/VaultfireBIPVaultV3.sol";
import { FeeOnTransferMockERC20 } from "../mocks/FeeOnTransferMockERC20.sol";
import { MockERC20 } from "../mocks/MockERC20.sol";
import { OutboundFeeMockERC20 } from "../mocks/MaliciousERC20s.sol";

/// @notice ERC-4626 accounting, rounding, and donation-safety properties.
contract VaultfireBIPVaultV3ERC4626Test is Test {
    uint256 private constant VIRTUAL_SHARES = 1e6;
    address private constant PROTOCOL_OWNER = address(0x0B0B);
    address private constant ALICE = address(0xA11CE);
    address private constant BOB = address(0xB0B);
    address private constant ATTACKER = address(0xA77AC);
    address private constant VICTIM = address(0xB1C71);
    address private constant DONOR = address(0xD0D0);

    MockERC20 private assetToken;
    VaultfireBIPVaultV3 private vault;

    function setUp() public {
        assetToken = new MockERC20();
        vault = new VaultfireBIPVaultV3(address(assetToken), PROTOCOL_OWNER, type(uint256).max);
    }

    // ---------------------------------------------------------------------
    // ERC-4626 deposit / withdraw / mint / redeem accounting
    // ---------------------------------------------------------------------

    function testFuzz_depositMintsExactlyPreviewedSharesAndTransfersAssets(uint96 rawAssets) external {
        uint256 assets = bound(uint256(rawAssets), 1, 1e24);
        _fundAndApprove(ALICE, assets);

        uint256 expectedShares = vault.previewDeposit(assets);
        uint256 aliceAssetsBefore = assetToken.balanceOf(ALICE);

        vm.prank(ALICE);
        uint256 mintedShares = vault.deposit(assets, ALICE);

        assertEq(mintedShares, expectedShares, "deposit differs from previewDeposit");
        assertEq(vault.balanceOf(ALICE), expectedShares, "receiver ERC-20 share balance incorrect");
        assertEq(vault.shareOf(ALICE), expectedShares, "legacy share alias incorrect");
        assertEq(vault.totalSupply(), expectedShares, "ERC-20 share supply incorrect");
        assertEq(vault.totalShares(), expectedShares, "legacy share supply alias incorrect");
        assertEq(assetToken.balanceOf(ALICE), aliceAssetsBefore - assets, "depositor asset debit incorrect");
        assertEq(vault.totalAssets(), assets, "vault asset accounting incorrect");
    }

    function testFuzz_roundTripDepositThenSelfWithdrawReturnsAllAssets(uint96 rawAssets) external {
        uint256 assets = bound(uint256(rawAssets), 1, 1e24);
        _fundAndApprove(ALICE, assets);

        uint256 aliceAssetsBefore = assetToken.balanceOf(ALICE);
        vm.startPrank(ALICE);
        uint256 mintedShares = vault.deposit(assets, ALICE);
        uint256 burnedShares = vault.withdraw(assets, ALICE, ALICE);
        vm.stopPrank();

        assertEq(burnedShares, mintedShares, "same-state round trip must burn minted shares");
        assertEq(vault.balanceOf(ALICE), 0, "round trip left user shares");
        assertEq(vault.totalSupply(), 0, "round trip left total shares");
        assertEq(vault.totalAssets(), 0, "round trip left vault assets");
        assertEq(assetToken.balanceOf(ALICE), aliceAssetsBefore, "round trip lost assets");
    }

    function testFuzz_roundTripMintThenSelfRedeemReturnsAllAssets(uint96 rawAssetUnits) external {
        uint256 shares = bound(uint256(rawAssetUnits), 1, 1e24) * VIRTUAL_SHARES;
        uint256 assetsForMint = vault.previewMint(shares);
        _fundAndApprove(ALICE, assetsForMint);

        vm.startPrank(ALICE);
        uint256 paidAssets = vault.mint(shares, ALICE);
        uint256 expectedRedeemAssets = vault.previewRedeem(shares);
        uint256 redeemedAssets = vault.redeem(shares, ALICE, ALICE);
        vm.stopPrank();

        assertEq(paidAssets, assetsForMint, "mint differs from previewMint");
        assertEq(redeemedAssets, expectedRedeemAssets, "redeem differs from previewRedeem");
        assertEq(redeemedAssets, assetsForMint, "same-state mint/redeem must round trip");
        assertEq(vault.balanceOf(ALICE), 0, "redeem left user shares");
        assertEq(vault.totalAssets(), 0, "redeem left vault assets");
    }

    function testFuzz_allPreviewsUseRequiredRoundingDirections(uint96 rawDonation, uint96 rawProbe) external {
        _fundAndApprove(ALICE, 17);
        vm.prank(ALICE);
        vault.deposit(17, ALICE);

        uint256 donation = bound(uint256(rawDonation), 1, 1e24);
        uint256 probe = bound(uint256(rawProbe), 1, 1e24);
        assetToken.mint(DONOR, donation);
        vm.prank(DONOR);
        assertTrue(assetToken.transfer(address(vault), donation));

        uint256 sharesNumerator = probe * (vault.totalSupply() + VIRTUAL_SHARES);
        uint256 assetsNumerator = probe * (vault.totalAssets() + 1);
        uint256 shareDenominator = vault.totalAssets() + 1;
        uint256 assetDenominator = vault.totalSupply() + VIRTUAL_SHARES;

        uint256 expectedSharesFloor = sharesNumerator / shareDenominator;
        uint256 expectedSharesCeil = (sharesNumerator + shareDenominator - 1) / shareDenominator;
        uint256 expectedAssetsFloor = assetsNumerator / assetDenominator;
        uint256 expectedAssetsCeil = (assetsNumerator + assetDenominator - 1) / assetDenominator;

        assertEq(vault.convertToShares(probe), expectedSharesFloor, "convertToShares must round down");
        assertEq(vault.previewDeposit(probe), expectedSharesFloor, "previewDeposit must round down");
        assertEq(vault.previewWithdraw(probe), expectedSharesCeil, "previewWithdraw must round up");
        assertEq(vault.convertToAssets(probe), expectedAssetsFloor, "convertToAssets must round down");
        assertEq(vault.previewRedeem(probe), expectedAssetsFloor, "previewRedeem must round down");
        assertEq(vault.previewMint(probe), expectedAssetsCeil, "previewMint must round up");
    }

    function testFuzz_totalSharesReconcileAndOutstandingSharesAreSolvent(
        uint96 rawAliceAssets,
        uint96 rawBobAssets,
        uint96 rawDonation
    ) external {
        uint256 aliceAssets = bound(uint256(rawAliceAssets), 1, 1e24);
        uint256 bobAssets = bound(uint256(rawBobAssets), 1, 1e24);
        uint256 donation = bound(uint256(rawDonation), 0, 1e24);

        _fundAndApprove(ALICE, aliceAssets);
        _fundAndApprove(BOB, bobAssets);
        vm.prank(ALICE);
        vault.deposit(aliceAssets, ALICE);
        vm.prank(BOB);
        vault.deposit(bobAssets, BOB);

        if (donation != 0) {
            assetToken.mint(DONOR, donation);
            vm.prank(DONOR);
            assertTrue(assetToken.transfer(address(vault), donation));
        }

        uint256 accountedShares = vault.balanceOf(ALICE) + vault.balanceOf(BOB);
        assertEq(vault.totalSupply(), accountedShares, "share ledger does not reconcile");
        assertEq(vault.totalShares(), accountedShares, "legacy totalShares alias does not reconcile");
        assertEq(assetToken.balanceOf(address(vault)), vault.totalAssets(), "totalAssets does not match asset balance");
        assertLe(
            vault.convertToAssets(vault.totalSupply()),
            vault.totalAssets(),
            "outstanding shares claim more assets than the vault holds"
        );
    }

    // ---------------------------------------------------------------------
    // ERC-20 share surface, allowances, and non-custodial access
    // ---------------------------------------------------------------------

    function test_erc4626SurfaceAndMaxFunctions() external {
        assertEq(vault.asset(), address(assetToken), "wrong asset");
        assertEq(vault.maxDeposit(ALICE), type(uint256).max, "unexpected deposit cap");
        assertEq(vault.maxMint(ALICE), type(uint256).max, "unexpected mint cap");
        assertEq(vault.maxWithdraw(ALICE), 0, "empty account can withdraw");
        assertEq(vault.maxRedeem(ALICE), 0, "empty account can redeem");

        _fundAndApprove(ALICE, 100);
        vm.prank(ALICE);
        uint256 mintedShares = vault.deposit(100, ALICE);

        assertEq(vault.maxRedeem(ALICE), mintedShares, "maxRedeem must equal balance");
        assertEq(vault.maxWithdraw(ALICE), vault.convertToAssets(mintedShares), "maxWithdraw incorrect");
        assertGt(vault.previewMint(VIRTUAL_SHARES), 0, "previewMint must be functional");
        assertGt(vault.previewRedeem(mintedShares), 0, "previewRedeem must be functional");
    }

    function test_shareTransfersAndDelegatedWithdrawSpendAllowance() external {
        _fundAndApprove(ALICE, 100);
        vm.prank(ALICE);
        vault.deposit(100, ALICE);

        vm.prank(ALICE);
        assertTrue(vault.approve(BOB, 3));
        vm.prank(ALICE);
        assertTrue(vault.transfer(BOB, 1));
        vm.prank(BOB);
        assertTrue(vault.transferFrom(ALICE, BOB, 2));

        assertEq(vault.balanceOf(BOB), 3, "share transfers did not credit delegate");
        assertEq(vault.allowance(ALICE, BOB), 1, "transferFrom did not spend allowance");

        uint256 assets = 10;
        uint256 sharesForWithdrawal = vault.previewWithdraw(assets);
        vm.prank(ALICE);
        assertTrue(vault.approve(BOB, sharesForWithdrawal));

        uint256 bobAssetsBefore = assetToken.balanceOf(BOB);
        vm.prank(BOB);
        uint256 burnedShares = vault.withdraw(assets, BOB, ALICE);

        assertEq(burnedShares, sharesForWithdrawal, "delegated withdrawal differs from preview");
        assertEq(assetToken.balanceOf(BOB), bobAssetsBefore + assets, "delegated withdrawal sent wrong assets");
        assertEq(vault.allowance(ALICE, BOB), 0, "withdraw did not spend allowance");
    }

    function test_unauthorizedUserAndProtocolOwnerCannotWithdrawDepositorAssets() external {
        _fundAndApprove(ALICE, 100);
        vm.prank(ALICE);
        vault.deposit(100, ALICE);

        vm.prank(BOB);
        vm.expectRevert();
        vault.withdraw(1, BOB, ALICE);

        // The ownership role never grants a claim on depositor assets.
        vm.prank(PROTOCOL_OWNER);
        vm.expectRevert();
        vault.withdraw(1, PROTOCOL_OWNER, ALICE);
    }

    function test_failedAssetPullCannotCreateFreeShares() external {
        assetToken.mint(ALICE, 100);
        uint256 expectedShares = vault.previewDeposit(100);

        vm.prank(ALICE);
        vm.expectRevert(MockERC20.InsufficientAllowance.selector);
        vault.deposit(100, ALICE);

        assertEq(vault.balanceOf(ALICE), 0, "failed pull minted user shares");
        assertEq(vault.totalSupply(), 0, "failed pull changed total shares");
        assertEq(vault.totalAssets(), 0, "failed pull changed assets");
        assertGt(expectedShares, 0, "test precondition: valid deposit would mint shares");
    }

    function test_feeOnTransferAssetCannotCreateUnbackedShares() external {
        FeeOnTransferMockERC20 feeAsset = new FeeOnTransferMockERC20();
        VaultfireBIPVaultV3 feeVault = new VaultfireBIPVaultV3(address(feeAsset), PROTOCOL_OWNER, type(uint256).max);
        feeAsset.mint(ALICE, 100);

        vm.startPrank(ALICE);
        feeAsset.approve(address(feeVault), 100);
        vm.expectRevert(VaultfireBIPVaultV3.UnsupportedAssetTransfer.selector);
        feeVault.deposit(100, ALICE);
        vm.stopPrank();

        assertEq(feeVault.balanceOf(ALICE), 0, "fee asset deposit minted unbacked shares");
        assertEq(feeVault.totalSupply(), 0, "fee asset deposit changed total supply");
        assertEq(feeAsset.balanceOf(address(feeVault)), 0, "fee asset deposit left assets after revert");
        assertEq(feeAsset.balanceOf(ALICE), 100, "fee asset deposit charged depositor after revert");
    }

    function test_zeroValueActionsAreRejected() external {
        vm.startPrank(ALICE);
        vm.expectRevert(VaultfireBIPVaultV3.ZeroAmount.selector);
        vault.deposit(0, ALICE);
        vm.expectRevert(VaultfireBIPVaultV3.ZeroAmount.selector);
        vault.mint(0, ALICE);
        vm.expectRevert(VaultfireBIPVaultV3.ZeroAmount.selector);
        vault.withdraw(0, ALICE, ALICE);
        vm.expectRevert(VaultfireBIPVaultV3.ZeroAmount.selector);
        vault.redeem(0, ALICE, ALICE);
        vm.stopPrank();
    }

    function test_immutableAssetCapDrivesMaxDepositAndMaxMint() external {
        VaultfireBIPVaultV3 capped = new VaultfireBIPVaultV3(address(assetToken), PROTOCOL_OWNER, 100);
        assertEq(capped.totalAssetCap(), 100);
        assertEq(capped.maxDeposit(ALICE), 100);
        assertEq(capped.maxMint(ALICE), capped.previewDeposit(100));

        assetToken.mint(ALICE, 101);
        vm.startPrank(ALICE);
        assetToken.approve(address(capped), type(uint256).max);
        capped.deposit(100, ALICE);
        vm.expectRevert();
        capped.deposit(1, ALICE);
        vm.stopPrank();
        assertEq(capped.maxDeposit(ALICE), 0);
        assertEq(capped.maxMint(ALICE), 0);
    }

    function test_feeOnOutboundTransferCannotBurnSharesWithoutExactAssetDelivery() external {
        OutboundFeeMockERC20 outboundFee = new OutboundFeeMockERC20();
        VaultfireBIPVaultV3 guarded = new VaultfireBIPVaultV3(address(outboundFee), PROTOCOL_OWNER, 100);
        outboundFee.mint(ALICE, 10);
        vm.startPrank(ALICE);
        outboundFee.approve(address(guarded), 10);
        guarded.deposit(10, ALICE);
        vm.expectRevert(VaultfireBIPVaultV3.UnsupportedAssetTransfer.selector);
        guarded.withdraw(10, ALICE, ALICE);
        vm.stopPrank();
        assertGt(guarded.balanceOf(ALICE), 0);
        assertEq(guarded.totalAssets(), 10);
    }

    // ---------------------------------------------------------------------
    // Direct-donation / forced ERC-20 transfer safety
    // ---------------------------------------------------------------------

    function testFuzz_directDonationMintsNoSharesAndCannotProfitTheFirstDepositor(uint96 rawDonation) external {
        uint256 donation = bound(uint256(rawDonation), 1, 1e24);
        _fundAndApprove(ATTACKER, 1);
        vm.prank(ATTACKER);
        uint256 attackerShares = vault.deposit(1, ATTACKER);

        assetToken.mint(DONOR, donation);
        vm.prank(DONOR);
        assertTrue(assetToken.transfer(address(vault), donation));

        assertEq(vault.totalSupply(), attackerShares, "direct transfer minted shares");
        assertEq(vault.balanceOf(DONOR), 0, "direct transfer credited donor shares");
        assertEq(vault.totalAssets(), donation + 1, "direct transfer not reflected in assets");
        assertLe(
            vault.convertToAssets(attackerShares),
            donation + 1,
            "first depositor can profit from a direct-donation inflation attack"
        );
    }

    function testFuzz_followerReceivesSharesAfterFirstDepositorDonation(uint96 rawDonation) external {
        uint256 donation = bound(uint256(rawDonation), 1, 1e24);
        _fundAndApprove(ATTACKER, 1);
        vm.prank(ATTACKER);
        vault.deposit(1, ATTACKER);

        assetToken.mint(ATTACKER, donation);
        vm.prank(ATTACKER);
        assertTrue(assetToken.transfer(address(vault), donation));

        uint256 followerAssets = donation + 2;
        _fundAndApprove(VICTIM, followerAssets);
        uint256 preview = vault.previewDeposit(followerAssets);
        assertGt(preview, 0, "test precondition: follower should receive shares");

        vm.prank(VICTIM);
        uint256 mintedShares = vault.deposit(followerAssets, VICTIM);

        assertEq(mintedShares, preview, "follower deposit differs from preview");
        assertEq(vault.balanceOf(VICTIM), mintedShares, "follower was not credited shares");
        assertEq(vault.balanceOf(ATTACKER), VIRTUAL_SHARES, "donation changed attacker shares");
    }

    function test_donationInflationCannotPullAssetsForZeroShares() external {
        _fundAndApprove(ATTACKER, 1);
        vm.prank(ATTACKER);
        vault.deposit(1, ATTACKER);

        // This makes previewDeposit(1) round down to zero without the guard.
        assetToken.mint(ATTACKER, 2 * VIRTUAL_SHARES);
        vm.prank(ATTACKER);
        assertTrue(assetToken.transfer(address(vault), 2 * VIRTUAL_SHARES));

        _fundAndApprove(VICTIM, 1);
        uint256 victimAssetsBefore = assetToken.balanceOf(VICTIM);
        uint256 victimSharesBefore = vault.balanceOf(VICTIM);

        vm.prank(VICTIM);
        vm.expectRevert(VaultfireBIPVaultV3.ZeroShares.selector);
        vault.deposit(1, VICTIM);

        assertEq(assetToken.balanceOf(VICTIM), victimAssetsBefore, "zero-share deposit pulled a victim asset");
        assertEq(vault.balanceOf(VICTIM), victimSharesBefore, "zero-share deposit minted shares");
        assertEq(vault.totalAssets(), 2 * VIRTUAL_SHARES + 1, "failed deposit changed vault assets");
    }

    function _fundAndApprove(address account, uint256 assets) private {
        assetToken.mint(account, assets);
        vm.prank(account);
        assetToken.approve(address(vault), type(uint256).max);
    }
}
