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

import {Test} from "forge-std/Test.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {IRouterClient as LegacyRouterClient} from "../../contracts/ccip/IRouterClient.sol";
import {CWMultiTokenBridgeL1} from "../../contracts/bridge/CWMultiTokenBridgeL1.sol";
import {CWMultiTokenBridgeL2} from "../../contracts/bridge/CWMultiTokenBridgeL2.sol";
import {CompliantWrappedToken} from "../../contracts/tokens/CompliantWrappedToken.sol";
import {CCIPRelayRouter} from "../../contracts/relay/CCIPRelayRouter.sol";

contract MockCanonicalToken is ERC20 {
    constructor() ERC20("Mock Canonical", "MCAN") {}

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

contract MockLegacyRouter is LegacyRouterClient {
    uint256 public fee;
    bytes32 public nextMessageId = keccak256("message");
    LegacyRouterClient.EVM2AnyMessage internal _lastMessage;
    uint64 public lastDestinationChainSelector;

    function setFee(uint256 newFee) external {
        fee = newFee;
    }

    function ccipSend(
        uint64 destinationChainSelector,
        LegacyRouterClient.EVM2AnyMessage memory message
    ) external payable returns (bytes32 messageId, uint256 fees) {
        if (message.feeToken == address(0)) {
            require(msg.value >= fee, "native fee");
        }

        lastDestinationChainSelector = destinationChainSelector;
        _lastMessage = message;
        return (nextMessageId, fee);
    }

    function getFee(uint64, LegacyRouterClient.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 MockChainlinkRouter is IRouterClient {
    uint256 public fee;
    bytes32 public nextMessageId = keccak256("message");
    Client.EVM2AnyMessage internal _lastMessage;
    uint64 public lastDestinationChainSelector;

    function setFee(uint256 newFee) external {
        fee = newFee;
    }

    function ccipSend(
        uint64 destinationChainSelector,
        Client.EVM2AnyMessage memory message
    ) external payable returns (bytes32 messageId) {
        if (message.feeToken == address(0)) {
            require(msg.value >= fee, "native fee");
        }

        lastDestinationChainSelector = destinationChainSelector;
        _lastMessage = message;
        messageId = nextMessageId;
        return messageId;
    }

    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 isChainSupported(uint64) external pure returns (bool) {
        return true;
    }

    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 MockReceiveBridge {
    bytes public lastData;

    function ccipReceive(LegacyRouterClient.Any2EVMMessage calldata message) external {
        lastData = message.data;
    }
}

contract CWMultiTokenBridgeTest is Test {
    uint64 internal constant CHAIN138_SELECTOR = 138;
    uint64 internal constant AVALANCHE_SELECTOR = 6433500567565415381;

    address internal user = address(0xBEEF);

    MockLegacyRouter internal router138;
    MockChainlinkRouter internal routerAvax;
    MockCanonicalToken internal canonical;
    CompliantWrappedToken internal wrapped;
    CWMultiTokenBridgeL1 internal l1Bridge;
    CWMultiTokenBridgeL2 internal l2Bridge;

    function setUp() public {
        router138 = new MockLegacyRouter();
        routerAvax = new MockChainlinkRouter();
        canonical = new MockCanonicalToken();
        wrapped = new CompliantWrappedToken("Wrapped Canonical", "cWMOCK", 6, address(this));

        l1Bridge = new CWMultiTokenBridgeL1(address(router138), address(0x138138), address(0));
        l2Bridge = new CWMultiTokenBridgeL2(address(routerAvax), address(0x4311443114), address(0));

        l1Bridge.configureSupportedCanonicalToken(address(canonical), true);
        l1Bridge.configureDestination(address(canonical), AVALANCHE_SELECTOR, address(l2Bridge), true);
        l2Bridge.configureDestination(CHAIN138_SELECTOR, address(l1Bridge), true);
        l2Bridge.configureTokenPair(address(canonical), address(wrapped));

        wrapped.grantRole(wrapped.MINTER_ROLE(), address(l2Bridge));
        wrapped.grantRole(wrapped.BURNER_ROLE(), address(l2Bridge));

        canonical.mint(user, 1_000_000e18);
    }

    function testRoundTripLockMintBurnRelease() public {
        uint256 amount = 125e6;

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

        assertEq(canonical.balanceOf(address(l1Bridge)), amount);
        assertEq(canonical.balanceOf(user), 1_000_000e18 - amount);
        assertEq(l1Bridge.lockedBalance(address(canonical)), amount);
        assertEq(l1Bridge.totalOutstanding(address(canonical)), amount);
        assertEq(l1Bridge.outstandingMinted(address(canonical), AVALANCHE_SELECTOR), amount);

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

        vm.prank(address(0x4311443114));
        l2Bridge.ccipReceive(_l2Message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData));
        assertEq(wrapped.balanceOf(user), amount);
        assertEq(wrapped.totalSupply(), amount);
        assertEq(l2Bridge.mintedTotal(address(wrapped)), amount);
        assertEq(l2Bridge.burnedTotal(address(wrapped)), 0);

        vm.prank(user);
        bytes32 returnMessageId = l2Bridge.burnAndSend(address(wrapped), CHAIN138_SELECTOR, user, amount);
        assertEq(wrapped.balanceOf(user), 0);
        assertEq(wrapped.totalSupply(), 0);
        assertEq(l2Bridge.mintedTotal(address(wrapped)), amount);
        assertEq(l2Bridge.burnedTotal(address(wrapped)), amount);

        (, bytes memory returnData, address feeToken, bytes memory extraArgs) = routerAvax.lastMessage();
        assertTrue(extraArgs.length > 0, "missing CCIP extraArgs");
        assertEq(feeToken, address(0));
        vm.prank(address(0x138138));
        l1Bridge.ccipReceive(_l1Message(returnMessageId, AVALANCHE_SELECTOR, address(l2Bridge), returnData));

        assertEq(canonical.balanceOf(user), 1_000_000e18);
        assertEq(canonical.balanceOf(address(l1Bridge)), 0);
        assertEq(l1Bridge.lockedBalance(address(canonical)), 0);
        assertEq(l1Bridge.totalOutstanding(address(canonical)), 0);
        assertEq(l1Bridge.outstandingMinted(address(canonical), AVALANCHE_SELECTOR), 0);
    }

    function testLockAndSendRespectsMaxOutstanding() public {
        uint256 amount = 125e6;

        l1Bridge.setMaxOutstanding(address(canonical), AVALANCHE_SELECTOR, 100e6);

        vm.startPrank(user);
        canonical.approve(address(l1Bridge), amount);
        vm.expectRevert(bytes("CWMultiTokenBridgeL1: exceeds escrow capacity"));
        l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount);
        vm.stopPrank();
    }

    function testSupportedCanonicalTokenCannotBeDisabledWhileFundsOutstanding() public {
        uint256 amount = 50e6;

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

        vm.expectRevert(bytes("CWMultiTokenBridgeL1: token still locked"));
        l1Bridge.configureSupportedCanonicalToken(address(canonical), false);
    }

    function testWithdrawTokenCannotDrainLockedCanonicalEscrow() public {
        uint256 amount = 50e6;

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

        vm.expectRevert(bytes("CWMultiTokenBridgeL1: amount locked"));
        l1Bridge.withdrawToken(address(canonical), user, 1);

        canonical.mint(address(l1Bridge), 10e6);
        l1Bridge.withdrawToken(address(canonical), user, 10e6);
        assertEq(canonical.balanceOf(user), 1_000_000e18 - amount + 10e6);
        assertEq(canonical.balanceOf(address(l1Bridge)), amount);
    }

    function testReleaseRequiresOutstandingBalance() public {
        bytes memory returnData = abi.encode(address(canonical), user, uint256(1));

        vm.expectRevert(bytes("CWMultiTokenBridgeL1: outstanding underflow"));
        vm.prank(address(0x138138));
        l1Bridge.ccipReceive(_l1Message(keccak256("no-lock"), AVALANCHE_SELECTOR, address(l2Bridge), returnData));
    }

    function testL2PauseBlocksMintAndBurn() public {
        uint256 amount = 20e6;

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

        (, bytes memory outboundData,,) = router138.lastMessage();

        l2Bridge.setTokenPaused(address(wrapped), true);
        vm.expectRevert(bytes("CWMultiTokenBridgeL2: token paused"));
        vm.prank(address(0x4311443114));
        l2Bridge.ccipReceive(_l2Message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData));

        l2Bridge.setTokenPaused(address(wrapped), false);
        vm.prank(address(0x4311443114));
        l2Bridge.ccipReceive(_l2Message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData));

        l2Bridge.setTokenPaused(address(wrapped), true);
        vm.expectRevert(bytes("CWMultiTokenBridgeL2: token paused"));
        vm.prank(user);
        l2Bridge.burnAndSend(address(wrapped), CHAIN138_SELECTOR, user, amount);
    }

    function testL2CirculatingSupplyTracksMintMinusBurn() public {
        uint256 amount = 35e6;

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

        (, bytes memory outboundData,,) = router138.lastMessage();

        vm.prank(address(0x4311443114));
        l2Bridge.ccipReceive(_l2Message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData));
        assertEq(l2Bridge.circulatingSupply(address(wrapped)), amount);

        vm.prank(user);
        l2Bridge.burnAndSend(address(wrapped), CHAIN138_SELECTOR, user, 10e6);
        assertEq(l2Bridge.circulatingSupply(address(wrapped)), amount - 10e6);
    }

    function testL2RejectsReplayOfInboundMintMessage() public {
        uint256 amount = 15e6;

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

        (, bytes memory outboundData,,) = router138.lastMessage();

        vm.prank(address(0x4311443114));
        l2Bridge.ccipReceive(_l2Message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData));

        vm.expectRevert(bytes("CWMultiTokenBridgeL2: replayed"));
        vm.prank(address(0x4311443114));
        l2Bridge.ccipReceive(_l2Message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData));
    }

    function testFrozenL2ConfigCannotBeRewritten() public {
        CompliantWrappedToken wrapped2 = new CompliantWrappedToken("Wrapped Canonical 2", "cWMOCK2", 6, address(this));

        l2Bridge.freezeTokenPair(address(canonical));
        vm.expectRevert(bytes("CWMultiTokenBridgeL2: token pair frozen"));
        l2Bridge.configureTokenPair(address(canonical), address(wrapped2));

        l2Bridge.freezeDestination(CHAIN138_SELECTOR);
        vm.expectRevert(bytes("CWMultiTokenBridgeL2: destination frozen"));
        l2Bridge.configureDestination(CHAIN138_SELECTOR, address(0x1234), true);
    }

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

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

        bytes32 returnMessageId = keccak256("return-replay");
        bytes memory returnData = abi.encode(address(canonical), user, amount);

        vm.prank(address(0x138138));
        l1Bridge.ccipReceive(_l1Message(returnMessageId, AVALANCHE_SELECTOR, address(l2Bridge), returnData));

        vm.expectRevert(bytes("CWMultiTokenBridgeL1: replayed"));
        vm.prank(address(0x138138));
        l1Bridge.ccipReceive(_l1Message(returnMessageId, AVALANCHE_SELECTOR, address(l2Bridge), returnData));
    }

    function testRelayRouterAcceptsThreeFieldPayload() public {
        CCIPRelayRouter relayRouter = new CCIPRelayRouter();
        MockReceiveBridge receiveBridge = new MockReceiveBridge();

        relayRouter.authorizeBridge(address(receiveBridge));
        relayRouter.grantRelayerRole(address(this));

        LegacyRouterClient.TokenAmount[] memory noTokens = new LegacyRouterClient.TokenAmount[](0);
        LegacyRouterClient.Any2EVMMessage memory message = LegacyRouterClient.Any2EVMMessage({
            messageId: keccak256("three-field"),
            sourceChainSelector: CHAIN138_SELECTOR,
            sender: abi.encode(address(0xCAFE)),
            data: abi.encode(address(canonical), user, uint256(42)),
            tokenAmounts: noTokens
        });

        relayRouter.relayMessage(address(receiveBridge), message);
        assertEq(keccak256(receiveBridge.lastData()), keccak256(message.data));
    }

    function testRelayRouterDeliversToCWMultiTokenBridgeL2() public {
        CCIPRelayRouter relayRouter = new CCIPRelayRouter();
        relayRouter.authorizeBridge(address(l2Bridge));
        relayRouter.grantRelayerRole(address(this));

        l2Bridge.setReceiveRouter(address(relayRouter));

        uint256 amount = 50e6;
        bytes memory outboundData = abi.encode(address(canonical), user, amount);
        bytes32 messageId = keccak256("relay-router-l2");

        LegacyRouterClient.TokenAmount[] memory noTokens = new LegacyRouterClient.TokenAmount[](0);
        LegacyRouterClient.Any2EVMMessage memory message = LegacyRouterClient.Any2EVMMessage({
            messageId: messageId,
            sourceChainSelector: CHAIN138_SELECTOR,
            sender: abi.encode(address(l1Bridge)),
            data: outboundData,
            tokenAmounts: noTokens
        });

        relayRouter.relayMessage(address(l2Bridge), message);

        assertTrue(l2Bridge.processed(messageId));
        assertEq(wrapped.balanceOf(user), amount);
        assertEq(l2Bridge.mintedTotal(address(wrapped)), amount);
    }

    function _l1Message(
        bytes32 messageId,
        uint64 sourceChainSelector,
        address sender,
        bytes memory data
    ) internal pure returns (LegacyRouterClient.Any2EVMMessage memory message) {
        message = LegacyRouterClient.Any2EVMMessage({
            messageId: messageId,
            sourceChainSelector: sourceChainSelector,
            sender: abi.encode(sender),
            data: data,
            tokenAmounts: new LegacyRouterClient.TokenAmount[](0)
        });
    }

    function _l2Message(
        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)
        });
    }
}
