import { generateSchnorrAccounts } from '@aztec/accounts/testing';
import { NO_FROM } from '@aztec/aztec.js/account';
import { NO_WAIT } from '@aztec/aztec.js/contracts';
import { L1FeeJuicePortalManager } from '@aztec/aztec.js/ethereum';
import { FeeJuicePaymentMethodWithClaim } from '@aztec/aztec.js/fee';
import { SponsoredFeePaymentMethod } from '@aztec/aztec.js/fee';
import { Fr } from '@aztec/aztec.js/fields';
import { createAztecNodeClient, waitForTx } from '@aztec/aztec.js/node';
import { createEthereumChain } from '@aztec/ethereum/chain';
import { createExtendedL1Client } from '@aztec/ethereum/client';
import { retryUntil } from '@aztec/foundation/retry';
import { TokenContract } from '@aztec/noir-contracts.js/Token';
import { registerInitialLocalNetworkAccountsInWallet } from '@aztec/wallets/testing';
import { getACVMConfig } from '../fixtures/get_acvm_config.js';
import { getBBConfig } from '../fixtures/get_bb_config.js';
import { getSponsoredFPCAddress, registerSponsoredFPC } from '../fixtures/utils.js';
import { TestWallet } from '../test-wallet/test_wallet.js';
import { proveInteraction } from '../test-wallet/utils.js';
import { WorkerWallet } from '../test-wallet/worker_wallet.js';
const TOKEN_NAME = 'USDC';
const TOKEN_SYMBOL = 'USD';
const TOKEN_DECIMALS = 18n;
export async function setupTestAccountsWithTokens(nodeUrl, mintAmount, logger) {
    const ACCOUNT_COUNT = 1; // TODO fix this to allow for 16 wallets again
    const aztecNode = createAztecNodeClient(nodeUrl);
    const wallet = await TestWallet.create(aztecNode);
    const [recipientAccount, ...accounts] = (await registerInitialLocalNetworkAccountsInWallet(wallet)).slice(0, ACCOUNT_COUNT + 1);
    const tokenAdmin = accounts[0];
    const tokenAddress = await deployTokenAndMint(wallet, accounts, tokenAdmin, mintAmount, undefined, logger);
    const tokenContract = TokenContract.at(tokenAddress, wallet);
    return {
        aztecNode,
        accounts,
        wallet,
        tokenAdminAddress: tokenAdmin,
        tokenName: TOKEN_NAME,
        tokenAddress,
        tokenContract,
        recipientAddress: recipientAccount
    };
}
export async function deploySponsoredTestAccountsWithTokens(wallet, aztecNode, mintAmount, logger, numberOfFundedWallets = 1) {
    const [recipient, ...funded] = await generateSchnorrAccounts(numberOfFundedWallets + 1);
    const recipientAccount = await wallet.createSchnorrAccount(recipient.secret, recipient.salt);
    const fundedAccounts = await Promise.all(funded.map((a)=>wallet.createSchnorrAccount(a.secret, a.salt)));
    await registerSponsoredFPC(wallet);
    const paymentMethod = new SponsoredFeePaymentMethod(await getSponsoredFPCAddress());
    const recipientDeployMethod = await recipientAccount.getDeployMethod();
    await recipientDeployMethod.send({
        from: NO_FROM,
        fee: {
            paymentMethod
        },
        wait: {
            timeout: 2400
        }
    });
    await Promise.all(fundedAccounts.map(async (a)=>{
        const deployMethod = await a.getDeployMethod();
        await deployMethod.send({
            from: NO_FROM,
            fee: {
                paymentMethod
            },
            wait: {
                timeout: 2400
            }
        }); // increase timeout on purpose in order to account for two empty epochs
        logger.info(`Account deployed at ${a.address}`);
    }));
    const tokenAdmin = fundedAccounts[0];
    const tokenAddress = await deployTokenAndMint(wallet, fundedAccounts.map((acc)=>acc.address), tokenAdmin.address, mintAmount, new SponsoredFeePaymentMethod(await getSponsoredFPCAddress()), logger);
    const tokenContract = TokenContract.at(tokenAddress, wallet);
    return {
        aztecNode,
        wallet,
        accounts: fundedAccounts.map((acc)=>acc.address),
        tokenAdminAddress: tokenAdmin.address,
        tokenName: TOKEN_NAME,
        tokenAddress,
        tokenContract,
        recipientAddress: recipientAccount.address
    };
}
async function deployAccountWithDiagnostics(account, paymentMethod, aztecNode, logger, accountLabel, estimateGas) {
    const deployMethod = await account.getDeployMethod();
    let txHash;
    try {
        let gasSettings;
        if (estimateGas) {
            const sim = await deployMethod.simulate({
                from: NO_FROM,
                fee: {
                    paymentMethod
                }
            });
            gasSettings = sim.estimatedGas;
            logger.info(`${accountLabel} estimated gas: DA=${gasSettings.gasLimits.daGas} L2=${gasSettings.gasLimits.l2Gas}`);
        }
        const deployResult = await deployMethod.send({
            from: NO_FROM,
            fee: {
                paymentMethod,
                gasSettings
            },
            wait: NO_WAIT
        });
        txHash = deployResult.txHash;
        await waitForTx(aztecNode, txHash, {
            timeout: 2400
        });
        logger.info(`${accountLabel} deployed at ${account.address}`);
    } catch (error) {
        const blockNumber = await aztecNode.getBlockNumber();
        let receipt;
        try {
            receipt = await aztecNode.getTxReceipt(txHash);
        } catch  {
            receipt = 'unavailable';
        }
        logger.error(`${accountLabel} deployment failed`, {
            txHash: txHash.toString(),
            receipt: JSON.stringify(receipt),
            currentBlockNumber: blockNumber,
            error: String(error)
        });
        throw error;
    }
}
async function deployAccountsInBatches(accounts, paymentMethod, aztecNode, logger, labelPrefix, batchSize = 2, estimateGas) {
    for(let i = 0; i < accounts.length; i += batchSize){
        const batch = accounts.slice(i, i + batchSize);
        await Promise.all(batch.map((account, idx)=>deployAccountWithDiagnostics(account, paymentMethod, aztecNode, logger, `${labelPrefix}${i + idx + 1}`, estimateGas)));
    }
}
export async function deploySponsoredTestAccounts(wallet, aztecNode, logger, numberOfFundedWallets = 1, opts) {
    const [recipient, ...funded] = await generateSchnorrAccounts(numberOfFundedWallets + 1);
    const recipientAccount = await wallet.createSchnorrAccount(recipient.secret, recipient.salt);
    const fundedAccounts = await Promise.all(funded.map((a)=>wallet.createSchnorrAccount(a.secret, a.salt)));
    await registerSponsoredFPC(wallet);
    const paymentMethod = new SponsoredFeePaymentMethod(await getSponsoredFPCAddress());
    await deployAccountWithDiagnostics(recipientAccount, paymentMethod, aztecNode, logger, 'Recipient account', opts?.estimateGas);
    await deployAccountsInBatches(fundedAccounts, paymentMethod, aztecNode, logger, 'Funded account ', 2, opts?.estimateGas);
    return {
        aztecNode,
        wallet,
        accounts: fundedAccounts.map((acc)=>acc.address),
        recipientAddress: recipientAccount.address
    };
}
export async function deployTestAccountsWithTokens(nodeUrl, l1RpcUrls, mnemonicOrPrivateKey, mintAmount, logger, numberOfFundedWallets = 1) {
    const aztecNode = createAztecNodeClient(nodeUrl);
    const wallet = await TestWallet.create(aztecNode);
    const [recipient, ...funded] = await generateSchnorrAccounts(numberOfFundedWallets + 1);
    const recipientAccount = await wallet.createSchnorrAccount(recipient.secret, recipient.salt);
    const fundedAccounts = await Promise.all(funded.map((a)=>wallet.createSchnorrAccount(a.secret, a.salt)));
    const claims = await Promise.all(fundedAccounts.map((a)=>bridgeL1FeeJuice(l1RpcUrls, mnemonicOrPrivateKey, aztecNode, a.address, undefined, logger)));
    // Progress by 3 L2 blocks so that the l1ToL2Message added above will be available to use on L2.
    await advanceL2Block(aztecNode);
    await advanceL2Block(aztecNode);
    await advanceL2Block(aztecNode);
    await Promise.all(fundedAccounts.map(async (a, i)=>{
        const paymentMethod = new FeeJuicePaymentMethodWithClaim(a.address, claims[i]);
        const deployMethod = await a.getDeployMethod();
        await deployMethod.send({
            from: NO_FROM,
            fee: {
                paymentMethod
            }
        });
        logger.info(`Account deployed at ${a.address}`);
    }));
    const tokenAdmin = fundedAccounts[0];
    const tokenAddress = await deployTokenAndMint(wallet, fundedAccounts.map((acc)=>acc.address), tokenAdmin.address, mintAmount, undefined, logger);
    const tokenContract = TokenContract.at(tokenAddress, wallet);
    return {
        aztecNode,
        wallet,
        accounts: fundedAccounts.map((acc)=>acc.address),
        tokenAdminAddress: tokenAdmin.address,
        tokenName: TOKEN_NAME,
        tokenAddress,
        tokenContract,
        recipientAddress: recipientAccount.address
    };
}
async function bridgeL1FeeJuice(l1RpcUrls, mnemonicOrPrivateKey, aztecNode, recipient, amount, log) {
    const { l1ChainId } = await aztecNode.getNodeInfo();
    const chain = createEthereumChain(l1RpcUrls, l1ChainId);
    const l1Client = createExtendedL1Client(chain.rpcUrls, mnemonicOrPrivateKey, chain.chainInfo);
    const portal = await L1FeeJuicePortalManager.new(aztecNode, l1Client, log);
    const claim = await portal.bridgeTokensPublic(recipient, amount, true);
    const isSynced = async ()=>await aztecNode.getL1ToL2MessageCheckpoint(Fr.fromHexString(claim.messageHash)) !== undefined;
    await retryUntil(isSynced, `message ${claim.messageHash} sync`, 24, 0.5);
    log.info(`Created a claim for ${amount} L1 fee juice to ${recipient}.`, claim);
    return claim;
}
async function advanceL2Block(aztecNode, nodeAdmin) {
    const initialBlockNumber = await aztecNode.getBlockNumber();
    let minTxsPerBlock = undefined;
    if (nodeAdmin) {
        ({ minTxsPerBlock } = await nodeAdmin.getConfig());
        await nodeAdmin.setConfig({
            minTxsPerBlock: 0
        }); // Set to 0 to ensure we can advance the block
    }
    await retryUntil(async ()=>await aztecNode.getBlockNumber() >= initialBlockNumber + 1);
    if (nodeAdmin && minTxsPerBlock !== undefined) {
        await nodeAdmin.setConfig({
            minTxsPerBlock
        });
    }
}
async function deployTokenAndMint(wallet, accounts, admin, mintAmount, paymentMethod, logger) {
    logger.verbose(`Deploying TokenContract...`);
    const { receipt: { contract: tokenContract } } = await TokenContract.deploy(wallet, admin, TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS).send({
        from: admin,
        fee: {
            paymentMethod
        },
        wait: {
            timeout: 600,
            returnReceipt: true
        }
    });
    const tokenAddress = tokenContract.address;
    logger.verbose(`Minting ${mintAmount} public assets to the ${accounts.length} accounts...`);
    await Promise.all(accounts.map((acc)=>TokenContract.at(tokenAddress, wallet).methods.mint_to_public(acc, mintAmount).send({
            from: admin,
            fee: {
                paymentMethod
            },
            wait: {
                timeout: 600
            }
        })));
    logger.verbose(`Minting complete.`);
    return tokenAddress;
}
export async function performTransfers({ wallet, testAccounts, rounds, transferAmount, logger, feePaymentMethod }) {
    const recipient = testAccounts.recipientAddress;
    // Default to sponsored fee payment if no fee method is provided
    const defaultFeePaymentMethod = feePaymentMethod || new SponsoredFeePaymentMethod(await getSponsoredFPCAddress());
    for(let i = 0; i < rounds; i++){
        const txs = testAccounts.accounts.map((acc)=>{
            const token = TokenContract.at(testAccounts.tokenAddress, testAccounts.wallet);
            return proveInteraction(wallet, token.methods.transfer_in_public(acc, recipient, transferAmount, 0), {
                from: acc,
                fee: {
                    paymentMethod: defaultFeePaymentMethod
                }
            });
        });
        const provenTxs = await Promise.all(txs);
        await Promise.all(provenTxs.map((t)=>t.send({
                wait: {
                    timeout: 600
                }
            })));
        logger.info(`Completed round ${i + 1} / ${rounds}`);
    }
}
export async function createWalletAndAztecNodeClient(nodeUrl, proverEnabled, logger) {
    const aztecNode = createAztecNodeClient(nodeUrl);
    const [bbConfig, acvmConfig] = await Promise.all([
        getBBConfig(logger),
        getACVMConfig(logger)
    ]);
    const pxeConfig = {
        dataDirectory: undefined,
        dataStoreMapSizeKb: 1024 * 1024,
        ...bbConfig,
        ...acvmConfig,
        proverEnabled
    };
    const wallet = await TestWallet.create(aztecNode, pxeConfig);
    return {
        wallet,
        aztecNode,
        async cleanup () {
            await wallet.stop();
            await bbConfig?.cleanup();
            await acvmConfig?.cleanup();
        }
    };
}
export async function createWorkerWalletClient(nodeUrl, proverEnabled, logger) {
    const aztecNode = createAztecNodeClient(nodeUrl);
    const [bbConfig, acvmConfig] = await Promise.all([
        getBBConfig(logger),
        getACVMConfig(logger)
    ]);
    // Strip cleanup functions — they can't be structured-cloned for worker transfer
    const { cleanup: bbCleanup, ...bbPaths } = bbConfig ?? {};
    const { cleanup: acvmCleanup, ...acvmPaths } = acvmConfig ?? {};
    const pxeConfig = {
        dataDirectory: undefined,
        dataStoreMapSizeKb: 1024 * 1024,
        ...bbPaths,
        ...acvmPaths,
        proverEnabled
    };
    const wallet = await WorkerWallet.create(nodeUrl, pxeConfig);
    return {
        wallet,
        aztecNode,
        async cleanup () {
            await wallet.stop();
            await bbCleanup?.();
            await acvmCleanup?.();
        }
    };
}
