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

import {Test} from "forge-std/Test.sol";

import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {OfficialStableMirrorToken} from "../../contracts/tokens/OfficialStableMirrorToken.sol";
import {CompliantUSDCTokenV2} from "../../contracts/tokens/CompliantUSDCTokenV2.sol";
import {CompliantUSDTTokenV2} from "../../contracts/tokens/CompliantUSDTTokenV2.sol";
import {StablecoinReserveVault} from "../../contracts/reserve/StablecoinReserveVault.sol";
import {CWMultiTokenBridgeL1} from "../../contracts/bridge/CWMultiTokenBridgeL1.sol";
import {CWMultiTokenBridgeL2} from "../../contracts/bridge/CWMultiTokenBridgeL2.sol";
import {CWReserveVerifier} from "../../contracts/bridge/integration/CWReserveVerifier.sol";
import {CompliantWrappedToken} from "../../contracts/tokens/CompliantWrappedToken.sol";

contract MockRouterVaultVerifierV2 is IRouterClient {
    uint256 public fee;
    bytes32 public nextMessageId = keccak256("cw-reserve-vault-v2-message");
    Client.EVM2AnyMessage internal _lastMessage;
    uint64 public lastDestinationChainSelector;

    function ccipSend(
        uint64 destinationChainSelector,
        Client.EVM2AnyMessage memory message
    ) external payable returns (bytes32 messageId) {
        lastDestinationChainSelector = destinationChainSelector;
        _lastMessage = message;
        return nextMessageId;
    }

    function getFee(uint64, Client.EVM2AnyMessage memory) external view returns (uint256) {
        return fee;
    }

    function getSupportedTokens(uint64) external pure returns (address[] memory tokens) {
        tokens = new address[](0);
    }

    function lastMessage()
        external
        view
        returns (bytes memory receiver, bytes memory data, address feeToken, bytes memory extraArgs)
    {
        return (_lastMessage.receiver, _lastMessage.data, _lastMessage.feeToken, _lastMessage.extraArgs);
    }
}

contract CWReserveVerifierVaultV2IntegrationTest is Test {
    uint64 internal constant AVALANCHE_SELECTOR = 6433500567565415381;

    address internal admin = address(0xABCD);
    address internal user = address(0xBEEF);
    address internal receiveRouterL1 = address(0x138138);
    address internal receiveRouterL2 = address(0x4311443114);
    address internal peerBridge = address(0x4311443114);

    MockRouterVaultVerifierV2 internal router;
    OfficialStableMirrorToken internal officialUsdt;
    OfficialStableMirrorToken internal officialUsdc;
    CompliantUSDTTokenV2 internal compliantUsdtV2;
    CompliantUSDCTokenV2 internal compliantUsdcV2;
    StablecoinReserveVault internal vault;
    CWMultiTokenBridgeL1 internal l1Bridge;
    CWMultiTokenBridgeL2 internal l2Bridge;
    CWReserveVerifier internal verifier;
    CompliantWrappedToken internal wrappedUsdc;

    function setUp() public {
        router = new MockRouterVaultVerifierV2();
        officialUsdt = new OfficialStableMirrorToken("Tether USD (Chain 138)", "USDT", 6, address(this), 0);
        officialUsdc = new OfficialStableMirrorToken("USD Coin (Chain 138)", "USDC", 6, address(this), 0);
        compliantUsdtV2 = new CompliantUSDTTokenV2(address(this), admin, 1_000_000 * 10 ** 6, true);
        compliantUsdcV2 = new CompliantUSDCTokenV2(address(this), admin, 1_000_000 * 10 ** 6, true);

        vault = new StablecoinReserveVault(
            admin,
            address(officialUsdt),
            address(officialUsdc),
            address(compliantUsdtV2),
            address(compliantUsdcV2)
        );

        l1Bridge = new CWMultiTokenBridgeL1(address(router), receiveRouterL1, address(0));
        l2Bridge = new CWMultiTokenBridgeL2(address(router), receiveRouterL2, address(0));
        verifier = new CWReserveVerifier(admin, address(l1Bridge), address(vault), address(0));
        wrappedUsdc = new CompliantWrappedToken("Wrapped cUSDC", "cWUSDC", 6, address(this));

        uint256 canonicalSupply = compliantUsdcV2.totalSupply();
        officialUsdc.mint(admin, canonicalSupply);

        vm.prank(admin);
        officialUsdc.approve(address(vault), canonicalSupply);

        vm.prank(admin);
        vault.seedUSDCReserve(canonicalSupply);

        vm.startPrank(admin);
        compliantUsdtV2.grantRole(compliantUsdtV2.MINTER_ROLE(), address(vault));
        compliantUsdtV2.grantRole(compliantUsdtV2.PAUSER_ROLE(), address(vault));
        compliantUsdtV2.transferOwnership(address(vault));
        compliantUsdcV2.grantRole(compliantUsdcV2.MINTER_ROLE(), address(vault));
        compliantUsdcV2.grantRole(compliantUsdcV2.PAUSER_ROLE(), address(vault));
        compliantUsdcV2.transferOwnership(address(vault));
        vm.stopPrank();

        vm.prank(address(this));
        compliantUsdcV2.transfer(user, 250e6);

        l1Bridge.configureSupportedCanonicalToken(address(compliantUsdcV2), true);
        l1Bridge.configureDestination(address(compliantUsdcV2), AVALANCHE_SELECTOR, address(l2Bridge), true);
        l1Bridge.setReserveVerifier(address(verifier));
        l2Bridge.configureDestination(138, address(l1Bridge), true);
        l2Bridge.configureTokenPair(address(compliantUsdcV2), address(wrappedUsdc));
        wrappedUsdc.grantRole(wrappedUsdc.MINTER_ROLE(), address(l2Bridge));
        wrappedUsdc.grantRole(wrappedUsdc.BURNER_ROLE(), address(l2Bridge));

        vm.prank(admin);
        verifier.configureToken(
            address(compliantUsdcV2),
            address(0),
            true,
            false,
            true
        );
    }

    function testVerifierAllowsLockForV2CanonicalTokenBackedByVault() public {
        uint256 amount = 25e6;

        vm.startPrank(user);
        compliantUsdcV2.approve(address(l1Bridge), amount);
        l1Bridge.lockAndSend(address(compliantUsdcV2), AVALANCHE_SELECTOR, user, amount);
        vm.stopPrank();

        assertEq(l1Bridge.lockedBalance(address(compliantUsdcV2)), amount);
        assertEq(l1Bridge.totalOutstanding(address(compliantUsdcV2)), amount);
        assertEq(l1Bridge.outstandingMinted(address(compliantUsdcV2), AVALANCHE_SELECTOR), amount);
    }

    function testV2CanonicalTokenCompletesFullTransportRoundTrip() public {
        uint256 amount = 25e6;

        vm.startPrank(user);
        compliantUsdcV2.approve(address(l1Bridge), amount);
        bytes32 outboundMessageId =
            l1Bridge.lockAndSend(address(compliantUsdcV2), AVALANCHE_SELECTOR, user, amount);
        vm.stopPrank();

        (bytes memory receiverData, bytes memory outboundData,,) = router.lastMessage();
        assertEq(abi.decode(receiverData, (address)), address(l2Bridge));

        vm.prank(receiveRouterL2);
        l2Bridge.ccipReceive(_message(outboundMessageId, 138, address(l1Bridge), outboundData));

        assertEq(wrappedUsdc.balanceOf(user), amount);
        assertEq(wrappedUsdc.totalSupply(), amount);
        assertEq(l2Bridge.mintedTotal(address(wrappedUsdc)), amount);
        assertEq(l2Bridge.burnedTotal(address(wrappedUsdc)), 0);

        vm.prank(user);
        bytes32 returnMessageId = l2Bridge.burnAndSend(address(wrappedUsdc), 138, user, amount);

        assertEq(wrappedUsdc.balanceOf(user), 0);
        assertEq(wrappedUsdc.totalSupply(), 0);
        assertEq(l2Bridge.mintedTotal(address(wrappedUsdc)), amount);
        assertEq(l2Bridge.burnedTotal(address(wrappedUsdc)), amount);

        (, bytes memory returnData,,) = router.lastMessage();

        vm.prank(receiveRouterL1);
        l1Bridge.ccipReceive(_message(returnMessageId, AVALANCHE_SELECTOR, address(l2Bridge), returnData));

        assertEq(compliantUsdcV2.balanceOf(user), 250e6);
        assertEq(compliantUsdcV2.balanceOf(address(l1Bridge)), 0);
        assertEq(l1Bridge.lockedBalance(address(compliantUsdcV2)), 0);
        assertEq(l1Bridge.totalOutstanding(address(compliantUsdcV2)), 0);
        assertEq(l1Bridge.outstandingMinted(address(compliantUsdcV2), AVALANCHE_SELECTOR), 0);
    }

    function _message(
        bytes32 messageId,
        uint64 sourceChainSelector,
        address sender,
        bytes memory data
    ) internal pure returns (Client.Any2EVMMessage memory message) {
        message = Client.Any2EVMMessage({
            messageId: messageId,
            sourceChainSelector: sourceChainSelector,
            sender: abi.encode(sender),
            data: data,
            destTokenAmounts: new Client.EVMTokenAmount[](0)
        });
    }
}
