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 {GhostTreasury} from "../../src/Treasury.sol";
import {GhostStaking} from "../../src/Staking.sol";
import {GhostBondDepository} from "../../src/BondDepository.sol";
import {ERC20Mock} from "../../src/mocks/ERC20Mock.sol";
import {WETH9} from "../../src/mocks/WETH9.sol";
import {GhostBondingCalculator} from "../../src/StandardBondingCalculator.sol";

import {ITreasury} from "../../src/interfaces/ITreasury.sol";
import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.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 INITIAL_MINT         = 1000000000000000000000000;
    uint256 public constant CAPACITY             = 10000e9;
    uint256 public constant INITIAL_PRICE        = 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 TIME_TO_CONCLUSION   = 60 * 60 * 24;
    uint256 public constant DEPOSIT_INTERVAL     = 60 * 60 * 4;
    uint256 public constant TUNE_INTERVAL        = 60 * 60;

    uint256 public conclusion;

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

    function setUp() public {
        vm.startPrank(INITIALIZER);
        weth = new WETH9();
        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),
            0
        );
        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),
            address(weth)
        );
        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, INITIAL_MINT);
        reserve.approve(address(treasury), type(uint256).max);
        treasury.deposit(address(reserve), INITIAL_MINT, treasury.tokenValue(address(reserve), INITIAL_MINT) / 2);
        assertEq(ftso.totalSupply(), treasury.baseSupply());

        reserve.mint(ALICE, INITIAL_MINT);
        reserve.approve(address(depository), type(uint256).max);
        vm.stopPrank();

        vm.startPrank(BOB);
        reserve.mint(BOB, INITIAL_MINT);
        reserve.approve(address(depository), type(uint256).max);
        vm.stopPrank();

        conclusion = block.timestamp + TIME_TO_CONCLUSION;

        vm.prank(POLICY);
        depository.create(
            [CAPACITY, INITIAL_PRICE, BUFFER],
            [VESTING, conclusion],
            address(reserve),
            [uint32(DEPOSIT_INTERVAL), uint32(TUNE_INTERVAL)], // forge-lint: disable-line(unsafe-typecast)
            [false, true]
        );
    }

    function _createNativeBond() internal returns (uint256) {
        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(weth), address(calculator));
        vm.stopPrank();

        uint256 amount = 69 * 1e18;
        vm.deal(ALICE, amount);

        conclusion = block.timestamp + TIME_TO_CONCLUSION;

        vm.prank(POLICY);
        depository.create(
            [CAPACITY, INITIAL_PRICE, BUFFER],
            [VESTING, conclusion],
            address(weth),
            [uint32(DEPOSIT_INTERVAL), uint32(TUNE_INTERVAL)], // forge-lint: disable-line(unsafe-typecast)
            [false, true]
        );

        return amount;
    }

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

    function test_shouldConcludeInCorrectAmountOfTime() public view {
        (, , , uint48 concludes,) = depository.terms(0);
        assertEq(concludes, uint48(conclusion)); // forge-lint: disable-line(unsafe-typecast)
        (, , uint48 length, , ,) = depository.metadatas(0);
        assertEq(length, TIME_TO_CONCLUSION);
    }

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

    function test_shouldReturnIdsOfAllMarkets() public {
        vm.prank(POLICY);
        depository.create(
            [CAPACITY, INITIAL_PRICE, BUFFER],
            [VESTING, conclusion],
            address(reserve),
            [uint32(DEPOSIT_INTERVAL), uint32(TUNE_INTERVAL)], // forge-lint: disable-line(unsafe-typecast)
            [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, INITIAL_PRICE, BUFFER],
            [VESTING, conclusion],
            address(reserve),
            [uint32(DEPOSIT_INTERVAL), uint32(TUNE_INTERVAL)], // forge-lint: disable-line(unsafe-typecast)
            [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), INITIAL_PRICE);
    }

    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(DEPOSIT_INTERVAL);
        vm.prank(ALICE);
        depository.deposit(0, 0, INITIAL_PRICE, ALICE, ALICE);

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

    function test_shouldStartAdjustmentIfBehindSchedule() public {
        skip(DEPOSIT_INTERVAL);

        uint256 amount = 10_000 * 1e18;
        vm.prank(ALICE);
        depository.deposit(0, amount, INITIAL_PRICE, 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, INITIAL_PRICE, ALICE, ALICE);
        skip(DEPOSIT_INTERVAL);
        (uint64 change, , ,) = depository.adjustments(0);
        depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE);
        vm.stopPrank();

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

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

        vm.startPrank(ALICE);
        depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE);
        (uint64 change, , ,) = depository.adjustments(0);
        skip(TUNE_INTERVAL / 2);

        depository.deposit(0, amount, INITIAL_PRICE, 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, INITIAL_PRICE, ALICE, ALICE);
        (uint64 change, , ,) = depository.adjustments(0);
        skip(TUNE_INTERVAL / 2);
        depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE);
        skip(TUNE_INTERVAL / 2);
        depository.deposit(0, amount, INITIAL_PRICE, 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, INITIAL_PRICE, 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, INITIAL_PRICE, ALICE, ALICE);
    }

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

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

        vm.startPrank(GOVERNOR);
        staking.setWarmupPeriod(1);
        vm.stopPrank();

        vm.prank(ALICE);
        depository.deposit(0, amount, type(uint256).max, ALICE, ALICE);
        vm.prank(BOB);
        depository.deposit(0, amount, type(uint256).max, BOB, BOB);

        skip(DEPOSIT_INTERVAL);
        vm.roll(block.number + 1);
        staking.rebase();

        vm.startPrank(ALICE);
        depository.redeemAll(ALICE, true);

        vm.startPrank(BOB);
        depository.forceRedeemAll(bytes32(0));
    }

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

        skip(DEPOSIT_INTERVAL);
        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, INITIAL_PRICE * 2, ALICE, ALICE);
        depository.deposit(0, amount, INITIAL_PRICE * 2, ALICE, ALICE);
        depository.deposit(0, amount, INITIAL_PRICE * 2, ALICE, ALICE);
        depository.deposit(0, amount, INITIAL_PRICE * 2, ALICE, ALICE);

        skip(DEPOSIT_INTERVAL);

        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, INITIAL_PRICE, ALICE, ALICE);
        assertEq(ghst.balanceOf(address(depository)), 0);

        skip(DEPOSIT_INTERVAL);
        vm.roll(block.number + 1);
        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, INITIAL_PRICE, ALICE, ALICE);
        skip(DEPOSIT_INTERVAL);
        uint256 newPrice = depository.marketPrice(0);
        assertEq(newPrice < INITIAL_PRICE, 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);
    }

    function test_abilityToPurchaseWithNative() public {
        uint256 amount = _createNativeBond();
        uint256 halfAmount = amount / 2;

        assertEq(ALICE.balance, amount);
        assertEq(IERC20(address(weth)).balanceOf(address(treasury)), 0);

        vm.prank(ALICE);
        depository.deposit{value: halfAmount}(1, halfAmount, halfAmount, ALICE, ALICE);
        assertEq(ALICE.balance, halfAmount);
        assertEq(IERC20(address(weth)).balanceOf(address(treasury)), halfAmount);
    }

    function test_nativeValueWillBeTaken() public {
        uint256 amount = _createNativeBond();
        uint256 halfAmount = amount / 2;

        assertEq(ALICE.balance, amount);
        assertEq(IERC20(address(weth)).balanceOf(address(treasury)), 0);

        vm.prank(ALICE);
        (uint256 payout,,) = depository.deposit{value: halfAmount}(1, 0, halfAmount, ALICE, ALICE);
        assertEq(ALICE.balance, halfAmount);
        assertEq(IERC20(address(weth)).balanceOf(address(treasury)), halfAmount);
        assertEq(payout, 0);
    }

    function test_nativeValueLessThenAmountFails() public {
        uint256 amount = _createNativeBond();
        uint256 halfAmount = amount / 2;

        assertEq(ALICE.balance, amount);
        assertEq(IERC20(address(weth)).balanceOf(address(treasury)), 0);

        vm.expectRevert();
        vm.prank(ALICE);
        depository.deposit{value: halfAmount}(1, amount, amount, ALICE, ALICE);

        assertEq(ALICE.balance, amount);
        assertEq(IERC20(address(weth)).balanceOf(address(treasury)), 0);
    }

    function test_forceRedeemWorksBasedOnTheBalanceRemidner() public {
        uint256 aliceAmount = _createNativeBond();
        uint256 aliceAmountUsed = aliceAmount / 8;
        assertEq(ALICE.balance, aliceAmount);

        uint256 i;
        for (; i < 8; ) {
            vm.prank(ALICE);
            depository.deposit{value: aliceAmountUsed}(1, aliceAmountUsed, type(uint256).max, ALICE, ALICE);
            unchecked { ++i; }
        }

        vm.startPrank(GOVERNOR);
        staking.setWarmupPeriod(10);
        vm.stopPrank();

        uint256 bobAmount = aliceAmount / 2;
        uint256 bobAmountUsed = bobAmount / 8;

        vm.deal(BOB, bobAmount);
        assertEq(BOB.balance, bobAmount);

        i = 0;
        for (; i < 7; ) {
            vm.prank(BOB);
            depository.deposit{value: bobAmountUsed}(1, bobAmountUsed, type(uint256).max, BOB, BOB);
            unchecked { ++i; }
        }

        skip(DEPOSIT_INTERVAL);

        (, uint256 payout,, bool matured) = staking.warmupInfo(address(depository));
        assertEq(ghst.balanceOf(address(depository)) > 0, true);
        assertEq(payout > 0, true);
        assertEq(matured, true);

        vm.prank(BOB);
        depository.forceRedeemAll(bytes32(0));

        vm.prank(ALICE);
        depository.forceRedeemAll(bytes32(0));

        (, payout,,) = staking.warmupInfo(address(depository));
        assertEq(ghst.balanceOf(address(depository)), 0);
        assertEq(payout, 0);
    }
}
