pragma solidity ^0.8.26;

import "../TychoRouterTestSetup.sol";
import {CommonBase} from "../../lib/forge-std/src/Base.sol";
import {Constants} from "../Constants.sol";
import {TransferManager} from "../../src/TransferManager.sol";
import {
    NativeWrapExecutor,
    NativeWrapExecutor__InvalidDataLength,
    IWrapped
} from "../../src/executors/NativeWrapExecutor.sol";
import {StdAssertions} from "../../lib/forge-std/src/StdAssertions.sol";
import {StdChains} from "../../lib/forge-std/src/StdChains.sol";
import {StdCheats, StdCheatsSafe} from "../../lib/forge-std/src/StdCheats.sol";
import {StdUtils} from "../../lib/forge-std/src/StdUtils.sol";
import {TestUtils} from "../TestUtils.sol";

contract NativeWrapExecutorExposed is NativeWrapExecutor {
    constructor(address wrappedAddress) NativeWrapExecutor(wrappedAddress) {}

    function decodeParams(bytes calldata data)
        external
        pure
        returns (bool isWrapping)
    {
        return _decodeData(data);
    }
}

contract NativeWrapExecutorTest is TestUtils, Constants {
    NativeWrapExecutorExposed nativeWrapExecutor;

    function setUp() public {
        vm.createSelectFork(vm.rpcUrl("mainnet"), 23899254);
        nativeWrapExecutor = new NativeWrapExecutorExposed(WETH_ADDR);
    }

    function testDecodeParamsWrap() public view {
        bytes memory params = abi.encodePacked(
            uint8(1) // isWrapping = true
        );

        bool isWrapping = nativeWrapExecutor.decodeParams(params);

        assertTrue(isWrapping);
    }

    function testDecodeParamsUnwrap() public view {
        bytes memory params = abi.encodePacked(
            uint8(0) // isWrapping = false
        );

        bool isWrapping = nativeWrapExecutor.decodeParams(params);

        assertFalse(isWrapping);
    }

    function testDecodeParamsInvalidDataLength() public {
        bytes memory invalidParams = abi.encodePacked(BOB);

        vm.expectRevert(NativeWrapExecutor__InvalidDataLength.selector);
        nativeWrapExecutor.decodeParams(invalidParams);
    }

    function testGetTransferDataWrap() public {
        bytes memory params = abi.encodePacked(
            uint8(1) // isWrapping = true
        );

        (
            TransferManager.TransferType transferType,
            address receiver,
            address tokenIn,
            address tokenOut,
            bool outputToRouter
        ) = nativeWrapExecutor.getTransferData(params);

        assertEq(
            uint8(transferType),
            uint8(TransferManager.TransferType.TransferNativeInExecutor)
        );
        assertEq(receiver, address(this));
        assertEq(tokenIn, ETH_ADDR);
        assertEq(tokenOut, WETH_ADDR);
        assertEq(outputToRouter, true);
    }

    function testGetTransferDataUnwrap() public {
        bytes memory params = abi.encodePacked(
            uint8(0) // isWrapping = false
        );

        (
            TransferManager.TransferType transferType,
            address receiver,
            address tokenIn,
            address tokenOut,
            bool outputToRouter
        ) = nativeWrapExecutor.getTransferData(params);

        assertEq(
            uint8(transferType),
            uint8(TransferManager.TransferType.ProtocolWillDebit)
        );
        assertEq(receiver, address(this));
        assertEq(tokenIn, WETH_ADDR);
        assertEq(tokenOut, ETH_ADDR);
        assertEq(outputToRouter, true);
    }

    function testSwapWrap() public {
        // ETH -> wETH
        IWrapped WETH = IWrapped(WETH_ADDR);
        uint256 amountIn = 1 ether;
        bytes memory protocolData = abi.encodePacked(
            uint8(1) // isWrapping = true
        );

        // Fund the executor with ETH
        vm.deal(address(nativeWrapExecutor), amountIn);
        nativeWrapExecutor.swap(amountIn, protocolData, BOB);

        assertEq(WETH.balanceOf(address(nativeWrapExecutor)), 1 ether);
    }

    function testSwapUnwrap() public {
        // wETH -> ETH
        uint256 amountIn = 1 ether;
        bytes memory protocolData = abi.encodePacked(
            uint8(0) // isWrapping = false
        );

        // Fund the executor with wETH
        deal(WETH_ADDR, address(nativeWrapExecutor), amountIn);

        uint256 ethBalanceBefore = address(nativeWrapExecutor).balance;
        nativeWrapExecutor.swap(amountIn, protocolData, BOB);

        assertEq(
            address(nativeWrapExecutor).balance - ethBalanceBefore, 1 ether
        );
    }

    function testDecodeWrapping() public view {
        // Generated by the SwapEncoder - test_encode_wrap_wrapping
        bytes memory protocolData =
            loadCallDataFromFile("test_encode_wrap_wrapping");

        bool isWrapping = nativeWrapExecutor.decodeParams(protocolData);

        assertTrue(isWrapping);
    }

    function testDecodeUnwrapping() public view {
        // Generated by the SwapEncoder - test_encode_wrap_unwrapping
        bytes memory protocolData =
            loadCallDataFromFile("test_encode_wrap_unwrapping");

        bool isWrapping = nativeWrapExecutor.decodeParams(protocolData);

        assertFalse(isWrapping);
    }
}

contract WrapTest is TychoRouterTestSetup {
    function testSingleSwapWrap() public {
        // ETH -> wETH
        IWrapped WETH = IWrapped(WETH_ADDR);
        uint256 amountIn = 1 ether;

        bytes memory callData =
            loadCallDataFromFile("test_single_encoding_strategy_wrap_wrapping");

        // Fund ALICE with ETH to send with the call
        vm.deal(ALICE, amountIn);

        vm.startPrank(ALICE);

        uint256 wethBalanceBefore = WETH.balanceOf(ALICE);
        (bool success,) = tychoRouterAddr.call{value: amountIn}(callData);
        uint256 wethBalanceAfter = WETH.balanceOf(ALICE);

        // Check balances
        assertTrue(success, "Call Failed");
        assertEq(wethBalanceAfter - wethBalanceBefore, 1 ether);
        assertEq(WETH.balanceOf(tychoRouterAddr), 0);
        assertEq(tychoRouterAddr.balance, 0);
    }

    function testSingleSwapUnwrap() public {
        // wETH -> ETH
        IWrapped WETH = IWrapped(WETH_ADDR);
        uint256 amountIn = 1 ether;

        bytes memory callData = loadCallDataFromFile(
            "test_single_encoding_strategy_wrap_unwrapping"
        );

        vm.startPrank(BOB);
        deal(WETH_ADDR, BOB, amountIn);

        WETH.approve(tychoRouterAddr, amountIn);

        uint256 wethBalanceBefore = WETH.balanceOf(BOB);
        uint256 ethBalanceBefore = BOB.balance;
        (bool success,) = tychoRouterAddr.call(callData);
        uint256 wethBalanceAfter = WETH.balanceOf(BOB);
        uint256 ethBalanceAfter = BOB.balance;

        // Check balances
        assertTrue(success, "Call Failed");
        assertEq(wethBalanceBefore - wethBalanceAfter, 1 ether);
        assertEq(ethBalanceAfter - ethBalanceBefore, 1 ether);
        assertEq(WETH.balanceOf(tychoRouterAddr), 0);
        assertEq(tychoRouterAddr.balance, 0);
    }

    function testSequentialSwapWrapAdded() public {
        //  ETH -> wETH -> DAI
        IWrapped WETH = IWrapped(WETH_ADDR);
        IERC20 DAI = IERC20(DAI_ADDR);

        uint256 amountIn = 1 ether;

        bytes memory callData = loadCallDataFromFile(
            "test_sequential_encoding_strategy_wrap_added"
        );

        // Fund Bob with ETH
        vm.deal(BOB, amountIn);
        vm.startPrank(BOB);

        WETH.approve(tychoRouterAddr, amountIn);

        uint256 ethBalanceBefore = BOB.balance;
        uint256 daiBalanceBefore = DAI.balanceOf(BOB);
        (bool success,) = tychoRouterAddr.call{value: amountIn}(callData);
        uint256 ethBalanceAfter = BOB.balance;
        uint256 daiBalanceAfter = DAI.balanceOf(BOB);

        // Check balances
        assertTrue(success, "Call Failed");
        assertEq(ethBalanceBefore - ethBalanceAfter, 1 ether);
        assertEq(
            daiBalanceAfter - daiBalanceBefore, 2_018_817_438_608_734_439_722
        );
        assertEq(WETH.balanceOf(tychoRouterAddr), 0);
        assertEq(tychoRouterAddr.balance, 0);
    }
}
