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

import "forge-std/Test.sol";
import "@agglayer/lib/DepositContractV2.sol";
import "@agglayer/lib/GlobalExitRootLib.sol";
import "../src/DepositContractTestHelpers.sol";

/**
 * @title ClaimAssetTestVectorsLocalTx
 * @notice Test contract that generates test vectors for an L1 bridgeAsset transaction.
 *         This simulates calling bridgeAsset() on the PolygonZkEVMBridgeV2 contract
 *         and captures all relevant data including VALID Merkle proofs.
 *
 * Run with:
 *   forge test -vv --match-contract ClaimAssetTestVectorsLocalTx
 *
 * To specify a custom destination address (e.g. from a Miden AccountId):
 *   DESTINATION_ADDRESS=0x00000000... forge test -vv --match-contract ClaimAssetTestVectorsLocalTx
 *
 * To specify a custom origin token address (e.g. matching a genesis faucet):
 *   ORIGIN_TOKEN_ADDRESS=0x... forge test -vv --match-contract ClaimAssetTestVectorsLocalTx
 *
 * The output can be used to verify Miden's ability to process L1 bridge transactions.
 */
/// @dev Miden network ID used as the bridge destination. Must match the agglayer bridge's
/// `MIDEN_NETWORK_ID` MASM constant (77 as of miden-protocol 0.15; was 20 in 0.14).
uint32 constant MIDEN_NETWORK_ID = 77;

/// @dev Default bridge amount in wei for test vectors.
uint256 constant DEFAULT_BRIDGE_AMOUNT = 100_000_000;

contract ClaimAssetTestVectorsLocalTx is Test, DepositContractV2, DepositContractTestHelpers {
    /**
     * @notice Public wrapper for verifyMerkleProof to allow external calls from tests.
     *         The base contract's verifyMerkleProof is internal, so we expose it here
     *         with a different name to enable `this.verifyProof()` calls with calldata conversion.
     */
    function verifyProof(
        bytes32 leafHash,
        bytes32[32] calldata smtProof,
        uint32 index,
        bytes32 root
    ) public pure returns (bool) {
        return verifyMerkleProof(leafHash, smtProof, index, root);
    }

    /**
     * @notice Resolves the destination address from the DESTINATION_ADDRESS environment variable.
     *         Falls back to a default address if the env var is not set.
     * @return The destination address to use in the test vectors
     */
    function _resolveDestinationAddress() internal view returns (address) {
        // Default destination address (used when env var is not set)
        address defaultAddress = 0x00000000AA0000000000bb000000cc000000Dd00;

        // Try to read DESTINATION_ADDRESS from environment
        try vm.envAddress("DESTINATION_ADDRESS") returns (address envAddr) {
            console.log("Using destination address from DESTINATION_ADDRESS env var:");
            console.log(envAddr);
            return envAddr;
        } catch {
            console.log("DESTINATION_ADDRESS env var not set, using default:");
            console.log(defaultAddress);
            return defaultAddress;
        }
    }

    /**
     * @notice Resolves the origin token address from the ORIGIN_TOKEN_ADDRESS environment variable.
     *         Falls back to a default address if the env var is not set.
     * @return The origin token address to use in the test vectors
     */
    function _resolveOriginTokenAddress() internal view returns (address) {
        address defaultAddress = 0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF;

        try vm.envAddress("ORIGIN_TOKEN_ADDRESS") returns (address envAddr) {
            console.log("Using origin token address from ORIGIN_TOKEN_ADDRESS env var:");
            console.log(envAddr);
            return envAddr;
        } catch {
            console.log("ORIGIN_TOKEN_ADDRESS env var not set, using default:");
            console.log(defaultAddress);
            return defaultAddress;
        }
    }

    /**
     * @notice Resolves the deposit offset from the DEPOSIT_OFFSET environment variable.
     *         Falls back to 0 if the env var is not set.
     *
     *         The deposit offset controls how many dummy leaves are added before the real
     *         deposit, resulting in a different leaf_index each time. This is critical for
     *         repeated test runs against the same node, because the bridge tracks claim
     *         nullifiers by (leaf_index, source_bridge_network) and rejects duplicates.
     * @return The number of dummy deposits to insert before the real one
     */
    function _resolveDepositOffset() internal view returns (uint256) {
        try vm.envUint("DEPOSIT_OFFSET") returns (uint256 offset) {
            console.log("Using deposit offset from DEPOSIT_OFFSET env var:", offset);
            return offset;
        } catch {
            return 0;
        }
    }

    /**
     * @notice Generates bridge asset test vectors with VALID Merkle proofs.
     *         Simulates a user calling bridgeAsset() to bridge tokens from L1 to Miden.
     *
     *         The destination address can be overridden via the DESTINATION_ADDRESS env var.
     *         This allows the Rust integration test to create a real wallet, convert its
     *         AccountId to an Ethereum address format, and pass it to this test.
     *
     *         Set DEPOSIT_OFFSET to a unique value per run to avoid claim nullifier collisions
     *         when running multiple times against the same node instance.
     *
     *         Output file: test-vectors/claim_asset_vectors_local_tx.json
     */
    function test_generateClaimAssetVectorsLocalTx() public {
        string memory obj = "root";

        // ====== BRIDGE TRANSACTION PARAMETERS ======

        uint8 leafType = 0;
        uint32 originNetwork = 0;
        address originTokenAddress = _resolveOriginTokenAddress();
        uint32 destinationNetwork = MIDEN_NETWORK_ID;
        address destinationAddress = _resolveDestinationAddress();
        uint256 amount = DEFAULT_BRIDGE_AMOUNT;

        bytes memory metadata = abi.encode("Test Token", "TEST", uint8(18));
        bytes32 metadataHash = keccak256(metadata);

        // ====== ADD DUMMY DEPOSITS FOR UNIQUE LEAF INDEX ======

        // Insert dummy leaves so the real deposit lands at a unique leaf_index.
        // The bridge tracks claimed deposits by Poseidon2(leaf_index, source_bridge_network),
        // so each test run needs a different leaf_index to avoid ERR_CLAIM_ALREADY_SPENT.
        uint256 depositOffset = _resolveDepositOffset();
        for (uint256 i = 0; i < depositOffset; i++) {
            _addLeaf(keccak256(abi.encodePacked("dummy_deposit", i)));
        }

        // ====== COMPUTE LEAF VALUE AND ADD TO TREE ======

        bytes32 leafValue = getLeafValue(
            leafType, originNetwork, originTokenAddress, destinationNetwork, destinationAddress, amount, metadataHash
        );

        // Add the leaf to the deposit tree to generate valid Merkle proof
        _addLeaf(leafValue);

        // Get the deposit count (leaf index) - depositCount is uint256 in DepositContractBase
        uint256 depositCountValue = uint256(depositCount);

        // Get the local exit root (root of the deposit tree)
        bytes32 localExitRoot = getRoot();

        // ====== GENERATE MERKLE PROOF ======

        // Generate canonical zeros for the Merkle proof
        bytes32[32] memory canonicalZeros = _computeCanonicalZeros();

        // Build the Merkle proof from _branch array and canonical zeros
        // The leaf index is depositCountValue - 1 (0-indexed)
        uint256 leafIndex = depositCountValue - 1;
        bytes32[32] memory smtProofLocal = _generateLocalProof(leafIndex, canonicalZeros);

        // For mainnet deposits, the rollup proof is all zeros
        bytes32[32] memory smtProofRollup;
        for (uint256 i = 0; i < 32; i++) {
            smtProofRollup[i] = bytes32(0);
        }

        // ====== COMPUTE EXIT ROOTS ======

        // For a simulated L1 bridge transaction:
        // - mainnetExitRoot is the local exit root from the deposit tree
        // - rollupExitRoot is simulated (deterministic for reproducibility)
        bytes32 mainnetExitRoot = localExitRoot;
        bytes32 rollupExitRoot = keccak256(abi.encodePacked("rollup_exit_root_simulated"));

        // Compute global exit root
        bytes32 globalExitRoot = GlobalExitRootLib.calculateGlobalExitRoot(mainnetExitRoot, rollupExitRoot);

        // ====== VERIFY MERKLE PROOF ======

        // Verify that the generated proof is valid
        require(
            this.verifyProof(leafValue, smtProofLocal, uint32(leafIndex), mainnetExitRoot),
            "Generated Merkle proof is invalid!"
        );

        // ====== COMPUTE GLOBAL INDEX ======

        // Global index for mainnet deposits: (1 << 64) | leafIndex
        // Note: leafIndex is 0-based (depositCount - 1), matching how the bridge contract
        // extracts it via uint32(globalIndex) in _verifyLeaf()
        uint256 globalIndex = (uint256(1) << 64) | uint256(leafIndex);

        // ====== SERIALIZE SMT PROOFS ======
        _serializeProofs(obj, smtProofLocal, smtProofRollup);

        // Scoped block 2: Serialize transaction parameters
        {
            vm.serializeUint(obj, "leaf_type", leafType);
            vm.serializeUint(obj, "origin_network", originNetwork);
            vm.serializeAddress(obj, "origin_token_address", originTokenAddress);
            vm.serializeUint(obj, "destination_network", destinationNetwork);
            vm.serializeAddress(obj, "destination_address", destinationAddress);
            vm.serializeUint(obj, "amount", amount);
            vm.serializeBytes(obj, "metadata", metadata);
            vm.serializeBytes32(obj, "metadata_hash", metadataHash);
            vm.serializeBytes32(obj, "leaf_value", leafValue);
        }

        // Scoped block 3: Serialize state, exit roots, and finalize
        {
            vm.serializeUint(obj, "deposit_count", depositCountValue);
            vm.serializeBytes32(obj, "global_index", bytes32(globalIndex));
            vm.serializeBytes32(obj, "local_exit_root", localExitRoot);
            vm.serializeBytes32(obj, "mainnet_exit_root", mainnetExitRoot);
            vm.serializeBytes32(obj, "rollup_exit_root", rollupExitRoot);
            vm.serializeBytes32(obj, "global_exit_root", globalExitRoot);

            string memory json = vm.serializeString(
                obj, "description", "L1 bridgeAsset transaction test vectors with valid Merkle proofs"
            );

            string memory outputPath = "test-vectors/claim_asset_vectors_local_tx.json";
            vm.writeJson(json, outputPath);

            console.log("Generated claim asset local tx test vectors with valid Merkle proofs");
            console.log("Output file:", outputPath);
            console.log("Leaf index:", leafIndex);
            console.log("Deposit count:", depositCountValue);
        }
    }

    /**
     * @notice Helper function to serialize SMT proofs (avoids stack too deep)
     * @param obj The JSON object key
     * @param smtProofLocal The local exit root proof
     * @param smtProofRollup The rollup exit root proof
     */
    function _serializeProofs(string memory obj, bytes32[32] memory smtProofLocal, bytes32[32] memory smtProofRollup)
        internal
    {
        bytes32[] memory smtProofLocalDyn = new bytes32[](32);
        bytes32[] memory smtProofRollupDyn = new bytes32[](32);
        for (uint256 i = 0; i < 32; i++) {
            smtProofLocalDyn[i] = smtProofLocal[i];
            smtProofRollupDyn[i] = smtProofRollup[i];
        }

        vm.serializeBytes32(obj, "smt_proof_local_exit_root", smtProofLocalDyn);
        vm.serializeBytes32(obj, "smt_proof_rollup_exit_root", smtProofRollupDyn);
    }
}
