pragma solidity 0.8.20;

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

import "../../src/FatsoERC20.sol";
import "../../src/GhostAuthority.sol";
import "../../src/Treasury.sol";
import "../../src/StandardBondingCalculator.sol";
import "../../src/mocks/ERC20Mock.sol";

contract GhostTreasuryTest 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;

    uint256 public constant amount = 69 * 1e18;

    Fatso ftso;
    ERC20Mock reserve;
    ERC20Mock liquidity;
    GhostTreasury treasury;
    GhostAuthority authority;
    GhostBondingCalculator calculator;

    function setUp() public {
        vm.startPrank(owner);
        authority = new GhostAuthority(
            governor,
            guardian,
            owner,
            owner
        );
        reserve = new ERC20Mock("Reserve Token", "RET");
        liquidity = new ERC20Mock("Liquidity Token", "LDT");
        ftso = new Fatso(address(authority), "Fatso", "FTSO");
        treasury = new GhostTreasury(address(ftso), 69, address(authority));
        calculator = new GhostBondingCalculator(address(ftso), 1, 1);
        vm.stopPrank();

        vm.prank(governor);
        authority.pushVault(address(treasury));

        vm.startPrank(alice);
        reserve.mint(alice, amount);
        reserve.approve(address(treasury), type(uint256).max);
        ftso.approve(address(treasury), type(uint256).max);
        vm.stopPrank();
    }

    function test_deposit_onlyIfApprovedTokenAndApprovedAddress() public {
        vm.expectRevert();
        vm.prank(alice);
        treasury.deposit(address(reserve), amount, 0);

        vm.prank(governor);
        treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, alice, address(0));

        vm.expectRevert();
        vm.prank(alice);
        treasury.deposit(address(reserve), amount, 0);

        vm.prank(governor);
        treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator));
        vm.prank(alice);
        uint256 send = treasury.deposit(address(reserve), amount, 0);

        assertEq(ftso.balanceOf(alice), send);
        assertEq(reserve.balanceOf(address(treasury)), amount);
    }

    function test_withdraw_onlyIfApprovedTokenAndApprovedAddress() public {
        vm.startPrank(governor);
        treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, alice, address(0));
        treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator));
        vm.stopPrank();

        vm.prank(alice);
        treasury.deposit(address(reserve), amount, 0);

        vm.expectRevert();
        vm.prank(alice);
        treasury.withdraw(address(reserve), amount);

        vm.prank(governor);
        treasury.enable(ITreasury.STATUS.RESERVESPENDER, alice, address(0));

        vm.prank(alice);
        treasury.withdraw(address(reserve), amount);

        assertEq(ftso.balanceOf(alice), 0);
        assertEq(reserve.balanceOf(address(treasury)), 0);

    }

    function test_manage_onlyIfApprovedTokenAndApprovedAddress() public {
        vm.startPrank(governor);
        treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, alice, address(0));
        treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator));
        vm.stopPrank();

        uint256 tokenValue = treasury.tokenValue(address(reserve), amount);
        vm.prank(alice);
        uint256 send = treasury.deposit(address(reserve), amount, tokenValue);

        vm.expectRevert();
        vm.prank(alice);
        treasury.manage(address(reserve), amount);

        vm.prank(governor);
        treasury.enable(ITreasury.STATUS.RESERVEMANAGER, alice, address(0));

        vm.prank(alice);
        treasury.manage(address(reserve), amount);

        assertEq(ftso.balanceOf(alice), send);
        assertEq(reserve.balanceOf(alice), amount);
        assertEq(reserve.balanceOf(address(treasury)), 0);
    }

    function test_mint_onlyIfApprovedTokenAndApprovedAddress() public {
        vm.startPrank(governor);
        treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, alice, address(0));
        treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator));
        vm.stopPrank();

        uint256 tokenValue = treasury.tokenValue(address(reserve), amount);
        vm.prank(alice);
        uint256 send = treasury.deposit(address(reserve), amount, tokenValue);

        vm.expectRevert();
        vm.prank(alice);
        treasury.mint(alice, 69);

        vm.prank(governor);
        treasury.enable(ITreasury.STATUS.REWARDMANAGER, alice, address(0));

        vm.prank(alice);
        treasury.mint(alice, 69);

        assertEq(ftso.balanceOf(alice), send + 69);
        assertEq(reserve.balanceOf(address(treasury)), amount);
    }

    function test_auditTreasuryReserves() public {
        vm.startPrank(governor);
        treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, alice, address(0));
        treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator));
        vm.stopPrank();

        uint256 tokenValue = treasury.tokenValue(address(reserve), amount);
        vm.prank(alice);
        treasury.deposit(address(reserve), amount, tokenValue);

        vm.prank(governor);
        treasury.auditReserves();

        assertEq(treasury.permissions(ITreasury.STATUS.RESERVETOKEN, address(reserve)), true);
        assertEq(treasury.registry(ITreasury.STATUS.RESERVETOKEN, 0), address(reserve));
        assertEq(
            treasury.tokenValue(address(reserve), reserve.balanceOf(address(treasury))),
            treasury.totalReserves());
    }

    function test_randomAddressCouldNotEnableStatusAndCalculator(address who) public {
        vm.assume(who != governor);

        vm.expectRevert();
        vm.prank(who);
        treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(0));

        vm.expectRevert();
        vm.prank(who);
        treasury.enable(ITreasury.STATUS.LIQUIDITYTOKEN, address(liquidity), address(calculator));

        vm.expectRevert();
        vm.prank(who);
        treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, msg.sender, address(0));
    }

    function test_enableStatusAndCalculator() public {
        vm.startPrank(governor);
        treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator));
        treasury.enable(ITreasury.STATUS.LIQUIDITYTOKEN, address(liquidity), address(calculator));
        treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, msg.sender, address(0));
        vm.stopPrank();

        assertEq(treasury.registry(ITreasury.STATUS.RESERVETOKEN, 0), address(reserve));
        assertEq(treasury.registry(ITreasury.STATUS.LIQUIDITYTOKEN, 0), address(liquidity));
        assertEq(treasury.bondCalculator(address(liquidity)), address(calculator));
        vm.expectRevert();
        treasury.registry(ITreasury.STATUS.RESERVEDEPOSITOR, 0);

        assertEq(treasury.permissions(ITreasury.STATUS.RESERVETOKEN, address(reserve)), true);
        assertEq(treasury.permissions(ITreasury.STATUS.LIQUIDITYTOKEN, address(liquidity)), true);
        assertEq(treasury.permissions(ITreasury.STATUS.RESERVEDEPOSITOR, msg.sender), true);
    }

    function test_randomAddressCouldNotDisableStatusByAddress(address who) public {
        vm.assume(who != governor && who != guardian);

        vm.startPrank(governor);
        treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator));
        treasury.enable(ITreasury.STATUS.LIQUIDITYTOKEN, address(liquidity), address(calculator));
        treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, msg.sender, address(0));
        vm.stopPrank();

        vm.expectRevert();
        vm.prank(who);
        treasury.disable(ITreasury.STATUS.RESERVETOKEN, address(reserve));

        vm.expectRevert();
        vm.prank(who);
        treasury.disable(ITreasury.STATUS.LIQUIDITYTOKEN, address(liquidity));

        vm.expectRevert();
        vm.prank(who);
        treasury.disable(ITreasury.STATUS.RESERVEDEPOSITOR, msg.sender);
    }

    function test_disableStatusByAddress() public {
        vm.startPrank(governor);
        treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator));
        treasury.enable(ITreasury.STATUS.LIQUIDITYTOKEN, address(liquidity), address(calculator));
        treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, msg.sender, address(0));
        vm.stopPrank();

        vm.startPrank(governor);
        treasury.disable(ITreasury.STATUS.RESERVETOKEN, address(reserve));
        treasury.disable(ITreasury.STATUS.LIQUIDITYTOKEN, address(liquidity));
        treasury.disable(ITreasury.STATUS.RESERVEDEPOSITOR, msg.sender);
        vm.stopPrank();

        assertEq(treasury.permissions(ITreasury.STATUS.RESERVETOKEN, address(reserve)), false);
        assertEq(treasury.permissions(ITreasury.STATUS.LIQUIDITYTOKEN, address(liquidity)), false);
        assertEq(treasury.permissions(ITreasury.STATUS.RESERVEDEPOSITOR, msg.sender), false);
    }

    function test_mainnet_disableReserveAndLiquidity() public {
        address realDexPair = 0xB20bd5D04BE54f870D5C0d3cA85d82b34B836405;

        vm.startPrank(governor);
        treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator));
        treasury.enable(ITreasury.STATUS.LIQUIDITYTOKEN, realDexPair, address(calculator));
        vm.stopPrank();

        assertEq(treasury.tokenValue(address(reserve), 1e18), 1e9);

        (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(realDexPair).getReserves();
        uint256 reserves = reserve0 * reserve1 / 1e6;
        uint256 reserveEps = reserves * 1e5 / 1e7; // 1%
        uint256 totalSupply = IUniswapV2Pair(realDexPair).totalSupply();
        uint256 liquidityValue = (treasury.tokenValue(realDexPair, totalSupply) / 2)**2;

        assertEq(liquidityValue + reserveEps >= reserves, true);
        assertEq(liquidityValue - reserveEps <= reserves, true);

        vm.startPrank(governor);
        treasury.disable(ITreasury.STATUS.RESERVETOKEN, address(reserve));
        treasury.disable(ITreasury.STATUS.LIQUIDITYTOKEN, realDexPair);
        vm.stopPrank();

        assertEq(treasury.tokenValue(address(reserve), 1e18), 0);
        assertEq(treasury.tokenValue(realDexPair, 1e18), 0);
    }

    function test_mainnet_tokenValueIsCorret() public {
        address realDexPair = 0xB20bd5D04BE54f870D5C0d3cA85d82b34B836405;

        vm.startPrank(governor);
        treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator));
        treasury.enable(ITreasury.STATUS.LIQUIDITYTOKEN, realDexPair, address(calculator));
        vm.stopPrank();

        assertEq(treasury.tokenValue(address(reserve), 1e18), 1e9);

        (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(realDexPair).getReserves();
        uint256 reserves = reserve0 * reserve1 / 1e6;
        uint256 reserveEps = reserves * 1e5 / 1e7; // 1%
        uint256 totalSupply = IUniswapV2Pair(realDexPair).totalSupply();
        uint256 liquidityValue = (treasury.tokenValue(realDexPair, totalSupply) / 2)**2;

        assertEq(liquidityValue + reserveEps >= reserves, true);
        assertEq(liquidityValue - reserveEps <= reserves, true);
    }

    function test_randomAddressCouldNotTriggerTimelock(address who) public {
        vm.assume(who != governor);
        vm.expectRevert();
        vm.prank(who);
        treasury.toggleTimelock();
    }

    function test_triggerTimelock() public {
        assertEq(treasury.timelockEnabled(), false);
        assertEq(treasury.onChainGovernanceTimelock(), 0);

        vm.prank(governor);
        treasury.toggleTimelock();
        uint256 queuedTime = block.number + 69 * 7;
        assertEq(treasury.timelockEnabled(), false);
        assertEq(treasury.onChainGovernanceTimelock(), queuedTime);

        vm.roll(queuedTime + 1);
        vm.prank(governor);
        treasury.toggleTimelock();
        assertEq(treasury.timelockEnabled(), true);
        assertEq(treasury.onChainGovernanceTimelock(), 0);

        vm.prank(governor);
        treasury.toggleTimelock();
        queuedTime = block.number + 69 * 7;
        assertEq(treasury.timelockEnabled(), true);
        assertEq(treasury.onChainGovernanceTimelock(), queuedTime);

        vm.roll(queuedTime + 1);
        vm.prank(governor);
        treasury.toggleTimelock();
        assertEq(treasury.timelockEnabled(), false);
        assertEq(treasury.onChainGovernanceTimelock(), 0);
    }
}
