import { EcdsaRAccountContractArtifact, getEcdsaRAccount } from '@aztec/accounts/ecdsa';
import { SchnorrAccountContractArtifact, getSchnorrAccount, getSchnorrWallet } from '@aztec/accounts/schnorr';
import { AccountWalletWithSecretKey, FeeJuicePaymentMethod, FeeJuicePaymentMethodWithClaim, PrivateFeePaymentMethod, SponsoredFeePaymentMethod, createLogger } from '@aztec/aztec.js';
import { CheatCodes } from '@aztec/aztec/testing';
import { RollupContract, createExtendedL1Client, deployL1Contract } from '@aztec/ethereum';
import { ChainMonitor } from '@aztec/ethereum/test';
import { randomBytes } from '@aztec/foundation/crypto';
import { EthAddress } from '@aztec/foundation/eth-address';
import { Fr } from '@aztec/foundation/fields';
import { TestERC20Abi } from '@aztec/l1-artifacts/TestERC20Abi';
import { TestERC20Bytecode } from '@aztec/l1-artifacts/TestERC20Bytecode';
import { AMMContract } from '@aztec/noir-contracts.js/AMM';
import { FPCContract } from '@aztec/noir-contracts.js/FPC';
import { FeeJuiceContract } from '@aztec/noir-contracts.js/FeeJuice';
import { SponsoredFPCContract } from '@aztec/noir-contracts.js/SponsoredFPC';
import { TokenContract as BananaCoin, TokenContract } from '@aztec/noir-contracts.js/Token';
import { ProtocolContractAddress } from '@aztec/protocol-contracts';
import { getCanonicalFeeJuice } from '@aztec/protocol-contracts/fee-juice';
import { createPXEService, getPXEServiceConfig } from '@aztec/pxe/server';
import { deriveSigningKey } from '@aztec/stdlib/keys';
import { MNEMONIC } from '../../fixtures/fixtures.js';
import { createSnapshotManager, deployAccounts } from '../../fixtures/snapshot_manager.js';
import { mintTokensToPrivate } from '../../fixtures/token_utils.js';
import { setupSponsoredFPC } from '../../fixtures/utils.js';
import { CrossChainTestHarness } from '../../shared/cross_chain_test_harness.js';
import { FeeJuicePortalTestingHarnessFactory } from '../../shared/gas_portal_test_harness.js';
import { ProxyLogger } from './benchmark.js';
import { FULL_FLOWS_CONFIG, KEY_FLOWS_CONFIG } from './config.js';
const { E2E_DATA_PATH: dataPath, BENCHMARK_CONFIG } = process.env;
export class ClientFlowsBenchmark {
    snapshotManager;
    logger;
    pxe;
    aztecNode;
    cheatCodes;
    context;
    chainMonitor;
    feeJuiceBridgeTestHarness;
    // The admin that aids in the setup of the test
    adminWallet;
    adminAddress;
    // Aztec Node config
    sequencerAddress;
    coinbase;
    // Contracts
    feeJuiceContract;
    // Asset in which fees are paid via FPC
    bananaCoin;
    bananaFPC;
    // Random asset we want to trade
    candyBarCoin;
    // AMM contract
    amm;
    // Liquidity token for AMM
    liquidityToken;
    // Sponsored FPC contract
    sponsoredFPC;
    // PXE used by the benchmarking user. It can be set up with client-side proving enabled
    userPXE;
    realProofs = [
        'true',
        '1'
    ].includes(process.env.REAL_PROOFS ?? '');
    paymentMethods = {
        // eslint-disable-next-line camelcase
        bridged_fee_juice: {
            forWallet: this.getBridgedFeeJuicePaymentMethodForWallet.bind(this),
            circuits: 2
        },
        // eslint-disable-next-line camelcase
        private_fpc: {
            forWallet: this.getPrivateFPCPaymentMethodForWallet.bind(this),
            circuits: 2 + // FPC entrypoint + kernel inner
            2 + // BananaCoin transfer_to_public + kernel inner
            2 + // Account verify_private_authwit + kernel inner
            2
        },
        // eslint-disable-next-line camelcase
        sponsored_fpc: {
            forWallet: this.getSponsoredFPCPaymentMethodForWallet.bind(this),
            circuits: 2
        },
        // eslint-disable-next-line camelcase
        fee_juice: {
            forWallet: this.getFeeJuicePaymentMethodForWallet.bind(this),
            circuits: 0
        }
    };
    config;
    proxyLogger;
    constructor(testName, setupOptions = {}){
        this.logger = createLogger(`bench:client_flows${testName ? `:${testName}` : ''}`);
        this.snapshotManager = createSnapshotManager(`bench_client_flows${testName ? `/${testName}` : ''}`, dataPath, {
            startProverNode: true,
            ...setupOptions
        }, {
            ...setupOptions
        });
        this.config = BENCHMARK_CONFIG === 'key_flows' ? KEY_FLOWS_CONFIG : FULL_FLOWS_CONFIG;
        ProxyLogger.create();
        this.proxyLogger = ProxyLogger.getInstance();
    }
    async setup() {
        const context = await this.snapshotManager.setup();
        await context.aztecNode.setConfig({
            feeRecipient: this.sequencerAddress,
            coinbase: this.coinbase
        });
        const rollupContract = RollupContract.getFromConfig(context.aztecNodeConfig);
        this.chainMonitor = new ChainMonitor(rollupContract, context.dateProvider, this.logger, 200).start();
        return this;
    }
    async teardown() {
        await this.chainMonitor.stop();
        await this.snapshotManager.teardown();
    }
    async mintAndBridgeFeeJuice(address) {
        const claim = await this.feeJuiceBridgeTestHarness.prepareTokensOnL1(address);
        const { claimSecret: secret, messageLeafIndex: index } = claim;
        await this.feeJuiceContract.methods.claim(address, claim.claimAmount, secret, index).send({
            from: this.adminAddress
        }).wait();
    }
    /** Admin mints bananaCoin tokens privately to the target address and redeems them. */ async mintPrivateBananas(amount, address) {
        const balanceBefore = await this.bananaCoin.methods.balance_of_private(address).simulate({
            from: this.adminAddress
        });
        await mintTokensToPrivate(this.bananaCoin, this.adminAddress, this.adminWallet, address, amount);
        const balanceAfter = await this.bananaCoin.methods.balance_of_private(address).simulate({
            from: this.adminAddress
        });
        expect(balanceAfter).toEqual(balanceBefore + amount);
    }
    async createBenchmarkingAccountManager(pxe, type) {
        const benchysSecretKey = Fr.random();
        const salt = Fr.random();
        let benchysPrivateSigningKey;
        let benchysAccountManager;
        if (type === 'schnorr') {
            benchysPrivateSigningKey = deriveSigningKey(benchysSecretKey);
            benchysAccountManager = await getSchnorrAccount(pxe, benchysSecretKey, benchysPrivateSigningKey, salt);
        } else if (type === 'ecdsar1') {
            benchysPrivateSigningKey = randomBytes(32);
            benchysAccountManager = await getEcdsaRAccount(pxe, benchysSecretKey, benchysPrivateSigningKey, salt);
        } else {
            throw new Error(`Unknown account type: ${type}`);
        }
        await benchysAccountManager.register();
        return benchysAccountManager;
    }
    async applyBaseSnapshots() {
        await this.applyInitialAccountsSnapshot();
        await this.applySetupFeeJuiceSnapshot();
    }
    async applyInitialAccountsSnapshot() {
        await this.snapshotManager.snapshot('initial_accounts', deployAccounts(2, this.logger), async ({ deployedAccounts }, { pxe, aztecNode, aztecNodeConfig })=>{
            this.pxe = pxe;
            this.aztecNode = aztecNode;
            this.cheatCodes = await CheatCodes.create(aztecNodeConfig.l1RpcUrls, pxe);
            const deployedWallets = await Promise.all(deployedAccounts.map((a)=>getSchnorrWallet(pxe, a.address, a.signingKey)));
            [this.adminWallet] = deployedWallets;
            this.adminAddress = this.adminWallet.getAddress();
            this.sequencerAddress = deployedWallets[1].getAddress();
            const canonicalFeeJuice = await getCanonicalFeeJuice();
            this.feeJuiceContract = await FeeJuiceContract.at(canonicalFeeJuice.address, this.adminWallet);
            this.coinbase = EthAddress.random();
            const userPXEConfig = getPXEServiceConfig();
            const l1Contracts = await aztecNode.getL1ContractAddresses();
            const userPXEConfigWithContracts = {
                ...userPXEConfig,
                proverEnabled: this.realProofs,
                l1Contracts
            };
            this.userPXE = await createPXEService(this.aztecNode, userPXEConfigWithContracts, {
                loggers: {
                    prover: this.proxyLogger.createLogger('pxe:bb:wasm:bundle:proxied')
                }
            });
        });
    }
    async applySetupFeeJuiceSnapshot() {
        await this.snapshotManager.snapshot('setup_fee_juice', async ()=>{}, async (_data, context)=>{
            this.context = context;
            this.feeJuiceContract = await FeeJuiceContract.at(ProtocolContractAddress.FeeJuice, this.adminWallet);
            this.feeJuiceBridgeTestHarness = await FeeJuicePortalTestingHarnessFactory.create({
                aztecNode: context.aztecNode,
                aztecNodeAdmin: context.aztecNode,
                pxeService: context.pxe,
                l1Client: context.deployL1ContractsValues.l1Client,
                wallet: this.adminWallet,
                logger: this.logger
            });
        });
    }
    async applyDeployBananaTokenSnapshot() {
        await this.snapshotManager.snapshot('deploy_banana_token', async ()=>{
            const bananaCoin = await BananaCoin.deploy(this.adminWallet, this.adminAddress, 'BC', 'BC', 18n).send({
                from: this.adminAddress
            }).deployed();
            this.logger.info(`BananaCoin deployed at ${bananaCoin.address}`);
            return {
                bananaCoinAddress: bananaCoin.address
            };
        }, async ({ bananaCoinAddress })=>{
            this.bananaCoin = await BananaCoin.at(bananaCoinAddress, this.adminWallet);
        });
    }
    async applyDeployCandyBarTokenSnapshot() {
        await this.snapshotManager.snapshot('deploy_candy_bar_token', async ()=>{
            const candyBarCoin = await TokenContract.deploy(this.adminWallet, this.adminAddress, 'CBC', 'CBC', 18n).send({
                from: this.adminAddress
            }).deployed();
            this.logger.info(`CandyBarCoin deployed at ${candyBarCoin.address}`);
            return {
                candyBarCoinAddress: candyBarCoin.address
            };
        }, async ({ candyBarCoinAddress })=>{
            this.candyBarCoin = await TokenContract.at(candyBarCoinAddress, this.adminWallet);
        });
    }
    async applyFPCSetupSnapshot() {
        await this.snapshotManager.snapshot('fpc_setup', async (context)=>{
            const feeJuiceContract = this.feeJuiceBridgeTestHarness.feeJuice;
            expect((await context.pxe.getContractMetadata(feeJuiceContract.address)).isContractPublished).toBe(true);
            const bananaCoin = this.bananaCoin;
            const bananaFPC = await FPCContract.deploy(this.adminWallet, bananaCoin.address, this.adminAddress).send({
                from: this.adminAddress
            }).deployed();
            this.logger.info(`BananaPay deployed at ${bananaFPC.address}`);
            await this.feeJuiceBridgeTestHarness.bridgeFromL1ToL2(bananaFPC.address, this.adminAddress);
            return {
                bananaFPCAddress: bananaFPC.address
            };
        }, async (data)=>{
            this.bananaFPC = await FPCContract.at(data.bananaFPCAddress, this.adminWallet);
        });
    }
    async applyDeploySponsoredFPCSnapshot() {
        await this.snapshotManager.snapshot('deploy_sponsored_fpc', async ()=>{
            const sponsoredFPC = await setupSponsoredFPC(this.pxe);
            this.logger.info(`SponsoredFPC at ${sponsoredFPC.address}`);
            return {
                sponsoredFPCAddress: sponsoredFPC.address
            };
        }, async ({ sponsoredFPCAddress })=>{
            this.sponsoredFPC = await SponsoredFPCContract.at(sponsoredFPCAddress, this.adminWallet);
        });
    }
    async createCrossChainTestHarness(owner) {
        const l1Client = createExtendedL1Client(this.context.aztecNodeConfig.l1RpcUrls, MNEMONIC);
        const underlyingERC20Address = await deployL1Contract(l1Client, TestERC20Abi, TestERC20Bytecode, [
            'Underlying',
            'UND',
            l1Client.account.address
        ]).then(({ address })=>address);
        this.logger.verbose(`Setting up cross chain harness...`);
        const crossChainTestHarness = await CrossChainTestHarness.new(this.aztecNode, this.pxe, l1Client, owner, owner.getAddress(), this.logger, underlyingERC20Address);
        this.logger.verbose(`L2 token deployed to: ${crossChainTestHarness.l2Token.address}`);
        return crossChainTestHarness;
    }
    async createAndFundBenchmarkingWallet(accountType) {
        const benchysAccountManager = await this.createBenchmarkingAccountManager(this.pxe, accountType);
        const benchysWallet = await benchysAccountManager.getWallet();
        const benchysAddress = benchysAccountManager.getAddress();
        const claim = await this.feeJuiceBridgeTestHarness.prepareTokensOnL1(benchysAddress);
        const paymentMethod = new FeeJuicePaymentMethodWithClaim(benchysWallet, claim);
        await benchysAccountManager.deploy({
            fee: {
                paymentMethod
            }
        }).wait();
        // Register benchy on the user's PXE, where we're going to be interacting from
        await this.userPXE.registerContract({
            instance: benchysAccountManager.getInstance(),
            artifact: accountType === 'ecdsar1' ? EcdsaRAccountContractArtifact : SchnorrAccountContractArtifact
        });
        await this.userPXE.registerAccount(benchysWallet.getSecretKey(), benchysWallet.getCompleteAddress().partialAddress);
        const entrypoint = await benchysAccountManager.getAccount();
        return new AccountWalletWithSecretKey(this.userPXE, entrypoint, benchysWallet.getSecretKey(), benchysAccountManager.salt);
    }
    async applyDeployAmmSnapshot() {
        await this.snapshotManager.snapshot('deploy_amm', async ()=>{
            const liquidityToken = await TokenContract.deploy(this.adminWallet, this.adminAddress, 'LPT', 'LPT', 18n).send({
                from: this.adminAddress
            }).deployed();
            const amm = await AMMContract.deploy(this.adminWallet, this.bananaCoin.address, this.candyBarCoin.address, liquidityToken.address).send({
                from: this.adminAddress
            }).deployed();
            this.logger.info(`AMM deployed at ${amm.address}`);
            await liquidityToken.methods.set_minter(amm.address, true).send({
                from: this.adminAddress
            }).wait();
            return {
                ammAddress: amm.address,
                liquidityTokenAddress: liquidityToken.address
            };
        }, async ({ ammAddress, liquidityTokenAddress })=>{
            this.liquidityToken = await TokenContract.at(liquidityTokenAddress, this.adminWallet);
            this.amm = await AMMContract.at(ammAddress, this.adminWallet);
        });
    }
    async getBridgedFeeJuicePaymentMethodForWallet(wallet) {
        const claim = await this.feeJuiceBridgeTestHarness.prepareTokensOnL1(wallet.getAddress());
        return new FeeJuicePaymentMethodWithClaim(wallet, claim);
    }
    getPrivateFPCPaymentMethodForWallet(wallet) {
        return Promise.resolve(new PrivateFeePaymentMethod(this.bananaFPC.address, wallet));
    }
    getSponsoredFPCPaymentMethodForWallet(_wallet) {
        return Promise.resolve(new SponsoredFeePaymentMethod(this.sponsoredFPC.address));
    }
    getFeeJuicePaymentMethodForWallet(wallet) {
        return Promise.resolve(new FeeJuicePaymentMethod(wallet.getAddress()));
    }
}
