// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;

import "../src/Kraiken.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
import "forge-std/Test.sol";
import "forge-std/console.sol";

contract KraikenTest is Test {
    Kraiken internal kraiken;
    address internal stakingPool;
    address internal liquidityPool;
    address internal liquidityManager;

    function setUp() public {
        kraiken = new Kraiken("KRAIKEN", "KRK");
        stakingPool = makeAddr("stakingPool");
        kraiken.setStakingPool(stakingPool);
        liquidityManager = makeAddr("liquidityManager");
        kraiken.setLiquidityManager(liquidityManager);
    }

    // Simulates staking by transferring tokens to the stakingPool address.
    function simulateStake(uint256 amount) internal {
        // the amount of token has to be available on the balance
        // of the test contract
        kraiken.transfer(stakingPool, amount);
    }

    // Simulates unstaking by transferring tokens from the stakingPool back to a given address.
    function simulateUnstake(uint256 amount) internal {
        // Direct transfer from the stakingPool to 'to' address to simulate unstaking
        vm.prank(stakingPool); // Assuming 'stake' contract would allow this in an actual scenario
        kraiken.transfer(address(this), amount);
    }

    function testKraikenConstructor() public view {
        // Check if the token details are set as expected
        assertEq(kraiken.name(), "KRAIKEN");
        assertEq(kraiken.symbol(), "KRK");

        // Confirm that the TwabController address is correctly set
        (address _lm, address _sp) = kraiken.peripheryContracts();
        assertEq(_lm, liquidityManager);
        assertEq(_sp, stakingPool);
    }

    function testMintWithEmptyStakingPool() public {
        uint256 initialSupply = kraiken.totalSupply();
        uint256 mintAmount = 1000 * 1e18; // 1000 HARB tokens

        vm.prank(address(liquidityManager));
        kraiken.mint(mintAmount);

        // Check if the total supply has increased correctly
        assertEq(kraiken.totalSupply(), initialSupply + mintAmount);
        // Check if the staking pool balance is still 0, as before
        assertEq(kraiken.balanceOf(stakingPool), 0);
    }

    function testBurnWithEmptyStakingPool() public {
        uint256 initialSupply = kraiken.totalSupply();
        uint256 burnAmount = 500 * 1e18; // 500 HARB tokens

        // First, mint some tokens to burn
        vm.prank(address(liquidityManager));
        kraiken.mint(burnAmount);

        vm.prank(address(liquidityManager));
        kraiken.burn(burnAmount);

        // Check if the total supply has decreased correctly
        assertEq(kraiken.totalSupply(), initialSupply);
        // Check if the staking pool balance has decreased correctly
        assertEq(kraiken.balanceOf(stakingPool), 0);
    }

    function testMintImpactOnSimulatedStaking() public {
        uint256 initialStakingPoolBalance = kraiken.balanceOf(stakingPool);
        uint256 mintAmount = 1000 * 1e18; // 1000 HARB tokens
        // Ensure the test contract has enough tokens to simulate staking
        vm.prank(address(liquidityManager));
        kraiken.mint(mintAmount);
        vm.prank(address(liquidityManager));
        kraiken.transfer(address(this), mintAmount);

        // Simulate staking of the minted amount
        simulateStake(mintAmount);

        // Check balances after simulated staking
        assertEq(kraiken.balanceOf(stakingPool), initialStakingPoolBalance + mintAmount);
    }

    function testUnstakeImpactOnTotalSupply() public {
        uint256 stakeAmount = 500 * 1e18; // 500 HARB tokens
        // Ensure the test contract has enough tokens to simulate staking
        vm.prank(address(liquidityManager));
        kraiken.mint(stakeAmount);
        vm.prank(address(liquidityManager));
        kraiken.transfer(address(this), stakeAmount);

        uint256 initialTotalSupply = kraiken.totalSupply();

        // Simulate staking and then unstaking
        simulateStake(stakeAmount);
        simulateUnstake(stakeAmount);

        // Check total supply remains unchanged after unstake
        assertEq(kraiken.totalSupply(), initialTotalSupply);
    }

    // Fuzz test for mint function with varying stake amounts
    function testMintWithStake(uint8 _stakePercentage, uint256 mintAmount) public {
        uint256 initialAmount = 500 * 1e18;
        // Ensure the test contract has enough tokens to simulate staking
        vm.prank(address(liquidityManager));
        kraiken.mint(initialAmount);
        vm.prank(address(liquidityManager));
        kraiken.transfer(address(this), initialAmount);

        // Limit fuzzing input to 0% - 20%
        uint8 effectiveStakePercentage = _stakePercentage % 21;
        uint256 stakeAmount = (initialAmount * effectiveStakePercentage) / 100;
        simulateStake(stakeAmount);

        uint256 initialTotalSupply = kraiken.totalSupply();
        uint256 initialStakingPoolBalance = kraiken.balanceOf(stakingPool);

        mintAmount = bound(mintAmount, 1, 500 * 1e18);
        uint256 expectedNewStake = initialStakingPoolBalance * mintAmount / (initialTotalSupply - initialStakingPoolBalance);

        // Expect Transfer events
        vm.expectEmit(true, true, true, true, address(kraiken));
        emit IERC20.Transfer(address(0), address(liquidityManager), mintAmount);

        vm.prank(address(liquidityManager));
        kraiken.mint(mintAmount);
        uint256 expectedStakingPoolBalance = initialStakingPoolBalance + expectedNewStake;
        uint256 expectedTotalSupply = initialTotalSupply + mintAmount + expectedNewStake;

        assertEq(kraiken.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after mint.");
        assertEq(kraiken.totalSupply(), expectedTotalSupply, "Total supply did not match expected after mint.");
    }

    // Fuzz test for burn function with varying stake amounts
    function testBurnWithStake(uint8 _stakePercentage, uint256 burnAmount) public {
        uint256 mintAmount = 500 * 1e18;
        // Ensure the test contract has enough tokens to simulate staking
        vm.prank(address(liquidityManager));
        kraiken.mint(mintAmount);

        // Limit fuzzing input to 0% - 20%
        uint8 effectiveStakePercentage = _stakePercentage % 21;
        uint256 stakeAmount = (mintAmount * effectiveStakePercentage) / 100;
        vm.prank(address(liquidityManager));
        kraiken.transfer(address(this), stakeAmount);
        simulateStake(stakeAmount);

        burnAmount = bound(burnAmount, 0, 200 * 1e18);
        uint256 initialTotalSupply = kraiken.totalSupply();
        uint256 initialStakingPoolBalance = kraiken.balanceOf(stakingPool);
        uint256 expectedExcessStake = initialStakingPoolBalance * burnAmount / (initialTotalSupply - initialStakingPoolBalance);

        vm.prank(address(liquidityManager));
        kraiken.burn(burnAmount);

        uint256 expectedStakingPoolBalance = initialStakingPoolBalance - expectedExcessStake;
        uint256 expectedTotalSupply = initialTotalSupply - burnAmount - expectedExcessStake;

        assertEq(kraiken.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after burn.");
        assertEq(kraiken.totalSupply(), expectedTotalSupply, "Total supply did not match expected after burn.");
    }

    // ========================================
    // SETTER VALIDATION TESTS
    // ========================================

    function testSetLiquidityManagerZeroAddress() public {
        Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK");
        vm.expectRevert(Kraiken.ZeroAddressInSetter.selector);
        freshKraiken.setLiquidityManager(address(0));
    }

    function testSetLiquidityManagerAlreadySet() public {
        // liquidityManager is already set in setUp — calling again must revert
        vm.expectRevert(Kraiken.AddressAlreadySet.selector);
        kraiken.setLiquidityManager(makeAddr("anotherLiquidityManager"));
    }

    function testSetLiquidityManagerRejectsStakingPool() public {
        Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK");
        address sp = makeAddr("stakingPool");
        freshKraiken.setStakingPool(sp);
        vm.expectRevert(Kraiken.InvalidAddress.selector);
        freshKraiken.setLiquidityManager(sp);
    }

    function testSetLiquidityManagerOnlyDeployer() public {
        Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK");
        address nonDeployer = makeAddr("nonDeployer");
        vm.prank(nonDeployer);
        vm.expectRevert("only deployer");
        freshKraiken.setLiquidityManager(makeAddr("someManager"));
    }

    function testSetStakingPoolZeroAddress() public {
        Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK");
        vm.expectRevert(Kraiken.ZeroAddressInSetter.selector);
        freshKraiken.setStakingPool(address(0));
    }

    function testSetStakingPoolAlreadySet() public {
        // stakingPool is already set in setUp — calling again must revert
        vm.expectRevert(Kraiken.AddressAlreadySet.selector);
        kraiken.setStakingPool(makeAddr("anotherStakingPool"));
    }

    function testSetStakingPoolRejectsLiquidityManager() public {
        Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK");
        address lm = makeAddr("liquidityManager");
        freshKraiken.setLiquidityManager(lm);
        vm.expectRevert(Kraiken.InvalidAddress.selector);
        freshKraiken.setStakingPool(lm);
    }

    function testSetStakingPoolOnlyDeployer() public {
        Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK");
        address nonDeployer = makeAddr("nonDeployer");
        vm.prank(nonDeployer);
        vm.expectRevert("only deployer");
        freshKraiken.setStakingPool(makeAddr("somePool"));
    }

    function testOnlyLiquidityManagerModifier() public {
        address nonLM = makeAddr("notLiquidityManager");
        vm.prank(nonLM);
        vm.expectRevert("only liquidity manager");
        kraiken.mint(1000 * 1e18);
    }

    // ========================================
    // ZERO AMOUNT TESTS
    // ========================================

    function testMintZeroAmount() public {
        // Mint a positive amount first so previousTotalSupply gets set
        vm.prank(address(liquidityManager));
        kraiken.mint(1000 * 1e18);

        uint256 totalSupplyBefore = kraiken.totalSupply();
        assertGt(kraiken.previousTotalSupply(), 0, "previousTotalSupply should be non-zero after first mint");

        // Mint zero: should skip the entire amount > 0 block
        // AND should skip the previousTotalSupply == 0 block (it is already set)
        vm.prank(address(liquidityManager));
        kraiken.mint(0);

        assertEq(kraiken.totalSupply(), totalSupplyBefore, "Total supply must not change when minting zero");
        assertEq(kraiken.balanceOf(stakingPool), 0, "Staking pool balance must not change when minting zero");
    }

    function testBurnZeroAmount() public {
        vm.prank(address(liquidityManager));
        kraiken.mint(1000 * 1e18);

        uint256 totalSupplyBefore = kraiken.totalSupply();

        // Burn zero: should skip the entire amount > 0 block
        vm.prank(address(liquidityManager));
        kraiken.burn(0);

        assertEq(kraiken.totalSupply(), totalSupplyBefore, "Total supply must not change when burning zero");
    }
}
