pragma solidity 0.8.20;

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

import {Gatekeeper} from "../../src/Gatekeeper.sol";
import {FullMath} from "../../src/libraries/FullMath.sol";
import {RequestPacking} from "../../src/libraries/Packing.sol";
import {IStorageHistory} from "../../src/interfaces/IStorageHistory.sol";

contract MockStaking is Test {
    Gatekeeper public gatekeeper;

    mapping(address => uint256) private _materializedAmounts;

    constructor(uint256 existential) {
        gatekeeper = new Gatekeeper(existential, address(0));
    }

    function runGhost(bytes32 receiver, uint256 amount) external {
        gatekeeper.ghost(receiver, amount);
    }

    function runMaterialize(
        uint256 exodusSession,
        uint256 amount,
        uint256 packed,
        address caller
    ) external payable {
        if (msg.value > 0) {
            vm.deal(address(gatekeeper), address(gatekeeper).balance + msg.value);
        }

        vm.prank(address(gatekeeper), caller);
        gatekeeper.materialize{value: msg.value}(exodusSession, amount, packed);
    }

    function materialize(address receiver, uint256 amount) external {
        _materializedAmounts[receiver] += amount;
    }

    function materializedAmount(address who) external view returns (uint256) {
        return _materializedAmounts[who];
    }

    function totalReserves() external pure returns (uint256) {
        return type(uint256).max;
    }

    function baseSupply() external pure returns (uint256) {
        return type(uint256).max;
    }
}

contract GatekeeperTest is Test {
    using RequestPacking for RequestPacking.RequestPayload;

    address constant ALICE       = 0x0000000000000000000000000000000000000001;
    address constant BOB         = 0x0000000000000000000000000000000000000002;
    uint256 constant EXISTENTIAL = 1337;
    uint256 constant INIT_AMOUNT = 69 * 1e18;

    Gatekeeper gatekeeper;
    MockStaking staking;

    event Ghosted(bytes32 indexed receiver, uint256 indexed amount);

    function setUp() public {
        vm.prank(ALICE, ALICE);
        staking = new MockStaking(EXISTENTIAL);
        gatekeeper = staking.gatekeeper();
    }

    function test_correctInitialization() public view {
        assertEq(gatekeeper.staking(), address(staking));
        assertEq(gatekeeper.ghostedSupply(), 0);
    }

    function test_ghostTokensWork(uint256 ghostAmount) public {
        vm.assume(ghostAmount >= EXISTENTIAL && ghostAmount < type(uint96).max);
        bytes32 receiver = bytes32(abi.encodePacked(ALICE));
        uint256 ghostedSupply = gatekeeper.ghostedSupply();

        vm.prank(ALICE);
        staking.runGhost(receiver, ghostAmount);
        assertEq(gatekeeper.ghostedSupply(), ghostedSupply + ghostAmount);
    }

    function test_couldNotGhostTokensFromArbitraryAddress(address someone) public {
        vm.assume(someone != ALICE);
        bytes32 receiver = bytes32(abi.encodePacked(ALICE));

        vm.expectRevert();
        vm.prank(someone);
        staking.runGhost(receiver, 69);
        assertEq(gatekeeper.ghostedSupply(), 0);
    }

    function test_ghostTokensEmitsEvent(uint256 ghostAmount) public {
        vm.assume(ghostAmount >= EXISTENTIAL);
        bytes32 receiver = bytes32(abi.encodePacked(ALICE));

        vm.expectEmit(true, true, true, false, address(gatekeeper));
        emit Ghosted(receiver, ghostAmount);
        vm.prank(ALICE);
        staking.runGhost(receiver, ghostAmount);
    }

    function test_materializeWork(uint256 exodusSession, uint256 bobAmount, uint32 bobCommission) public {
        vm.assume(bobAmount > 1337 && bobAmount < 1_000 ether);
        vm.assume(bobCommission > 0 && bobCommission <= type(uint32).max);

        address storageHistory = gatekeeper.storageHistory();
        uint256 commissionAmount = FullMath.mulDiv(bobAmount, uint256(bobCommission), type(uint32).max);
        uint256 bobPacked = RequestPacking.pack(uint32(bobCommission), uint64(block.chainid), BOB);

        vm.prank(ALICE);
        staking.runGhost(bytes32(abi.encodePacked(ALICE)), bobAmount);

        uint256 nativeNeeded = FullMath.mulDiv(commissionAmount, staking.totalReserves(), staking.baseSupply());

        vm.deal(ALICE, nativeNeeded + 1 ether);
        assertEq(BOB.balance, 0 ether);
        uint256 aliceStartingNative = ALICE.balance;

        uint256 previousAliceAmount = staking.materializedAmount(ALICE);
        uint256 previousBobAmount = staking.materializedAmount(BOB);

        vm.prank(ALICE);
        staking.runMaterialize{value: nativeNeeded}(exodusSession, bobAmount, bobPacked, ALICE);

        assertApproxEqAbs(previousAliceAmount + commissionAmount, staking.materializedAmount(ALICE), 1000);
        assertApproxEqAbs(previousBobAmount + (bobAmount - commissionAmount), staking.materializedAmount(BOB), 1000);

        assertEq(BOB.balance, nativeNeeded);
        assertEq(ALICE.balance, aliceStartingNative - nativeNeeded);

        assert(IStorageHistory(storageHistory).isTransactionExecuted(exodusSession));
    }

    function test_rotateWork(bytes32 aggregatedPublicKey) public {
        vm.warp(block.timestamp + 1);
        vm.prank(address(gatekeeper));
        gatekeeper.rotate(block.timestamp, aggregatedPublicKey, 0);
    }

    function test_couldNotBridgeBelowExistential(uint256 amount) public {
        vm.assume(amount < EXISTENTIAL);
        bytes32 receiver = bytes32(abi.encodePacked(ALICE));

        vm.expectRevert();
        vm.prank(ALICE);
        staking.runGhost(receiver, amount);

        assertEq(gatekeeper.ghostedSupply(), 0);
    }

    function test_signatureVerificationsWorks() public {
        assertEq(gatekeeper.deployer(), ALICE);

        vm.prank(ALICE);
        staking.runGhost(bytes32(abi.encodePacked(ALICE)), type(uint104).max);

        vm.prank(ALICE);
        gatekeeper.updatePublicKeyMetadata(0, 0x875cdcba4ae5494518fa2602791e667ca0998402b6a69e5b7cb4c72dba4e4669, 0);

        (bytes32 prevPublicKey, uint8 prevParity,) = gatekeeper.latestPublicKeyInfo();

        // genesis validators 4 validators, DKG #0
        // exodus session #0
        gatekeeper.verify(
            hex"49f03a670000000000000000000000000000000000000000000000000000000000000000875cdcba4ae5494518fa2602791e667ca0998402b6a69e5b7cb4c72dba4e46690000000000000000000000000000000000000000000000000000000000000000",
            hex"044d9fa18df9da04381ed256981570a6ef6a0893496ded966fc994662df931ed423b29e2737394b31d8498594caad7b77e36ab31ac79df3796a1b6f26baa5552ae",
            0x24029a571fcaeadd14f4afe8be62afe3d07876ae270c3caf446ecb6cfc7c08f2
        );

        (bytes32 publicKey, uint8 parity, uint64 session) = gatekeeper.latestPublicKeyInfo();
        assertEq(publicKey, prevPublicKey);
        assertEq(parity, prevParity);
        assertEq(session, 0);

        // could not apply signature from DKG #2
        vm.expectRevert();
        gatekeeper.verify(
            hex"49f03a6700000000000000000000000000000000000000000000000000000000000000043492bc8bb10848f1f788496ccadde7d458eac4ac2c1eca2dd2c7a506d8fa2c5b0000000000000000000000000000000000000000000000000000000000000000",
            hex"0496bacdcc4260a610c9ef465cc1d9f973dda7d4794a65c1d1ea8ea2bb8aa3ddb08d171f5aa267c1ddc64343b4f054424af0c61b57e1f359cd4e23876a0fa4a704",
            0x34383aacbe014a6fed8b5976ccfcdcbb93cef7e41a6634f934432d9ede912c14
        );

        // genesis validators 7 validators, DKG #1
        // exodus session #2
        gatekeeper.verify(
            hex"49f03a6700000000000000000000000000000000000000000000000000000000000000024e8c1fe96d6737cdabbcc96501d6656b9d0b659b3a528ef507f2d52bef29e1570000000000000000000000000000000000000000000000000000000000000000",
            hex"04460e7dce1ebd4cc7527ba80852b6fcdbf8810da3b2173c2f3b3da63206a44407f79d5b5b4c3adcdcce2404c7488240873babb856f065fa551caa6dddea68ca06",
            0x7f6ac70e1e3f3ddd7101a439e7da6f8622a0c8cc38ce42ce81d911d8ad23af90
        );

        (publicKey, parity, session) = gatekeeper.latestPublicKeyInfo();
        assert(publicKey != prevPublicKey);
        assertEq(parity, prevParity);
        assertEq(session, 2);

        prevPublicKey = publicKey;
        prevParity = parity;

        // genesis validators 5 validators, DKG #2
        // exodus session #4
        gatekeeper.verify(
            hex"49f03a6700000000000000000000000000000000000000000000000000000000000000043492bc8bb10848f1f788496ccadde7d458eac4ac2c1eca2dd2c7a506d8fa2c5b0000000000000000000000000000000000000000000000000000000000000000",
            hex"0496bacdcc4260a610c9ef465cc1d9f973dda7d4794a65c1d1ea8ea2bb8aa3ddb08d171f5aa267c1ddc64343b4f054424af0c61b57e1f359cd4e23876a0fa4a704",
            0x34383aacbe014a6fed8b5976ccfcdcbb93cef7e41a6634f934432d9ede912c14
        );

        (publicKey, parity, session) = gatekeeper.latestPublicKeyInfo();
        assert(publicKey != prevPublicKey);
        assertEq(parity, prevParity);
        assertEq(session, 4);

        // could not apply DKG #0 no more
        vm.expectRevert();
        gatekeeper.verify(
            hex"49f03a670000000000000000000000000000000000000000000000000000000000000000875cdcba4ae5494518fa2602791e667ca0998402b6a69e5b7cb4c72dba4e46690000000000000000000000000000000000000000000000000000000000000000",
            hex"044d9fa18df9da04381ed256981570a6ef6a0893496ded966fc994662df931ed423b29e2737394b31d8498594caad7b77e36ab31ac79df3796a1b6f26baa5552ae",
            0x24029a571fcaeadd14f4afe8be62afe3d07876ae270c3caf446ecb6cfc7c08f2
        );

        // could not apply DKG #1 no more
        vm.expectRevert();
        gatekeeper.verify(
            hex"49f03a6700000000000000000000000000000000000000000000000000000000000000024e8c1fe96d6737cdabbcc96501d6656b9d0b659b3a528ef507f2d52bef29e1570000000000000000000000000000000000000000000000000000000000000000",
            hex"04460e7dce1ebd4cc7527ba80852b6fcdbf8810da3b2173c2f3b3da63206a44407f79d5b5b4c3adcdcce2404c7488240873babb856f065fa551caa6dddea68ca06",
            0x7f6ac70e1e3f3ddd7101a439e7da6f8622a0c8cc38ce42ce81d911d8ad23af90
        );

        // could not apply DKG #2 no more
        vm.expectRevert();
        gatekeeper.verify(
            hex"49f03a6700000000000000000000000000000000000000000000000000000000000000043492bc8bb10848f1f788496ccadde7d458eac4ac2c1eca2dd2c7a506d8fa2c5b0000000000000000000000000000000000000000000000000000000000000000",
            hex"0496bacdcc4260a610c9ef465cc1d9f973dda7d4794a65c1d1ea8ea2bb8aa3ddb08d171f5aa267c1ddc64343b4f054424af0c61b57e1f359cd4e23876a0fa4a704",
            0x34383aacbe014a6fed8b5976ccfcdcbb93cef7e41a6634f934432d9ede912c14
        );

        uint256 aliceAmountBefore = staking.materializedAmount(ALICE);
        uint256 bobAmountBefore = staking.materializedAmount(BOB);
        uint256 aliceBalanceBefore = ALICE.balance;
        uint256 bobBalanceBefore = BOB.balance;

        // execute bridge out, happened on DKG #0
        // amount: 69; commission: 0%; ALICE
        // exodus session #1
        gatekeeper.verify(
            hex"39cd99050000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004500000000000000000000000000000000000000010000000000007a6900000000",
            hex"04bf0bcb3d42d8187c543a82ee201a221525047020ff0ac03e4d60e322d3eb6d8ab5bfdae43ea0fbf59128d201b8076727c3c63fe8a5c2b890fdb3c97a7da5f207",
            0x08f121a1615782c83cce41d7fc2064be57fdeeae8b45f716211b3c8c873f3e4c
        );

        assertEq(aliceAmountBefore + 69, staking.materializedAmount(ALICE));
        assertEq(bobAmountBefore, staking.materializedAmount(BOB));
        assertEq(aliceBalanceBefore, ALICE.balance);
        assertEq(bobBalanceBefore, BOB.balance);

        vm.deal(ALICE, 220);

        aliceAmountBefore = staking.materializedAmount(ALICE);
        bobAmountBefore = staking.materializedAmount(BOB);
        aliceBalanceBefore = ALICE.balance;
        bobBalanceBefore = BOB.balance;

        uint256 buyback = 220;

        // execute bridge out, happened on DKG #1
        // amount: 420; commission: 50%; BOB
        // exodus session #3
        vm.prank(ALICE, ALICE);
        gatekeeper.verify{value: buyback}(
            hex"39cd9905000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000001a400000000000000000000000000000000000000020000000000007a6980000000",
            hex"04089940ed9bb8d7f3a77adfafb0d5ebc45af558ade46844856c049aa912b9d8401826e17f24c0aa625524ff4a65b430a19422b4f9754cec3b862641f17f99b1a9",
            0x9d2cf40ab0b1820858fe6f8950e2f511b4acb57b00b37bb93760978e2add4243
        );

        assertEq(aliceAmountBefore + 210, staking.materializedAmount(ALICE));
        assertEq(bobAmountBefore + 210, staking.materializedAmount(BOB));
        assertEq(aliceBalanceBefore - buyback, ALICE.balance);
        assertEq(bobBalanceBefore + buyback, BOB.balance);
    }
}
