pragma solidity 0.8.20;

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

import "../../src/FatsoERC20.sol";
import "../../src/StinkyERC20.sol";
import "../../src/GhstERC20.sol";
import "../../src/GhostAuthority.sol";
import "../../src/Staking.sol";

import "./Permit.t.sol";
import "./Allowance.t.sol";
import "./Transfer.t.sol";

import "@openzeppelin-contracts/token/ERC20/IERC20.sol";

contract StinkyTest is Test, ERC20PermitTest, ERC20AllowanceTest, ERC20TransferTest {
    uint256 private constant INITIAL_SHARES_SUPPLY = 5 * 10**15; // 15 or 16
    uint256 private constant TOTAL_SHARES = type(uint256).max - (type(uint256).max % INITIAL_SHARES_SUPPLY);

    uint256 constant public INITIAL_INDEX = 10819917194513808e56;
    uint256 constant public TOTAL_INITIAL_SUPPLY = 5000000000000000;

    Fatso public ftso;
    Stinky public stnk;
    Ghost public ghst;
    GhostAuthority public authority;
    GhostStaking public staking;

    address constant initializer       = 0x0000000000000000000000000000000000000001;
    address constant alice             = 0x0000000000000000000000000000000000000003;
    address constant bob               = 0x0000000000000000000000000000000000000004;
    address constant treasury          = 0x0000000000000000000000000000000000000005;
    uint256 constant amount            = 69;
    uint256 constant maxAmount         = type(uint256).max;

    string constant name               = "Stinky Test Name";
    string constant symbol             = "STNKTST";

    event Transfer(address indexed from, address indexed to, uint256 value);
    event LogStakingContractUpdated(address stakingContract);

    function setUp() public {
        vm.startPrank(initializer);
        authority = new GhostAuthority(
            initializer,
            initializer,
            initializer,
            initializer
        );
        ftso = new Fatso(address(authority), "Fatso", "FTSO");
        stnk = new Stinky(INITIAL_INDEX, name, symbol);
        ghst = new Ghost(address(stnk), "Ghost", "GHST");
        staking = new GhostStaking(
            address(ftso),
            address(stnk),
            address(ghst),
            69,
            1337,
            1337,
            address(authority)
        );
        ghst.initialize(address(staking));
        vm.stopPrank();

        initializePermit(address(stnk), amount, maxAmount);
        initializeAllowance(alice, bob, address(stnk), amount, maxAmount, TOTAL_INITIAL_SUPPLY);
        initializeTransfer(alice, bob, address(stnk), amount, TOTAL_INITIAL_SUPPLY);
    }

    function test_isConstructedCorrectly() public view {
        assertEq(stnk.name(), name);
        assertEq(stnk.symbol(), symbol);
        assertEq(stnk.decimals(), 9);
        assertEq(stnk.totalSupply(), TOTAL_INITIAL_SUPPLY);
        assertEq(stnk.index(), INITIAL_INDEX / (TOTAL_SHARES / INITIAL_SHARES_SUPPLY));
    }

    function test_initialization_couldBeDoneByInitializer() public {
        vm.prank(initializer);
        stnk.initialize(address(staking), treasury, address(ghst));
        assertEq(stnk.staking(), address(staking));
        assertEq(stnk.treasury(), treasury);
        assertEq(stnk.ghst(), address(ghst));
    }

    function test_initialization_couldNotBeDoneByArbitraryAddress(address who) public {
        vm.assume(who != initializer);
        vm.expectRevert();
        vm.prank(who);
        stnk.initialize(who, who, who);
    }

    function test_initialization_couldBeDoneOnlyOnce() public {
        vm.prank(initializer);
        stnk.initialize(address(staking), treasury, address(ghst));

        vm.expectRevert();
        vm.prank(initializer);
        stnk.initialize(initializer, initializer, initializer);
    }

    function test_initialization_correctTotalSharesOfStaking() public {
        _initialize();
        assertEq(stnk.balanceOf(address(staking)), 5000000000000000);
    }

    function test_initialization_emitsTransferEvent() public {
        vm.expectEmit(true, true, true, true, address(stnk));
        emit Transfer(address(0), address(staking), 5000000000000000);
        _initialize();
    }

    function test_initialization_emitsLogStakingContractUpdatedEvent() public {
        vm.expectEmit(true, true, true, true, address(stnk));
        emit LogStakingContractUpdated(address(staking));
        _initialize();
    }

    function test_circulatingSupplyIsCorrecBasedOnTotalSupply() public {
        _mintTokens(alice, amount);
        assertEq(stnk.circulatingSupply(), amount);
        assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY - amount);
    }

    function test_circulatingSupplyIsCorrecBasedOnStakingBalance() public {
        _mintTokens(alice, amount);
        assertEq(stnk.circulatingSupply(), amount);
        vm.prank(alice);
        stnk.transfer(address(staking), amount);
        assertEq(stnk.circulatingSupply(), 0);
        assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY);
    }

    function test_circulatingSupplyIsCorrectBasedOnSupplyInWarmup() public {
        vm.startPrank(initializer);
        stnk.initialize(address(staking), treasury, address(ghst));
        staking.setWarmupPeriod(1337);
        ftso.mint(alice, amount);
        vm.stopPrank();

        assertEq(stnk.circulatingSupply(), 0);
        assertEq(ftso.totalSupply(), amount);
        assertEq(stnk.totalSupply(), TOTAL_INITIAL_SUPPLY);
        assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY);

        vm.startPrank(alice);
        ftso.approve(address(staking), amount);
        staking.stake(amount, alice, true, true);
        vm.stopPrank();

        assertEq(stnk.circulatingSupply(), amount);
        assertEq(ftso.totalSupply(), amount);
        assertEq(stnk.totalSupply(), TOTAL_INITIAL_SUPPLY);
        assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY);
    }

    function test_circulatingSupplyIsCorrectBasedOnGhostSupply() public {
        vm.startPrank(initializer);
        stnk.initialize(address(staking), treasury, address(ghst));
        ftso.mint(alice, amount);
        vm.stopPrank();

        vm.startPrank(alice);
        ftso.approve(address(staking), amount);
        staking.stake(amount, alice, false, true);
        vm.stopPrank();

        uint256 balanceTo = amount * 1e18 / ghst.index();
        assertEq(ghst.balanceOf(alice), balanceTo);
        uint256 balanceFrom = ghst.totalSupply() * ghst.index() / 1e18;
        assertEq(stnk.circulatingSupply(), balanceFrom);
    }

    function test_toGhstCorrect(uint256 amountToTest) public {
        vm.assume(amountToTest < type(uint64).max);
        _initialize();
        uint256 balanceTo = amountToTest * 1e18 / ghst.index();
        assertEq(stnk.toGhst(amountToTest), ghst.balanceTo(amountToTest));
        assertEq(stnk.toGhst(amountToTest), balanceTo);
    }

    function test_fromGhstCorrect(uint256 amountToTest) public {
        vm.assume(amountToTest < type(uint64).max);
        _initialize();
        uint256 balanceFrom = amountToTest * ghst.index() / 1e18;
        assertEq(stnk.fromGhst(amountToTest), ghst.balanceFrom(amountToTest));
        assertEq(stnk.fromGhst(amountToTest), balanceFrom);
    }

    function test_sharesForBalanceCorrect(uint256 amountToTest) public view {
        vm.assume(amountToTest < TOTAL_INITIAL_SUPPLY);
        uint256 sharesPerUnit = stnk.sharesForBalance(1);
        assertEq(stnk.sharesForBalance(1), sharesPerUnit);
        assertEq(stnk.sharesForBalance(amountToTest), amountToTest * sharesPerUnit);
    }

    function test_balanceForSharesCorrect(uint256 amountToTest) public view {
        vm.assume(amountToTest < type(uint128).max);
        uint256 sharesPerUnit = stnk.sharesForBalance(1);
        assertEq(stnk.balanceForShares(amountToTest), amountToTest / sharesPerUnit);
    }

    function test_debt_CouldBeChangedByTreasury() public {
        _mintTokens(alice, amount);
        assertEq(stnk.debtBalances(alice), 0);
        vm.prank(treasury);
        stnk.changeDebt(amount, alice, true);
        assertEq(stnk.debtBalances(alice), amount);
        vm.prank(treasury);
        stnk.changeDebt(amount, alice, false);
        assertEq(stnk.debtBalances(alice), 0);
    }

    function test_debt_CouldNotBeChangeByArbitraryAddress(address someone) public {
        vm.assume(someone != treasury);
        _mintTokens(alice, amount);
        assertEq(stnk.debtBalances(alice), 0);
        vm.expectRevert();
        vm.prank(someone);
        stnk.changeDebt(amount, alice, true);
    }

    function test_bala_nceCouldNotDropBelowDebt() public {
        _mintTokens(alice, amount * 3);
        vm.prank(treasury);
        stnk.changeDebt(amount, alice, true);
        assertEq(stnk.balanceOf(bob), 0);
        vm.prank(alice);
        stnk.transfer(bob, amount);
        assertEq(stnk.balanceOf(bob), amount);
        vm.expectRevert();
        vm.prank(alice);
        stnk.transfer(bob, amount * 2);
        assertEq(stnk.balanceOf(bob), amount);
    }

    function test_rebase_couldNotBeDoneFromArbitraryAddress(address someone) public {
        vm.assume(someone != address(staking));
        vm.expectRevert();
        vm.prank(someone);
        stnk.rebase(amount, 1337);
    }

    function test_rebase_nothingChangesIfDeltaIsZero() public {
        uint256 epoch = 1337;
        uint256 prevSupply = stnk.totalSupply();
        uint256 prevIndex = stnk.index();
        _mintTokens(alice, amount);
        assertEq(stnk.balanceOf(alice), amount);

        vm.prank(address(staking));
        stnk.rebase(0, epoch);

        assertEq(stnk.totalSupply(), prevSupply);
        assertEq(stnk.index(), prevIndex);
        assertEq(stnk.balanceOf(alice), amount);
    }

    function test_rebase_changesIfProfitAndCirculatingSupply() public {
        uint256 epoch = 1337;
        _mintTokens(alice, amount);
        assertEq(stnk.balanceOf(alice), amount);
        vm.prank(address(staking));
        stnk.rebase(amount, epoch);
        assertEq(stnk.balanceOf(alice), amount * 2);
        assertEq(stnk.totalSupply(), TOTAL_INITIAL_SUPPLY * 2);
    }

    function test_rebase_changesIfProfitAndNoCirculatingSupply() public {
        uint256 epoch = 1337;
        _initialize();
        assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY);
        vm.prank(address(staking));
        stnk.rebase(amount, epoch);
        assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY + amount);
        assertEq(stnk.totalSupply(), TOTAL_INITIAL_SUPPLY + amount);
    }

    function test_rebase_couldNotExceedMaxSupply() public {
        uint256 epoch = 1337;
        _initialize();
        assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY);
        vm.prank(address(staking));
        stnk.rebase(type(uint128).max, epoch);
        assertEq(stnk.balanceOf(address(staking)), type(uint128).max);
        assertEq(stnk.totalSupply(), type(uint128).max);
    }

    function _initialize() internal {
        vm.prank(initializer);
        stnk.initialize(address(staking), treasury, address(ghst));
        assertEq(stnk.staking(), address(staking));
        assertEq(stnk.treasury(), treasury);
        assertEq(stnk.ghst(), address(ghst));
    }

    function _mintTokens(address who, uint256 value) internal {
        _initialize();
        uint256 totalSupply = stnk.totalSupply();
        vm.prank(address(staking));
        stnk.transfer(who, value);
        assertEq(stnk.totalSupply(), totalSupply);
    }

    function _mintTransferTokens(address who, uint256 value) internal override {
        _mintTokens(who, value);
    }

    function _mintAllowanceTokens(address who, uint256 value) internal override {
        _mintTokens(who, value);
    }

    function _mintPermitTokens(address who, uint256 value) internal override {
        _mintTokens(who, value);
    }
}
