pragma solidity 0.8.20;

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

import {Fatso} from "../../src/FatsoERC20.sol";
import {Stinky} from "../../src/StinkyERC20.sol";
import {Ghost} from "../../src/GhstERC20.sol";
import {GhostAuthority} from "../../src/GhostAuthority.sol";
import {GhostDistributor} from "../../src/StakingDistributor.sol";
import {GhostTreasury} from "../../src/Treasury.sol";
import {GhostStaking} from "../../src/Staking.sol";
import {ERC20Mock} from "../../src/mocks/ERC20Mock.sol";
import {GhostBondingCalculator} from "../../src/StandardBondingCalculator.sol";

import {ITreasury} from "../../src/interfaces/ITreasury.sol";

contract StakingDistributorTest is Test {
    address public constant OWNER    = 0x0000000000000000000000000000000000000001;
    address public constant GOVERNOR = 0x0000000000000000000000000000000000000002;
    address public constant GUARDIAN = 0x0000000000000000000000000000000000000003;
    address public constant OTHER    = 0x0000000000000000000000000000000000000004;
    address public constant ALICE    = 0x0000000000000000000000000000000000000005;
    address public constant BOB      = 0x0000000000000000000000000000000000000006;

    uint48 public constant EPOCH_LENGTH   = 2200;
    uint48 public constant EPOCH_NUMBER   = 1;
    uint48 public constant EPOCH_END_TIME = 1337;

    uint256 public constant INITIAL_INDEX  = 10819917194513808e56;
    uint256 public constant AMOUNT = 69 * 1e18;

    Fatso ftso;
    Stinky stnk;
    Ghost ghst;
    ERC20Mock reserve;

    GhostTreasury treasury;
    GhostStaking staking;
    GhostDistributor distributor;
    GhostAuthority authority;
    GhostBondingCalculator calculator;

    uint256 public constant TEN_PERCENT = 1e5;

    function setUp() public {
        vm.startPrank(OWNER);
        authority = new GhostAuthority(
            GOVERNOR,
            GUARDIAN,
            OWNER,
            OWNER
        );
        reserve = new ERC20Mock("Reserve Token", "RET");
        ftso = new Fatso(address(authority), "Fatso", "FTSO");
        stnk = new Stinky(INITIAL_INDEX, "Stinky", "STNK");
        ghst = new Ghost(address(stnk), "Ghost", "GHST");
        staking = new GhostStaking(
            address(ftso),
            address(stnk),
            address(ghst),
            EPOCH_LENGTH,
            EPOCH_NUMBER,
            EPOCH_END_TIME,
            address(authority),
            0
        );
        treasury = new GhostTreasury(address(ftso), 69, address(authority));
        calculator = new GhostBondingCalculator(address(ftso), 1, 1);
        distributor = new GhostDistributor(
            address(treasury),
            address(ftso),
            address(staking),
            address(authority),
            TEN_PERCENT
        );
        stnk.initialize(address(staking), address(treasury), address(ghst));
        ghst.initialize(address(staking));
        vm.stopPrank();

        vm.startPrank(GOVERNOR);
        staking.setDistributor(address(distributor));
        authority.pushVault(address(treasury));
        vm.stopPrank();
    }

    function test_distribute_onlyByStaking() public {
        _prepareTreasury();
        vm.expectRevert();
        vm.prank(OTHER);
        distributor.distribute();

        vm.prank(address(staking));
        distributor.distribute();
    }

    function test_bounty_onlyByGovernor(address who) public {
        vm.assume(who != GOVERNOR);
        _prepareTreasury();
        assertEq(ftso.balanceOf(address(staking)), 0);

        vm.prank(GOVERNOR);
        distributor.setBounty(1337);

        vm.prank(address(staking));
        distributor.retrieveBounty();
        assertEq(ftso.balanceOf(address(staking)), 1337);
    }

    function test_nextRewardFor_zeroIfAddressIsEmpty() public view {
        assertEq(ftso.balanceOf(BOB), 0);
        assertEq(distributor.nextRewardFor(BOB), 0);
    }

    function test_nextRewardFor_nextRewardForAddress() public {
        _prepareTreasury();
        vm.startPrank(BOB);
        reserve.mint(BOB, AMOUNT);
        reserve.approve(address(treasury), AMOUNT);
        uint256 send = treasury.deposit(address(reserve), AMOUNT, treasury.tokenValue(address(reserve), AMOUNT));
        vm.stopPrank();
        assertEq(ftso.balanceOf(BOB), send);
        assertEq(distributor.nextRewardFor(BOB), send * TEN_PERCENT / 1e6);
    }

    function test_setBounty_shouldRevertIfNotByGovernor(address who) public {
        vm.assume(who != GOVERNOR);
        assertEq(distributor.bounty(), 0);
        vm.expectRevert();
        vm.prank(who);
        distributor.setBounty(69);
        assertEq(distributor.bounty(), 0);
    }

    function test_setBounty_GOVERNORShouldSet() public {
        _prepareTreasury();
        assertEq(distributor.bounty(), 0);
        vm.prank(GOVERNOR);
        distributor.setBounty(1337);
        assertEq(distributor.bounty(), 1337);
    }

    function test_setPools_shouldRevertIfNotGovernor(address who) public {
        vm.assume(who != GOVERNOR);
        address[] memory newPools = new address[](1);
        newPools[0] = who;
        vm.expectRevert();
        vm.prank(who);
        distributor.setPools(newPools);
    }

    function test_setPools_GOVERNORShouldSet() public {
        address[] memory newPools = new address[](1);
        newPools[0] = OTHER;
        vm.prank(GOVERNOR);
        distributor.setPools(newPools);
        assertEq(distributor.pools(0), OTHER);
    }

    function test_removePools_shouldRevertIfNotGovernor(address who) public {
        vm.assume(who != GOVERNOR);
        address[] memory newPools = new address[](1);
        newPools[0] = OTHER;
        vm.prank(GOVERNOR);
        distributor.setPools(newPools);

        vm.expectRevert();
        vm.prank(who);
        distributor.removePool(0);
    }

    function test_removePools_GOVERNORShouldRemove() public {
        address[] memory newPools = new address[](1);
        newPools[0] = OTHER;
        vm.startPrank(GOVERNOR);
        distributor.setPools(newPools);
        assertEq(distributor.pools(0), OTHER);
        distributor.removePool(0);
        vm.stopPrank();
    }

    function test_addPool_shouldRevertIfNotGovernor(address who) public {
        vm.assume(who != GOVERNOR);
        vm.expectRevert();
        vm.prank(who);
        distributor.addPool(OTHER);
    }

    function test_addPool_shouldPushPoolWhenIndexTaken() public {
        vm.prank(GOVERNOR);
        distributor.addPool(OTHER);
        assertEq(distributor.pools(0), OTHER);
    }

    function test_setAdjustment_shouldRevertIfNotGovernor(address who) public {
        vm.assume(who != GOVERNOR && who != GUARDIAN);
        vm.expectRevert();
        vm.prank(who);
        distributor.setAdjustment(69, 1337, true);
    }

    function test_setAdjustment_GOVERNORShouldIncreaseAdjustment() public {
        vm.prank(GOVERNOR);
        distributor.setAdjustment(69, 1337, false);
    }

    function test_setAdjustment_shouldPopulateAdjustmentToRewardRateAfterRebase() public {
        vm.prank(GOVERNOR);
        distributor.setAdjustment(69, TEN_PERCENT * 2, true);
        _prepareTreasury();

        assertEq(distributor.rewardRate(), TEN_PERCENT);
        (,, uint256 end,) = staking.epoch();
        skip(end);
        vm.roll(block.number + 1);
        staking.rebase();
        assertEq(distributor.rewardRate(), TEN_PERCENT + 69);
    }

    function test_setAdjustment_willNotAdjustIfRateIsZero() public {
        vm.prank(GOVERNOR);
        distributor.setAdjustment(0, 1337, true);
        _prepareTreasury();

        assertEq(distributor.rewardRate(), TEN_PERCENT);
        (,, uint256 end,) = staking.epoch();
        skip(end);
        staking.rebase();
        assertEq(distributor.rewardRate(), TEN_PERCENT);
    }

    function test_setAdjustment_willStopIncreaseWhenTargetMet() public {
        vm.prank(GOVERNOR);
        distributor.setAdjustment(69, 1337, true);
        _prepareTreasury();

        assertEq(distributor.rewardRate(), TEN_PERCENT);
        (,, uint256 end,) = staking.epoch();
        skip(end);
        vm.roll(block.number + 1);
        staking.rebase();
        assertEq(distributor.rewardRate(), 1337);
    }

    function test_setAdjustment_willStopDecreaseWhenTargetMet() public {
        vm.prank(GOVERNOR);
        distributor.setAdjustment(TEN_PERCENT, 1337, false);
        _prepareTreasury();

        assertEq(distributor.rewardRate(), TEN_PERCENT);
        (,, uint256 end,) = staking.epoch();
        skip(end);
        vm.roll(block.number + 1);
        staking.rebase();
        assertEq(distributor.rewardRate(), 1337);
    }

    function _prepareTreasury() internal {
        vm.startPrank(GOVERNOR);
        treasury.enable(ITreasury.STATUS.REWARDMANAGER, address(distributor), address(0));
        treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, ALICE, address(0));
        treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, BOB, address(0));
        treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator));
        vm.stopPrank();

        vm.startPrank(ALICE);
        reserve.mint(ALICE, AMOUNT);
        reserve.approve(address(treasury), AMOUNT);
        treasury.deposit(address(reserve), AMOUNT, treasury.tokenValue(address(reserve), AMOUNT));
        vm.stopPrank();
    }
}
