pragma solidity 0.8.20;

import {Test} from "forge-std/Test.sol";
import {Strings} from "@openzeppelin-contracts/utils/Strings.sol";

import "../../src/governance/GhostGovernor.sol";
import "../../src/GhstERC20.sol";
import "../../src/StinkyERC20.sol";

contract GhostGovernorExposed is GhostGovernor {
    constructor(
        IVotes _ghst,
        uint48 _initialVoteExtension,
        uint48 _initialVotingDelay,
        uint32 _initialVotingPeriod,
        uint256 _initialProposalThreshold,
        uint256 _quorumFraction
    ) GhostGovernor(
        _ghst,
        _initialVoteExtension,
        _initialVotingDelay,
        _initialVotingPeriod,
        _initialProposalThreshold,
        _quorumFraction
    ) {}

    function isValidDescriptionForProposer(
        address proposer,
        string memory description
    ) public view returns (bool) {
        return super._isValidDescriptionForProposer(proposer, description);
    }

    function quorumReached(uint256 proposalId) public view returns (bool) {
        return super._quorumReached(proposalId);
    }

    function voteSucceeded(uint256 proposalId) public view returns (bool) {
        return super._voteSucceeded(proposalId);
    }
}

contract GhostGovernorTest is Test {
    using Strings for address;

    uint48  public constant VOTE_EXTENSION     = 69;
    uint48  public constant VOTING_DELAY       = 420;
    uint32  public constant VOTING_PERIOD      = 1337;
    uint256 public constant PROPOSAL_THRESHOLD = 69 * 1e18;
    uint256 public constant QUORUM_FRACTION    = 20; // percent

    address constant init  = 0x0000000000000000000000000000000000000001;
    address constant alice = 0x0000000000000000000000000000000000000002;
    address constant bob   = 0x0000000000000000000000000000000000000003;
    address constant carol = 0x0000000000000000000000000000000000000004;
    address constant dave  = 0x0000000000000000000000000000000000000005;
    address constant eve   = 0x0000000000000000000000000000000000000006;

    GhostGovernorExposed public governor;
    Stinky public stnk;
    Ghost public ghst;

    function setUp() public {
        vm.startPrank(init);
        stnk = new Stinky(10819917194513808e56, "Stinky Test Name", "STNKTST");
        ghst = new Ghost(address(stnk), "Ghost Test Name", "GHSTTST");
        ghst.initialize(init);
        governor = new GhostGovernorExposed(
            ghst,
            VOTE_EXTENSION,
            VOTING_DELAY,
            VOTING_PERIOD,
            PROPOSAL_THRESHOLD,
            QUORUM_FRACTION
        );
        vm.stopPrank();

        vm.prank(alice);
        ghst.approve(address(governor), type(uint256).max);

        vm.prank(bob);
        ghst.approve(address(governor), type(uint256).max);

        vm.prank(carol);
        ghst.approve(address(governor), type(uint256).max);
    }

    function test_correctGovernanceName() public view {
        assertEq(governor.name(), "GhostGovernorV1");
    }

    function test_correctGovernanceVersion() public view {
        assertEq(governor.version(), "1");
    }

    function test_correctGovernanceConfig() public view {
        assertEq(governor.COUNTING_MODE(), "support=bravo&quorum=for");
    }

    function test_correctGovernanceProposalThreshold() public view {
        assertEq(governor.proposalThreshold(), PROPOSAL_THRESHOLD);
    }

    function test_correctGovernanceVotingDelay() public view {
        assertEq(governor.votingDelay(), VOTING_DELAY);
    }

    function test_correctGovernanceVotingPeriod() public view {
        assertEq(governor.votingPeriod(), VOTING_PERIOD);
    }

    function test_correctGovernanceVoteExtension() public view {
        assertEq(governor.lateQuorumVoteExtension(), VOTE_EXTENSION);
    }

    function test_correctGovernanceQuorumFraction() public view {
        assertEq(governor.quorumNumerator(), QUORUM_FRACTION);
    }

    function test_validDescriptionForProposer(
        string memory description,
        address proposer,
        bool includeProposer
    ) public view {
        string memory finalDescription = description;

        if (includeProposer) {
            finalDescription = string.concat(
                description,
                "#proposer=",
                Strings.toHexString(proposer)
            );
        }

        bool isValid = governor.isValidDescriptionForProposer(proposer, finalDescription);
        assertTrue(isValid);
    }

    function test_invalidDescriptionForProposer(
        string memory description,
        address commitProposer,
        address actualProposer
    ) public view {
        vm.assume(commitProposer != actualProposer);

        string memory wrongDescription = string.concat(
            description,
            "#proposer=",
            Strings.toHexString(commitProposer)
        );

        bool isValid = governor.isValidDescriptionForProposer(actualProposer, wrongDescription);
        assertFalse(isValid);
    }

    function test_succeededOnAbsoluteMajority() public {
        vm.startPrank(init);
        ghst.mint(alice, PROPOSAL_THRESHOLD);
        ghst.mint(bob, 555 * 1e17);
        ghst.mint(carol, 345 * 1e17 + 1);
        ghst.mint(dave, 21 * 1e18);
        vm.stopPrank();

        vm.roll(block.number + 1);

        (uint256 proposalId,,,,) = _proposeDummy(alice, 69);
        _waitForActive(proposalId);

        assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active));
        _castVoteWrapper(proposalId, bob, 1, true, true);
        assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active));
        _castVoteWrapper(proposalId, carol, 1, true, true);
        assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Succeeded));

        vm.expectRevert();
        vm.prank(dave);
        governor.castVote(proposalId, 0);
    }

    function test_defeatedOnAbsoluteMajority() public {
        vm.startPrank(init);
        ghst.mint(alice, PROPOSAL_THRESHOLD);
        ghst.mint(bob, 555 * 1e17);
        ghst.mint(carol, 345 * 1e17 + 1);
        ghst.mint(dave, 21 * 1e18);
        vm.stopPrank();

        vm.roll(block.number + 1);

        (uint256 proposalId,,,,) = _proposeDummy(alice, 69);
        _waitForActive(proposalId);

        assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active));
        _castVoteWrapper(proposalId, bob, 0, true, false);
        assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active));
        _castVoteWrapper(proposalId, carol, 0, true, false);
        assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Defeated));

        vm.expectRevert();
        vm.prank(dave);
        governor.castVote(proposalId, 1);
    }

    function test_onlyOneActiveProposal() public {
        uint256 amount = PROPOSAL_THRESHOLD;
        vm.startPrank(init);
        ghst.mint(alice, amount);
        ghst.mint(bob, 2 * amount);
        ghst.mint(carol, 3 * amount);
        vm.stopPrank();

        vm.roll(block.number + 1);

        (uint256 proposalId,,,,) = _proposeDummy(alice, 69);
        _waitForActive(proposalId);

        vm.prank(alice);
        assertEq(governor.releaseLocked(proposalId), 0);
        assertEq(governor.lockedAmounts(proposalId), amount);
        assertEq(ghst.balanceOf(alice), 0);

        _castVoteWrapper(proposalId, alice, 1, false, false);
        (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) = governor.proposalVotes(proposalId);
        assertEq(againstVotes, 0);
        assertEq(forVotes, 0);
        assertEq(abstainVotes, 0);

        _castVoteWrapper(proposalId, bob, 1, true, true);
        (againstVotes, forVotes, abstainVotes) = governor.proposalVotes(proposalId);
        assertEq(againstVotes, 0);
        assertEq(forVotes, 2 * amount);
        assertEq(abstainVotes, 0);

        assertEq(ghst.balanceOf(alice), 0);
        assertEq(ghst.balanceOf(bob), 2 * amount);
        assertEq(ghst.balanceOf(carol), 3 * amount);

        _waitForSucceed(proposalId);

        vm.prank(alice);
        assertEq(governor.releaseLocked(proposalId), amount);
        assertEq(governor.lockedAmounts(proposalId), 0);
        assertEq(ghst.balanceOf(alice), amount);

        vm.prank(alice);
        assertEq(governor.releaseLocked(proposalId), 0);
        assertEq(governor.lockedAmounts(proposalId), 0);
        assertEq(ghst.balanceOf(alice), amount);

        assertEq(ghst.balanceOf(alice), amount);
        assertEq(ghst.balanceOf(bob), 2 * amount);
        assertEq(ghst.balanceOf(carol), 3 * amount);

        (proposalId,,,,) = _proposeDummy(bob, 420);
        _waitForActive(proposalId);

        _castVoteWrapper(proposalId, alice, 1, false, true);
        _castVoteWrapper(proposalId, carol, 1, true, true);

        vm.roll(block.number + 3);

        (uint256 newProposalId,,,,) = _proposeDummy(carol, 1337);
        assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Succeeded));
        assertEq(uint8(governor.state(newProposalId)), uint8(IGovernor.ProposalState.Pending));

        assertEq(ghst.balanceOf(alice), amount);
        assertEq(ghst.balanceOf(bob), 0);
        assertEq(ghst.balanceOf(carol), 0);

        vm.prank(bob);
        assertEq(governor.releaseLocked(proposalId), 0);
        assertEq(governor.lockedAmounts(proposalId), 2 * amount);
        assertEq(ghst.balanceOf(bob), 0);

        vm.roll(governor.proposalDeadline(proposalId) + 1);

        vm.prank(bob);
        assertEq(governor.releaseLocked(proposalId), 2 * amount);
        assertEq(governor.lockedAmounts(proposalId), 0);
        assertEq(ghst.balanceOf(bob), 2 * amount);

        vm.prank(carol);
        assertEq(governor.releaseLocked(newProposalId), 0);
        assertEq(governor.lockedAmounts(newProposalId), 3 * amount);
        assertEq(ghst.balanceOf(carol), 0);
    }

    function test_proposalCountWorks() public {
        assertEq(governor.proposalCount(), 0);

        uint256 i = 1;
        for (; i < 69; ) {
            {
                uint256 amount = governor.activeProposedLock() + PROPOSAL_THRESHOLD;

                if (amount + ghst.totalSupply() > type(uint208).max) {
                    break;
                }

                vm.prank(init);
                ghst.mint(alice, amount);
                vm.roll(block.number + 1);
            }

            (
                uint256 proposalId,
                address[] memory targets,
                uint256[] memory values,
                bytes[] memory calldatas,
                string memory description
            ) = _proposeDummy(alice, i);

            {
                (
                    address[] memory thisTargets,
                    uint256[] memory thisValues,
                    bytes[] memory thisCalldatas,
                    bytes32 thisDescriptionHash
                ) = governor.proposalDetails(proposalId);

                assertEq(thisTargets.length, targets.length);
                assertEq(thisValues.length, values.length);
                assertEq(thisCalldatas.length, calldatas.length);

                assertEq(thisTargets[0], targets[0]);
                assertEq(thisValues[0], values[0]);
                assertEq(thisCalldatas[0], calldatas[0]);
                assertEq(thisDescriptionHash, keccak256(bytes(description)));
            }

            {
                (
                    uint256 otherProposalId,
                    address[] memory otherTargets,
                    uint256[] memory otherValues,
                    bytes[] memory otherCalldatas,
                    bytes32 otherDescriptionHash
                ) = governor.proposalDetailsAt(i - 1);

                assertEq(otherTargets.length, targets.length);
                assertEq(otherValues.length, values.length);
                assertEq(otherCalldatas.length, calldatas.length);

                assertEq(otherProposalId, governor.hashProposal(targets, values, calldatas, keccak256(bytes(description))));
                assertEq(otherTargets[0], targets[0]);
                assertEq(otherValues[0], values[0]);
                assertEq(otherCalldatas[0], calldatas[0]);
                assertEq(otherDescriptionHash, keccak256(bytes(description)));
            }

            assertEq(governor.proposalCount(), i);
            unchecked { ++i; }
        }
    }

    function test_preventLateQuorumWorks() public {
        vm.startPrank(init);
        ghst.mint(alice, PROPOSAL_THRESHOLD);
        ghst.mint(bob, 50 * 1e18);
        ghst.mint(carol, 100 * 1e18);
        ghst.mint(dave, 100 * 1e18);
        ghst.mint(eve, 500 * 1e18);
        vm.stopPrank();

        vm.roll(block.number + 1);
        (uint256 proposalId,,,,) = _proposeDummy(alice, 69);

        assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Pending));
        vm.roll(block.number + VOTING_DELAY + 1);
        assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active));

        assertEq(governor.voteOf(proposalId, bob), 0);
        assertEq(governor.voteOf(proposalId, carol), 0);
        assertEq(governor.voteOf(proposalId, dave), 0);
        assertEq(governor.voteOf(proposalId, eve), 0);

        vm.roll(governor.proposalDeadline(proposalId));
        _castVoteWrapper(proposalId, bob, 1, false, false);
        vm.roll(governor.proposalDeadline(proposalId));
        _castVoteWrapper(proposalId, carol, 0, false, false);
        vm.roll(governor.proposalDeadline(proposalId));
        _castVoteWrapper(proposalId, dave, 1, true, false);
        vm.roll(governor.proposalDeadline(proposalId));
        _castVoteWrapper(proposalId, eve, 1, true, true);

        vm.prank(alice);
        governor.execute(proposalId);
        assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Executed));

        assertEq(governor.voteOf(proposalId, bob), 2);
        assertEq(governor.voteOf(proposalId, carol), 1);
        assertEq(governor.voteOf(proposalId, dave), 2);
        assertEq(governor.voteOf(proposalId, eve), 2);
    }

    function test_quorumNumeratorWorks() public {
        uint256 numerator = governor.quorumNumerator();
        uint256 denominator = governor.quorumDenominator();
        uint256 currentBlock = block.number;

        vm.startPrank(init);
        ghst.mint(alice, 1 * 1e18);
        ghst.mint(bob, 69 * 1e18);
        ghst.mint(carol, 420 * 1e18);
        ghst.mint(dave, 1337 * 1e18);
        ghst.mint(eve, 69420 * 1e18);
        vm.stopPrank();

        vm.roll(currentBlock + 2);
        assertEq(governor.quorum(currentBlock), 71247 * 1e18 * numerator / denominator);
    }

    function test_votingPowerTransfer() public {
        vm.startPrank(init);
        ghst.mint(alice, 35 * 1e18);
        ghst.mint(bob, 34 * 1e18);
        vm.stopPrank();

        vm.roll(block.number + 1);

        vm.expectRevert();
        vm.prank(alice);
        ghst.delegate(alice);

        vm.expectRevert();
        vm.prank(bob);
        ghst.delegate(bob);

        _assertVotesEqualToBalance();

        vm.prank(alice);
        ghst.transfer(bob, 420);
        _assertVotesEqualToBalance();

        uint256 aliceBalance = ghst.balanceOf(alice);
        vm.prank(alice);
        ghst.transfer(bob, aliceBalance);
        _assertVotesEqualToBalance();

        vm.prank(bob);
        ghst.transfer(carol, 1337);
        _assertVotesEqualToBalance();

        uint256 bobBalance = ghst.balanceOf(bob);
        vm.prank(bob);
        ghst.transfer(carol, bobBalance);
        _assertVotesEqualToBalance();

        vm.expectRevert();
        vm.prank(carol);
        ghst.delegate(carol);

        assertEq(ghst.getVotes(alice), 0);
        assertEq(ghst.getVotes(bob), 0);
        assertEq(ghst.getVotes(carol), 69 * 1e18);

        assertEq(ghst.balanceOf(alice), 0);
        assertEq(ghst.balanceOf(bob), 0);
        assertEq(ghst.balanceOf(carol), 69 * 1e18);

        vm.prank(init);
        ghst.burn(carol, 69 * 1e18);

        assertEq(ghst.getVotes(carol), 0);
        assertEq(ghst.balanceOf(carol), 0);
        assertEq(ghst.totalSupply(), 0);
    }

    function test_changesOfTotalSupplyHasNoAffectOnPastTotalSupply() public {
        vm.prank(init);
        ghst.mint(alice, 100);

        vm.roll(block.number + 1);
        assertEq(ghst.getPastTotalSupply(block.number - 1), 100);
        assertEq(ghst.totalSupply(), 100);

        vm.prank(init);
        ghst.burn(alice, 50);

        vm.roll(block.number + 1);
        assertEq(ghst.getPastTotalSupply(block.number - 2), 100);
        assertEq(ghst.getPastTotalSupply(block.number - 1), 50);
        assertEq(ghst.totalSupply(), 50);

        vm.prank(init);
        ghst.mint(bob, 150);

        vm.roll(block.number + 1);
        assertEq(ghst.getPastTotalSupply(block.number - 3), 100);
        assertEq(ghst.getPastTotalSupply(block.number - 2), 50);
        assertEq(ghst.getPastTotalSupply(block.number - 1), 200);
        assertEq(ghst.totalSupply(), 200);
    }

    function _proposeDummy(address who, uint256 index)
        private
        returns (
            uint256 proposalId,
            address[] memory targets,
            uint256[] memory values,
            bytes[] memory calldatas,
            string memory description
        )
    {
        targets = new address[](1);
        targets[0] = address(uint160(index + 1));

        values = new uint256[](1);
        values[0] = 0;

        calldatas = new bytes[](1);
        calldatas[0] = abi.encodeWithSignature("setVotingDelay(uint48)", 0);

        description = string.concat("Proposal #", Strings.toString(index));

        vm.prank(who);
        proposalId = governor.propose(
            targets,
            values,
            calldatas,
            description
        );

        vm.roll(block.number + 1);
    }

    function _castVoteWrapper(
        uint256 proposalId,
        address who,
        uint8 vote,
        bool isQuorumReached,
        bool isVoteSucceeded
    ) private {
        assertEq(governor.hasVoted(proposalId, who), false);
        vm.prank(who);
        governor.castVote(proposalId, vote);
        assertEq(governor.hasVoted(proposalId, who), true);
        assertEq(governor.quorumReached(proposalId), isQuorumReached);
        assertEq(governor.voteSucceeded(proposalId), isVoteSucceeded);
    }

    function _assertVotesEqualToBalance() private view {
        assertEq(ghst.getVotes(alice), ghst.balanceOf(alice));
        assertEq(ghst.getVotes(bob), ghst.balanceOf(bob));
        assertEq(ghst.getVotes(carol), ghst.balanceOf(carol));
    }

    function _waitForActive(uint256 proposalId) private {
        assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Pending));
        vm.roll(block.number + VOTING_DELAY + 1);
        assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active));
    }

    function _waitForSucceed(uint256 proposalId) private {
        assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active));
        vm.roll(governor.proposalDeadline(proposalId) + 1);
        assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Succeeded));
    }
}
