import { NO_FROM } from '@aztec/aztec.js/account';
import { FeeJuicePaymentMethodWithClaim } from '@aztec/aztec.js/fee';
import { PrivateFeePaymentMethod, SponsoredFeePaymentMethod } from '@aztec/aztec.js/fee';
import { createLogger } from '@aztec/aztec.js/log';
import { getTokenAllowedSetupFunctions } from '@aztec/aztec/testing';
import { createExtendedL1Client } from '@aztec/ethereum/client';
import { RollupContract } from '@aztec/ethereum/contracts';
import { deployL1Contract } from '@aztec/ethereum/deploy-l1-contract';
import { ChainMonitor } from '@aztec/ethereum/test';
import { randomBytes } from '@aztec/foundation/crypto/random';
import { Fr } from '@aztec/foundation/curves/bn254';
import { EthAddress } from '@aztec/foundation/eth-address';
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 { getPXEConfig } from '@aztec/pxe/server';
import { GasSettings } from '@aztec/stdlib/gas';
import { deriveSigningKey } from '@aztec/stdlib/keys';
import { MNEMONIC } from '../../fixtures/fixtures.js';
import { deployAccounts, setup, teardown } from '../../fixtures/setup.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 { TestWallet } from '../../test-wallet/test_wallet.js';
import { ProxyLogger } from './benchmark.js';
import { FULL_FLOWS_CONFIG, KEY_FLOWS_CONFIG } from './config.js';
const { BENCHMARK_CONFIG } = process.env;
export class ClientFlowsBenchmark {
    logger;
    aztecNode;
    cheatCodes;
    context;
    chainMonitor;
    feeJuiceBridgeTestHarness;
    adminWallet;
    // The admin that aids in the setup of the test
    adminAddress;
    // Aztec Node config
    sequencerAddress;
    coinbase;
    // Contracts
    feeJuiceContract;
    // Asset in which fees are paid via FPC
    bananaCoin;
    bananaCoinInstance;
    bananaFPC;
    bananaFPCInstance;
    // Random asset we want to trade
    candyBarCoin;
    candyBarCoinInstance;
    // AMM contract
    amm;
    ammInstance;
    // Liquidity token for AMM
    liquidityToken;
    liquidityTokenInstance;
    // Sponsored FPC contract
    sponsoredFPC;
    sponsoredFPCInstance;
    // PXE and Wallet used by the benchmarking user. It can be set up with client-side proving enabled
    userWallet;
    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: ()=>Promise.resolve(undefined),
            circuits: 0
        }
    };
    config;
    proxyLogger;
    setupOptions;
    constructor(testName, setupOptions = {}){
        this.logger = createLogger(`bench:client_flows${testName ? `:${testName}` : ''}`);
        this.setupOptions = {
            startProverNode: true,
            ...setupOptions
        };
        this.config = BENCHMARK_CONFIG === 'key_flows' ? KEY_FLOWS_CONFIG : FULL_FLOWS_CONFIG;
        ProxyLogger.create();
        this.proxyLogger = ProxyLogger.getInstance();
    }
    async setup() {
        this.logger.info('Setting up subsystems from fresh');
        // Token allowlist entries are test-only: FPC-based fee payment with custom tokens won't work on mainnet alpha.
        const tokenAllowList = await getTokenAllowedSetupFunctions();
        this.context = await setup(0, {
            ...this.setupOptions,
            fundSponsoredFPC: true,
            skipAccountDeployment: true,
            l1ContractsArgs: this.setupOptions,
            txPublicSetupAllowListExtend: [
                ...this.setupOptions.txPublicSetupAllowListExtend ?? [],
                ...tokenAllowList
            ]
        });
        await this.applyBaseSetup();
        await this.context.aztecNodeService.setConfig({
            feeRecipient: this.sequencerAddress,
            coinbase: this.coinbase
        });
        const rollupContract = RollupContract.getFromConfig(this.context.config);
        this.chainMonitor = new ChainMonitor(rollupContract, this.context.dateProvider, this.logger, 200).start();
        return this;
    }
    async teardown() {
        await this.chainMonitor.stop();
        await teardown(this.context);
    }
    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
        });
    }
    /** Admin mints bananaCoin tokens privately to the target address and redeems them. */ async mintPrivateBananas(amount, address) {
        const { result: balanceBefore } = await this.bananaCoin.methods.balance_of_private(address).simulate({
            from: address
        });
        await mintTokensToPrivate(this.bananaCoin, this.adminAddress, address, amount);
        const { result: balanceAfter } = await this.bananaCoin.methods.balance_of_private(address).simulate({
            from: address
        });
        expect(balanceAfter).toEqual(balanceBefore + amount);
    }
    createBenchmarkingAccountManager(wallet, type) {
        const benchysSecret = Fr.random();
        const salt = Fr.random();
        let benchysPrivateSigningKey;
        if (type === 'schnorr') {
            benchysPrivateSigningKey = deriveSigningKey(benchysSecret);
            return wallet.createSchnorrAccount(benchysSecret, salt, benchysPrivateSigningKey);
        } else if (type === 'ecdsar1') {
            benchysPrivateSigningKey = randomBytes(32);
            return wallet.createECDSARAccount(benchysSecret, salt, benchysPrivateSigningKey);
        } else {
            throw new Error(`Unknown account type: ${type}`);
        }
    }
    async applyBaseSetup() {
        await this.applyInitialAccounts();
        await this.applySetupFeeJuice();
    }
    async applyInitialAccounts() {
        this.logger.info('Applying initial accounts setup');
        const { deployedAccounts } = await deployAccounts(2, this.logger)({
            wallet: this.context.wallet,
            initialFundedAccounts: this.context.initialFundedAccounts
        });
        const [{ address: adminAddress }, { address: sequencerAddress }] = deployedAccounts;
        this.adminWallet = this.context.wallet;
        this.aztecNode = this.context.aztecNodeService;
        this.cheatCodes = this.context.cheatCodes;
        this.adminAddress = adminAddress;
        this.sequencerAddress = sequencerAddress;
        const canonicalFeeJuice = await getCanonicalFeeJuice();
        this.feeJuiceContract = FeeJuiceContract.at(canonicalFeeJuice.address, this.adminWallet);
        this.coinbase = EthAddress.random();
        const userPXEConfig = getPXEConfig();
        const userPXEConfigWithContracts = {
            ...userPXEConfig,
            proverEnabled: this.realProofs
        };
        this.userWallet = await TestWallet.create(this.aztecNode, userPXEConfigWithContracts, {
            loggers: {
                prover: this.proxyLogger.createLogger('pxe:bb:wasm:bundle:proxied')
            }
        });
    }
    async applySetupFeeJuice() {
        this.logger.info('Applying fee juice setup');
        this.feeJuiceContract = FeeJuiceContract.at(ProtocolContractAddress.FeeJuice, this.adminWallet);
        this.feeJuiceBridgeTestHarness = await FeeJuicePortalTestingHarnessFactory.create({
            aztecNode: this.context.aztecNodeService,
            aztecNodeAdmin: this.context.aztecNodeService,
            l1Client: this.context.deployL1ContractsValues.l1Client,
            wallet: this.adminWallet,
            logger: this.logger
        });
    }
    async applyDeployBananaToken() {
        this.logger.info('Applying banana token deployment');
        const { receipt: { contract: bananaCoin, instance: bananaCoinInstance } } = await BananaCoin.deploy(this.adminWallet, this.adminAddress, 'BC', 'BC', 18n).send({
            from: this.adminAddress,
            wait: {
                returnReceipt: true
            }
        });
        this.logger.info(`BananaCoin deployed at ${bananaCoin.address}`);
        this.bananaCoin = bananaCoin;
        this.bananaCoinInstance = bananaCoinInstance;
    }
    async applyDeployCandyBarToken() {
        this.logger.info('Applying candy bar token deployment');
        const { receipt: { contract: candyBarCoin, instance: candyBarCoinInstance } } = await TokenContract.deploy(this.adminWallet, this.adminAddress, 'CBC', 'CBC', 18n).send({
            from: this.adminAddress,
            wait: {
                returnReceipt: true
            }
        });
        this.logger.info(`CandyBarCoin deployed at ${candyBarCoin.address}`);
        this.candyBarCoin = candyBarCoin;
        this.candyBarCoinInstance = candyBarCoinInstance;
    }
    async applyFPCSetup() {
        this.logger.info('Applying FPC setup');
        const feeJuiceContract = this.feeJuiceBridgeTestHarness.feeJuice;
        expect((await this.context.wallet.getContractMetadata(feeJuiceContract.address)).isContractPublished).toBe(true);
        const bananaCoin = this.bananaCoin;
        const { receipt: { contract: bananaFPC, instance: bananaFPCInstance } } = await FPCContract.deploy(this.adminWallet, bananaCoin.address, this.adminAddress).send({
            from: this.adminAddress,
            wait: {
                returnReceipt: true
            }
        });
        this.logger.info(`BananaPay deployed at ${bananaFPC.address}`);
        await this.feeJuiceBridgeTestHarness.bridgeFromL1ToL2(bananaFPC.address, this.adminAddress);
        this.bananaFPC = bananaFPC;
        this.bananaFPCInstance = bananaFPCInstance;
    }
    async applyDeploySponsoredFPC() {
        this.logger.info('Applying sponsored FPC deployment');
        const sponsoredFPCInstance = await setupSponsoredFPC(this.adminWallet);
        this.logger.info(`SponsoredFPC at ${sponsoredFPCInstance.address}`);
        this.sponsoredFPC = SponsoredFPCContract.at(sponsoredFPCInstance.address, this.adminWallet);
        this.sponsoredFPCInstance = sponsoredFPCInstance;
    }
    async createCrossChainTestHarness(owner) {
        const l1Client = createExtendedL1Client(this.context.config.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, l1Client, this.adminWallet, owner, this.logger, underlyingERC20Address);
        this.logger.verbose(`L2 token deployed to: ${crossChainTestHarness.l2Token.address}`);
        return crossChainTestHarness;
    }
    async createAndFundBenchmarkingAccountOnUserWallet(accountType) {
        const benchysAccountManager = await this.createBenchmarkingAccountManager(this.adminWallet, accountType);
        const benchysAccount = await benchysAccountManager.getAccount();
        const benchysAddress = benchysAccountManager.address;
        const claim = await this.feeJuiceBridgeTestHarness.prepareTokensOnL1(benchysAddress);
        const behchysDeployMethod = await benchysAccountManager.getDeployMethod();
        await behchysDeployMethod.send({
            from: NO_FROM,
            fee: {
                paymentMethod: new FeeJuicePaymentMethodWithClaim(benchysAddress, claim)
            }
        });
        // Register benchy on the user's Wallet, where we're going to be interacting from
        const accountManager = await this.userWallet.createAccount({
            secret: benchysAccount.getSecretKey(),
            salt: new Fr(benchysAccount.salt),
            contract: benchysAccountManager.getAccountContract()
        });
        return accountManager.address;
    }
    async applyDeployAmm() {
        this.logger.info('Applying AMM deployment');
        const { receipt: { contract: liquidityToken, instance: liquidityTokenInstance } } = await TokenContract.deploy(this.adminWallet, this.adminAddress, 'LPT', 'LPT', 18n).send({
            from: this.adminAddress,
            wait: {
                returnReceipt: true
            }
        });
        const { receipt: { contract: amm, instance: ammInstance } } = await AMMContract.deploy(this.adminWallet, this.bananaCoin.address, this.candyBarCoin.address, liquidityToken.address).send({
            from: this.adminAddress,
            wait: {
                returnReceipt: true
            }
        });
        this.logger.info(`AMM deployed at ${amm.address}`);
        await liquidityToken.methods.set_minter(amm.address, true).send({
            from: this.adminAddress
        });
        this.liquidityToken = liquidityToken;
        this.liquidityTokenInstance = liquidityTokenInstance;
        this.amm = amm;
        this.ammInstance = ammInstance;
    }
    async getBridgedFeeJuicePaymentMethodForWallet(_wallet, sender) {
        const claim = await this.feeJuiceBridgeTestHarness.prepareTokensOnL1(sender);
        return new FeeJuicePaymentMethodWithClaim(sender, claim);
    }
    async getPrivateFPCPaymentMethodForWallet(wallet, sender) {
        // The private fee paying method assembled on the app side requires knowledge of the maximum
        // fee the user is willing to pay
        const maxFeesPerGas = (await this.aztecNode.getCurrentMinFees()).mul(1.5);
        const gasSettings = GasSettings.fallback({
            maxFeesPerGas
        });
        return new PrivateFeePaymentMethod(this.bananaFPC.address, sender, wallet, gasSettings);
    }
    getSponsoredFPCPaymentMethodForWallet(_wallet, _sender) {
        return Promise.resolve(new SponsoredFeePaymentMethod(this.sponsoredFPC.address));
    }
}
