// 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 "../../contracts/ccip/IRouterClient.sol";
import {CWMultiTokenBridgeL1} from "../../contracts/bridge/CWMultiTokenBridgeL1.sol";
import {CWReserveVerifier} from "../../contracts/bridge/integration/CWReserveVerifier.sol";

contract MockCanonicalOwnableToken is ERC20 {
    address internal _owner;

    constructor(address initialOwner) ERC20("Mock Canonical Stable", "MCS") {
        _owner = initialOwner;
    }

    function owner() external view returns (address) {
        return _owner;
    }

    function setOwner(address newOwner) external {
        _owner = newOwner;
    }

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

contract MockRouterVerifier is IRouterClient {
    uint256 public fee;
    bytes32 public nextMessageId = keccak256("cw-reserve-message");
    EVM2AnyMessage internal _lastMessage;

    function ccipSend(
        uint64,
        EVM2AnyMessage memory message
    ) external payable returns (bytes32 messageId, uint256 fees) {
        fees = fee;
        _lastMessage = message;
        return (nextMessageId, fees);
    }

    function getFee(uint64, 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 MockStablecoinReserveVaultForCW {
    address public compliantUSDT;
    address public compliantUSDC;

    mapping(address => uint256) public reserveBalance;
    mapping(address => uint256) public backingRatio;

    bool public usdtAdequate = true;
    bool public usdcAdequate = true;

    constructor(address compliantUSDT_, address compliantUSDC_) {
        compliantUSDT = compliantUSDT_;
        compliantUSDC = compliantUSDC_;
    }

    function setBacking(address token, uint256 reserveBalance_, uint256 backingRatio_) external {
        reserveBalance[token] = reserveBalance_;
        backingRatio[token] = backingRatio_;
    }

    function setAdequacy(bool usdtAdequate_, bool usdcAdequate_) external {
        usdtAdequate = usdtAdequate_;
        usdcAdequate = usdcAdequate_;
    }

    function getBackingRatio(address token) external view returns (
        uint256 reserveBalance_,
        uint256 tokenSupply,
        uint256 backingRatio_
    ) {
        reserveBalance_ = reserveBalance[token];
        tokenSupply = ERC20(token).totalSupply();
        backingRatio_ = backingRatio[token];
    }

    function checkReserveAdequacy() external view returns (bool, bool) {
        return (usdtAdequate, usdcAdequate);
    }
}

contract MockReserveSystemForCW {
    mapping(address => uint256) internal reserveBalances;

    function setReserveBalance(address asset, uint256 amount) external {
        reserveBalances[asset] = amount;
    }

    function getReserveBalance(address asset) external view returns (uint256) {
        return reserveBalances[asset];
    }
}

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

    address internal user = address(0xBEEF);
    address internal receiveRouter = address(0x138138);
    address internal peerBridge = address(0x4311443114);
    address internal officialReserveAsset = address(0xA0b8);

    MockRouterVerifier internal router;
    MockCanonicalOwnableToken internal canonical;
    CWMultiTokenBridgeL1 internal l1Bridge;
    MockStablecoinReserveVaultForCW internal reserveVault;
    MockReserveSystemForCW internal reserveSystem;
    CWReserveVerifier internal verifier;

    function setUp() public {
        router = new MockRouterVerifier();
        canonical = new MockCanonicalOwnableToken(address(this));
        l1Bridge = new CWMultiTokenBridgeL1(address(router), receiveRouter, address(0));
        reserveVault = new MockStablecoinReserveVaultForCW(address(0), address(canonical));
        reserveSystem = new MockReserveSystemForCW();
        verifier = new CWReserveVerifier(address(this), address(l1Bridge), address(reserveVault), address(reserveSystem));

        canonical.setOwner(address(reserveVault));

        l1Bridge.configureSupportedCanonicalToken(address(canonical), true);
        l1Bridge.configureDestination(address(canonical), AVALANCHE_SELECTOR, peerBridge, true);
        l1Bridge.setReserveVerifier(address(verifier));

        verifier.configureToken(
            address(canonical),
            officialReserveAsset,
            true,
            true,
            true
        );

        canonical.mint(user, 1_000_000e6);

        uint256 canonicalSupply = canonical.totalSupply();
        reserveVault.setBacking(address(canonical), canonicalSupply, 10000);
        reserveVault.setAdequacy(true, true);
        reserveSystem.setReserveBalance(officialReserveAsset, canonicalSupply);
    }

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

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

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

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

        reserveVault.setBacking(address(canonical), canonical.totalSupply() - 1, 9999);
        reserveVault.setAdequacy(true, false);

        vm.startPrank(user);
        canonical.approve(address(l1Bridge), amount);
        vm.expectRevert(CWReserveVerifier.VaultBackingInsufficient.selector);
        l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount);
        vm.stopPrank();
    }

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

        reserveSystem.setReserveBalance(officialReserveAsset, canonical.totalSupply() - 1);

        vm.startPrank(user);
        canonical.approve(address(l1Bridge), amount);
        vm.expectRevert(CWReserveVerifier.ReserveSystemBackingInsufficient.selector);
        l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount);
        vm.stopPrank();
    }

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

        reserveSystem.setReserveBalance(officialReserveAsset, canonical.totalSupply() - 1);

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

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

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

        canonical.setOwner(address(0xCAFE));

        vm.startPrank(user);
        canonical.approve(address(l1Bridge), amount);
        vm.expectRevert(CWReserveVerifier.TokenOwnerMismatch.selector);
        l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount);
        vm.stopPrank();
    }

    function testReleaseRemainsAvailableAfterVerifierWouldLaterFail() public {
        uint256 amount = 60e6;

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

        reserveVault.setBacking(address(canonical), canonical.totalSupply() - 10, 9990);
        reserveVault.setAdequacy(true, false);
        reserveSystem.setReserveBalance(officialReserveAsset, canonical.totalSupply() - 10);

        bytes memory returnData = abi.encode(address(canonical), user, amount);
        vm.prank(receiveRouter);
        l1Bridge.ccipReceive(_message(keccak256("return"), AVALANCHE_SELECTOR, peerBridge, returnData));

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

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