pragma solidity 0.8.20;

import {Test} from "forge-std/Test.sol";
import {ERC20Votes} from "@openzeppelin-contracts/token/ERC20/extensions/ERC20Votes.sol";
import {Checkpoints} from "@openzeppelin-contracts/utils/structs/Checkpoints.sol";
import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";

abstract contract ERC20VotesTest is Test {
    using SafeERC20 for ERC20Votes;

    ERC20Votes tokenVotes;
    uint256 amountVotes;

    address public aliceVotes;
    address public bobVotes;

    address constant public CHARLIE_VOTES = 0x0000000000000000000000000000000000000069;
    address constant public EVE_VOTES     = 0x0000000000000000000000000000000000001337;

    event DelegateChanged(
        address indexed delegator,
        address indexed fromDelegate,
        address indexed toDelegate
    );
    event DelegateVotesChanged(
        address indexed delegate,
        uint256 previousBalance,
        uint256 newBalance
    );

    function initializeVotes(
        address alice,
        address bob,
        address token,
        uint256 amount
    ) public {
        aliceVotes = alice;
        bobVotes = bob;
        amountVotes = amount;
        tokenVotes = ERC20Votes(token);
    }

    function test_votes_delegate() external {
        // case 1: first delegation without balance
        assertEq(tokenVotes.delegates(address(this)), address(this));

        vm.expectRevert();
        tokenVotes.delegate(aliceVotes);
        assertEq(tokenVotes.delegates(address(this)), address(this));
        // no votes in aliceVotes
        assertEq(tokenVotes.getVotes(aliceVotes), 0);
        // no Checkpoint generated
        assertEq(tokenVotes.numCheckpoints(aliceVotes), 0);

        // case 2: first delegate with balance
        _mintVotesTokens(aliceVotes, amountVotes);
        assertEq(tokenVotes.delegates(aliceVotes), address(aliceVotes));

        vm.expectRevert();
        vm.prank(aliceVotes);
        tokenVotes.delegate(bobVotes);

        assertEq(tokenVotes.delegates(aliceVotes), aliceVotes);
        assertEq(tokenVotes.getVotes(aliceVotes), amountVotes);
        assertEq(tokenVotes.getVotes(bobVotes), 0);

        // 1 Checkpoint generated
        assertEq(tokenVotes.numCheckpoints(aliceVotes), 1);
        Checkpoints.Checkpoint208 memory ckpt = tokenVotes.checkpoints(aliceVotes, 0);
        assertEq(ckpt._key, 1);
        assertEq(ckpt._value, amountVotes);

        // case 3: delegate with balance not first
        vm.roll(2);

        vm.expectRevert();
        vm.prank(aliceVotes);
        tokenVotes.delegate(CHARLIE_VOTES);

        assertEq(tokenVotes.delegates(aliceVotes), aliceVotes);
        assertEq(tokenVotes.delegates(bobVotes), bobVotes);
        assertEq(tokenVotes.delegates(CHARLIE_VOTES), CHARLIE_VOTES);

        assertEq(tokenVotes.getVotes(aliceVotes), amountVotes);
        assertEq(tokenVotes.getVotes(bobVotes), 0);
        assertEq(tokenVotes.getVotes(CHARLIE_VOTES), 0);

        // 1 Checkpoint generated
        assertEq(tokenVotes.numCheckpoints(aliceVotes), 1);
        ckpt = tokenVotes.checkpoints(aliceVotes, 0);
        assertEq(ckpt._key, 1);
        assertEq(ckpt._value, amountVotes);

    }

    function test_votes_mintAndBurnAndMaxSupply() external {
        // test for {_mint}
        // case 1: receiver has no delegatee
        assertEq(tokenVotes.totalSupply(), 0);
        _mintVotesTokens(address(this), 1);
        assertEq(tokenVotes.totalSupply(), 1);
        assertEq(tokenVotes.balanceOf(address(this)), 1);

        // revert if total supply exceeds the ceiling
        vm.expectRevert();
        _mintVotesTokens(aliceVotes, type(uint224).max + 1);

        // case 2: receiver has a delegatee
        vm.prank(aliceVotes);
        tokenVotes.delegate(address(this));
        assertEq(tokenVotes.getVotes(address(this)), 0);

        _mintVotesTokens(aliceVotes, 2);
        assertEq(tokenVotes.totalSupply(), 1 + 2);
        assertEq(tokenVotes.balanceOf(aliceVotes), 0 + 2);
        // delegatee's votes increased
        assertEq(tokenVotes.getVotes(address(this)), 0 + 2);

        // revert if total supply exceeds the ceiling (happens in {_afterTokenTransfer})
        vm.expectRevert();
        _mintVotesTokens(aliceVotes, type(uint224).max);

        // test for {_burn}
        // case 3: receiver has no delegatee
        _burnVotesTokens(address(this), 1);
        assertEq(tokenVotes.totalSupply(), 3 - 1);

        // case 4: receiver has a delegatee
        _burnVotesTokens(aliceVotes, 1);
        assertEq(tokenVotes.totalSupply(), 2 - 1);
        // delegatee's votes decreased
        assertEq(tokenVotes.getVotes(address(this)), 2 - 1);
    }

    function test_votes_afterTokenTransfer() external {
        _mintVotesTokens(address(this), 100);
        vm.expectRevert();
        tokenVotes.delegate(aliceVotes);

        assertEq(tokenVotes.delegates(address(this)), address(this));
        assertEq(tokenVotes.numCheckpoints(aliceVotes), 0);
        assertEq(tokenVotes.numCheckpoints(address(this)), 1);

        // test for {transfer}
        // case 1: 'to' has no delegatee
        vm.roll(2);
        vm.expectEmit(true, false, false, true, address(tokenVotes));
        emit DelegateVotesChanged(address(this), 100, 100 - 1);

        vm.prank(address(this));
        tokenVotes.safeTransfer(bobVotes, 1);
        assertEq(tokenVotes.getVotes(address(this)), 100 - 1);

        // case 2: 'to' has a delegatee
        _mintVotesTokens(CHARLIE_VOTES, 100);
        vm.expectRevert();
        vm.prank(CHARLIE_VOTES);
        tokenVotes.delegate(EVE_VOTES);

        vm.roll(3);
        vm.expectEmit(true, false, false, true, address(tokenVotes));
        emit DelegateVotesChanged(CHARLIE_VOTES, 100, 100 - 1);
        vm.expectEmit(true, false, false, true, address(tokenVotes));
        emit DelegateVotesChanged(EVE_VOTES, 0, 1);

        vm.prank(CHARLIE_VOTES);
        tokenVotes.safeTransfer(EVE_VOTES, 1);
        assertEq(tokenVotes.getVotes(CHARLIE_VOTES), 100 - 1);
        assertEq(tokenVotes.getVotes(EVE_VOTES), 1);

        // test for {transferFrom}
        // case 3: 'to' has no delegatee
        vm.roll(4);
        assertEq(tokenVotes.delegates(aliceVotes), aliceVotes);
        assertEq(tokenVotes.delegates(bobVotes), bobVotes);
        assertEq(tokenVotes.delegates(CHARLIE_VOTES), CHARLIE_VOTES);
        assertEq(tokenVotes.delegates(EVE_VOTES), EVE_VOTES);

        tokenVotes.approve(aliceVotes, 100);
        assertEq(tokenVotes.delegates(aliceVotes), aliceVotes);
        assertEq(tokenVotes.delegates(bobVotes), bobVotes);
        assertEq(tokenVotes.delegates(CHARLIE_VOTES), CHARLIE_VOTES);
        assertEq(tokenVotes.delegates(EVE_VOTES), EVE_VOTES);

        vm.startPrank(aliceVotes);
        vm.expectEmit(true, false, false, true, address(tokenVotes));
        emit DelegateVotesChanged(address(this), 99, 99 - 1);
        vm.expectEmit(true, false, false, true, address(tokenVotes));
        emit DelegateVotesChanged(bobVotes, 1, 2);

        tokenVotes.safeTransferFrom(address(this), bobVotes, 1);

        // case 4: 'to' has a delegatee
        vm.roll(5);
        assertEq(tokenVotes.delegates(aliceVotes), aliceVotes);
        assertEq(tokenVotes.delegates(bobVotes), bobVotes);
        assertEq(tokenVotes.delegates(CHARLIE_VOTES), CHARLIE_VOTES);
        assertEq(tokenVotes.delegates(EVE_VOTES), EVE_VOTES);

        vm.expectEmit(true, false, false, true, address(tokenVotes));
        emit DelegateVotesChanged(address(this), 98, 98 - 1);
        vm.expectEmit(true, false, false, true, address(tokenVotes));
        emit DelegateVotesChanged(CHARLIE_VOTES, 99, 99 + 1);

        tokenVotes.safeTransferFrom(address(this), CHARLIE_VOTES, 1);
    }

    function test_votes_getPastVotesAndGetPastTotalSupply() external {
        // 6 Checkpoints of aliceVotes:
        //       block             votes             index
        //          2                10                0
        //          3                15                1
        //          6                19                2
        //          10               20                3
        //          11               23                4
        //          13               31                5
        //
        // 6 Checkpoints of total supply:
        //       block          total supply         index
        //          2                10                0
        //          3                15                1
        //          6                19                2
        //          10               20                3
        //          11               23                4
        //          13               31                5

        vm.expectRevert();
        tokenVotes.delegate(aliceVotes);
        vm.roll(2);
        _mintVotesTokens(address(this), 10);
        vm.roll(3);
        _mintVotesTokens(address(this), 15 - 10);
        vm.roll(6);
        _mintVotesTokens(address(this), 19 - 15);
        vm.roll(10);
        _mintVotesTokens(address(this), 20 - 19);
        vm.roll(11);
        _mintVotesTokens(address(this), 23 - 20);
        vm.roll(13);
        _mintVotesTokens(address(this), 31 - 23);
        vm.roll(20);

        // check {getPastVotes} && {getPastTotalSupply}
        assertEq(tokenVotes.numCheckpoints(address(this)), 6);

        assertEq(tokenVotes.getPastVotes(address(this), 1), 0);
        assertEq(tokenVotes.getPastTotalSupply(1), 0);

        assertEq(tokenVotes.getPastVotes(address(this), 2), 10);
        assertEq(tokenVotes.getPastTotalSupply(2), 10);

        assertEq(tokenVotes.getPastVotes(address(this), 4), 15);
        assertEq(tokenVotes.getPastTotalSupply(4), 15);

        assertEq(tokenVotes.getPastVotes(address(this), 6), 19);
        assertEq(tokenVotes.getPastTotalSupply(6), 19);

        assertEq(tokenVotes.getPastVotes(address(this), 9), 19);
        assertEq(tokenVotes.getPastTotalSupply(9), 19);

        assertEq(tokenVotes.getPastVotes(address(this), 10), 20);
        assertEq(tokenVotes.getPastTotalSupply(10), 20);

        assertEq(tokenVotes.getPastVotes(address(this), 12), 23);
        assertEq(tokenVotes.getPastTotalSupply(12), 23);

        assertEq(tokenVotes.getPastVotes(address(this), 13), 31);
        assertEq(tokenVotes.getPastTotalSupply(13), 31);

        assertEq(tokenVotes.getPastVotes(address(this), 19), 31);
        assertEq(tokenVotes.getPastTotalSupply(19), 31);

        // revert if block not mined
        vm.expectRevert();
        tokenVotes.getPastVotes(aliceVotes, 9999);
        vm.expectRevert();
        tokenVotes.getPastTotalSupply(9999);
    }

    function _mintVotesTokens(address who, uint256 value) internal virtual;
    function _burnVotesTokens(address who, uint256 amount) internal virtual;
}
