// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";
import {stdStorage, StdStorage} from "forge-std/Test.sol";
import {RaidGeld, Army, Player} from "../src/RaidGeld.sol";
import "../src/RaidGeldUtils.sol";
import {Constants} from "../src/Constants.sol";

contract raid_geldTest is Test, Constants {
    using stdStorage for StdStorage;

    RaidGeld public raid_geld;
    address public player1;
    address public player2;
    address public owner;

    event Approval(address indexed owner, address indexed spender, uint256 value);
    event PlayerRegistered(address indexed player, uint256 initialGeld);
    event RaidPerformed(
        address indexed player,
        uint256 totalMinted,
        uint256 geldBalance
    );
    event UnitAdded(
        address indexed player,
        uint8 unitType,
        uint16 nUnits,
        uint256 cost,
        uint256 geldBalance,
        uint16 molochDenierLevel,
        uint16 apprenticeLevel,
        uint16 anointedLevel,
        uint16 championLevel
    );

    function setUp() public {
        owner = address(0x126);
        player1 = address(0x123);
        vm.deal(owner, 10 ether);
        fundAccount(player1);
        vm.prank(owner);
        raid_geld = new RaidGeld(DAO_TOKEN, POOL);
        raid_geld.weth().deposit{value: 5 ether}();
    }

    function fundAccount(address _acc) private {
        vm.deal(_acc, 10 ether);
        stdstore.target(DAO_TOKEN).sig("balanceOf(address)").with_key(_acc).checked_write(100 ether);
    }

    function registerPlayer() private {
        raid_geld.register_eth{value: raid_geld.BUY_IN_AMOUNT()}();
    }

    function registerPlayerWithDaoToken() private {
        raid_geld.daoToken().approve(address(raid_geld), raid_geld.BUY_IN_DAO_TOKEN_AMOUNT());
        raid_geld.register_dao();
    }

    function test_00_no_fallback() public {
        vm.expectRevert();
        // Send Ether with some data to trigger fallback
        (bool success, ) = address(raid_geld).call{value: 0.1 ether}("0x1234");
    }

    function test_01_no_receive() public {
        vm.startPrank(player1);
        vm.expectRevert();
        payable(address(raid_geld)).transfer(0.1 ether);
    }

    function test_02_1_registrationWithEth() public {
        vm.startPrank(player1);

        uint256 contractBalance = raid_geld.daoToken().balanceOf(address(raid_geld));
        uint256 userBalance = address(player1).balance;

        // Making sure event is emitted when player is registered
        vm.expectEmit(address(raid_geld));
        emit PlayerRegistered(player1, raid_geld.INITIAL_GELD());
        registerPlayer();

        // Check that initialraid_geld.is received by the player
        assertEq(raid_geld.balanceOf(player1), raid_geld.INITIAL_GELD());

        // Verify the contract balance is updated
        uint256 contractBalance2 = raid_geld.daoToken().balanceOf(address(raid_geld));
        uint256 userBalance2 = address(player1).balance;

        // Contract should get DAO tokens
        assertLt(contractBalance, contractBalance2);
        // Player should lose ETH
        assertEq(userBalance2, userBalance - raid_geld.BUY_IN_AMOUNT());

        // Verify player is set initially
        Player memory player = raid_geld.getPlayer(player1);
        assertEq(player.total_minted, raid_geld.INITIAL_GELD());
        assertEq(player.created_at, block.timestamp);
        assertEq(player.last_raided_at, block.timestamp);

        Army memory army = raid_geld.getArmy(player1);

        assertEq(army.moloch_denier.level, 0);
        assertEq(army.apprentice.level, 0);
        assertEq(army.anointed.level, 0);
        assertEq(army.champion.level, 0);
    }

    function test_02_2_registrationWithDaoToken() public {
        vm.startPrank(player1);

        uint256 initialBalance = raid_geld.daoToken().balanceOf(address(raid_geld));

        // Making sure event is emitted when player is registered
        // doesnt test player emitted event because other events get emitted before it 
        registerPlayerWithDaoToken();

        // Check that initial raid_geld is received by the player
        assertEq(raid_geld.balanceOf(player1), raid_geld.INITIAL_GELD());

        // Verify the contract dao token balance is updated
        assertEq(
            raid_geld.daoToken().balanceOf(address(raid_geld)), initialBalance + raid_geld.BUY_IN_DAO_TOKEN_AMOUNT()
        );

        // Verify player is set initially
        Player memory player = raid_geld.getPlayer(player1);
        assertEq(player.total_minted, raid_geld.INITIAL_GELD());
        assertEq(player.created_at, block.timestamp);
        assertEq(player.last_raided_at, block.timestamp);

        Army memory army = raid_geld.getArmy(player1);

        assertEq(army.moloch_denier.level, 0);
        assertEq(army.apprentice.level, 0);
        assertEq(army.anointed.level, 0);
        assertEq(army.champion.level, 0);
    } 

    function test_03_dao_token_can_be_withdrawn() public {
        uint256 initialBalance = raid_geld.daoToken().balanceOf(address(raid_geld));

        // Switch to Player 1 and register it
        vm.startPrank(player1);

        // doesnt test player emitted event because other events get emitted before it 
        registerPlayerWithDaoToken();

        // Switch back to owner and withdraw funds
        vm.startPrank(owner);
        raid_geld.withdraw();
        uint256 newBalance = raid_geld.daoToken().balanceOf(address(owner));
        uint256 newContractBalance = raid_geld.daoToken().balanceOf(address(raid_geld));

        // contract balance should be empty
        assertEq(newContractBalance, 0);
        // owner should have the extra funds
        assertGt(newBalance, initialBalance);
    }

    function test_04_only_owner_can_withdraw() public {
        // Register player 1
        vm.startPrank(player1);
        registerPlayer();

        // attempt to withdraw with player 1, it should fail
        vm.expectRevert();
        raid_geld.withdraw();
    }

    function test_05_is_registered() public {
        bool is_registered = raid_geld.isRegistered(player1);
        assertEq(is_registered, false);
        vm.startPrank(player1);

        // Making sure event is emitted when player is registered
        vm.expectEmit(address(raid_geld));
        emit PlayerRegistered(player1, raid_geld.INITIAL_GELD());

        registerPlayer();
        is_registered = raid_geld.isRegistered(player1);
        assertEq(is_registered, true);
    }

    function test_06_add_unit() public {
        vm.startPrank(player1);

        // Making sure event is emitted when player is registered
        vm.expectEmit(address(raid_geld));
        emit PlayerRegistered(player1, raid_geld.INITIAL_GELD());

        registerPlayer();

        vm.expectRevert();
        // units should be in range
        raid_geld.addUnit(100, 1);

        // Player should have enough tokens to burn to get units
        vm.expectRevert();
        raid_geld.addUnit(1, 100);

        Army memory army = raid_geld.getArmy(player1);
        uint256 unit_level = army.moloch_denier.level;
        uint256 balance = raid_geld.balanceOf(player1);
        uint256 income_per_sec = army.profit_per_second;

        uint256 cost = RaidGeldUtils.calculateUnitPrice(0, 0, 1);
        uint256 playerBalance = raid_geld.balanceOf(address(player1));

        // Making sure event is emitted when player adds a unit
        vm.expectEmit(address(raid_geld));

        emit UnitAdded(
            address(player1),
            0,
            1,
            cost,
            playerBalance - cost,
            1,
            0,
            0,
            0
        );

        // Add 1 unit
        raid_geld.addUnit(0, 1);
        uint256 unitPrice = RaidGeldUtils.calculateUnitPrice(0, 0, 1);

        // Check that those tokens were burnt
        // WARN: In addUnit will mint additional tokens but they are not calculated
        // into this because the test doesnt move the blockchain so no extra tokens
        // are minted to the user
        uint256 newBalance = raid_geld.balanceOf(player1);
        assertEq(newBalance, balance - unitPrice);
        army = raid_geld.getArmy(player1);

        // Check that unit level increased
        uint256 new_unit_level = army.moloch_denier.level;
        assertEq(new_unit_level, unit_level + 1);
        // Check that user income per second increased
        uint256 new_income_per_sec = army.profit_per_second;
        assertLt(income_per_sec, new_income_per_sec);
    }

    function test_07_raid() public {
        // Let some time pass so we dont start at block timestamp 0
        vm.warp(120);

        // Register player 1
        vm.startPrank(player1);
        registerPlayer();

        uint256 cost = RaidGeldUtils.calculateUnitPrice(0, 0, 1);
        uint256 playerBalance = raid_geld.balanceOf(address(player1));

        // Making sure event is emitted when player adds a unit
        vm.expectEmit(address(raid_geld));

        emit UnitAdded(
            address(player1),
            0,
            1,
            cost,
            playerBalance - cost,
            1,
            0,
            0,
            0
        );

        // bought 1 moloch_denier
        raid_geld.addUnit(0, 1);
        vm.warp(block.timestamp + 15);

        uint256 balance = raid_geld.balanceOf(player1);
        Army memory army = raid_geld.getArmy(player1);
        Player memory player = raid_geld.getPlayer(player1);

        uint256 amountMinted = army.profit_per_second * 15;

        // Making sure event is emitted when player performs a raid
        vm.expectEmit(address(raid_geld));

        emit RaidPerformed(
            address(player1),
            player.total_minted + amountMinted,
            balance + amountMinted
        );

        // Trigger raid funds minting
        raid_geld.raid();

        // New balance should be larger
        uint256 newBalance = raid_geld.balanceOf(player1);
        player = raid_geld.getPlayer(player1);
        uint256 last_raided_at = player.last_raided_at;
        assertLt(balance, newBalance);

        // After wait time passes raid should bring in profits again
        vm.warp(block.timestamp + 15);

        amountMinted = army.profit_per_second * 15;

        emit RaidPerformed(
            address(player1),
            player.total_minted + amountMinted,
            balance + amountMinted
        );

        raid_geld.raid();

        // Balance should reflect that
        uint256 newestBalance = raid_geld.balanceOf(player1);
        player = raid_geld.getPlayer(player1);
        uint256 last_raided_at_2 = player.last_raided_at;
        assertLt(newBalance, newestBalance);
        assertLt(last_raided_at, last_raided_at_2);
    }
}
