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/BondDepository.sol";
import "../../src/mocks/ERC20Mock.sol";
import "../../src/StandardBondingCalculator.sol";

contract GhostBondDepositoryTest is Test {
    uint256 public constant TOTAL_INITIAL_SUPPLY = 5000000000000000;
    uint256 public constant LARGE_APPROVAL   = 100000000000000000000000000000000;
    uint256 public constant INITIAL_INDEX    = 10819917194513808e56;
    uint48 public constant EPOCH_LENGTH      = 2200;
    uint48 public constant EPOCH_NUMBER      = 1;
    uint48 public constant EPOCH_END_TIME    = 1337;

    uint256 public constant initialMint      = 1000000000000000000000000;
    uint256 public constant initialDeposit   = 100000000000000000000000;
    uint256 public constant capacity         = 10000e9;
    uint256 public constant initialPrice     = 400e9;
    uint256 public constant buffer           = 2e5;

    address constant initializer             = 0x0000000000000000000000000000000000000001;
    address constant governor                = 0x0000000000000000000000000000000000000003;
    address constant guardian                = 0x0000000000000000000000000000000000000004;
    address constant policy                  = 0x0000000000000000000000000000000000000005;
    address constant vault                   = 0x0000000000000000000000000000000000000006;
    address constant alice                   = 0x0000000000000000000000000000000000000007;
    address constant bob                     = 0x0000000000000000000000000000000000000008;

    uint256 public constant vesting          = 100;
    uint256 public constant timeToConclusion = 60 * 60 * 24;
    uint256 public constant depositInterval  = 60 * 60 * 4;
    uint256 public constant tuneInterval     = 60 * 60;

    uint256 public conclusion;

    ERC20Mock reserve;
    Fatso ftso;
    Stinky stnk;
    Ghost ghst;
    GhostStaking staking;
    GhostTreasury treasury;
    GhostAuthority authority;
    GhostBondDepository depository;
    GhostBondingCalculator calculator;

    function setUp() public {
        vm.startPrank(initializer);
        authority = new GhostAuthority(
            governor,
            guardian,
            policy,
            vault
        );
        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);
        stnk.initialize(address(staking), address(treasury), address(ghst));
        ghst.initialize(address(staking));
        depository = new GhostBondDepository(
            address(authority),
            address(ftso),
            address(ghst),
            address(staking),
            address(treasury)
        );
        vm.stopPrank();
        _createFirstBond();
    }

    function _createFirstBond() internal {
        vm.startPrank(governor);
        authority.pushVault(address(treasury));
        treasury.enable(ITreasury.STATUS.REWARDMANAGER, address(depository), address(0));
        treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, alice, address(0));
        treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator));
        vm.stopPrank();

        vm.startPrank(alice);
        reserve.mint(alice, initialMint);
        reserve.approve(address(treasury), type(uint256).max);
        treasury.deposit(address(reserve), initialMint, treasury.tokenValue(address(reserve), initialMint) / 2);
        assertEq(ftso.totalSupply(), treasury.baseSupply());

        reserve.mint(alice, initialMint);
        reserve.approve(address(depository), type(uint256).max);
        vm.stopPrank();

        conclusion = block.timestamp + timeToConclusion;

        vm.prank(policy);
        depository.create(
            [capacity, initialPrice, buffer],
            [vesting, conclusion],
            address(reserve),
            [uint32(depositInterval), uint32(tuneInterval)],
            [false, true]
        );
    }

    function test_shouldCreateMarket() public view {
        assertEq(depository.isLive(0), true);
    }

    function test_shouldConcludeInCorrectAmountOfTime() public view {
        (, , , uint48 concludes,) = depository.terms(0);
        assertEq(concludes, uint48(conclusion));
        (, , uint48 length, , ,) = depository.metadatas(0);
        assertEq(length, timeToConclusion);
    }

    function test_shouldSetMaxPayoutToCorrectPercentageOfCapacity() public view {
        (, , , , uint256 maxPayout, ,) = depository.markets(0);
        assertEq(maxPayout, capacity / 6);
    }

    function test_shouldReturnIdsOfAllMarkets() public {
        vm.prank(policy);
        depository.create(
            [capacity, initialPrice, buffer],
            [vesting, conclusion],
            address(reserve),
            [uint32(depositInterval), uint32(tuneInterval)],
            [false, true]
        );

        uint256[] memory liveMarkets = depository.liveMarkets();
        assertEq(liveMarkets.length, 2);
        assertEq(liveMarkets[0], 0);
        assertEq(liveMarkets[1], 1);
    }

    function test_shouldUpdateIdsOfMarkets() public {
        vm.startPrank(policy);
        depository.create(
            [capacity, initialPrice, buffer],
            [vesting, conclusion],
            address(reserve),
            [uint32(depositInterval), uint32(tuneInterval)],
            [false, true]
        );
        depository.close(0);
        vm.stopPrank();

        uint256[] memory liveMarkets = depository.liveMarkets();
        assertEq(liveMarkets.length, 1);
        assertEq(liveMarkets[0], 1);
    }

    function test_shouldIncludeIdInLiveMarketsForQuotePeople() public view {
        uint256[] memory liveMarketsFor = depository.liveMarketsFor(address(reserve));
        assertEq(liveMarketsFor.length, 1);
        assertEq(liveMarketsFor[0], 0);
    }

    function test_shouldStartWithPriceAtInitialPrice() public view {
        assertEq(depository.marketPrice(0), initialPrice);
    }

    function test_shouldGiveAccuratePayoutForPrice() public view {
        uint256 price = depository.marketPrice(0);
        uint256 amount = 10_000 * 1e18;
        uint256 expectedPrice = amount / price;
        assertEq(depository.payoutFor(0, amount), expectedPrice);
    }

    function test_shouldDecayDebt() public {
        (, , , uint256 totalDebt, , ,) = depository.markets(0);

        skip(depositInterval);
        vm.prank(alice);
        depository.deposit(0, 0, initialPrice, alice, alice);

        (, , , uint256 newTotalDebt, , ,) = depository.markets(0);
        assertEq(totalDebt > newTotalDebt, true);
    }

    function test_shouldStartAdjustmentIfBehindSchedule() public {
        skip(depositInterval);

        uint256 amount = 10_000 * 1e18;
        vm.prank(alice);
        depository.deposit(0, amount, initialPrice, alice, alice);

        (, , , bool active) = depository.adjustments(0);
        assertEq(active, true);
    }

    function test_adjustmentShouldLowerControlVariableByChangeInTuneIntervalIfBehind() public {
        (, uint64 ctrlVariable, , ,) = depository.terms(0);
        uint256 amount = 10_000 * 1e18;

        vm.startPrank(alice);
        depository.deposit(0, amount, initialPrice, alice, alice);
        skip(depositInterval);
        (uint64 change, , ,) = depository.adjustments(0);
        depository.deposit(0, amount, initialPrice, alice, alice);
        vm.stopPrank();

        (, uint64 newCtrlVariable, , ,) = depository.terms(0);
        assertEq(newCtrlVariable, ctrlVariable - change);
    }

    function test_adjustmentShouldLowerControlVariableByHalfOfTuneInterval() public {
        skip(depositInterval);
        (, uint64 ctrlVariable, , ,) = depository.terms(0);
        uint256 amount = 10_000 * 1e18;

        vm.startPrank(alice);
        depository.deposit(0, amount, initialPrice, alice, alice);
        (uint64 change, , ,) = depository.adjustments(0);
        skip(tuneInterval / 2);

        depository.deposit(0, amount, initialPrice, alice, alice);
        (, uint64 newCtrlVariable, , ,) = depository.terms(0);
        vm.stopPrank();

        uint256 lowerBound = (ctrlVariable - change / 2) * 9999 / 10000;
        assertEq(newCtrlVariable <= ctrlVariable - (change / 2), true);
        assertEq(newCtrlVariable > lowerBound, true);
    }

    function test_adjustmentShouldContinueLoweringOverMultipleDepositsInSameInterval() public {
        (, uint64 ctrlVariable, , ,) = depository.terms(0);
        uint256 amount = 10_000 * 1e18;

        vm.startPrank(alice);
        depository.deposit(0, amount, initialPrice, alice, alice);
        (uint64 change, , ,) = depository.adjustments(0);
        skip(tuneInterval / 2);
        depository.deposit(0, amount, initialPrice, alice, alice);
        skip(tuneInterval / 2);
        depository.deposit(0, amount, initialPrice, alice, alice);
        vm.stopPrank();

        (, uint64 newCtrlVariable, , ,) = depository.terms(0);
        assertEq(newCtrlVariable, ctrlVariable - change);
    }

    function test_shouldAllowDeposit() public {
        uint256 amount = 10_000 * 1e18;
        vm.prank(alice);
        depository.deposit(0, amount, initialPrice, alice, alice);

        uint256[] memory arr = depository.indexesFor(alice);
        assertEq(arr.length, 1);
    }

    function test_shouldNotAllowDepositGreaterThanMaxPayout() public {
        uint256  amount = 6_700_000 * 1e18;
        vm.expectRevert();
        vm.prank(alice);
        depository.deposit(0, amount, initialPrice, alice, alice);
    }

    function test_shouldNotRedeemAfterVested() public {
        uint256 balance = ftso.balanceOf(alice);
        uint256 amount = 10_000 * 1e18; // 10,000
        vm.startPrank(alice);
        depository.deposit(0, amount, initialPrice, alice, alice);
        depository.redeemAll(alice, true);
        vm.stopPrank();
        assertEq(ftso.balanceOf(alice), balance);
    }

    function test_shouldRedeemAfterVested() public {
        uint256 amount = 10_000 * 1e18; // 10,000
        vm.startPrank(alice);
        (uint256 expectedPayout, ,) = depository.deposit(0, amount, initialPrice, alice, alice);

        skip(depositInterval);
        depository.redeemAll(alice, true);
        vm.stopPrank();

        uint256 aliceBalance = ghst.balanceOf(alice);
        assertEq(aliceBalance >= ghst.balanceTo(expectedPayout), true);
        assertEq(aliceBalance < ghst.balanceTo(expectedPayout * 10001 / 10000), true);
    }

    function test_shouldCorrectlyRedeemPartially() public {
        uint256 amount = 1 * 1e18;
        vm.startPrank(alice);
        depository.deposit(0, amount, initialPrice * 2, alice, alice);
        depository.deposit(0, amount, initialPrice * 2, alice, alice);
        depository.deposit(0, amount, initialPrice * 2, alice, alice);
        depository.deposit(0, amount, initialPrice * 2, alice, alice);

        skip(depositInterval);

        uint256[] memory indexesToRemove = new uint256[](1);
        indexesToRemove[0] = 1;

        uint256[] memory nextIndexToRemove = new uint256[](1);
        nextIndexToRemove[0] = 3;

        uint256[] memory allIndexes = depository.indexesFor(alice);
        assertEq(allIndexes.length, 4);
        assertEq(allIndexes[0], 0);
        assertEq(allIndexes[1], 1);
        assertEq(allIndexes[2], 2);
        assertEq(allIndexes[3], 3);

        depository.redeem(alice, true, indexesToRemove);
        uint256[] memory allIndexesOneRemoved = depository.indexesFor(alice);
        assertEq(allIndexesOneRemoved.length, 3);
        assertEq(allIndexesOneRemoved[0], 0);
        assertEq(allIndexesOneRemoved[1], 3);
        assertEq(allIndexesOneRemoved[2], 2);

        depository.redeem(alice, true, nextIndexToRemove);
        uint256[] memory allIndexesTwoRemoved = depository.indexesFor(alice);
        assertEq(allIndexesTwoRemoved.length, 2);
        assertEq(allIndexesTwoRemoved[0], 0);
        assertEq(allIndexesTwoRemoved[1], 2);

        vm.stopPrank();
    }

    function test_afterSuccesfullWarmupAutoClaimExecuted() public {
        uint256 amount = 10_000 * 1e18; // 10,000

        vm.prank(governor);
        staking.setWarmupPeriod(1);

        vm.startPrank(alice);

        assertEq(ghst.balanceOf(alice), 0);
        (uint256 expectedPayout, ,) = depository.deposit(0, amount, initialPrice, alice, alice);
        assertEq(ghst.balanceOf(address(depository)), 0);

        skip(depositInterval);
        staking.rebase();

        depository.redeemAll(alice, true);
        uint256 aliceBalance = ghst.balanceOf(alice);
        assertEq(aliceBalance >= ghst.balanceTo(expectedPayout), true);
        assertEq(aliceBalance < ghst.balanceTo(expectedPayout * 10001 / 10000), true);

        vm.stopPrank();
    }

    function test_externalAccountCouldNotClaimFromWarmup() public {
        vm.startPrank(alice);

        vm.expectRevert();
        staking.claim(address(depository), false);

        vm.expectRevert();
        staking.claim(address(depository), true);

        vm.stopPrank();
    }

    function test_shouldDecayMaxPayoutInTargetDepositInterval() public {
        (, , , , uint64 maxPayout, ,) = depository.markets(0);
        uint256 price = depository.marketPrice(0);
        uint256 amount = maxPayout * price;
        vm.prank(alice);
        depository.deposit(0, amount, initialPrice, alice, alice);
        skip(depositInterval);
        uint256 newPrice = depository.marketPrice(0);
        assertEq(newPrice < initialPrice, true);
    }

    function test_shouldCloseMarket() public {
        (uint256 cap, , , , , ,) = depository.markets(0);
        assertEq(cap > 0, true);
        vm.prank(policy);
        depository.close(0);
        (uint256 newCap, , , , , ,) = depository.markets(0);
        assertEq(newCap, 0);
    }
}
