pragma solidity 0.8.20;

import {Test} from "forge-std/Test.sol";
import "../../src/mocks/Reserve.sol";

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

contract ReserveTest is Test {
    address constant initializer    = 0x0000000000000000000000000000000000000001;
    address constant faucetAddress  = 0x0000000000000000000000000000000000000002;
    address constant aliceAddress   = 0x0000000000000000000000000000000000000003;
    address constant bobAddress     = 0x0000000000000000000000000000000000000004;
    uint256 constant conversionRate = 69 * 10e5;
    uint256 constant sendAmount     = 1e16;

    string constant name   = "Test DAI";
    string constant symbol = "tDAI";

    Reserve reserve;

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

    function setUp() public {
        vm.prank(initializer);
        reserve = new Reserve(
            "Test DAI",
            "tDAI",
            conversionRate
        );
    }

    function test_isConstructedCorrectly() public view {
        assertEq(reserve.name(), name);
        assertEq(reserve.symbol(), symbol);
        assertEq(reserve.decimals(), 18);
        assertEq(reserve.totalSupply(), 0);

        assertEq(reserve.donationRate(), 0);
        assertEq(reserve.conversionRate(), conversionRate);
    }

    function test_mint_couldBeDoneWithValue() public {
        assertEq(reserve.balanceOf(aliceAddress), 0);
        deal(aliceAddress, sendAmount);

        vm.prank(aliceAddress);
        reserve.mint{ value: sendAmount }(aliceAddress);

        assertEq(reserve.balanceOf(aliceAddress), sendAmount * conversionRate);
    }

    function test_mint_couldNotBeDoneWithoutEmptyValue() public {
        assertEq(reserve.balanceOf(aliceAddress), 0);
        vm.prank(aliceAddress);
        reserve.mint(aliceAddress);
        assertEq(reserve.balanceOf(aliceAddress), 0);
    }

    function test_mint_couldNotMintIfNotEnoughValue() public {
        assertEq(reserve.balanceOf(aliceAddress), 0);
        bool didRevert = false;

        vm.prank(aliceAddress);
        try reserve.mint{ value: type(uint256).max }(aliceAddress) {
        } catch {
            didRevert = true;
        }

        assertEq(didRevert, true);
        assertEq(reserve.balanceOf(aliceAddress), 0);
    }

    function test_mint_superMintCouldBeDoneFromDeployer() public {
        assertEq(reserve.balanceOf(initializer), 0);
        vm.prank(initializer);
        reserve.superMint(initializer, 420 * 1e18);
        assertEq(reserve.balanceOf(initializer),  420 * 1e18);
    }

    function test_mint_superMintCouldNotBeDoneFromArbitraryAddress() public {
        assertEq(reserve.balanceOf(aliceAddress), 0);
        vm.expectRevert();
        vm.prank(aliceAddress);
        reserve.superMint(aliceAddress, 420 * 1e18);
        assertEq(reserve.balanceOf(aliceAddress),  0);
    }

    function test_mint_donationNotTakenIfRateNotSet() public {
        assertEq(reserve.totalSupply(), 0);
        assertEq(reserve.donationRate(), 0);
        assertEq(reserve.accumulatedDonation(), 0);

        deal(aliceAddress, sendAmount);
        vm.prank(aliceAddress);
        reserve.mint{ value: sendAmount }(aliceAddress);

        assertEq(reserve.totalSupply(), sendAmount * conversionRate);
        assertEq(reserve.donationRate(), 0);
        assertEq(reserve.accumulatedDonation(), 0);
    }

    function test_mint_donationIsTakenIfRateExists() public {
        assertEq(reserve.totalSupply(), 0);
        assertEq(reserve.donationRate(), 0);
        assertEq(reserve.accumulatedDonation(), 0);

        vm.prank(initializer);
        reserve.changeDonationRate(1e4); // 10%

        deal(aliceAddress, sendAmount);
        vm.prank(aliceAddress);
        reserve.mint{ value: sendAmount }(aliceAddress);

        assertEq(reserve.totalSupply(), sendAmount * conversionRate);
        assertEq(reserve.donationRate(), 1e4);
        assertEq(reserve.accumulatedDonation(), sendAmount * 1e4 / 1e5);
    }

    function test_rate_couldBeChangedByDeployer() public {
        assertEq(reserve.conversionRate(), conversionRate);
        vm.prank(initializer);
        reserve.changeRate(1337);
        assertEq(reserve.conversionRate(), 1337);
    }

    function test_rate_couldNotBeChangedByArbitraryAddress() public {
        assertEq(reserve.conversionRate(), conversionRate);
        vm.expectRevert();
        vm.prank(aliceAddress);
        reserve.changeRate(1337);
        assertEq(reserve.conversionRate(), conversionRate);
    }

    function test_withdraw_couldBeDoneByDeployer() public {
        assertEq(address(reserve).balance, 0);
        vm.prank(initializer);
        reserve.changeDonationRate(1e5);

        deal(aliceAddress, sendAmount);
        vm.prank(aliceAddress);
        reserve.mint{ value: sendAmount }(aliceAddress);

        assertEq(address(reserve).balance, sendAmount);

        vm.prank(initializer);
        reserve.withdraw(payable(faucetAddress));

        assertEq(address(reserve).balance, 0);
        assertEq(faucetAddress.balance, sendAmount);
    }

    function test_withdraw_onlyAccumulatedDonationsCouldBeWithdrawn() public {
        assertEq(address(reserve).balance, 0);
        vm.prank(initializer);
        reserve.changeDonationRate(5 * 1e4); // 50%

        deal(aliceAddress, sendAmount);
        vm.prank(aliceAddress);
        reserve.mint{ value: sendAmount }(aliceAddress);

        assertEq(address(reserve).balance, sendAmount);

        vm.prank(initializer);
        reserve.withdraw(payable(faucetAddress));
        assertEq(address(reserve).balance, sendAmount / 2);
        assertEq(faucetAddress.balance, sendAmount / 2);

        vm.prank(initializer);
        reserve.withdraw(payable(faucetAddress));
        assertEq(address(reserve).balance, sendAmount / 2);
        assertEq(faucetAddress.balance, sendAmount / 2);
    }

    function test_withdraw_couldNotBeDoneByArbitraryAddress() public {
        assertEq(address(reserve).balance, 0);

        deal(aliceAddress, sendAmount);
        vm.prank(aliceAddress);
        reserve.mint{ value: sendAmount }(aliceAddress);

        assertEq(address(reserve).balance, sendAmount);

        vm.expectRevert();
        vm.prank(aliceAddress);
        reserve.withdraw(payable(aliceAddress));

        assertEq(address(reserve).balance, sendAmount);
        assertEq(aliceAddress.balance, 0);
    }

    function test_burn_shouldReturnFullAmountIfRateNotSet() public {
        deal(aliceAddress, sendAmount);
        vm.prank(aliceAddress);
        reserve.mint{ value: sendAmount }(aliceAddress);

        uint256 balance = reserve.balanceOf(aliceAddress);
        assertEq(reserve.totalSupply(), sendAmount * conversionRate);
        assertEq(reserve.totalSupply(), balance);
        assertEq(reserve.accumulatedDonation(), 0);
        assertEq(aliceAddress.balance, 0);

        vm.prank(aliceAddress);
        reserve.burn(balance);

        assertEq(reserve.totalSupply(), 0);
        assertEq(reserve.balanceOf(aliceAddress), 0);
        assertEq(reserve.accumulatedDonation(), 0);
        assertEq(aliceAddress.balance, sendAmount);
    }

    function test_burn_shouldTakeDonationInAccordanceToRate() public {
        vm.prank(initializer);
        reserve.changeDonationRate(5 * 1e4); // 50%

        deal(aliceAddress, sendAmount);
        vm.prank(aliceAddress);
        reserve.mint{ value: sendAmount }(aliceAddress);

        uint256 balance = reserve.balanceOf(aliceAddress);
        assertEq(reserve.totalSupply(), sendAmount * conversionRate);
        assertEq(reserve.totalSupply(), balance);
        assertEq(reserve.accumulatedDonation(), sendAmount / 2);
        assertEq(aliceAddress.balance, 0);

        vm.prank(aliceAddress);
        reserve.burn(balance);

        assertEq(reserve.totalSupply(), 0);
        assertEq(reserve.balanceOf(aliceAddress), 0);
        assertEq(reserve.accumulatedDonation(), sendAmount / 2);
        assertEq(aliceAddress.balance, sendAmount / 2);
    }

    function test_burn_multipleUsersAccumulateInTotal() public {
        vm.prank(initializer);
        reserve.changeDonationRate(5 * 1e4); // 50%

        deal(aliceAddress, sendAmount);
        vm.prank(aliceAddress);
        reserve.mint{ value: sendAmount }(aliceAddress);

        vm.prank(initializer);
        reserve.changeRate(conversionRate * 9);

        deal(bobAddress, sendAmount);
        vm.prank(bobAddress);
        reserve.mint{ value: sendAmount }(bobAddress);

        assertEq(aliceAddress.balance, 0);
        assertEq(bobAddress.balance, 0);
        uint256 aliceBalance = reserve.balanceOf(aliceAddress);
        uint256 bobBalance = reserve.balanceOf(bobAddress);

        uint256 bobEstimation = reserve.estimateAmount(bobBalance);
        uint256 aliceEstimation = reserve.estimateAmount(aliceBalance);

        vm.prank(bobAddress);
        reserve.burn(bobBalance);

        vm.prank(aliceAddress);
        reserve.burn(aliceBalance);

        assertEq(bobAddress.balance, sendAmount * 9 / 10);
        assertEq(bobAddress.balance, bobEstimation);
        assertEq(aliceAddress.balance, sendAmount / 10);
        assertEq(aliceAddress.balance, aliceEstimation);

        assertEq(reserve.totalSupply(), 0);
        assertEq(reserve.accumulatedDonation(), sendAmount);
        assertEq(address(reserve).balance, sendAmount);
    }

    function test_reserveFallback() public {
        assertEq(address(reserve).balance, 0);
        assertEq(reserve.balanceOf(aliceAddress), 0);

        vm.prank(aliceAddress);
        (bool success, ) = address(reserve).call(abi.encodeWithSignature("nonExistentFunction()"));
        require(success, "Fallback call failed");

        assertEq(address(reserve).balance, 0);
        assertEq(reserve.balanceOf(aliceAddress), 0);
    }

    function test_reserveReceive() public {
        assertEq(address(reserve).balance, 0);
        assertEq(reserve.balanceOf(aliceAddress), 0);

        deal(aliceAddress, sendAmount);
        vm.prank(aliceAddress);
        (bool success, ) = address(reserve).call{value: sendAmount}("");
        require(success, "Transfer of native failed");

        uint256 estimatedReceiveAmount = sendAmount * reserve.conversionRate();
        assertEq(address(reserve).balance, sendAmount);
        assertEq(reserve.balanceOf(aliceAddress), estimatedReceiveAmount);
    }
}
