pragma solidity 0.8.20;

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

import "../../src/FatsoERC20.sol";
import "../../src/StinkyERC20.sol";
import "../../src/GhstERC20.sol";
import "../../src/GhostAuthority.sol";
import "../../src/StakingDistributor.sol";
import "../../src/Treasury.sol";
import "../../src/Staking.sol";
import "../../src/mocks/ERC20Mock.sol";
import "../../src/StandardBondingCalculator.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)
        );
        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);
        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);
        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);
        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();
    }
}
