pragma solidity 0.8.20;

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

import {Ghost} from "../../src/GhstERC20.sol";
import {Stinky} from "../../src/StinkyERC20.sol";
import {GhostAuthority} from "../../src/GhostAuthority.sol";
import {GhostStaking} from "../../src/Staking.sol";

import {ERC20PermitTest} from "./Permit.t.sol";
import {ERC20AllowanceTest} from "./Allowance.t.sol";
import {ERC20TransferTest} from "./Transfer.t.sol";
import {ERC20VotesTest} from "./Votes.t.sol";

contract GhostTest is
    Test,
    ERC20PermitTest,
    ERC20AllowanceTest,
    ERC20TransferTest,
    ERC20VotesTest
{
    uint256 constant public INITIAL_INDEX = 10819917194513808e56;

    address constant INITIALIZER       = 0x0000000000000000000000000000000000000001;
    address constant ALICE             = 0x0000000000000000000000000000000000000003;
    address constant BOB               = 0x0000000000000000000000000000000000000004;
    address constant TREASURY          = 0x0000000000000000000000000000000000000005;
    uint256 constant AMOUNT            = 69;
    uint256 constant MAX_AMOUNT        = type(uint256).max;

    string constant NAME               = "Ghost Test Name";
    string constant SYMBOL             = "GHSTTST";

    Stinky stnk;
    Ghost ghst;
    GhostAuthority public authority;
    GhostStaking public staking;

    function setUp() public {
        vm.startPrank(INITIALIZER);
        authority = new GhostAuthority(
            INITIALIZER,
            INITIALIZER,
            INITIALIZER,
            INITIALIZER
        );
        stnk = new Stinky(INITIAL_INDEX, "Stinky", "STNK");
        ghst = new Ghost(address(stnk), NAME, SYMBOL);
        staking = new GhostStaking(
            address(0),
            address(stnk),
            address(ghst),
            69,
            1337,
            1337,
            address(authority),
            0
        );
        stnk.initialize(address(staking), TREASURY, address(ghst));
        ghst.initialize(address(staking));
        vm.stopPrank();

        initializePermit(address(ghst), AMOUNT, MAX_AMOUNT);
        initializeAllowance(ALICE, BOB, address(ghst), AMOUNT, MAX_AMOUNT, AMOUNT);
        initializeTransfer(ALICE, BOB, address(ghst), AMOUNT, MAX_AMOUNT);
        initializeVotes(ALICE, BOB, address(ghst), AMOUNT);
    }

    function test_isConstructedCorrectly() public view {
        assertEq(ghst.name(), NAME);
        assertEq(ghst.symbol(), SYMBOL);
        assertEq(ghst.decimals(), 18);
        assertEq(ghst.staking(), address(staking));
        assertEq(ghst.stnk(), address(stnk));
        assertEq(ghst.index(), stnk.index());
    }

    function test_mint_couldBeDoneByStaking() public {
        assertEq(ghst.totalSupply(), 0);
        vm.prank(address(staking));
        ghst.mint(ALICE, AMOUNT);
        assertEq(ghst.totalSupply(), AMOUNT);
        assertEq(ghst.balanceOf(ALICE), AMOUNT);
    }

    function test_mint_couldNotFromArbitraryAddress(address who) public {
        vm.assume(who != address(staking));
        vm.expectRevert();
        vm.prank(who);
        ghst.mint(ALICE, AMOUNT);
        assertEq(ghst.totalSupply(), 0);
        assertEq(ghst.balanceOf(who), 0);
    }

    function test_burn_couldBeDoneByOwner() public {
        _mintTokens(ALICE, AMOUNT);
        assertEq(ghst.totalSupply(), AMOUNT);
        vm.prank(address(staking));
        ghst.burn(ALICE, AMOUNT);
        assertEq(ghst.totalSupply(), 0);
        assertEq(ghst.balanceOf(ALICE), 0);
    }

    function test_burn_couldBeDoneByOwnerPartially(uint256 part) public {
        vm.assume(part <= AMOUNT);
        _mintTokens(ALICE, AMOUNT);
        assertEq(ghst.totalSupply(), AMOUNT);
        vm.prank(address(staking));
        ghst.burn(ALICE, part);
        assertEq(ghst.totalSupply(), AMOUNT - part);
        assertEq(ghst.balanceOf(ALICE), AMOUNT - part);
    }

    function test_burn_couldNotBurnMoreThanBalance() public {
        _mintTokens(ALICE, AMOUNT);
        assertEq(ghst.totalSupply(), AMOUNT);
        vm.expectRevert();
        vm.prank(ALICE);
        ghst.burn(ALICE, AMOUNT + 1);
        assertEq(ghst.totalSupply(), AMOUNT);
        assertEq(ghst.balanceOf(ALICE), AMOUNT);
    }

    function test_burn_couldNotFromArbitraryAddress(address who) public {
        vm.assume(who != address(staking));
        _mintTokens(ALICE, AMOUNT);
        vm.expectRevert();
        vm.prank(who);
        ghst.burn(ALICE, AMOUNT);
        assertEq(ghst.totalSupply(), AMOUNT);
        assertEq(ghst.balanceOf(ALICE), AMOUNT);
    }

    function test_balanceToCalculatesCorrectly(uint256 balanceToAmount) public view {
        vm.assume(balanceToAmount < type(uint128).max - 1);
        uint256 index = ghst.index();
        assertEq(ghst.balanceTo(balanceToAmount), balanceToAmount * 1e18 / index);
    }

    function test_balanceFromCalculatesCorrectly(uint256 balanceFromAmount) public view {
        vm.assume(balanceFromAmount < type(uint128).max - 1);
        uint256 index = ghst.index();
        assertEq(ghst.balanceFrom(balanceFromAmount), balanceFromAmount * index / 1e18);
    }

    function _mintTokens(address who, uint256 value) internal {
        uint256 totalSupply = ghst.totalSupply();
        vm.prank(address(staking));
        ghst.mint(who, value);
        assertEq(ghst.totalSupply(), totalSupply + value);
    }

    function _mintTransferTokens(address who, uint256 value) internal override {
        _mintTokens(who, value);
    }

    function _mintAllowanceTokens(address who, uint256 value) internal override {
        _mintTokens(who, value);
    }

    function _mintPermitTokens(address who, uint256 value) internal override {
        _mintTokens(who, value);
    }

    function _mintVotesTokens(address who, uint256 value) internal override {
        _mintTokens(who, value);
    }

    function _burnVotesTokens(address who, uint256 value) internal override {
        vm.prank(address(staking));
        ghst.burn(who, value);
    }

    function _getCurrentTotalSupply() internal override view returns (uint256) {
        return ghst.totalSupply();
    }
}
