pragma solidity 0.8.20;

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

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

contract MockStaking is Test {
    Gatekeeper public gatekeeper;

    mapping(address => uint256) private _materializedAmounts;

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

    function redoGatekeeper(uint256 existential) external {
        gatekeeper = new Gatekeeper(existential, address(gatekeeper));
    }

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

    function runMaterialize(uint256 exodusSession, uint256 amount, uint256 packed) external {
        vm.prank(address(gatekeeper));
        gatekeeper.materialize(exodusSession, amount, packed);
    }

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

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

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

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

    Gatekeeper gatekeeper;
    MockStaking staking;

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

        staking.runGhost(bytes32(abi.encodePacked(ALICE)), INIT_AMOUNT);
        gatekeeper = staking.gatekeeper();
    }

    function test_correctStorageHistoryInitialization() public view {
        address storageHistory = gatekeeper.storageHistory();
        IStorageHistory.DeploymentSnapshot memory snapshot = IStorageHistory(storageHistory).deploymentSnapshot();

        assertEq(snapshot.deployedAt, 0);
        assertEq(snapshot.amountIn, INIT_AMOUNT);
        assertEq(snapshot.amountOut, 0);
        assertEq(gatekeeper.ghostedSupply(), INIT_AMOUNT);
    }

    function test_historicalAmountsOnlyIncrease(uint256 ghostAmount, uint64 exodusSession) public {
        vm.assume(ghostAmount > 0 && ghostAmount < INIT_AMOUNT);

        uint256 amountToGhost = ghostAmount;
        uint256 amountToMaterialize = ghostAmount / 2;

        bytes32 receiver = bytes32(abi.encodePacked(ALICE));
        address storageHistory = gatekeeper.storageHistory();

        IStorageHistory.DeploymentSnapshot memory snapshot = IStorageHistory(storageHistory).deploymentSnapshot();
        uint104 amountIn = snapshot.amountIn;
        uint104 amountOut = snapshot.amountOut;

        if (ghostAmount % 2 == 0) {
            staking.runGhost(receiver, amountToGhost);
            // forge-lint: disable-next-line(unsafe-typecast)
            amountIn += uint104(amountToGhost);
        } else {
            // forge-lint: disable-next-line(unsafe-typecast)
            uint256 packed = RequestPacking.pack(0, uint64(block.chainid), BOB);
            uint256 previousAmount = staking.materializedAmount(BOB);

            staking.runMaterialize(exodusSession, amountToMaterialize, packed);
            assertEq(previousAmount + amountToMaterialize, staking.materializedAmount(BOB));

            // forge-lint: disable-next-line(unsafe-typecast)
            amountOut += uint104(amountToMaterialize);
        }

        IStorageHistory.DeploymentSnapshot memory newSnapshot = IStorageHistory(storageHistory).deploymentSnapshot();
        assertEq(newSnapshot.amountIn, amountIn);
        assertEq(newSnapshot.amountOut, amountOut);
        assertEq(gatekeeper.ghostedSupply(), amountIn - amountOut);
    }

    function test_inheritanceWorksForHistoricalStorge() public {
        uint256 exodusSession = 69;
        uint256 packed = RequestPacking.pack(0, uint64(block.chainid), BOB);
        staking.runMaterialize(exodusSession, INIT_AMOUNT, packed);
        staking.runGhost(bytes32(abi.encodePacked(ALICE)), INIT_AMOUNT);

        address prevStorageHistory = gatekeeper.storageHistory();
        IStorageHistory.DeploymentSnapshot memory prevSnapshot = IStorageHistory(prevStorageHistory).deploymentSnapshot();

        staking.redoGatekeeper(EXISTENTIAL + 1);
        assert(gatekeeper != staking.gatekeeper());

        address currStorageHistory = gatekeeper.storageHistory();
        IStorageHistory.DeploymentSnapshot memory currSnapshot = IStorageHistory(currStorageHistory).deploymentSnapshot();

        assertEq(prevSnapshot.amountIn, currSnapshot.amountIn);
        assertEq(prevSnapshot.amountOut, currSnapshot.amountOut);
        assertEq(prevSnapshot.deployedAt, currSnapshot.deployedAt);

        assert(IStorageHistory(prevStorageHistory).isTransactionExecuted(exodusSession));
        assert(IStorageHistory(currStorageHistory).isTransactionExecuted(exodusSession));

        assertEq(gatekeeper.existentialDeposit(), EXISTENTIAL);
        assertEq(staking.gatekeeper().existentialDeposit(), EXISTENTIAL + 1);
    }
}
