// 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, Boss} 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(1000 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_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_04_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_05_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);
    }

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

        // Register player 1
        vm.startPrank(player1);
        registerPlayerWithDaoToken();
        raid_geld.addUnit(0, 1);

        uint256 initialDaoBalance = raid_geld.daoToken().balanceOf(player1);
        uint256 initialContractBalance = raid_geld.daoToken().balanceOf(address(raid_geld));

        Boss memory boss = raid_geld.getBoss(player1);
        // assert boss is initialized
        assertEq(boss.level, 0);
        // make sure variants shuffled
        assertNotEq(boss.variants[1], boss.variants[2]);

        // Make a lot of time pass so user deffo has GELD to attack the boss
        vm.warp(1200000);
        bool[2] memory results = raid_geld.battle_with_boss();
        // Should almost always defeat first boss
        assertEq(results[0], true);
        // First boss doesnt grant a new prestige level (ascension)
        assertEq(results[1], false);

        uint256 afterBossDaoBalance = raid_geld.daoToken().balanceOf(player1);
        uint256 afterBossContractBalance = raid_geld.daoToken().balanceOf(address(raid_geld));

        // User should receive funs, contract should lose them
        assertLt(initialDaoBalance, afterBossDaoBalance);
        assertGt(initialContractBalance, afterBossContractBalance);

        // Boss should level up
        boss = raid_geld.getBoss(player1);
        assertEq(boss.level, 1);
        
        Player memory player = raid_geld.getPlayer(player1);
        // Players total rewards should increase
        assertGt(player.total_rewards, 0);
    }

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

        // Register player 1
        vm.startPrank(player1);
        registerPlayerWithDaoToken();
        raid_geld.addUnit(0, 1);

        bool[2] memory results = raid_geld.battle_with_boss();
        // Should lose with just starting GELD 
        assertEq(results[0], false);
        // First boss doesnt grant a new prestige level (ascension)
        assertEq(results[1], false);

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

        // player sessions should end
        assertEq(player.has_active_session, false);
        // player should lose all geld
        assertEq(raid_geld.balanceOf(player1), 0);
        // Units should reset
        assertEq(army.moloch_denier.level, 0);
    }

    function test_08_player_who_lost_can_restart() public {
        // Let some time pass so we dont start at block timestamp 0
        vm.warp(120);
        uint256 balance1 = raid_geld.daoToken().balanceOf(address(raid_geld));

        // Register player 1
        vm.startPrank(player1);
        registerPlayerWithDaoToken();
        raid_geld.addUnit(0, 1);
        uint256 balance2 = raid_geld.daoToken().balanceOf(address(raid_geld));
        // Contract gets DAO Tokens with first register
        assertLt(balance1, balance2);

        bool[2] memory results = raid_geld.battle_with_boss();
        // Should lose with just starting GELD 
        assertEq(results[0], false);
        Player memory player = raid_geld.getPlayer(player1);
        // player sessions should end
        assertEq(player.has_active_session, false);
        assertEq(player.is_registered, true);

        registerPlayerWithDaoToken();
        player = raid_geld.getPlayer(player1);
        assertEq(player.has_active_session, true);
        uint256 balance3 = raid_geld.daoToken().balanceOf(address(raid_geld));
        assertLt(balance2, balance3);
    }

    function test_09_player_can_gain_prestige(uint256 prevrandao) public {
        vm.assume(prevrandao < type(uint256).max - 1e4);
        vm.startPrank(player1);
        stdstore.target(DAO_TOKEN).sig("balanceOf(address)").with_key(player1).checked_write(10000000 ether);
        stdstore.target(DAO_TOKEN).sig("balanceOf(address)").with_key(address(raid_geld)).checked_write(10000000 ether);
        bool success = false;
        uint256 tries = 0;
        uint256 streak = 0;
        for (uint i = 0; i < 1000; i++) {
            vm.prevrandao(prevrandao + i * 59);
            bool[2] memory results;
            bool alreadyLost = false;
            uint256 newStreak = 0;
            tries += 1;
            console.log("NEW TRY, number: ", tries);
            registerPlayerWithDaoToken();
            // Give bajillion GELD to player so player can battle boss after boss
            stdstore.target(address(raid_geld)).sig("balanceOf(address)").with_key(player1).checked_write(10000000 ether);
            for (uint j = 0; j < 6; j++) {
                vm.prevrandao(prevrandao + j * 7 + i * 59);
                newStreak += 1;
                results = raid_geld.battle_with_boss();
                if (results[0] == false) {
                    alreadyLost = true;
                    break;
                }
            }
            if (alreadyLost) {
                if (newStreak > streak) {
                    streak = newStreak;
                }
                continue;
            }
            results = raid_geld.battle_with_boss();
            if (results[0] == true && results[1] == true) {
                success = true;
                Player memory player = raid_geld.getPlayer(player1);
                vm.assertEq(player.prestige_level, 1);
                vm.assertEq(player.n_runs, tries);
                break;
            }
        }
        require(success, "Player should eventually succeed");
    }
}
