// 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 {CWAssetReserveVerifier} from "../../contracts/bridge/integration/CWAssetReserveVerifier.sol";

contract MockCanonicalAssetToken is ERC20 {
    address internal _owner;

    constructor(string memory name_, string memory symbol_, address initialOwner) ERC20(name_, symbol_) {
        _owner = initialOwner;
    }

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

    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 MockReserveAsset is ERC20 {
    constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {}

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

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

    function burn(address from, uint256 amount) external {
        _burn(from, amount);
    }
}

contract MockRouterAssetVerifier is IRouterClient {
    function ccipSend(
        uint64,
        EVM2AnyMessage memory
    ) external payable returns (bytes32 messageId, uint256 fees) {
        return (bytes32(0), fees);
    }

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

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

contract MockReserveSystemAssetVerifier {
    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 CWAssetReserveVerifierTest is Test {
    uint64 internal constant MAINNET_SELECTOR = 5009297550715157269;
    uint64 internal constant BSC_SELECTOR = 11344663589394136015;

    address internal user = address(0xBEEF);
    address internal receiveRouter = address(0x138138);
    address internal strictPeerBridge = address(0x010101);
    address internal hybridPeerBridge = address(0x020202);
    address internal assetVault = address(0xCAFE);

    MockRouterAssetVerifier internal router;
    MockCanonicalAssetToken internal strictCanonical;
    MockCanonicalAssetToken internal hybridCanonical;
    MockReserveAsset internal strictReserveAsset;
    MockReserveAsset internal hybridReserveAsset;
    MockReserveSystemAssetVerifier internal reserveSystem;
    CWMultiTokenBridgeL1 internal l1Bridge;
    CWAssetReserveVerifier internal verifier;

    function setUp() public {
        router = new MockRouterAssetVerifier();
        strictCanonical = new MockCanonicalAssetToken("Ethereum Mainnet Gas (Compliant)", "cETH", assetVault);
        hybridCanonical = new MockCanonicalAssetToken("BNB Gas (Compliant)", "cBNB", address(this));
        strictReserveAsset = new MockReserveAsset("Wrapped Ether", "WETH");
        hybridReserveAsset = new MockReserveAsset("Wrapped BNB", "WBNB");
        reserveSystem = new MockReserveSystemAssetVerifier();
        l1Bridge = new CWMultiTokenBridgeL1(address(router), receiveRouter, address(0));
        verifier = new CWAssetReserveVerifier(address(this), address(l1Bridge), assetVault, address(reserveSystem));

        l1Bridge.configureSupportedCanonicalToken(address(strictCanonical), true);
        l1Bridge.configureDestination(address(strictCanonical), MAINNET_SELECTOR, strictPeerBridge, true);
        l1Bridge.configureSupportedCanonicalToken(address(hybridCanonical), true);
        l1Bridge.configureDestination(address(hybridCanonical), BSC_SELECTOR, hybridPeerBridge, true);
        l1Bridge.setReserveVerifier(address(verifier));

        verifier.configureToken(address(strictCanonical), address(strictReserveAsset), true, false, true);
        verifier.configureToken(address(hybridCanonical), address(hybridReserveAsset), false, true, false);

        strictCanonical.mint(user, 100e18);
        hybridCanonical.mint(user, 75e18);
        strictReserveAsset.mint(assetVault, strictCanonical.totalSupply());
        reserveSystem.setReserveBalance(address(hybridReserveAsset), hybridCanonical.totalSupply());
    }

    function testStrictVerifierAllowsLockWhenVaultBalanceMatchesSupply() public {
        uint256 amount = 10e18;

        vm.startPrank(user);
        strictCanonical.approve(address(l1Bridge), amount);
        l1Bridge.lockAndSend(address(strictCanonical), MAINNET_SELECTOR, user, amount);
        vm.stopPrank();

        assertEq(l1Bridge.lockedBalance(address(strictCanonical)), amount);
        assertEq(l1Bridge.totalOutstanding(address(strictCanonical)), amount);
    }

    function testStrictVerifierBlocksLockWhenVaultBalanceFallsShort() public {
        uint256 amount = 10e18;
        strictReserveAsset.burn(assetVault, 1);

        vm.startPrank(user);
        strictCanonical.approve(address(l1Bridge), amount);
        vm.expectRevert(CWAssetReserveVerifier.VaultBackingInsufficient.selector);
        l1Bridge.lockAndSend(address(strictCanonical), MAINNET_SELECTOR, user, amount);
        vm.stopPrank();
    }

    function testStrictVerifierBlocksLockWhenTokenOwnerDoesNotMatchVault() public {
        uint256 amount = 10e18;
        strictCanonical.setOwner(address(0xDEAD));

        vm.startPrank(user);
        strictCanonical.approve(address(l1Bridge), amount);
        vm.expectRevert(CWAssetReserveVerifier.TokenOwnerMismatch.selector);
        l1Bridge.lockAndSend(address(strictCanonical), MAINNET_SELECTOR, user, amount);
        vm.stopPrank();
    }

    function testHybridVerifierAllowsLockWhenReserveSystemBalanceMatchesSupply() public {
        uint256 amount = 10e18;

        vm.startPrank(user);
        hybridCanonical.approve(address(l1Bridge), amount);
        l1Bridge.lockAndSend(address(hybridCanonical), BSC_SELECTOR, user, amount);
        vm.stopPrank();

        assertEq(l1Bridge.lockedBalance(address(hybridCanonical)), amount);
        assertEq(l1Bridge.totalOutstanding(address(hybridCanonical)), amount);
    }

    function testHybridVerifierBlocksLockWhenReserveSystemFallsShort() public {
        uint256 amount = 10e18;
        reserveSystem.setReserveBalance(address(hybridReserveAsset), hybridCanonical.totalSupply() - 1);

        vm.startPrank(user);
        hybridCanonical.approve(address(l1Bridge), amount);
        vm.expectRevert(CWAssetReserveVerifier.ReserveSystemBackingInsufficient.selector);
        l1Bridge.lockAndSend(address(hybridCanonical), BSC_SELECTOR, user, amount);
        vm.stopPrank();
    }
}
