// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

// Testing utilities
import { CommonTest } from "./CommonTest.t.sol";

// Libraries
import { Types } from "../src/libraries/Types.sol";
import { Hashing } from "../src/libraries/Hashing.sol";

// Target contract
import { L2ToL1MessagePasser } from "../src/L2/L2ToL1MessagePasser.sol";

contract L2ToL1MessagePasserTest is CommonTest {
    L2ToL1MessagePasser messagePasser;

    event MessagePassed(
        uint256 indexed nonce,
        address indexed sender,
        address indexed target,
        uint256 value,
        uint256 gasLimit,
        bytes data,
        bytes32 withdrawalHash
    );

    event WithdrawerBalanceBurnt(uint256 indexed amount);

    /// @dev Sets up the test suite.
    function setUp() public virtual override {
        super.setUp();
        messagePasser = new L2ToL1MessagePasser();
    }

    /// @dev Tests that `initiateWithdrawal` succeeds and correctly sets the state
    ///      of the message passer for the withdrawal hash.
    function testFuzz_initiateWithdrawal_succeeds(
        address _sender,
        address _target,
        uint256 _value,
        uint256 _gasLimit,
        bytes memory _data
    ) external {
        uint256 nonce = messagePasser.messageNonce();

        bytes32 withdrawalHash = Hashing.hashWithdrawal(
            Types.WithdrawalTransaction({
                nonce: nonce,
                sender: _sender,
                target: _target,
                value: _value,
                gasLimit: _gasLimit,
                data: _data
            })
        );

        vm.expectEmit(true, true, true, true);
        emit MessagePassed(nonce, _sender, _target, _value, _gasLimit, _data, withdrawalHash);

        vm.deal(_sender, _value);
        vm.prank(_sender);
        messagePasser.initiateWithdrawal{ value: _value }(_target, _gasLimit, _data);

        assertEq(messagePasser.sentMessages(withdrawalHash), true);

        bytes32 slot = keccak256(bytes.concat(withdrawalHash, bytes32(0)));

        assertEq(vm.load(address(messagePasser), slot), bytes32(uint256(1)));
    }

    /// @dev Tests that `initiateWithdrawal` succeeds and emits the correct MessagePassed
    ///      log when called by a contract.
    function test_initiateWithdrawal_fromContract_succeeds() external {
        bytes32 withdrawalHash = Hashing.hashWithdrawal(
            Types.WithdrawalTransaction(
                messagePasser.messageNonce(),
                address(this),
                address(4),
                100,
                64000,
                hex""
            )
        );

        vm.expectEmit(true, true, true, true);
        emit MessagePassed(
            messagePasser.messageNonce(),
            address(this),
            address(4),
            100,
            64000,
            hex"",
            withdrawalHash
        );

        vm.deal(address(this), 2**64);
        messagePasser.initiateWithdrawal{ value: 100 }(address(4), 64000, hex"");
    }

    /// @dev Tests that `initiateWithdrawal` succeeds and emits the correct MessagePassed
    ///      log when called by an EOA.
    function test_initiateWithdrawal_fromEOA_succeeds() external {
        uint256 gasLimit = 64000;
        address target = address(4);
        uint256 value = 100;
        bytes memory data = hex"ff";
        uint256 nonce = messagePasser.messageNonce();

        // EOA emulation
        vm.prank(alice, alice);
        vm.deal(alice, 2**64);
        bytes32 withdrawalHash = Hashing.hashWithdrawal(
            Types.WithdrawalTransaction(nonce, alice, target, value, gasLimit, data)
        );

        vm.expectEmit(true, true, true, true);
        emit MessagePassed(nonce, alice, target, value, gasLimit, data, withdrawalHash);

        messagePasser.initiateWithdrawal{ value: value }(target, gasLimit, data);

        // the sent messages mapping is filled
        assertEq(messagePasser.sentMessages(withdrawalHash), true);
        // the nonce increments
        assertEq(nonce + 1, messagePasser.messageNonce());
    }

    /// @dev Tests that `burn` succeeds and destroys the ETH held in the contract.
    function test_burn_succeeds() external {
        messagePasser.initiateWithdrawal{ value: NON_ZERO_VALUE }(
            NON_ZERO_ADDRESS,
            NON_ZERO_GASLIMIT,
            NON_ZERO_DATA
        );

        assertEq(address(messagePasser).balance, NON_ZERO_VALUE);
        vm.expectEmit(true, false, false, false);
        emit WithdrawerBalanceBurnt(NON_ZERO_VALUE);
        messagePasser.burn();

        // The Withdrawer should have no balance
        assertEq(address(messagePasser).balance, 0);
    }
}
