// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.26;

import "./AdapterTest.sol";
import {BalancerV3Errors} from "src/balancer-v3/lib/BalancerV3Errors.sol";
import {
    BalancerV3SwapAdapter,
    IERC20,
    IVault,
    IBatchRouter,
    IERC4626,
    IPermit2
} from "src/balancer-v3/BalancerV3SwapAdapter.sol";
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";

import {FractionMath} from "src/libraries/FractionMath.sol";
import "./mocks/MockSUSDC.sol";
import "./mocks/MockSETHx.sol";
import "./mocks/MockSGOETH.sol";
import {IBufferRouter} from "./interfaces/IBufferRouter.sol";

contract BalancerV3SwapAdapterTest is AdapterTest, ERC20, BalancerV3Errors {
    using FractionMath for Fraction;

    IVault constant balancerV3Vault =
        IVault(payable(0xbA1333333333a1BA1108E8412f11850A5C319bA9));
    BalancerV3SwapAdapter adapter;
    IBatchRouter router =
        IBatchRouter(0x136f1EFcC3f8f88516B9E94110D56FDBfB1778d1); // Batch router
    address constant bufferRouter_address =
        0x9179C06629ef7f17Cb5759F501D89997FE0E7b45;
    address constant permit2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;

    // ETHx waWETH - Stable Pool
    address constant ERC4626_ERC20_ETHx_waWETH_STABLE_POOL =
        0x4AB7aB316D43345009B2140e0580B072eEc7DF16;
    address constant ERC4626_waEthWETH =
        0x0bfc9d54Fc184518A81162F8fB99c2eACa081202;
    address constant ERC20_ETHx = 0xA35b1B31Ce002FBF2058D22F30f95D405200A15b;

    // 50USDC-50@G - Weighted Pool
    address constant ERC20_ERC20_GOETH_USDC_WEIGHTED_POOL =
        0xf91c11BA4220b7a72E1dc5E92f2b48D3fdF62726;
    address constant ERC20_GOETH = 0x440017A1b021006d556d7fc06A54c32E42Eb745B;
    address constant ERC20_USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

    // Aave Lido wETH-wstETH - Stable Pool
    address constant ERC4626_ERC4626_WETH_wstETH_STABLE_POOL =
        0xc4Ce391d82D164c166dF9c8336DDF84206b2F812;
    address constant ERC20_WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address constant ERC20_wstETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;
    address constant ERC4626_waEthLidoWETH =
        0x0FE906e030a44eF24CA8c7dC7B7c53A6C4F00ce9;
    address constant ERC4626_waEthLidowstETH =
        0x775F661b0bD1739349b9A2A3EF60be277c5d2D29;
    address constant ETH = address(0);

    uint256 constant TEST_ITERATIONS = 100;

    MockSUSDC public ERC4626_sUSDC;
    MockSETHx public ERC4626_sETHx;
    MockSGOETH public ERC4626_sGOETH;

    constructor() ERC20("", "") {}

    function setUp() public {
        uint256 forkBlock = 21421638;
        vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);

        adapter = new BalancerV3SwapAdapter(
            payable(address(balancerV3Vault)),
            address(router),
            permit2,
            ERC20_WETH
        );

        // Create ERC4626_sUSDC first
        ERC4626_sUSDC = new MockSUSDC(IERC20(ERC20_USDC));
        vm.label(address(ERC4626_sUSDC), "ERC4626_sUSDC");

        // Create sGOETH
        ERC4626_sGOETH = new MockSGOETH(IERC20(ERC20_GOETH));
        vm.label(address(ERC4626_sGOETH), "ERC4626_sGOETH");

        // Create sETHx first
        ERC4626_sETHx = new MockSETHx(IERC20(ERC20_ETHx));
        vm.label(address(ERC4626_sETHx), "ERC4626_sETHx");

        // Deal ERC20_USDC to this contract for buffer initialization
        deal(ERC20_USDC, address(this), 1000000 * 10 ** 6);
        // Deal ETHx to this contract for buffer initialization
        deal(ERC20_ETHx, address(this), 100000 * 10 ** 18);
        deal(ERC20_GOETH, address(this), 10000000 * (10 ** 18));
        IERC20(ERC20_GOETH).approve(permit2, type(uint256).max);
        IERC20(ERC4626_sGOETH).approve(permit2, type(uint256).max);

        // Approve ERC20_USDC spending through Permit2
        IERC20(ERC20_USDC).approve(permit2, type(uint256).max);
        // Approve ETHx spending through Permit2
        IERC20(ERC20_ETHx).approve(permit2, type(uint256).max);

        IPermit2(permit2).approve(
            ERC20_GOETH,
            address(bufferRouter_address),
            uint160(type(uint256).max),
            uint48(block.timestamp + 1 days)
        );

        IPermit2(permit2).approve(
            address(ERC4626_sGOETH),
            address(bufferRouter_address),
            uint160(type(uint256).max),
            uint48(block.timestamp + 1 days)
        );

        // Approve both tokens for Buffer Router through Permit2
        IPermit2(permit2).approve(
            ERC20_USDC,
            address(bufferRouter_address), // Buffer
                // Router
            uint160(type(uint256).max),
            uint48(block.timestamp + 1 days)
        );

        IPermit2(permit2).approve(
            ERC20_ETHx,
            address(bufferRouter_address), // Buffer
                // Router
            uint160(type(uint256).max),
            uint48(block.timestamp + 1 days)
        );

        // Also approve ERC4626_sUSDC for Buffer Router
        IERC20(address(ERC4626_sUSDC)).approve(permit2, type(uint256).max);
        IPermit2(permit2).approve(
            address(ERC4626_sUSDC),
            address(bufferRouter_address), // Buffer
                // Router
            uint160(type(uint256).max),
            uint48(block.timestamp + 1 days)
        );

        // Also approve sETHx for Buffer Router
        IERC20(address(ERC4626_sETHx)).approve(permit2, type(uint256).max);
        IPermit2(permit2).approve(
            address(ERC4626_sETHx),
            address(bufferRouter_address), // Buffer
                // Router
            uint160(type(uint256).max),
            uint48(block.timestamp + 1 days)
        );

        // Approve Permit2 to spend sUSDC for the Balancer vault
        IPermit2(permit2).approve(
            address(ERC4626_sUSDC),
            address(balancerV3Vault),
            uint160(type(uint256).max),
            uint48(block.timestamp + 1 days)
        );

        // Approve Permit2 to spend sETHx for the Balancer vault
        IPermit2(permit2).approve(
            address(ERC4626_sETHx),
            address(balancerV3Vault),
            uint160(type(uint256).max),
            uint48(block.timestamp + 1 days)
        );

        // Approve Permit2 to spend sETHx for the Balancer vault
        IPermit2(permit2).approve(
            address(ERC4626_sGOETH),
            address(balancerV3Vault),
            uint160(type(uint256).max),
            uint48(block.timestamp + 1 days)
        );

        // Initialize Balancer's internal ERC4626 buffer through the Buffer
        // Router
        IBufferRouter bufferRouter = IBufferRouter(bufferRouter_address);

        IERC20(ERC20_USDC).approve(address(ERC4626_sUSDC), type(uint256).max);

        IERC20(ERC20_ETHx).approve(address(ERC4626_sETHx), type(uint256).max);

        IERC20(ERC20_GOETH).approve(address(ERC4626_sGOETH), type(uint256).max);

        // Mint some ERC4626_sUSDC first
        ERC4626_sUSDC.deposit(1000 * 10 ** 6, address(this));

        // Mint some sETHx first
        ERC4626_sETHx.deposit(1000 * 10 ** 18, address(this));

        // Mint some sGOETH first
        ERC4626_sGOETH.deposit(1000000 * (10 ** 18), address(this));

        // Initialize buffer with equal amounts of underlying and wrapped tokens
        bufferRouter.initializeBuffer(
            IERC4626(address(ERC4626_sUSDC)), // wrapped token
            10 * 10 ** 6, // exactAmountUnderlyingIn (10 ERC20_USDC)
            10 * 10 ** 6, // exactAmountWrappedIn (10 ERC4626_sUSDC)
            9 * 10 ** 6 // minIssuedShares (90% of input as safety)
        );

        bufferRouter.initializeBuffer(
            IERC4626(address(ERC4626_sETHx)), // wrapped token
            10 * 10 ** 18, // exactAmountUnderlyingIn (10 ETHx)
            10 * 10 ** 18, // exactAmountWrappedIn (10 sETHx)
            9 * 10 ** 18 // minIssuedShares (90% of input as safety)
        );

        bufferRouter.initializeBuffer(
            IERC4626(address(ERC4626_sGOETH)), // wrapped token
            10 * 10 ** 18, // exactAmountUnderlyingIn (10 ETHx)
            10 * 10 ** 18, // exactAmountWrappedIn (10 sETHx)
            9 * 10 ** 18 // minIssuedShares (90% of input as safety)
        );

        // Deal ERC20_USDC to test contract
        deal(ERC20_USDC, address(this), 1000000 * 10 ** 6);

        // Deal ETHx to test contract
        deal(ERC20_ETHx, address(this), 1000 * 10 ** 18);

        // Approve ERC20_USDC spending to ERC4626_sUSDC vault
        IERC20(ERC20_USDC).approve(address(ERC4626_sUSDC), type(uint256).max);

        // Approve ETHx spending to sETHx vault
        IERC20(ERC20_ETHx).approve(address(ERC4626_sETHx), type(uint256).max);

        // Deposit ERC20_USDC to get ERC4626_sUSDC
        ERC4626_sUSDC.deposit(1000000 * 10 ** 6, address(this));

        // Deposit ETHx to get sETHx
        ERC4626_sETHx.deposit(1000 * 10 ** 18, address(this));

        IERC20(ERC20_USDC).approve(address(balancerV3Vault), type(uint256).max);

        IERC20(ERC20_ETHx).approve(address(balancerV3Vault), type(uint256).max);

        vm.label(address(balancerV3Vault), "BalancerV3Vault");
        vm.label(address(router), "BalancerV3BatchRouter");
        vm.label(address(adapter), "BalancerV3SwapAdapter");
        vm.label(ERC4626_waEthWETH, "ERC4626_waEthWETH");
        vm.label(ERC20_ETHx, "ERC20_ETHx");
        vm.label(
            ERC4626_ERC20_ETHx_waWETH_STABLE_POOL,
            "ERC4626_ERC20_ETHx_waWETH_STABLE_POOL"
        );
        vm.label(
            ERC20_ERC20_GOETH_USDC_WEIGHTED_POOL,
            "ERC20_ERC20_GOETH_USDC_WEIGHTED_POOL"
        );
        vm.label(ERC20_GOETH, "ERC20_GOETH");
        vm.label(ERC20_USDC, "ERC20_USDC");
        vm.label(
            ERC4626_ERC4626_WETH_wstETH_STABLE_POOL,
            "ERC4626_ERC4626_WETH_wstETH_STABLE_POOL"
        );
        vm.label(ERC20_WETH, "ERC20_WETH");
        vm.label(ERC20_wstETH, "ERC20_wstETH");
        vm.label(ERC4626_waEthLidoWETH, "ERC4626_waEthLidoWETH");
        vm.label(ERC4626_waEthLidoWETH, "ERC4626_waEthLidoWETH");
        vm.label(permit2, "Permit2");
    }

    ///////////////////////////////////////// ERC4626_ERC20_DIRECT
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    function testPriceFuzzBalancerV3_ERC4626_ERC20_DIRECT(uint256 amount0)
        public
    {
        address token0 = ERC4626_waEthWETH;
        address token1 = ERC20_ETHx;

        bytes32 pool = bytes32(bytes20(ERC4626_ERC20_ETHx_waWETH_STABLE_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);
        uint256 minTradeAmount = getMinTradeAmount(token0);

        vm.assume(amount0 < limits[0]);
        vm.assume(amount0 > minTradeAmount);

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = amount0;

        __prankStaticCall();
        Fraction[] memory prices = adapter.price(pool, token0, token1, amounts);

        for (uint256 i = 0; i < prices.length; i++) {
            assertGt(prices[i].numerator, 0);
            assertGt(prices[i].denominator, 0);
        }
    }

    function testSwapFuzzBalancerV3_ERC4626_ERC20_DIRECT(
        uint256 specifiedAmount,
        bool isBuy
    ) public {
        address token0 = ERC4626_waEthWETH;
        address token1 = ERC20_ETHx;

        OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
        bytes32 pool = bytes32(bytes20(ERC4626_ERC20_ETHx_waWETH_STABLE_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        if (side == OrderSide.Buy) {
            vm.assume(
                specifiedAmount < limits[1]
                    && specifiedAmount > getMinTradeAmount(token0)
            );
        } else {
            vm.assume(
                specifiedAmount < limits[0]
                    && specifiedAmount > getMinTradeAmount(token0)
            );
        }

        deal(token0, address(this), type(uint256).max);
        IERC4626(token0).approve(address(adapter), type(uint256).max);

        uint256 bal0 = IERC4626(token0).balanceOf(address(this));
        uint256 bal1 = IERC20(token1).balanceOf(address(this));

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = specifiedAmount;
        Trade memory trade =
            adapter.swap(pool, token0, token1, side, specifiedAmount);

        if (side == OrderSide.Buy) {
            assertEq(
                specifiedAmount, IERC20(token1).balanceOf(address(this)) - bal1
            );
            assertEq(
                trade.calculatedAmount,
                bal0 - IERC4626(token0).balanceOf(address(this))
            );
        } else {
            assertEq(
                specifiedAmount,
                bal0 - IERC4626(token0).balanceOf(address(this))
            );
            assertEq(
                trade.calculatedAmount,
                IERC20(token1).balanceOf(address(this)) - bal1
            );
        }
    }

    ///////////////////////////////////////// ERC20_ERC20_DIRECT
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    function testPriceFuzzBalancerV3_ERC20_ERC20_DIRECT(uint256 amount0)
        public
    {
        address token0 = ERC20_GOETH;
        address token1 = ERC20_USDC;

        bytes32 pool = bytes32(bytes20(ERC20_ERC20_GOETH_USDC_WEIGHTED_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        vm.assume(amount0 < limits[0] && amount0 > getMinTradeAmount(token0));

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = amount0;

        __prankStaticCall();
        Fraction[] memory prices = adapter.price(pool, token0, token1, amounts);

        for (uint256 i = 0; i < prices.length; i++) {
            // assertGt(prices[i].numerator, 0);
            assertGt(prices[i].denominator, 0);
        }
    }

    function testSwapFuzzBalancerV3_ERC20_ERC20_DIRECT(
        uint256 specifiedAmount,
        bool isBuy
    ) public {
        address token0 = ERC20_GOETH;
        address token1 = ERC20_USDC;

        OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
        bytes32 pool = bytes32(bytes20(ERC20_ERC20_GOETH_USDC_WEIGHTED_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        if (side == OrderSide.Buy) {
            vm.assume(
                specifiedAmount < limits[1]
                    && specifiedAmount > getMinTradeAmount(token1)
            );
        } else {
            vm.assume(
                specifiedAmount < limits[0]
                    && specifiedAmount > getMinTradeAmount(token0)
            );
        }

        deal(token0, address(this), type(uint256).max);
        IERC20(token0).approve(address(adapter), type(uint256).max);

        uint256 bal0 = IERC20(token0).balanceOf(address(this));
        uint256 bal1 = IERC20(token1).balanceOf(address(this));

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = specifiedAmount;
        Trade memory trade =
            adapter.swap(pool, token0, token1, side, specifiedAmount);

        if (side == OrderSide.Buy) {
            assertEq(
                specifiedAmount, IERC20(token1).balanceOf(address(this)) - bal1
            );
            assertEq(
                trade.calculatedAmount,
                bal0 - IERC20(token0).balanceOf(address(this))
            );
        } else {
            assertEq(
                specifiedAmount, bal0 - IERC20(token0).balanceOf(address(this))
            );
            assertEq(
                trade.calculatedAmount,
                IERC20(token1).balanceOf(address(this)) - bal1
            );
        }
    }

    ///////////////////////////////////////// ERC20-->ERC20-->ERC4626 SWAP_WRAP
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    function testPriceFuzzBalancerV3_ERC20_ERC20_ERC4626_SWAP_WRAP(
        uint256 amount0
    ) public {
        address token0 = ERC20_GOETH;
        address token1 = address(ERC4626_sUSDC);

        bytes32 pool = bytes32(bytes20(ERC20_ERC20_GOETH_USDC_WEIGHTED_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        // Use same bounds as swap test for sell orders
        amount0 = bound(amount0, 10 ** 17, 1000 * 10 ** 18);
        vm.assume(amount0 < limits[0]);

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = amount0;

        __prankStaticCall();
        Fraction[] memory prices = adapter.price(pool, token0, token1, amounts);

        for (uint256 i = 0; i < prices.length; i++) {
            assertGt(prices[i].numerator, 0);
            assertGt(prices[i].denominator, 0);
        }
    }

    function testSwapFuzzBalancerV3_ERC20_ERC20_ERC4626_SWAP_WRAP(
        uint256 specifiedAmount,
        bool isBuy
    ) public {
        // Scale bounds based on whether it's a buy or sell order
        if (isBuy) {
            specifiedAmount =
                bound(specifiedAmount, 10 * 10 ** 6, 100000 * 10 ** 6);
        } else {
            specifiedAmount = bound(specifiedAmount, 10 ** 17, 1000 * 10 ** 18);
        }

        address token0 = ERC20_GOETH;
        address token1 = address(ERC4626_sUSDC);

        OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
        bytes32 pool = bytes32(bytes20(ERC20_ERC20_GOETH_USDC_WEIGHTED_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        if (side == OrderSide.Buy) {
            vm.assume(specifiedAmount < limits[1]);
        } else {
            vm.assume(specifiedAmount < limits[0]);
        }

        deal(token0, address(this), IERC20(token0).totalSupply() * 2);
        IERC20(token0).approve(address(adapter), type(uint256).max);

        uint256 bal0 = IERC20(token0).balanceOf(address(this));
        uint256 bal1 = IERC4626(token1).balanceOf(address(this));

        Trade memory trade =
            adapter.swap(pool, token0, token1, side, specifiedAmount);

        if (side == OrderSide.Buy) {
            assertEq(
                specifiedAmount,
                IERC4626(token1).balanceOf(address(this)) - bal1
            );
            assertEq(
                trade.calculatedAmount,
                bal0 - IERC20(token0).balanceOf(address(this))
            );
        } else {
            assertEq(
                specifiedAmount, bal0 - IERC20(token0).balanceOf(address(this))
            );
            assertEq(
                trade.calculatedAmount,
                IERC4626(token1).balanceOf(address(this)) - bal1
            );
        }
    }

    ////////////////////////////////////////
    // !!!//////////////////////////////////////////////////
    ///////////////////////////////////////// ERC4626-->ERC20-->ERC20
    // UNWRAP_SWAP
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    function testPriceFuzzBalancerV3_ERC4626_ERC20_ERC20_UNWRAP_SWAP(
        uint256 amount0
    ) public {
        address token0 = address(ERC4626_sUSDC);
        address token1 = ERC20_GOETH;

        bytes32 pool = bytes32(bytes20(ERC20_ERC20_GOETH_USDC_WEIGHTED_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        vm.assume(amount0 < limits[0] && amount0 > getMinTradeAmount(token0));

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = amount0;

        __prankStaticCall();
        Fraction[] memory prices = adapter.price(pool, token0, token1, amounts);

        for (uint256 i = 0; i < prices.length; i++) {
            assertGt(prices[i].numerator, 0);
            assertGt(prices[i].denominator, 0);
        }
    }

    function testSwapFuzzBalancerV3_ERC4626_ERC20_ERC20_UNWRAP_SWAP(
        uint256 specifiedAmount,
        bool isBuy
    ) public {
        address token0 = address(ERC4626_sUSDC);
        address token1 = ERC20_GOETH;

        OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;

        bytes32 pool = bytes32(bytes20(ERC20_ERC20_GOETH_USDC_WEIGHTED_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);
        if (isBuy) {
            vm.assume(
                specifiedAmount < limits[1] && specifiedAmount > 10 ** 18 // as
                    // using a mock USDC, we set a custom limit here.
            );
        } else {
            vm.assume(
                specifiedAmount < limits[0]
                    && specifiedAmount > getMinTradeAmount(token0)
            );
        }

        if (side == OrderSide.Buy) {
            vm.assume(specifiedAmount < limits[1]);
        } else {
            vm.assume(specifiedAmount < limits[0]);
        }

        deal(token0, address(this), IERC20(token0).totalSupply() * 2);
        IERC4626(token0).approve(address(adapter), type(uint256).max);

        uint256 bal0 = IERC4626(token0).balanceOf(address(this));
        uint256 bal1 = IERC20(token1).balanceOf(address(this));

        Trade memory trade =
            adapter.swap(pool, token0, token1, side, specifiedAmount);

        if (side == OrderSide.Buy) {
            assertEq(
                specifiedAmount, IERC20(token1).balanceOf(address(this)) - bal1
            );
            assertEq(
                trade.calculatedAmount,
                bal0 - IERC4626(token0).balanceOf(address(this))
            );
        } else {
            assertEq(
                specifiedAmount,
                bal0 - IERC4626(token0).balanceOf(address(this))
            );
            assertEq(
                trade.calculatedAmount,
                IERC20(token1).balanceOf(address(this)) - bal1
            );
        }
    }

    ///////////////////////////////////////// ERC4626_ERC4626_DIRECT
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    function testPriceFuzzBalancerV3_ERC4626_ERC4626_DIRECT(uint256 amount0)
        public
    {
        address token0 = ERC4626_waEthLidoWETH;
        address token1 = ERC4626_waEthLidowstETH;

        bytes32 pool = bytes32(bytes20(ERC4626_ERC4626_WETH_wstETH_STABLE_POOL));

        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        uint256 minTradeAmount = getMinTradeAmount(token0);

        vm.assume(amount0 < limits[0]);
        vm.assume(amount0 > minTradeAmount);

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = amount0;

        __prankStaticCall();
        Fraction[] memory prices = adapter.price(pool, token0, token1, amounts);

        for (uint256 i = 0; i < prices.length; i++) {
            assertGt(prices[i].numerator, 0);
            assertGt(prices[i].denominator, 0);
        }
    }

    function testSwapFuzzBalancerV3_ERC4626_ERC4626_DIRECT(
        uint256 specifiedAmount,
        bool isBuy
    ) public {
        address token0 = ERC4626_waEthLidoWETH;
        address token1 = ERC4626_waEthLidowstETH;

        OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
        bytes32 pool = bytes32(bytes20(ERC4626_ERC4626_WETH_wstETH_STABLE_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        if (side == OrderSide.Buy) {
            vm.assume(
                specifiedAmount < limits[1]
                    && specifiedAmount > getMinTradeAmount(token1)
            );
        } else {
            vm.assume(
                specifiedAmount < limits[0]
                    && specifiedAmount > getMinTradeAmount(token0)
            );
        }

        deal(token0, address(this), IERC4626(token0).totalSupply() * 2);
        IERC4626(token0).approve(address(adapter), type(uint256).max);

        uint256 bal0 = IERC4626(token0).balanceOf(address(this));
        uint256 bal1 = IERC4626(token1).balanceOf(address(this));

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = specifiedAmount;
        Trade memory trade =
            adapter.swap(pool, token0, token1, side, specifiedAmount);

        if (side == OrderSide.Buy) {
            assertEq(
                specifiedAmount,
                IERC4626(token1).balanceOf(address(this)) - bal1
            );
            assertEq(
                trade.calculatedAmount,
                bal0 - IERC4626(token0).balanceOf(address(this))
            );
        } else {
            assertEq(
                specifiedAmount,
                bal0 - IERC4626(token0).balanceOf(address(this))
            );
            assertEq(
                trade.calculatedAmount,
                IERC4626(token1).balanceOf(address(this)) - bal1
            );
        }
    }

    ///////////////////////////////////////// ERC20-->(ERC4626 --> ERC4626)
    // WRAP_SWAP
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    function testPriceFuzzBalancerV3_ERC20_ERC4626_ERC4626_WRAP_SWAP(
        uint256 amount0
    ) public {
        address token0 = ERC20_WETH;
        address token1 = ERC4626_waEthLidowstETH;

        bytes32 pool = bytes32(bytes20(ERC4626_ERC4626_WETH_wstETH_STABLE_POOL));

        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        uint256 minTradeAmount = getMinTradeAmount(token0);

        vm.assume(amount0 < limits[0]);
        vm.assume(amount0 > minTradeAmount);

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = amount0;

        __prankStaticCall();
        Fraction[] memory prices = adapter.price(pool, token0, token1, amounts);

        for (uint256 i = 0; i < prices.length; i++) {
            assertGt(prices[i].numerator, 0);
            assertGt(prices[i].denominator, 0);
        }
    }

    function testSwapFuzzBalancerV3_ERC20_ERC4626_ERC4626_WRAP_SWAP(
        uint256 specifiedAmount,
        bool isBuy
    ) public {
        address token0 = ERC20_WETH;
        address token1 = ERC4626_waEthLidowstETH;

        OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
        bytes32 pool = bytes32(bytes20(ERC4626_ERC4626_WETH_wstETH_STABLE_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        if (side == OrderSide.Buy) {
            vm.assume(
                specifiedAmount < limits[1]
                    && specifiedAmount > getMinTradeAmount(token1)
            );
        } else {
            vm.assume(
                specifiedAmount < limits[0]
                    && specifiedAmount > getMinTradeAmount(token0)
            );
        }

        deal(token0, address(this), IERC20(token0).totalSupply() * 2);
        IERC20(token0).approve(address(adapter), type(uint256).max);

        uint256 bal0 = IERC20(token0).balanceOf(address(this));
        uint256 bal1 = IERC4626(token1).balanceOf(address(this));

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = specifiedAmount;
        Trade memory trade =
            adapter.swap(pool, token0, token1, side, specifiedAmount);

        if (side == OrderSide.Buy) {
            assertEq(
                specifiedAmount,
                IERC4626(token1).balanceOf(address(this)) - bal1
            );
            assertEq(
                trade.calculatedAmount,
                bal0 - IERC20(token0).balanceOf(address(this))
            );
        } else {
            assertEq(
                specifiedAmount, bal0 - IERC20(token0).balanceOf(address(this))
            );
            assertEq(
                trade.calculatedAmount,
                IERC4626(token1).balanceOf(address(this)) - bal1
            );
        }
    }

    ///////////////////////////////////////// (ERC4626-->ERC4626)--> ERC20
    // SWAP_UNWRAP
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    function testPriceFuzzBalancerV3_ERC4626_ERC4626_ERC20_SWAP_UNWRAP(
        uint256 amount0
    ) public {
        address token0 = ERC4626_waEthLidowstETH;
        address token1 = ERC20_WETH;

        bytes32 pool = bytes32(bytes20(ERC4626_ERC4626_WETH_wstETH_STABLE_POOL));

        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        uint256 minTradeAmount = getMinTradeAmount(token0);

        vm.assume(amount0 < limits[0]);
        vm.assume(amount0 > minTradeAmount);

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = amount0;

        __prankStaticCall();
        Fraction[] memory prices = adapter.price(pool, token0, token1, amounts);

        for (uint256 i = 0; i < prices.length; i++) {
            assertGt(prices[i].numerator, 0);
            assertGt(prices[i].denominator, 0);
        }
    }

    function testSwapFuzzBalancerV3_ERC4626_ERC4626_ERC20_SWAP_UNWRAP(
        uint256 specifiedAmount,
        bool isBuy
    ) public {
        address token0 = ERC4626_waEthLidowstETH;
        address token1 = ERC20_WETH;

        OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
        bytes32 pool = bytes32(bytes20(ERC4626_ERC4626_WETH_wstETH_STABLE_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        if (side == OrderSide.Buy) {
            vm.assume(
                specifiedAmount < limits[1]
                    && specifiedAmount > getMinTradeAmount(token1)
            );
        } else {
            vm.assume(
                specifiedAmount < limits[0]
                    && specifiedAmount > getMinTradeAmount(token0)
            );
        }

        deal(token0, address(this), type(uint256).max);
        IERC4626(token0).approve(address(adapter), type(uint256).max);

        uint256 bal0 = IERC4626(token0).balanceOf(address(this));
        uint256 bal1 = IERC20(token1).balanceOf(address(this));

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = specifiedAmount;
        Trade memory trade =
            adapter.swap(pool, token0, token1, side, specifiedAmount);

        if (side == OrderSide.Buy) {
            assertEq(
                specifiedAmount, IERC20(token1).balanceOf(address(this)) - bal1
            );
            assertEq(
                trade.calculatedAmount,
                bal0 - IERC4626(token0).balanceOf(address(this))
            );
        } else {
            assertEq(
                specifiedAmount, bal0 - IERC20(token0).balanceOf(address(this))
            );
            assertEq(
                trade.calculatedAmount,
                IERC20(token1).balanceOf(address(this)) - bal1
            );
        }
    }

    ///////////////////////////////////////// (ERC20-->ERC4626)--> ERC20
    // SWAP_UNWRAP
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    function testPriceFuzzBalancerV3_ERC20_ERC4626_ERC20_SWAP_UNWRAP(
        uint256 amount0
    ) public {
        address token0 = ERC20_ETHx;
        address token1 = ERC20_WETH;

        bytes32 pool = bytes32(bytes20(ERC4626_ERC20_ETHx_waWETH_STABLE_POOL));

        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        uint256 minTradeAmount = getMinTradeAmount(token0);

        vm.assume(amount0 < limits[0]);
        vm.assume(amount0 > minTradeAmount);

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = amount0;

        __prankStaticCall();
        Fraction[] memory prices = adapter.price(pool, token0, token1, amounts);

        for (uint256 i = 0; i < prices.length; i++) {
            assertGt(prices[i].numerator, 0);
            assertGt(prices[i].denominator, 0);
        }
    }

    function testSwapFuzzBalancerV3_ERC20_ERC4626_ERC20_SWAP_UNWRAP(
        uint256 specifiedAmount,
        bool isBuy
    ) public {
        address token0 = ERC20_ETHx;
        address token1 = ERC20_WETH;

        OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
        bytes32 pool = bytes32(bytes20(ERC4626_ERC20_ETHx_waWETH_STABLE_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        if (side == OrderSide.Buy) {
            vm.assume(
                specifiedAmount < limits[1]
                    && specifiedAmount > getMinTradeAmount(token1)
            );
        } else {
            vm.assume(
                specifiedAmount < limits[0]
                    && specifiedAmount > getMinTradeAmount(token0)
            );
        }

        deal(token0, address(this), IERC20(token0).totalSupply() * 2);
        IERC20(token0).approve(address(adapter), type(uint256).max);

        uint256 bal0 = IERC20(token0).balanceOf(address(this));
        uint256 bal1 = IERC20(token1).balanceOf(address(this));

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = specifiedAmount;
        Trade memory trade =
            adapter.swap(pool, token0, token1, side, specifiedAmount);

        if (side == OrderSide.Buy) {
            assertEq(
                specifiedAmount, IERC20(token1).balanceOf(address(this)) - bal1
            );
            assertEq(
                trade.calculatedAmount,
                bal0 - IERC20(token0).balanceOf(address(this))
            );
        } else {
            assertEq(
                specifiedAmount, bal0 - IERC20(token0).balanceOf(address(this))
            );
            assertEq(
                trade.calculatedAmount,
                IERC20(token1).balanceOf(address(this)) - bal1
            );
        }
    }

    ///////////////////////////////////////// (ERC20-->ERC4626)--> ERC20
    // SWAP_UNWRAP
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    function testPriceFuzzBalancerV3_ERC20_ERC4626_ERC20_ALTERNATIVE_SWAP_UNWRAP(
        uint256 amount0
    ) public {
        address token0 = ERC20_WETH;
        address token1 = ERC20_ETHx;

        bytes32 pool = bytes32(bytes20(ERC4626_ERC20_ETHx_waWETH_STABLE_POOL));

        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        uint256 minTradeAmount = getMinTradeAmount(token0);

        vm.assume(amount0 < limits[0]);
        vm.assume(amount0 > minTradeAmount);

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = amount0;

        __prankStaticCall();
        Fraction[] memory prices = adapter.price(pool, token0, token1, amounts);

        for (uint256 i = 0; i < prices.length; i++) {
            assertGt(prices[i].numerator, 0);
            assertGt(prices[i].denominator, 0);
        }
    }

    function testSwapFuzzBalancerV3_ERC20_ERC4626_ERC20_ALTERNATIVE_SWAP_UNWRAP(
        uint256 specifiedAmount,
        bool isBuy
    ) public {
        address token0 = ERC20_WETH;
        address token1 = ERC20_ETHx;

        OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
        bytes32 pool = bytes32(bytes20(ERC4626_ERC20_ETHx_waWETH_STABLE_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        if (side == OrderSide.Buy) {
            vm.assume(
                specifiedAmount < limits[1]
                    && specifiedAmount > getMinTradeAmount(token1)
            );
        } else {
            vm.assume(
                specifiedAmount < limits[0]
                    && specifiedAmount > getMinTradeAmount(token0)
            );
        }

        deal(token0, address(this), IERC20(token0).totalSupply() * 2);
        IERC20(token0).approve(address(adapter), type(uint256).max);

        uint256 bal0 = IERC20(token0).balanceOf(address(this));
        uint256 bal1 = IERC20(token1).balanceOf(address(this));

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = specifiedAmount;
        Trade memory trade =
            adapter.swap(pool, token0, token1, side, specifiedAmount);

        if (side == OrderSide.Buy) {
            assertEq(
                specifiedAmount, IERC20(token1).balanceOf(address(this)) - bal1
            );
            assertEq(
                trade.calculatedAmount,
                bal0 - IERC20(token0).balanceOf(address(this))
            );
        } else {
            assertEq(
                specifiedAmount, bal0 - IERC20(token0).balanceOf(address(this))
            );
            assertEq(
                trade.calculatedAmount,
                IERC20(token1).balanceOf(address(this)) - bal1
            );
        }
    }

    ///////////////////////////////////////// ERC20-->ERC20 UNDERLYING DIRECT
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    function testPriceFuzzBalancerV3_ERC20_ERC20_UNDERLYING_DIRECT(
        uint256 amount0
    ) public {
        address token0 = ERC20_WETH;
        address token1 = ERC20_wstETH;

        bytes32 pool = bytes32(bytes20(ERC4626_ERC4626_WETH_wstETH_STABLE_POOL));

        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        uint256 minTradeAmount = getMinTradeAmount(token0);

        vm.assume(amount0 < limits[0]);
        vm.assume(amount0 > minTradeAmount);

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = amount0;

        __prankStaticCall();
        Fraction[] memory prices = adapter.price(pool, token0, token1, amounts);

        for (uint256 i = 0; i < prices.length; i++) {
            assertGt(prices[i].numerator, 0);
            assertGt(prices[i].denominator, 0);
        }
    }

    function testSwapFuzzBalancerV3_ERC20_ERC20_UNDERLYING_DIRECT(
        uint256 specifiedAmount,
        bool isBuy
    ) public {
        address token0 = ERC20_WETH;
        address token1 = ERC20_wstETH;

        OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
        bytes32 pool = bytes32(bytes20(ERC4626_ERC4626_WETH_wstETH_STABLE_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        if (side == OrderSide.Buy) {
            vm.assume(
                specifiedAmount < limits[1]
                    && specifiedAmount > getMinTradeAmount(token1)
            );
        } else {
            vm.assume(
                specifiedAmount < limits[0]
                    && specifiedAmount > getMinTradeAmount(token0)
            );
        }

        deal(token0, address(this), IERC20(token0).totalSupply() * 2);
        IERC20(token0).approve(address(adapter), type(uint256).max);

        uint256 bal0 = IERC20(token0).balanceOf(address(this));
        uint256 bal1 = IERC20(token1).balanceOf(address(this));

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = specifiedAmount;
        Trade memory trade =
            adapter.swap(pool, token0, token1, side, specifiedAmount);

        if (side == OrderSide.Buy) {
            assertEq(
                specifiedAmount, IERC20(token1).balanceOf(address(this)) - bal1
            );
            assertEq(
                trade.calculatedAmount,
                bal0 - IERC20(token0).balanceOf(address(this))
            );
        } else {
            assertEq(
                specifiedAmount, bal0 - IERC20(token0).balanceOf(address(this))
            );
            assertEq(
                trade.calculatedAmount,
                IERC20(token1).balanceOf(address(this)) - bal1
            );
        }
    }

    ///////////////////////////////////////// ERC4626-->ERC20-->ERC4626
    // UNWRAP_SWAP
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    function testPriceFuzzBalancerV3_ERC4626_ERC20_ERC4626_UNWRAP_SWAP(
        uint256 amount0
    ) public {
        address token0 = address(ERC4626_sETHx);
        address token1 = ERC4626_waEthWETH;

        bytes32 pool = bytes32(bytes20(ERC4626_ERC20_ETHx_waWETH_STABLE_POOL));

        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        // Use same bounds as swap test
        amount0 = bound(amount0, 10 ** 15, 1000 * 10 ** 18);
        vm.assume(amount0 < limits[0]);

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = amount0;

        __prankStaticCall();
        Fraction[] memory prices = adapter.price(pool, token0, token1, amounts);

        for (uint256 i = 0; i < prices.length; i++) {
            assertGt(prices[i].numerator, 0);
            assertGt(prices[i].denominator, 0);
        }
    }

    function testSwapFuzzBalancerV3_ERC4626_ERC20_ERC4626_UNWRAP_SWAP(
        uint256 specifiedAmount,
        bool isBuy
    ) public {
        // Scale bounds based on whether it's a buy or sell order
        if (isBuy) {
            specifiedAmount = bound(specifiedAmount, 10 ** 15, 1000 * 10 ** 18);
        } else {
            specifiedAmount = bound(specifiedAmount, 10 ** 15, 1000 * 10 ** 18);
        }

        address token0 = address(ERC4626_sETHx);
        address token1 = ERC4626_waEthWETH;

        OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
        bytes32 pool = bytes32(bytes20(ERC4626_ERC20_ETHx_waWETH_STABLE_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        if (side == OrderSide.Buy) {
            vm.assume(specifiedAmount < limits[1]);
        } else {
            vm.assume(specifiedAmount < limits[0]);
        }

        // Deal tokens to test contract
        deal(token0, address(this), IERC4626(token0).totalSupply() * 2);
        IERC4626(token0).approve(address(adapter), type(uint256).max);

        uint256 bal0 = IERC4626(token0).balanceOf(address(this));
        uint256 bal1 = IERC4626(token1).balanceOf(address(this));

        Trade memory trade =
            adapter.swap(pool, token0, token1, side, specifiedAmount);

        if (side == OrderSide.Buy) {
            assertEq(
                specifiedAmount,
                IERC4626(token1).balanceOf(address(this)) - bal1
            );
            assertEq(
                trade.calculatedAmount,
                bal0 - IERC4626(token0).balanceOf(address(this))
            );
        } else {
            assertEq(
                specifiedAmount,
                bal0 - IERC4626(token0).balanceOf(address(this))
            );
            assertEq(
                trade.calculatedAmount,
                IERC4626(token1).balanceOf(address(this)) - bal1
            );
        }
    }

    ///////////////////////////////////////// ERC4626-->ERC20-->ERC20-->ERC4626
    // UNWRAP_SWAP_WRAP
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    function testPriceFuzzBalancerV3_ERC4626_ERC20_ERC20_ERC4626_UNWRAP_SWAP_WRAP(
        uint256 amount0
    ) public {
        address token0 = address(ERC4626_sGOETH);
        address token1 = address(ERC4626_sUSDC);

        bytes32 pool = bytes32(bytes20(ERC20_ERC20_GOETH_USDC_WEIGHTED_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        vm.assume(amount0 < limits[0] && amount0 > 1e17);

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = amount0;

        __prankStaticCall();
        Fraction[] memory prices = adapter.price(pool, token0, token1, amounts);

        for (uint256 i = 0; i < prices.length; i++) {
            assertGt(prices[i].numerator, 0);
            assertGt(prices[i].denominator, 0);
        }
    }

    function testSwapFuzzBalancerV3_ERC4626_ERC20_ERC20_ERC4626_UNWRAP_SWAP_WRAP(
        uint256 specifiedAmount,
        bool isBuy
    ) public {
        address token0 = address(ERC4626_sGOETH);
        address token1 = address(ERC4626_sUSDC);

        bytes32 pool = bytes32(bytes20(ERC20_ERC20_GOETH_USDC_WEIGHTED_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);
        OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;

        if (isBuy) {
            vm.assume(specifiedAmount < limits[1] && specifiedAmount > 1e17);
            deal(token0, address(this), type(uint256).max);
        } else {
            vm.assume(specifiedAmount < limits[0] && specifiedAmount > 1e17);
            deal(token0, address(this), type(uint256).max);
        }

        deal(token0, address(this), specifiedAmount);

        IERC4626(token0).approve(address(this), type(uint256).max);
        IERC4626(token0).approve(address(adapter), type(uint256).max);

        uint256 bal0 = IERC4626(token0).balanceOf(address(this));
        uint256 bal1 = IERC4626(token1).balanceOf(address(this));

        Trade memory trade =
            adapter.swap(pool, token0, token1, side, specifiedAmount);

        if (side == OrderSide.Buy) {
            assertEq(
                specifiedAmount,
                IERC4626(token1).balanceOf(address(this)) - bal1
            );
            assertEq(
                trade.calculatedAmount,
                bal0 - IERC4626(token0).balanceOf(address(this))
            );
        } else {
            assertEq(
                specifiedAmount,
                bal0 - IERC4626(token0).balanceOf(address(this))
            );
            assertEq(
                trade.calculatedAmount,
                IERC4626(token1).balanceOf(address(this)) - bal1
            );
        }
    }

    ///////////////////////////////////////// ERC20-->ERC4626-->ERC4626-->ERC20
    // UNWRAP_SWAP_WRAP
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    function testPriceFuzzBalancerV3_ERC20_ERC4626_ERC4626_ERC20_WRAP_SWAP_UNWRAP(
        uint256 amount0
    ) public {
        address token0 = address(ERC20_GOETH);
        address token1 = address(ERC20_USDC);

        bytes32 pool = bytes32(bytes20(ERC20_ERC20_GOETH_USDC_WEIGHTED_POOL));
        uint256[] memory limits = adapter.getLimits(pool, token0, token1);

        vm.assume(amount0 < limits[0] && amount0 > 1e17);

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = amount0;

        __prankStaticCall();
        Fraction[] memory prices = adapter.price(pool, token0, token1, amounts);

        for (uint256 i = 0; i < prices.length; i++) {
            assertGt(prices[i].numerator, 0);
            assertGt(prices[i].denominator, 0);
        }
    }

    function __prankStaticCall() internal {
        // Prank address 0x0 for both msg.sender and tx.origin (to identify as a
        // staticcall).
        vm.prank(address(0), address(0));
    }

    function getMinTradeAmount(address token) internal view returns (uint256) {
        uint256 decimals = ERC20(token).decimals();
        uint256 decimalFactor = decimals; // n, e.g. stablecoins
        if (decimals > 6) {
            decimalFactor = decimals - 1; // 0.n
        }
        if (decimals > 12) {
            decimalFactor = decimals - 3; // e.g. ETH, BTC, ...
        }

        uint256 minTradeAmount = 10 ** decimalFactor;

        return minTradeAmount;
    }
}
