// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {DBISEngineXFlashProofBorrower} from "../../contracts/flash/DBISEngineXFlashProofBorrower.sol";
import {DBISEngineXVirtualBatchVault} from "../../contracts/flash/DBISEngineXVirtualBatchVault.sol";
import {MockMintableToken} from "../dbis/MockMintableToken.sol";

contract EngineXFlashBorrower is IERC3156FlashBorrower {
    bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
    bool public repay = true;

    function setRepay(bool repay_) external {
        repay = repay_;
    }

    function onFlashLoan(address, address token, uint256 amount, uint256 fee, bytes calldata)
        external
        override
        returns (bytes32)
    {
        if (repay) {
            IERC20(token).transfer(msg.sender, amount + fee);
        }
        return _RETURN_VALUE;
    }
}

contract DBISEngineXVirtualBatchVaultTest is Test {
    MockMintableToken internal cwusdc;
    MockMintableToken internal usdc;
    MockMintableToken internal xaut;
    DBISEngineXVirtualBatchVault internal vault;

    address internal constant USER = address(0xBEEF);
    address internal constant SURPLUS_RECEIVER = address(0xCAFE);
    address internal constant OUTPUT_RECIPIENT = address(0xD00D);
    address internal constant ROUNDING_RECEIVER = address(0xA11CE);

    uint256 internal constant LIVE_POOL_RESERVE = 85_763_529;
    uint256 internal constant LENDER_USDC = 5_000_000;
    uint256 internal constant XAUT_USD_PRICE6 = 3_226_640_000;
    uint256 internal constant LTV_BPS = 8_000;
    uint256 internal constant MAX_ROUND_TRIP_LOSS_BPS = 100;
    bytes32 internal constant ISO_HASH = bytes32(uint256(0x1001));
    bytes32 internal constant AUDIT_HASH = bytes32(uint256(0x1002));
    bytes32 internal constant PEG_HASH = bytes32(uint256(0x1003));

    event VirtualProofAuditEvidence(
        bytes32 indexed proofId,
        address indexed operator,
        address indexed outputRecipient,
        uint256 exactOutputAmount,
        uint256 outputRoundingAmount,
        address roundingReceiver,
        bytes32 iso20022DocumentHash,
        bytes32 auditEnvelopeHash,
        bytes32 pegProofHash
    );

    function setUp() public {
        cwusdc = new MockMintableToken("Wrapped cWUSDC", "cWUSDC", 6, address(this));
        usdc = new MockMintableToken("USD Coin", "USDC", 6, address(this));
        xaut = new MockMintableToken("Tether Gold", "XAUt", 6, address(this));

        vault = new DBISEngineXVirtualBatchVault(
            address(cwusdc),
            address(usdc),
            address(xaut),
            address(this),
            SURPLUS_RECEIVER,
            XAUT_USD_PRICE6,
            LTV_BPS,
            MAX_ROUND_TRIP_LOSS_BPS
        );

        cwusdc.mint(address(this), LIVE_POOL_RESERVE);
        usdc.mint(address(this), LIVE_POOL_RESERVE + LENDER_USDC);
        cwusdc.approve(address(vault), type(uint256).max);
        usdc.approve(address(vault), type(uint256).max);
        vault.seedPool(LIVE_POOL_RESERVE, LIVE_POOL_RESERVE);
        vault.fundLender(LENDER_USDC);

        cwusdc.mint(USER, 10_000_000_000_000_000);
        xaut.mint(USER, 10_000_000);
        vm.startPrank(USER);
        cwusdc.approve(address(vault), type(uint256).max);
        xaut.approve(address(vault), type(uint256).max);
        vm.stopPrank();
    }

    function testPreviewMatchesLiveMaintainedLoopMath() public view {
        (
            uint256 collateralXaut,
            uint256 cwusdcInPerLoop,
            uint256 cwusdcOutPerLoop,
            uint256 cwusdcLossPerLoop,
            uint256 totalCwusdcInAmount,
            uint256 totalCwusdcOutAmount,
            uint256 totalNeutralizedCwusdcAmount
        ) = vault.previewVirtualProof(LENDER_USDC, 3);

        assertEq(collateralXaut, 1_937, "collateral should match live 5 USDC floor");
        assertEq(cwusdcInPerLoop, 5_325_523, "cwusdc in should match live proof");
        assertEq(cwusdcOutPerLoop, 5_295_471, "cwusdc out should match live proof");
        assertEq(cwusdcLossPerLoop, 30_052, "neutralized loss should match live proof");
        assertEq(totalCwusdcInAmount, 15_976_569, "three-loop cWUSDC in should match live proof");
        assertEq(totalCwusdcOutAmount, 15_886_413, "three-loop cWUSDC out should match live proof");
        assertEq(totalNeutralizedCwusdcAmount, 90_156, "three-loop neutralization should match live proof");
    }

    function testVirtualProofNetSettlesAndKeepsPoolMaintained() public {
        uint256 userCwusdcBefore = cwusdc.balanceOf(USER);
        uint256 userXautBefore = xaut.balanceOf(USER);

        vm.prank(USER);
        vault.runVirtualProof(bytes32("proof-1"), LENDER_USDC, 3);

        assertEq(vault.poolCwusdcReserve(), LIVE_POOL_RESERVE, "pool cWUSDC reserve should not drift");
        assertEq(vault.poolUsdcReserve(), LIVE_POOL_RESERVE, "pool USDC reserve should not drift");
        assertEq(vault.currentSurplusCwusdc(), 0, "pool surplus should stay neutralized");
        assertEq(vault.lenderUsdcAvailable(), LENDER_USDC, "lender bucket should be reusable");

        assertEq(cwusdc.balanceOf(USER), userCwusdcBefore - 90_156, "user only loses neutralized amount");
        assertEq(cwusdc.balanceOf(SURPLUS_RECEIVER), 90_156, "receiver gets neutralized cWUSDC");
        assertEq(xaut.balanceOf(USER), userXautBefore, "XAUt collateral should be returned");

        assertEq(vault.totalVirtualLoops(), 3, "virtual loop counter");
        assertEq(vault.totalVirtualDebtUsdc(), 15_000_000, "virtual debt counter");
        assertEq(vault.totalVirtualCwusdcIn(), 15_976_569, "virtual cWUSDC in counter");
        assertEq(vault.totalNeutralizedCwusdc(), 90_156, "neutralized counter");
        assertTrue(vault.usedProofIds(bytes32("proof-1")), "proof id should be consumed");
    }

    function testVirtualProofCanSendOutputToRecipient() public {
        uint256 userCwusdcBefore = cwusdc.balanceOf(USER);
        uint256 userXautBefore = xaut.balanceOf(USER);

        vm.prank(USER);
        vault.runVirtualProofTo(bytes32("proof-to"), LENDER_USDC, 3, OUTPUT_RECIPIENT);

        assertEq(vault.poolCwusdcReserve(), LIVE_POOL_RESERVE, "pool cWUSDC reserve should not drift");
        assertEq(vault.poolUsdcReserve(), LIVE_POOL_RESERVE, "pool USDC reserve should not drift");
        assertEq(cwusdc.balanceOf(USER), userCwusdcBefore - 15_976_569, "sender supplies gross cWUSDC");
        assertEq(cwusdc.balanceOf(OUTPUT_RECIPIENT), 15_886_413, "recipient gets cWUSDC output");
        assertEq(cwusdc.balanceOf(SURPLUS_RECEIVER), 90_156, "receiver gets neutralized cWUSDC");
        assertEq(xaut.balanceOf(USER), userXautBefore, "XAUt collateral should be returned");
    }

    function testVirtualProofExactOutToAnchorsAuditProofsAndSendsRounding() public {
        uint256 userCwusdcBefore = cwusdc.balanceOf(USER);
        uint256 userXautBefore = xaut.balanceOf(USER);
        uint256 exactOutput = 15_886_000;
        bytes32 proofId = bytes32("proof-audit");

        vm.expectEmit(true, true, true, true, address(vault));
        emit VirtualProofAuditEvidence(
            proofId, USER, OUTPUT_RECIPIENT, exactOutput, 413, ROUNDING_RECEIVER, ISO_HASH, AUDIT_HASH, PEG_HASH
        );
        vm.prank(USER);
        vault.runVirtualProofExactOutTo(
            proofId, LENDER_USDC, 3, OUTPUT_RECIPIENT, exactOutput, ROUNDING_RECEIVER, ISO_HASH, AUDIT_HASH, PEG_HASH
        );

        assertEq(vault.poolCwusdcReserve(), LIVE_POOL_RESERVE, "pool cWUSDC reserve should not drift");
        assertEq(vault.poolUsdcReserve(), LIVE_POOL_RESERVE, "pool USDC reserve should not drift");
        assertEq(cwusdc.balanceOf(USER), userCwusdcBefore - 15_976_569, "sender supplies gross cWUSDC");
        assertEq(cwusdc.balanceOf(OUTPUT_RECIPIENT), exactOutput, "recipient gets exact cWUSDC");
        assertEq(cwusdc.balanceOf(ROUNDING_RECEIVER), 413, "rounding receiver gets output dust");
        assertEq(cwusdc.balanceOf(SURPLUS_RECEIVER), 90_156, "receiver gets neutralized cWUSDC");
        assertEq(xaut.balanceOf(USER), userXautBefore, "XAUt collateral should be returned");
    }

    function testVirtualLoopCountScalesTotalsButNotSequentialCollateral() public view {
        uint256 loops = 938_874_924;
        (
            uint256 collateralXaut,
            uint256 cwusdcInPerLoop,,
            uint256 cwusdcLossPerLoop,
            uint256 totalCwusdcInAmount,,
            uint256 totalNeutralizedCwusdcAmount
        ) = vault.previewVirtualProof(LENDER_USDC, loops);

        assertEq(collateralXaut, 1_937, "sequential virtual batch reuses one loop of XAUt collateral");
        assertEq(cwusdcInPerLoop, 5_325_523, "per-loop input stays constant");
        assertEq(cwusdcLossPerLoop, 30_052, "per-loop neutralization stays constant");
        assertEq(totalCwusdcInAmount, 5_000_000_001_885_252, "virtual batch covers 5B cWUSDC gross");
        assertEq(totalNeutralizedCwusdcAmount, 28_215_069_216_048, "5B batch neutralized amount");
    }

    function testPreviewRevertsWhenPoolIsNotMaintained() public {
        DBISEngineXVirtualBatchVault unevenVault = new DBISEngineXVirtualBatchVault(
            address(cwusdc),
            address(usdc),
            address(xaut),
            address(this),
            SURPLUS_RECEIVER,
            XAUT_USD_PRICE6,
            LTV_BPS,
            MAX_ROUND_TRIP_LOSS_BPS
        );
        cwusdc.mint(address(this), LIVE_POOL_RESERVE + 1);
        usdc.mint(address(this), LIVE_POOL_RESERVE + LENDER_USDC);
        cwusdc.approve(address(unevenVault), type(uint256).max);
        usdc.approve(address(unevenVault), type(uint256).max);
        unevenVault.seedPool(LIVE_POOL_RESERVE + 1, LIVE_POOL_RESERVE);
        unevenVault.fundLender(LENDER_USDC);

        vm.expectRevert(bytes("pool not maintained"));
        unevenVault.previewVirtualProof(LENDER_USDC, 1);
    }

    function testRunVirtualProofRejectsDuplicateProofIds() public {
        vm.prank(USER);
        vault.runVirtualProof(bytes32("proof-1"), LENDER_USDC, 1);

        vm.expectRevert(bytes("proof used"));
        vm.prank(USER);
        vault.runVirtualProof(bytes32("proof-1"), LENDER_USDC, 1);
    }

    function testRunVirtualProofToRejectsZeroRecipient() public {
        vm.expectRevert(bytes("zero output"));
        vm.prank(USER);
        vault.runVirtualProofTo(bytes32("proof-zero"), LENDER_USDC, 1, address(0));
    }

    function testRunVirtualProofExactOutToRejectsMissingAuditHashes() public {
        vm.expectRevert(bytes("zero iso hash"));
        vm.prank(USER);
        vault.runVirtualProofExactOutTo(
            bytes32("proof-no-iso"),
            LENDER_USDC,
            1,
            OUTPUT_RECIPIENT,
            1,
            ROUNDING_RECEIVER,
            bytes32(0),
            AUDIT_HASH,
            PEG_HASH
        );
    }

    function testRunVirtualProofExactOutToRejectsOutputAbovePreview() public {
        vm.expectRevert(bytes("exact output too high"));
        vm.prank(USER);
        vault.runVirtualProofExactOutTo(
            bytes32("proof-too-high"),
            LENDER_USDC,
            1,
            OUTPUT_RECIPIENT,
            5_295_472,
            ROUNDING_RECEIVER,
            ISO_HASH,
            AUDIT_HASH,
            PEG_HASH
        );
    }

    function testWithdrawPoolLiquidityUpdatesAccountingAndPreservesMaintainedPool() public {
        uint256 withdrawAmount = 10_000_000;
        uint256 ownerCwusdcBefore = cwusdc.balanceOf(address(this));
        uint256 ownerUsdcBefore = usdc.balanceOf(address(this));

        vault.withdrawPoolLiquidity(address(this), withdrawAmount, withdrawAmount);

        assertEq(vault.poolCwusdcReserve(), LIVE_POOL_RESERVE - withdrawAmount, "pool cWUSDC accounting");
        assertEq(vault.poolUsdcReserve(), LIVE_POOL_RESERVE - withdrawAmount, "pool USDC accounting");
        assertEq(vault.lenderUsdcAvailable(), LENDER_USDC, "lender accounting unchanged");
        assertEq(cwusdc.balanceOf(address(this)), ownerCwusdcBefore + withdrawAmount, "owner cWUSDC received");
        assertEq(usdc.balanceOf(address(this)), ownerUsdcBefore + withdrawAmount, "owner USDC received");
    }

    function testWithdrawPoolLiquidityRejectsBreakingMaintainedPool() public {
        vm.expectRevert(bytes("would break maintained pool"));
        vault.withdrawPoolLiquidity(address(this), 1, 0);
    }

    function testWithdrawLenderUsdcUpdatesAccounting() public {
        uint256 withdrawAmount = 1_000_000;
        uint256 ownerUsdcBefore = usdc.balanceOf(address(this));

        vault.withdrawLenderUsdc(address(this), withdrawAmount);

        assertEq(vault.lenderUsdcAvailable(), LENDER_USDC - withdrawAmount, "lender accounting");
        assertEq(vault.poolUsdcReserve(), LIVE_POOL_RESERVE, "pool accounting unchanged");
        assertEq(usdc.balanceOf(address(this)), ownerUsdcBefore + withdrawAmount, "owner USDC received");
    }

    function testGenericWithdrawCannotTouchAccountedBalances() public {
        vm.expectRevert(bytes("accounting undercollateralized"));
        vault.withdraw(address(usdc), address(this), 1);
    }

    function testGenericWithdrawCanRescueUnaccountedTokens() public {
        uint256 dust = 123;
        usdc.mint(address(vault), dust);
        uint256 ownerUsdcBefore = usdc.balanceOf(address(this));

        vault.withdraw(address(usdc), address(this), dust);

        assertEq(vault.poolUsdcReserve(), LIVE_POOL_RESERVE, "pool accounting unchanged");
        assertEq(vault.lenderUsdcAvailable(), LENDER_USDC, "lender accounting unchanged");
        assertEq(usdc.balanceOf(address(this)), ownerUsdcBefore + dust, "owner receives unaccounted dust");
    }

    function testFlashLoanUsesLenderBucketAndCollectsFee() public {
        EngineXFlashBorrower borrower = new EngineXFlashBorrower();
        uint256 amount = 1_000_000;
        uint256 fee = vault.flashFee(address(usdc), amount);
        usdc.mint(address(borrower), fee);

        vm.prank(USER);
        vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), amount, "");

        assertEq(vault.lenderUsdcAvailable(), LENDER_USDC + fee, "fee stays in lender bucket");
        assertEq(vault.totalFlashFeesCollectedUsdc(), fee, "fee accounting");
        assertEq(usdc.balanceOf(address(vault)), LIVE_POOL_RESERVE + LENDER_USDC + fee, "USDC backing");
    }

    function testFlashLoanCanPullRepaymentByAllowance() public {
        EngineXFlashBorrower borrower = new EngineXFlashBorrower();
        uint256 amount = 1_000_000;
        uint256 fee = vault.flashFee(address(usdc), amount);
        usdc.mint(address(borrower), fee);

        borrower.setRepay(false);
        vm.prank(address(borrower));
        usdc.approve(address(vault), type(uint256).max);

        vm.prank(USER);
        vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), amount, "");

        assertEq(vault.lenderUsdcAvailable(), LENDER_USDC + fee, "fee stays in lender bucket");
        assertEq(vault.totalFlashFeesCollectedUsdc(), fee, "fee accounting");
    }

    function testFlashLoanRejectsBorrowingPoolUsdc() public {
        EngineXFlashBorrower borrower = new EngineXFlashBorrower();

        vm.expectRevert(bytes("insufficient lender usdc"));
        vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), LENDER_USDC + 1, "");
    }

    function testFlashLoanRejectsUnsupportedToken() public {
        EngineXFlashBorrower borrower = new EngineXFlashBorrower();

        vm.expectRevert(bytes("unsupported flash token"));
        vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(cwusdc), 1, "");
    }

    function testFlashLoanCanBeCapped() public {
        EngineXFlashBorrower borrower = new EngineXFlashBorrower();
        vault.setMaxFlashLoanAmount(999_999);

        vm.expectRevert(bytes("flash amount too high"));
        vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), 1_000_000, "");
    }

    function testFlashLoanAllowlistRejectsUnapprovedBorrower() public {
        EngineXFlashBorrower borrower = new EngineXFlashBorrower();
        vault.setFlashBorrowerAllowlistEnabled(true);

        vm.expectRevert(bytes("flash borrower not approved"));
        vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), 1, "");
    }

    function testFlashLoanAllowlistAllowsApprovedBorrower() public {
        EngineXFlashBorrower borrower = new EngineXFlashBorrower();
        uint256 amount = 1_000_000;
        uint256 fee = vault.flashFee(address(usdc), amount);
        usdc.mint(address(borrower), fee);

        vault.setFlashBorrowerAllowlistEnabled(true);
        vault.setFlashBorrowerApproved(address(borrower), true);

        vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), amount, "");

        assertEq(vault.lenderUsdcAvailable(), LENDER_USDC + fee, "fee stays in lender bucket");
    }

    function testPauseBlocksProofsAndFlashLoans() public {
        EngineXFlashBorrower borrower = new EngineXFlashBorrower();
        vault.pause();

        vm.expectRevert(bytes("paused"));
        vm.prank(USER);
        vault.runVirtualProof(bytes32("proof-paused"), LENDER_USDC, 1);

        vm.expectRevert(bytes("paused"));
        vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), 1, "");

        assertEq(vault.maxFlashLoan(address(usdc)), 0, "paused max flash");
    }

    function testEngineXFlashProofBorrowerRunsProofFlash() public {
        DBISEngineXFlashProofBorrower borrower =
            new DBISEngineXFlashProofBorrower(address(vault), address(usdc), address(this));
        uint256 amount = 1_000_000;
        uint256 fee = vault.flashFee(address(usdc), amount);
        bytes32 proofId = bytes32("flash-proof");
        usdc.mint(address(borrower), fee);

        borrower.runFlashProof(amount, proofId, ISO_HASH, AUDIT_HASH, PEG_HASH);

        assertTrue(borrower.usedProofIds(proofId), "proof consumed");
        assertEq(vault.lenderUsdcAvailable(), LENDER_USDC + fee, "fee stays in lender bucket");
    }
}
