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

import {Test} from "forge-std/Test.sol";
import "../../../contracts/bridge/trustless/EnhancedSwapRouterV2.sol";
import "../../../contracts/bridge/trustless/IntentBridgeCoordinatorV2.sol";
import "../../../contracts/bridge/trustless/RouteTypesV2.sol";
import "../../../contracts/bridge/trustless/adapters/DodoRouteExecutorAdapter.sol";
import "../../../contracts/bridge/trustless/adapters/DodoV3RouteExecutorAdapter.sol";
import "../../../contracts/bridge/trustless/adapters/UniswapV3RouteExecutorAdapter.sol";
import "../../../contracts/bridge/trustless/adapters/BalancerRouteExecutorAdapter.sol";
import "../../../contracts/bridge/trustless/adapters/CurveRouteExecutorAdapter.sol";
import "../../../contracts/bridge/trustless/interfaces/IBalancerVault.sol";
import "../../../contracts/bridge/trustless/interfaces/IBridgeIntentExecutor.sol";
import "../../../contracts/bridge/trustless/interfaces/ISwapRouter.sol";
import "../../../contracts/liquidity/interfaces/ILiquidityProvider.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract MockERC20V2 is ERC20 {
    constructor(string memory name_, string memory symbol_, uint256 supply) ERC20(name_, symbol_) {
        _mint(msg.sender, supply);
    }

    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
}

contract MockDodoProviderV2 is ILiquidityProvider {
    mapping(address => mapping(address => bool)) public supported;
    mapping(address => mapping(address => uint256)) public quoteAmount;

    function setSupport(address tokenIn, address tokenOut, bool isSupported) external {
        supported[tokenIn][tokenOut] = isSupported;
    }

    function setQuote(address tokenIn, address tokenOut, uint256 amountOut) external {
        quoteAmount[tokenIn][tokenOut] = amountOut;
    }

    function getQuote(address tokenIn, address tokenOut, uint256)
        external
        view
        returns (uint256 amountOut, uint256 slippageBps)
    {
        if (!supported[tokenIn][tokenOut]) return (0, 10000);
        return (quoteAmount[tokenIn][tokenOut], 30);
    }

    function executeSwap(address tokenIn, address tokenOut, uint256 amountIn, uint256)
        external
        returns (uint256 amountOut)
    {
        require(supported[tokenIn][tokenOut], "unsupported");
        amountOut = quoteAmount[tokenIn][tokenOut];
        ERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
        ERC20(tokenOut).transfer(msg.sender, amountOut);
    }

    function supportsTokenPair(address tokenIn, address tokenOut) external view returns (bool) {
        return supported[tokenIn][tokenOut];
    }

    function providerName() external pure returns (string memory) {
        return "MockDodo";
    }

    function estimateGas(address, address, uint256) external pure returns (uint256) {
        return 140000;
    }
}

contract MockUniswapQuoterV2 {
    mapping(bytes32 => uint256) public quotes;

    function setQuote(address tokenIn, address tokenOut, uint24 fee, uint256 amountOut) external {
        quotes[keccak256(abi.encode(tokenIn, tokenOut, fee))] = amountOut;
    }

    function quoteExactInputSingle(address tokenIn, address tokenOut, uint24 fee, uint256, uint160)
        external
        view
        returns (uint256)
    {
        return quotes[keccak256(abi.encode(tokenIn, tokenOut, fee))];
    }

    function quoteExactInput(bytes calldata, uint256) external pure returns (uint256) {
        return 0;
    }
}

contract MockUniswapRouterV2 {
    mapping(bytes32 => uint256) public quotes;

    function setQuote(address tokenIn, address tokenOut, uint24 fee, uint256 amountOut) external {
        quotes[keccak256(abi.encode(tokenIn, tokenOut, fee))] = amountOut;
    }

    function exactInputSingle(ISwapRouter.ExactInputSingleParams calldata params) external returns (uint256 amountOut) {
        amountOut = quotes[keccak256(abi.encode(params.tokenIn, params.tokenOut, params.fee))];
        ERC20(params.tokenIn).transferFrom(msg.sender, address(this), params.amountIn);
        ERC20(params.tokenOut).transfer(params.recipient, amountOut);
    }

    function exactInput(ISwapRouter.ExactInputParams calldata) external pure returns (uint256) {
        revert("path unsupported in mock");
    }
}

contract MockBalancerVaultV2 {
    bytes32 public poolId;
    address[] public tokens;
    uint256[] public balances;

    function setPool(bytes32 newPoolId, address tokenA, address tokenB, uint256 balanceA, uint256 balanceB) external {
        poolId = newPoolId;
        tokens = [tokenA, tokenB];
        balances = [balanceA, balanceB];
    }

    function swap(
        IBalancerVault.SingleSwap memory singleSwap,
        IBalancerVault.FundManagement memory funds,
        uint256,
        uint256
    ) external returns (uint256 amountCalculated) {
        require(singleSwap.poolId == poolId, "bad pool");
        ERC20(singleSwap.assetIn).transferFrom(msg.sender, address(this), singleSwap.amount);
        amountCalculated = (singleSwap.amount * balances[1]) / balances[0];
        amountCalculated = (amountCalculated * 9950) / 10000;
        ERC20(singleSwap.assetOut).transfer(funds.recipient, amountCalculated);
    }

    function getPool(bytes32) external pure returns (address poolAddress, uint8 specialization) {
        return (address(0xBEEF), 0);
    }

    function queryBatchSwap(IBalancerVault.SwapKind, IBalancerVault.SingleSwap[] memory, address[] memory)
        external
        pure
        returns (int256[] memory assetDeltas)
    {
        assetDeltas = new int256[](0);
    }

    function getPoolTokens(bytes32 requestedPoolId)
        external
        view
        returns (address[] memory, uint256[] memory, uint256)
    {
        require(requestedPoolId == poolId, "bad pool");
        return (tokens, balances, block.number);
    }
}

contract MockCurvePoolV2 {
    mapping(bytes32 => uint256) public quotes;

    function setQuote(int128 i, int128 j, uint256 amountOut) external {
        quotes[keccak256(abi.encode(i, j))] = amountOut;
    }

    function exchange(int128 i, int128 j, uint256 dx, uint256) external returns (uint256) {
        uint256 amountOut = quotes[keccak256(abi.encode(i, j))];
        dx;
        return amountOut;
    }

    function exchange_underlying(int128 i, int128 j, uint256 dx, uint256) external returns (uint256) {
        uint256 amountOut = quotes[keccak256(abi.encode(i, j))];
        dx;
        return amountOut;
    }

    function get_dy(int128 i, int128 j, uint256) external view returns (uint256) {
        return quotes[keccak256(abi.encode(i, j))];
    }

    function coins(uint256) external pure returns (address) {
        return address(0);
    }
}

contract MockBridgeIntentExecutorV2 is IBridgeIntentExecutor {
    event Bridged(bytes32 indexed bridgeType, address indexed token, uint256 amount, address recipient);

    function validateBridge(bytes32, bytes calldata, address token, uint256 amount, address recipient)
        external
        pure
        returns (bool ok, string memory reason)
    {
        if (token == address(0) || amount == 0 || recipient == address(0)) {
            return (false, "invalid bridge request");
        }
        return (true, "");
    }

    function executeBridge(bytes32 bridgeType, bytes calldata, address token, uint256 amount, address recipient)
        external
        payable
        returns (bytes32 referenceId)
    {
        ERC20(token).transferFrom(msg.sender, address(this), amount);
        emit Bridged(bridgeType, token, amount, recipient);
        return keccak256(abi.encode(bridgeType, token, amount, recipient));
    }
}

contract MockD3ApproveV2 {
    function claimTokens(address token, address who, address dest, uint256 amount) external {
        IERC20(token).transferFrom(who, dest, amount);
    }
}

contract MockD3ApproveProxyV2 {
    address public immutable _DODO_APPROVE_;
    mapping(address => bool) public allowedProxy;

    constructor(address approve) {
        _DODO_APPROVE_ = approve;
    }

    function setAllowedProxy(address proxy, bool allowed) external {
        allowedProxy[proxy] = allowed;
    }

    function claimTokens(address token, address who, address dest, uint256 amount) external {
        require(allowedProxy[msg.sender], "NOT_ALLOWED_PROXY");
        MockD3ApproveV2(_DODO_APPROVE_).claimTokens(token, who, dest, amount);
    }
}

interface IMockD3SwapCallbackV2 {
    function d3MMSwapCallBack(address token, uint256 value, bytes calldata data) external;
}

contract MockD3MMV2 {
    mapping(address => mapping(address => uint256)) public quoteAmount;

    function setQuote(address tokenIn, address tokenOut, uint256 amountOut) external {
        quoteAmount[tokenIn][tokenOut] = amountOut;
    }

    function querySellTokens(address fromToken, address toToken, uint256 fromAmount)
        external
        view
        returns (uint256 payFromAmount, uint256 receiveToAmount, uint256 vusdAmount, uint256 swapFee, uint256 mtFee)
    {
        payFromAmount = fromAmount;
        receiveToAmount = quoteAmount[fromToken][toToken];
        vusdAmount = 0;
        swapFee = 0;
        mtFee = 0;
    }

    function sellToken(
        address to,
        address fromToken,
        address toToken,
        uint256 fromAmount,
        uint256 minReceiveAmount,
        bytes calldata data
    ) external returns (uint256 receiveToAmount) {
        receiveToAmount = quoteAmount[fromToken][toToken];
        require(receiveToAmount >= minReceiveAmount, "insufficient output");
        IMockD3SwapCallbackV2(msg.sender).d3MMSwapCallBack(fromToken, fromAmount, data);
        IERC20(toToken).transfer(to, receiveToAmount);
    }
}

contract MockD3ProxyV2 {
    address public immutable _DODO_APPROVE_PROXY_;

    struct SwapCallbackData {
        bytes data;
        address payer;
    }

    constructor(address approveProxy) {
        _DODO_APPROVE_PROXY_ = approveProxy;
    }

    function sellTokens(
        address pool,
        address to,
        address fromToken,
        address toToken,
        uint256 fromAmount,
        uint256 minReceiveAmount,
        bytes calldata data,
        uint256 deadLine
    ) external payable returns (uint256 receiveToAmount) {
        require(deadLine >= block.timestamp, "expired");
        SwapCallbackData memory swapData = SwapCallbackData({data: data, payer: msg.sender});
        receiveToAmount =
            MockD3MMV2(pool).sellToken(to, fromToken, toToken, fromAmount, minReceiveAmount, abi.encode(swapData));
    }

    function d3MMSwapCallBack(address token, uint256 value, bytes calldata callbackData) external {
        SwapCallbackData memory decoded = abi.decode(callbackData, (SwapCallbackData));
        MockD3ApproveProxyV2(_DODO_APPROVE_PROXY_).claimTokens(token, decoded.payer, msg.sender, value);
    }
}

contract EnhancedSwapRouterV2Test is Test {
    EnhancedSwapRouterV2 internal router;
    IntentBridgeCoordinatorV2 internal coordinator;
    DodoRouteExecutorAdapter internal dodoAdapter;
    DodoV3RouteExecutorAdapter internal dodoV3Adapter;
    UniswapV3RouteExecutorAdapter internal uniswapAdapter;
    BalancerRouteExecutorAdapter internal balancerAdapter;
    CurveRouteExecutorAdapter internal curveAdapter;
    MockBridgeIntentExecutorV2 internal bridgeExecutor;

    MockERC20V2 internal weth;
    MockERC20V2 internal usdt;
    MockERC20V2 internal usdc;
    MockERC20V2 internal dai;

    MockDodoProviderV2 internal dodoProvider;
    MockUniswapQuoterV2 internal uniswapQuoter;
    MockUniswapRouterV2 internal uniswapRouter;
    MockBalancerVaultV2 internal balancerVault;
    MockCurvePoolV2 internal curvePool;
    MockD3ApproveV2 internal d3Approve;
    MockD3ApproveProxyV2 internal d3ApproveProxy;
    MockD3MMV2 internal d3Pool;
    MockD3ProxyV2 internal d3Proxy;

    address internal user = address(0x1111);

    function setUp() public {
        weth = new MockERC20V2("Wrapped Ether", "WETH", 1_000_000 ether);
        usdt = new MockERC20V2("Tether", "USDT", 1_000_000 ether);
        usdc = new MockERC20V2("USD Coin", "USDC", 1_000_000 ether);
        dai = new MockERC20V2("Dai", "DAI", 1_000_000 ether);

        dodoProvider = new MockDodoProviderV2();
        uniswapQuoter = new MockUniswapQuoterV2();
        uniswapRouter = new MockUniswapRouterV2();
        balancerVault = new MockBalancerVaultV2();
        curvePool = new MockCurvePoolV2();

        dodoAdapter = new DodoRouteExecutorAdapter();
        dodoV3Adapter = new DodoV3RouteExecutorAdapter();
        uniswapAdapter = new UniswapV3RouteExecutorAdapter();
        balancerAdapter = new BalancerRouteExecutorAdapter();
        curveAdapter = new CurveRouteExecutorAdapter();
        bridgeExecutor = new MockBridgeIntentExecutorV2();
        d3Approve = new MockD3ApproveV2();
        d3ApproveProxy = new MockD3ApproveProxyV2(address(d3Approve));
        d3Pool = new MockD3MMV2();
        d3Proxy = new MockD3ProxyV2(address(d3ApproveProxy));

        router = new EnhancedSwapRouterV2(address(weth), address(usdt), address(usdc), address(dai));
        coordinator = new IntentBridgeCoordinatorV2(address(router));

        router.setProviderAdapter(RouteTypesV2.Provider.Dodo, address(dodoAdapter));
        router.setProviderAdapter(RouteTypesV2.Provider.DodoV3, address(dodoV3Adapter));
        router.setProviderAdapter(RouteTypesV2.Provider.UniswapV3, address(uniswapAdapter));
        router.setProviderAdapter(RouteTypesV2.Provider.Balancer, address(balancerAdapter));
        router.setProviderAdapter(RouteTypesV2.Provider.Curve, address(curveAdapter));

        coordinator.setBridgeExecutor(bytes32("CCIP"), address(bridgeExecutor));

        weth.mint(user, 100 ether);
        usdt.mint(address(dodoProvider), 1_000_000 ether);
        usdc.mint(address(dodoProvider), 1_000_000 ether);
        usdc.mint(address(uniswapRouter), 1_000_000 ether);
        usdc.mint(address(balancerVault), 1_000_000 ether);
        usdc.mint(address(curveAdapter), 1_000_000 ether);
        usdt.mint(address(d3Pool), 1_000_000 ether);

        dodoProvider.setSupport(address(weth), address(usdt), true);
        dodoProvider.setQuote(address(weth), address(usdt), 1_800 ether);
        dodoProvider.setSupport(address(usdt), address(usdc), true);
        dodoProvider.setQuote(address(usdt), address(usdc), 1_790 ether);
        d3Pool.setQuote(address(weth), address(usdt), 2_100 ether);
        d3Pool.setQuote(address(usdt), address(weth), 0.095 ether);
        d3ApproveProxy.setAllowedProxy(address(d3Proxy), true);

        router.setProviderRoute(
            address(weth),
            address(usdt),
            RouteTypesV2.Provider.Dodo,
            address(dodoProvider),
            abi.encode(address(0xA11CE)),
            true
        );
        router.setProviderRoute(
            address(usdt),
            address(usdc),
            RouteTypesV2.Provider.Dodo,
            address(dodoProvider),
            abi.encode(address(0xB0B)),
            true
        );

        uniswapQuoter.setQuote(address(weth), address(usdc), 3000, 1_700 ether);
        uniswapRouter.setQuote(address(weth), address(usdc), 3000, 1_700 ether);
        router.setProviderRoute(
            address(weth),
            address(usdc),
            RouteTypesV2.Provider.UniswapV3,
            address(uniswapRouter),
            abi.encode(bytes(""), uint24(3000), address(uniswapQuoter), false),
            true
        );

        bytes32 poolId = keccak256("balancer-weth-usdc");
        balancerVault.setPool(poolId, address(weth), address(usdc), 100 ether, 160_000 ether);
        router.setProviderRoute(
            address(weth),
            address(usdc),
            RouteTypesV2.Provider.Balancer,
            address(balancerVault),
            abi.encode(poolId),
            true
        );

        curvePool.setQuote(0, 1, 1_000 ether);
        router.setProviderRoute(
            address(usdt),
            address(usdc),
            RouteTypesV2.Provider.Curve,
            address(curvePool),
            abi.encode(int128(0), int128(1), false),
            true
        );
    }

    function _singleLegPlan(
        RouteTypesV2.Provider provider,
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 minAmountOut,
        address target,
        bytes memory providerData
    ) internal view returns (RouteTypesV2.RoutePlan memory plan) {
        RouteTypesV2.RouteLeg[] memory legs = new RouteTypesV2.RouteLeg[](1);
        legs[0] = RouteTypesV2.RouteLeg({
            provider: provider,
            tokenIn: tokenIn,
            tokenOut: tokenOut,
            amountSource: RouteTypesV2.AmountSource.UserInput,
            minAmountOut: minAmountOut,
            target: target,
            providerData: providerData
        });

        plan = RouteTypesV2.RoutePlan({
            chainId: block.chainid,
            inputToken: tokenIn,
            outputToken: tokenOut,
            amountIn: amountIn,
            minAmountOut: minAmountOut,
            recipient: user,
            deadline: block.timestamp + 1 hours,
            legs: legs
        });
    }

    function testDodoDirectExecution() public {
        vm.startPrank(user);
        weth.approve(address(router), 1 ether);
        RouteTypesV2.RoutePlan memory plan = _singleLegPlan(
            RouteTypesV2.Provider.Dodo,
            address(weth),
            address(usdt),
            1 ether,
            1_700 ether,
            address(dodoProvider),
            abi.encode(address(0xA11CE))
        );
        uint256 amountOut = router.executeRoute(plan);
        vm.stopPrank();

        assertEq(amountOut, 1_800 ether);
        assertEq(usdt.balanceOf(user), 1_800 ether);
    }

    function testUniswapV3Execution() public {
        vm.startPrank(user);
        weth.approve(address(router), 1 ether);
        RouteTypesV2.RoutePlan memory plan = _singleLegPlan(
            RouteTypesV2.Provider.UniswapV3,
            address(weth),
            address(usdc),
            1 ether,
            1_650 ether,
            address(uniswapRouter),
            abi.encode(bytes(""), uint24(3000), address(uniswapQuoter), false)
        );
        uint256 amountOut = router.executeRoute(plan);
        vm.stopPrank();

        assertEq(amountOut, 1_700 ether);
        assertEq(usdc.balanceOf(user), 1_700 ether);
    }

    function testDodoV3DirectExecution() public {
        vm.startPrank(user);
        weth.approve(address(router), 1 ether);
        RouteTypesV2.RoutePlan memory plan = _singleLegPlan(
            RouteTypesV2.Provider.DodoV3,
            address(weth),
            address(usdt),
            1 ether,
            2_000 ether,
            address(d3Proxy),
            abi.encode(address(d3Pool))
        );
        uint256 amountOut = router.executeRoute(plan);
        vm.stopPrank();

        assertEq(amountOut, 2_100 ether);
        assertEq(usdt.balanceOf(user), 2_100 ether);
    }

    function testBalancerExecution() public {
        vm.startPrank(user);
        weth.approve(address(router), 1 ether);
        RouteTypesV2.RoutePlan memory plan = _singleLegPlan(
            RouteTypesV2.Provider.Balancer,
            address(weth),
            address(usdc),
            1 ether,
            1_500 ether,
            address(balancerVault),
            abi.encode(keccak256("balancer-weth-usdc"))
        );
        uint256 amountOut = router.executeRoute(plan);
        vm.stopPrank();

        assertGt(amountOut, 0);
        assertEq(usdc.balanceOf(user), amountOut);
    }

    function testCurveStableStableExecutionOnly() public {
        usdt.mint(user, 2_000 ether);
        vm.startPrank(user);
        usdt.approve(address(router), 1_100 ether);
        RouteTypesV2.RoutePlan memory plan = _singleLegPlan(
            RouteTypesV2.Provider.Curve,
            address(usdt),
            address(usdc),
            1_100 ether,
            900 ether,
            address(curvePool),
            abi.encode(int128(0), int128(1), false)
        );
        uint256 amountOut = router.executeRoute(plan);
        vm.stopPrank();

        assertEq(amountOut, 1_000 ether);
        assertEq(usdc.balanceOf(user), amountOut);
    }

    function testInvalidProviderDataRejected() public {
        vm.startPrank(user);
        weth.approve(address(router), 1 ether);
        RouteTypesV2.RoutePlan memory plan = _singleLegPlan(
            RouteTypesV2.Provider.Dodo,
            address(weth),
            address(usdt),
            1 ether,
            1_700 ether,
            address(dodoProvider),
            hex"1234"
        );
        vm.expectRevert();
        router.executeRoute(plan);
        vm.stopPrank();
    }

    function testQuoteConfiguredProviderQuotesSingleDodoRoute() public view {
        RouteTypesV2.ProviderQuote memory quote =
            router.quoteConfiguredProvider(address(weth), address(usdt), 1 ether, RouteTypesV2.Provider.Dodo);

        assertEq(uint256(quote.provider), uint256(RouteTypesV2.Provider.Dodo));
        assertEq(quote.target, address(dodoProvider));
        assertEq(quote.amountOut, 1_800 ether);
        assertEq(quote.estimatedGas, 140000);
        assertTrue(quote.executable);
        assertEq(abi.decode(quote.providerData, (address)), address(0xA11CE));
    }

    function testQuoteConfiguredProviderRevertsWhenProviderDisabled() public {
        router.setProviderEnabled(RouteTypesV2.Provider.Dodo, false);

        vm.expectRevert(EnhancedSwapRouterV2.ProviderDisabled.selector);
        router.quoteConfiguredProvider(address(weth), address(usdt), 1 ether, RouteTypesV2.Provider.Dodo);
    }

    function testQuoteConfiguredProviderRevertsWhenRouteMissing() public {
        vm.expectRevert(EnhancedSwapRouterV2.RouteNotConfigured.selector);
        router.quoteConfiguredProvider(address(weth), address(dai), 1 ether, RouteTypesV2.Provider.Dodo);
    }

    function testQuoteConfiguredProvidersStillDiscoversConfiguredRoutes() public view {
        RouteTypesV2.ProviderQuote[] memory quotes =
            router.quoteConfiguredProviders(address(weth), address(usdt), 1 ether);

        assertEq(quotes.length, 1);
        assertEq(uint256(quotes[0].provider), uint256(RouteTypesV2.Provider.Dodo));
        assertEq(quotes[0].amountOut, 1_800 ether);
    }

    function testMultiLegExecutionUsesPreviousLegOutput() public {
        vm.startPrank(user);
        weth.approve(address(router), 1 ether);

        RouteTypesV2.RouteLeg[] memory legs = new RouteTypesV2.RouteLeg[](2);
        legs[0] = RouteTypesV2.RouteLeg({
            provider: RouteTypesV2.Provider.Dodo,
            tokenIn: address(weth),
            tokenOut: address(usdt),
            amountSource: RouteTypesV2.AmountSource.UserInput,
            minAmountOut: 1_700 ether,
            target: address(dodoProvider),
            providerData: abi.encode(address(0xA11CE))
        });
        legs[1] = RouteTypesV2.RouteLeg({
            provider: RouteTypesV2.Provider.Dodo,
            tokenIn: address(usdt),
            tokenOut: address(usdc),
            amountSource: RouteTypesV2.AmountSource.PreviousLeg,
            minAmountOut: 1_750 ether,
            target: address(dodoProvider),
            providerData: abi.encode(address(0xB0B))
        });

        RouteTypesV2.RoutePlan memory plan = RouteTypesV2.RoutePlan({
            chainId: block.chainid,
            inputToken: address(weth),
            outputToken: address(usdc),
            amountIn: 1 ether,
            minAmountOut: 1_750 ether,
            recipient: user,
            deadline: block.timestamp + 1 hours,
            legs: legs
        });

        uint256 amountOut = router.executeRoute(plan);
        vm.stopPrank();

        assertEq(amountOut, 1_790 ether);
        assertEq(usdc.balanceOf(user), 1_790 ether);
    }

    function testIntentBridgeCoordinatorExecutesSourcePlanAndBridge() public {
        vm.startPrank(user);
        weth.approve(address(coordinator), 1 ether);

        RouteTypesV2.RouteLeg[] memory sourceLegs = new RouteTypesV2.RouteLeg[](1);
        sourceLegs[0] = RouteTypesV2.RouteLeg({
            provider: RouteTypesV2.Provider.Dodo,
            tokenIn: address(weth),
            tokenOut: address(usdt),
            amountSource: RouteTypesV2.AmountSource.UserInput,
            minAmountOut: 1_700 ether,
            target: address(dodoProvider),
            providerData: abi.encode(address(0xA11CE))
        });

        RouteTypesV2.RoutePlan memory sourcePlan = RouteTypesV2.RoutePlan({
            chainId: block.chainid,
            inputToken: address(weth),
            outputToken: address(usdt),
            amountIn: 1 ether,
            minAmountOut: 1_700 ether,
            recipient: address(coordinator),
            deadline: block.timestamp + 1 hours,
            legs: sourceLegs
        });

        RouteTypesV2.RoutePlan memory destinationPlan = RouteTypesV2.RoutePlan({
            chainId: block.chainid + 1,
            inputToken: address(usdt),
            outputToken: address(usdt),
            amountIn: 1_800 ether,
            minAmountOut: 1_800 ether,
            recipient: user,
            deadline: block.timestamp + 2 hours,
            legs: new RouteTypesV2.RouteLeg[](0)
        });

        RouteTypesV2.BridgeIntentPlan memory intent = RouteTypesV2.BridgeIntentPlan({
            sourcePlan: sourcePlan,
            bridgeType: bytes32("CCIP"),
            bridgeData: abi.encode(address(bridgeExecutor), "CCIP"),
            destinationPlan: destinationPlan,
            recipient: user,
            deadline: block.timestamp + 1 hours
        });

        (bytes32 bridgeReference,) = coordinator.executeIntent(intent);
        vm.stopPrank();

        assertTrue(bridgeReference != bytes32(0));
        assertEq(usdt.balanceOf(address(bridgeExecutor)), 1_800 ether);
    }
}
