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

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";

contract MockCanonicalBTC is ERC20 {
    constructor() ERC20("Bitcoin (Compliant)", "cBTC") {}

    function decimals() public pure override returns (uint8) {
        return 8;
    }

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

contract MockLegacyRouterBTC is LegacyRouterClient {
    bytes32 public nextMessageId = keccak256("btc-message");
    LegacyRouterClient.EVM2AnyMessage internal _lastMessage;

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

    function getFee(uint64, LegacyRouterClient.EVM2AnyMessage memory) external pure returns (uint256) {
        return 0;
    }

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

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

contract MockChainlinkRouterBTC is IRouterClient {
    bytes32 public nextMessageId = keccak256("btc-message");
    Client.EVM2AnyMessage internal _lastMessage;

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

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

    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) {
        return (_lastMessage.receiver, _lastMessage.data);
    }
}

contract CWMultiTokenBridgeBTCTest is Test {
    uint64 internal constant CHAIN138_SELECTOR = 138;
    uint64 internal constant ETHEREUM_SELECTOR = 5009297550715157269;

    address internal user = address(0xBEEF);

    MockLegacyRouterBTC internal router138;
    MockChainlinkRouterBTC internal routerEth;
    MockCanonicalBTC internal canonical;
    CompliantWrappedToken internal wrapped;
    CWMultiTokenBridgeL1 internal l1Bridge;
    CWMultiTokenBridgeL2 internal l2Bridge;

    function setUp() public {
        router138 = new MockLegacyRouterBTC();
        routerEth = new MockChainlinkRouterBTC();
        canonical = new MockCanonicalBTC();
        wrapped = new CompliantWrappedToken("Wrapped cBTC", "cWBTC", 8, address(this));

        l1Bridge = new CWMultiTokenBridgeL1(address(router138), address(0x138138), address(0));
        l2Bridge = new CWMultiTokenBridgeL2(address(routerEth), address(0x010101), address(0));

        l1Bridge.configureSupportedCanonicalToken(address(canonical), true);
        l1Bridge.configureDestination(address(canonical), ETHEREUM_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, 2_100_000_000_000_000);
    }

    function testRoundTripPreservesSatoshiPrecision() public {
        uint256 amount = 125_000_000;

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

        (, bytes memory outboundData) = router138.lastMessage();
        vm.prank(address(0x010101));
        l2Bridge.ccipReceive(_l2Message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData));

        assertEq(wrapped.balanceOf(user), amount);
        assertEq(wrapped.totalSupply(), amount);

        vm.prank(user);
        bytes32 returnMessageId = l2Bridge.burnAndSend(address(wrapped), CHAIN138_SELECTOR, user, amount);

        (, bytes memory returnData) = routerEth.lastMessage();
        vm.prank(address(0x138138));
        l1Bridge.ccipReceive(_l1Message(returnMessageId, ETHEREUM_SELECTOR, address(l2Bridge), returnData));

        assertEq(canonical.balanceOf(user), 2_100_000_000_000_000);
        assertEq(wrapped.totalSupply(), 0);
        assertEq(l1Bridge.totalOutstanding(address(canonical)), 0);
    }

    function testPerDestinationOutstandingCapStillAppliesForBTCRoute() public {
        l1Bridge.setMaxOutstanding(address(canonical), ETHEREUM_SELECTOR, 100_000_000);

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

    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)
        });
    }
}
