pragma solidity 0.8.20;

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

import {Gatekeeper} from "../../src/Gatekeeper.sol";
import {Hashes} from "../../src/libraries/Hashes.sol";
import {Checkpoints} from "../../src/libraries/Checkpoints.sol";
import {IGatekeeper} from "../../src/interfaces/IGatekeeper.sol";
import {IStorageHistory} from "../../src/interfaces/IStorageHistory.sol";

contract MockStaking {
    GatekeeperVerification public gatekeeper;
    address public governor;

    constructor(uint256 existential) {
        gatekeeper = new GatekeeperVerification(existential, address(0));
        governor = msg.sender;
    }

    function ghost(bytes32 receiver, uint256 amount) external returns (uint256) {
        require(msg.sender == governor);
        return gatekeeper.ghost(receiver, amount);
    }

    function createNewGatekeeper(uint256 existential, address previousGatekeeper) external {
        require(msg.sender == governor);
        GatekeeperVerification newGatekeeper = new GatekeeperVerification(existential, address(gatekeeper));
        address storageHistory = IGatekeeper(previousGatekeeper).storageHistory();
        IStorageHistory(storageHistory).setOwner(address(newGatekeeper));
        gatekeeper = newGatekeeper;
    }
}

contract GatekeeperVerification is Gatekeeper {
    using Checkpoints for Checkpoints.Trace256;
    using Checkpoints for Checkpoints.Trace160;

    constructor(uint256 existential, address previousWeaver) Gatekeeper(existential, previousWeaver) {}

    function filledEntries(uint256 session) public view returns (uint256) {
        return _filledEntries[session];
    }

    function slotLengths(uint256 session, uint256 globalIndex) public view returns (uint160) {
        uint256 slotIndex = globalIndex % SLOTS;
        return _slotLenghts[session][slotIndex].latest();
    }

    function slotValues(uint256 session, uint256 globalIndex) public view returns (bytes32) {
        uint256 slotIndex = globalIndex % SLOTS;
        uint256 valueIndex = globalIndex / SLOTS;
        return _slotValues[session][slotIndex][valueIndex];
    }

    function treeNodesLatest(uint256 session, uint256 globalIndex) public view returns (bytes32) {
        uint256 slotIndex = globalIndex % SLOTS;
        return bytes32(_treeNodes[session][slotIndex].latest());
    }

    function computePreimage(uint256 globalIndex, uint256 a, bytes32 r) public pure returns (bytes32) {
        uint256 valueIndex = globalIndex / SLOTS;
        return _computeArgumentsHash(valueIndex, a, r);
    }

    function testVerify(
        bytes32[] calldata proof,
        bytes32[] calldata values,
        uint256 globalIndex,
        uint256 amount,
        bytes32 who
    ) public pure returns (bytes32) {
        if (proof.length != DEPTH) { return bytes32(0); }
        if (values.length == 0) { return bytes32(0); }

        uint256 slotIndex = globalIndex % SLOTS;
        uint256 valueIndex = globalIndex / SLOTS;
        if (valueIndex >= values.length) { return bytes32(0); }

        bytes32 computedHash = _computeArgumentsHash(valueIndex, amount, who);
        if (computedHash != values[valueIndex]) { return bytes32(0); }
        computedHash = bytes32(0);

        uint256 i;
        for (; i < values.length;) {
            bytes32 currHash = Hashes.efficientKeccak256(values[i]);
            computedHash = Hashes.efficientKeccak256(computedHash, currHash);
            unchecked { ++i; }
        }

        computedHash = Hashes.efficientKeccak256(computedHash);

        i = 0;
        for (; i < proof.length; ) {
            if (slotIndex % 2 == 0) {
                 computedHash = Hashes.efficientKeccak256(computedHash, proof[i]);
            } else {
                 computedHash = Hashes.efficientKeccak256(proof[i], computedHash);
            }
            slotIndex >>= 1;
            unchecked { ++i; }
        }

        return computedHash;
    }
}

contract GatekeeperWeaverTest is Test {
    address constant ALICE       = 0x0000000000000000000000000000000000000001;
    address constant BOB         = 0x0000000000000000000000000000000000000002;
    uint256 constant EXISTENTIAL = 1337;
    uint256 constant AMOUNT      = 1 * 1e7;

    MockStaking staking;
    GatekeeperVerification gatekeeper;

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

    function test_insertationWorksAsExpected() public {
        uint256 currentWeavingSession = gatekeeper.currentWeavingSession();
        uint256 maxCount = gatekeeper.ENTRIES();
        uint256 globalIndex;

        (bytes32[] memory whos, uint256[] memory amounts) = _prepareArrays(maxCount);

        for (uint256 i = 0; i < maxCount; i++) {
            if (i % 5 == 0) { vm.roll(block.number + 1); }
            globalIndex = _insertWithAssert(currentWeavingSession, amounts[i], whos[i]);
            assertTrue(_verifyProof(globalIndex, currentWeavingSession, block.number, amounts[i], whos[i]));
        }

        globalIndex = _insertWithAssert(currentWeavingSession, amounts[69], whos[69]);
        uint256 newSession = gatekeeper.currentWeavingSession();

        assertEq(currentWeavingSession + 1, newSession);
        vm.roll(block.number + 1337);

        assertTrue(_verifyProof(globalIndex, newSession, block.number, amounts[69], whos[69]));
        assertTrue(_verifyProof(0, currentWeavingSession, block.number, amounts[0], whos[0]));
        assertTrue(_verifyProof(69, currentWeavingSession, block.number, amounts[69], whos[69]));
        assertTrue(_verifyProof(420, currentWeavingSession, block.number, amounts[420], whos[420]));
        assertTrue(_verifyProof(1337, currentWeavingSession, block.number, amounts[1337], whos[1337]));
        assertTrue(_verifyProof(2047, currentWeavingSession, block.number, amounts[2047], whos[2047]));

        assertTrue(_verifyProof(0, currentWeavingSession, 100, amounts[0], whos[0]));
        assertTrue(_verifyProof(69, currentWeavingSession, 100, amounts[69], whos[69]));
        assertTrue(_verifyProof(420, currentWeavingSession, 100, amounts[420], whos[420]));

        assertFalse(_verifyProof(globalIndex, newSession, 100, amounts[69], whos[69]));
        assertFalse(_verifyProof(1337, currentWeavingSession, 100, amounts[1337], whos[1337]));
        assertFalse(_verifyProof(2047, currentWeavingSession, 100, amounts[2047], whos[2047]));

        vm.roll(block.number + 420);

        vm.prank(ALICE);
        staking.createNewGatekeeper(EXISTENTIAL, address(gatekeeper));
        gatekeeper = staking.gatekeeper();

        uint256 finalSession = gatekeeper.currentWeavingSession();

        for (uint256 i = 0; i < 69; i++) {
            if (i % 2 == 0) { vm.roll(block.number + 1); }
            vm.prank(ALICE);
            staking.ghost(whos[i], amounts[i]);
        }

        assertTrue(_verifyProof(globalIndex, newSession, block.number, amounts[69], whos[69]));
        assertTrue(_verifyProof(0, currentWeavingSession, block.number, amounts[0], whos[0]));
        assertTrue(_verifyProof(69, currentWeavingSession, block.number, amounts[69], whos[69]));
        assertTrue(_verifyProof(420, currentWeavingSession, block.number, amounts[420], whos[420]));
        assertTrue(_verifyProof(1337, currentWeavingSession, block.number, amounts[1337], whos[1337]));
        assertTrue(_verifyProof(2047, currentWeavingSession, block.number, amounts[2047], whos[2047]));

        assertTrue(_verifyProof(0, currentWeavingSession, 100, amounts[0], whos[0]));
        assertTrue(_verifyProof(69, currentWeavingSession, 100, amounts[69], whos[69]));
        assertTrue(_verifyProof(420, currentWeavingSession, 100, amounts[420], whos[420]));

        assertFalse(_verifyProof(globalIndex, newSession, 100, amounts[69], whos[69]));
        assertFalse(_verifyProof(1337, currentWeavingSession, 100, amounts[1337], whos[1337]));
        assertFalse(_verifyProof(2047, currentWeavingSession, 100, amounts[2047], whos[2047]));

        assertTrue(_verifyProof(0, finalSession, block.number, amounts[0], whos[0]));
        assertTrue(_verifyProof(34, finalSession, block.number, amounts[34], whos[34]));
        assertTrue(_verifyProof(35, finalSession, block.number, amounts[35], whos[35]));
    }

    function _verifyProof(
        uint256 globalIndex,
        uint256 session,
        uint256 atBlock,
        uint256 amount,
        bytes32 who
    ) private view returns (bool) {
        bytes32[] memory proof = gatekeeper.getProof(globalIndex, session, atBlock);
        bytes32[] memory values = gatekeeper.getSlotValues(globalIndex, session, atBlock);

        bytes32 root1 = gatekeeper.testVerify(proof, values, globalIndex, amount, who);
        (bytes32 root2, uint256 entries) = gatekeeper.getRoot(session, atBlock);
        assertTrue(entries <= gatekeeper.ENTRIES());

        return root1 == root2;
    }

    function _insertWithAssert(
        uint256 session,
        uint256 amount,
        bytes32 who
    ) private returns (uint256 globalIndex) {
        uint256 targetSlot = gatekeeper.filledEntries(session);
        bytes32 previousHash = gatekeeper.treeNodesLatest(session, targetSlot);
        uint256 prevEntries = gatekeeper.filledEntries(session);
        uint160 prevLength = gatekeeper.slotLengths(session, targetSlot);

        vm.prank(ALICE);
        globalIndex = staking.ghost(who, amount);

        if (session + 1 == gatekeeper.currentWeavingSession()) {
            assertEq(prevLength, gatekeeper.DEPTH());
            assertEq(gatekeeper.slotLengths(session + 1, 0), 1);
            assertEq(prevEntries, gatekeeper.ENTRIES());
            assertEq(gatekeeper.filledEntries(session + 1), 1);
            assertEq(session + 1, gatekeeper.currentWeavingSession());

            session += 1;
            previousHash = bytes32(0);
            prevLength = gatekeeper.slotLengths(session, 0);
        } else  {
            assertEq(prevLength + 1, gatekeeper.slotLengths(session, targetSlot));
            assertEq(prevEntries + 1, gatekeeper.filledEntries(session));
        }

        {
            bytes32 preimage1 = gatekeeper.slotValues(session, globalIndex);
            bytes32 preimage2 = gatekeeper.computePreimage(globalIndex, amount, who);
            assertEq(preimage1, preimage2);
        }

        {
            bytes32 nodeHash1 = gatekeeper.treeNodesLatest(session, globalIndex);
            bytes32 preimageHash = Hashes.efficientKeccak256(
                gatekeeper.computePreimage(globalIndex, amount, who)
            );
            bytes32 nodeHash2 = Hashes.efficientKeccak256(previousHash, preimageHash);
            assertEq(nodeHash1, nodeHash2);
        }
    }

    function _prepareArrays(uint256 count) private pure returns (bytes32[] memory, uint256[] memory) {
        bytes32[] memory whos = new bytes32[](count);
        uint256[] memory amounts = new uint256[](count);

        for (uint256 i = 0; i < count; i++) {
            whos[i] = keccak256(abi.encodePacked("user", i));
            amounts[i] = EXISTENTIAL + 1 + i;
        }

        return (whos, amounts);
    }
}
