import { EcdsaKAccountContract, EcdsaRAccountContract } from '@aztec/accounts/ecdsa';
import { SchnorrAccountContract } from '@aztec/accounts/schnorr';
import { StubEcdsaAccountContractArtifact, createStubEcdsaAccount } from '@aztec/accounts/stub/ecdsa';
import { StubSchnorrAccountContractArtifact, createStubSchnorrAccount } from '@aztec/accounts/stub/schnorr';
import { NO_FROM } from '@aztec/aztec.js/account';
import { SetPublicAuthwitContractInteraction, computeInnerAuthWitHashFromAction, isContractFunctionInteractionCallIntent, lookupValidity } from '@aztec/aztec.js/authorization';
import { AccountManager } from '@aztec/aztec.js/wallet';
import { TxSimulationResultWithAppOffset } from '@aztec/aztec.js/wallet';
import { DefaultEntrypoint } from '@aztec/entrypoints/default';
import { Fr } from '@aztec/foundation/curves/bn254';
import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin';
import { getPXEConfig } from '@aztec/pxe/config';
import { createPXE } from '@aztec/pxe/server';
import { getContractInstanceFromInstantiationParams } from '@aztec/stdlib/contract';
import { deriveSigningKey } from '@aztec/stdlib/keys';
import { SimulationOverrides } from '@aztec/stdlib/tx';
import { mergeExecutionPayloads } from '@aztec/stdlib/tx';
import { BaseWallet } from '@aztec/wallet-sdk/base-wallet';
import { AztecNodeProxy, ProvenTx } from './utils.js';
/**
 * Wallet implementation that stores accounts in memory and provides extra debugging
 * utilities
 * It is intended to be used in e2e tests.
 */ export class TestWallet extends BaseWallet {
    nodeRef;
    constructor(pxe, nodeRef){
        super(pxe, nodeRef), this.nodeRef = nodeRef, this.accounts = new Map(), this.simulationMode = 'kernelless';
    }
    static async create(node, overridePXEConfig, options = {
        loggers: {}
    }) {
        const nodeRef = new AztecNodeProxy(node);
        const pxeConfig = Object.assign(getPXEConfig(), {
            proverEnabled: overridePXEConfig?.proverEnabled ?? false,
            ...overridePXEConfig
        });
        const pxe = await createPXE(nodeRef, pxeConfig, options);
        return new TestWallet(pxe, nodeRef);
    }
    /**
   * Updates the underlying node that this wallet and its PXE communicate with.
   * @param node - The new AztecNode to forward all calls to.
   */ updateNode(node) {
        this.nodeRef.updateTargetNode(node);
    }
    createSchnorrAccount(secret, salt, signingKey) {
        signingKey = signingKey ?? deriveSigningKey(secret);
        return this.createAccount({
            secret,
            salt,
            type: 'schnorr',
            contract: new SchnorrAccountContract(signingKey)
        });
    }
    createECDSARAccount(secret, salt, signingKey) {
        return this.createAccount({
            secret,
            salt,
            type: 'ecdsasecp256r1',
            contract: new EcdsaRAccountContract(signingKey)
        });
    }
    createECDSAKAccount(secret, salt, signingKey) {
        return this.createAccount({
            secret,
            salt,
            type: 'ecdsasecp256k1',
            contract: new EcdsaKAccountContract(signingKey)
        });
    }
    /**
   * Builds contract overrides for all provided addresses by replacing their account contracts with stub implementations.
   */ async buildAccountOverrides(addresses) {
        const accounts = await this.getAccounts();
        const contracts = {};
        const filtered = accounts.filter((acc)=>addresses.some((addr)=>addr.equals(acc.item)));
        for (const account of filtered){
            const address = account.item;
            const originalAccount = await this.getAccountFromAddress(address);
            const completeAddress = originalAccount.getCompleteAddress();
            const contractInstance = await this.pxe.getContractInstance(completeAddress.address);
            if (!contractInstance) {
                throw new Error(`No contract instance found for address: ${completeAddress.address} during account override building. This is a bug!`);
            }
            const stubArtifact = this.getStubArtifactFor(address);
            const stubConstructorArgs = this.getTypeFor(address) === 'schnorr' ? [
                Fr.ZERO,
                Fr.ZERO
            ] : [
                Buffer.alloc(32),
                Buffer.alloc(32)
            ];
            const stubInstance = await getContractInstanceFromInstantiationParams(stubArtifact, {
                salt: Fr.random(),
                constructorArgs: stubConstructorArgs
            });
            contracts[address.toString()] = {
                instance: stubInstance,
                artifact: stubArtifact
            };
        }
        return contracts;
    }
    accounts;
    getTypeFor(address) {
        return this.accounts.get(address.toString())?.type ?? 'schnorr';
    }
    getStubArtifactFor(address) {
        return this.getTypeFor(address) === 'schnorr' ? StubSchnorrAccountContractArtifact : StubEcdsaAccountContractArtifact;
    }
    getStubAccountFor(address, completeAddress) {
        return this.getTypeFor(address) === 'schnorr' ? createStubSchnorrAccount(completeAddress) : createStubEcdsaAccount(completeAddress);
    }
    /**
   * Controls how the test wallet simulates transactions:
   * - `kernelless`: Skips kernel circuits but uses the real account contract. Default.
   * - `kernelless-override`: Skips kernels and replaces the account with a stub that doesn't do authwit validation.
   * - `full`: Uses real kernel circuits and real account contracts. Slow!
   */ simulationMode;
    setSimulationMode(mode) {
        this.simulationMode = mode;
    }
    setMinFeePadding(value) {
        this.minFeePadding = value ?? 0.5;
    }
    getAccountFromAddress(address) {
        const entry = this.accounts.get(address?.toString() ?? '');
        if (!entry) {
            throw new Error(`Account not found in wallet for address: ${address}`);
        }
        return Promise.resolve(entry.account);
    }
    getAccounts() {
        return Promise.resolve(Array.from(this.accounts.values()).map((entry)=>({
                alias: '',
                item: entry.account.getAddress()
            })));
    }
    async createAccount(accountData) {
        const secret = accountData?.secret ?? Fr.random();
        const salt = accountData?.salt ?? Fr.random();
        const type = accountData?.type ?? 'schnorr';
        const contract = accountData?.contract ?? new SchnorrAccountContract(GrumpkinScalar.random());
        const accountManager = await AccountManager.create(this, secret, contract, salt);
        const instance = accountManager.getInstance();
        const artifact = await contract.getContractArtifact();
        await this.registerContract(instance, artifact, secret);
        const address = accountManager.address.toString();
        this.accounts.set(address, {
            account: await accountManager.getAccount(),
            type
        });
        return accountManager;
    }
    lookupValidity(onBehalfOf, intent, witness) {
        return lookupValidity(this, onBehalfOf, intent, witness);
    }
    setPublicAuthWit(from, messageHashOrIntent, authorized) {
        return SetPublicAuthwitContractInteraction.create(this, from, messageHashOrIntent, authorized);
    }
    async createAuthWit(from, intent) {
        const account = await this.getAccountFromAddress(from);
        const chainInfo = await this.getChainInfo();
        let intentInnerHash;
        if ('caller' in intent) {
            const call = isContractFunctionInteractionCallIntent(intent) ? await intent.action.getFunctionCall() : intent.call;
            const innerHash = await computeInnerAuthWitHashFromAction(intent.caller, call);
            intentInnerHash = {
                innerHash,
                consumer: call.to
            };
        } else {
            intentInnerHash = intent;
        }
        return account.createAuthWit(intentInnerHash, chainInfo);
    }
    async simulateViaEntrypoint(executionPayload, opts) {
        const { from, feeOptions, additionalScopes, skipTxValidation, skipFeeEnforcement, sendMessagesAs } = opts;
        const scopes = this.scopesFrom(from, additionalScopes);
        const skipKernels = this.simulationMode !== 'full';
        const useOverride = this.simulationMode === 'kernelless-override';
        const feeExecutionPayload = await feeOptions.walletFeePaymentMethod?.getExecutionPayload();
        const finalExecutionPayload = feeExecutionPayload ? mergeExecutionPayloads([
            feeExecutionPayload,
            executionPayload
        ]) : executionPayload;
        const chainInfo = await this.getChainInfo();
        let overrides;
        let txRequest;
        if (useOverride) {
            const accountOverrides = await this.buildAccountOverrides(scopes);
            overrides = new SimulationOverrides(accountOverrides);
        }
        if (from === NO_FROM) {
            const entrypoint = new DefaultEntrypoint();
            txRequest = await entrypoint.createTxExecutionRequest(finalExecutionPayload, feeOptions.gasSettings, chainInfo);
        } else {
            let fromAccount;
            if (useOverride) {
                const originalAccount = await this.getAccountFromAddress(from);
                fromAccount = this.getStubAccountFor(from, originalAccount.getCompleteAddress());
            } else {
                fromAccount = await this.getAccountFromAddress(from);
            }
            const executionOptions = {
                txNonce: Fr.random(),
                cancellable: this.cancellableTransactions,
                // If from is an address, feeOptions include the way the account contract should handle the fee payment
                feePaymentMethodOptions: feeOptions.accountFeePaymentMethodOptions
            };
            txRequest = await fromAccount.createTxExecutionRequest(finalExecutionPayload, feeOptions.gasSettings, chainInfo, executionOptions);
        }
        const result = await this.pxe.simulateTx(txRequest, {
            simulatePublic: true,
            skipKernels,
            skipFeeEnforcement,
            skipTxValidation,
            overrides,
            scopes,
            senderForTags: this.senderForTagsFrom(from, sendMessagesAs)
        });
        const appCallOffset = await this.computeAppCallOffset(from, feeOptions);
        return TxSimulationResultWithAppOffset.fromResultAndOffset(result, appCallOffset);
    }
    async proveTx(exec, opts) {
        const fee = await this.completeFeeOptions({
            from: opts.from,
            feePayer: exec.feePayer,
            gasSettings: opts.fee?.gasSettings
        });
        const txRequest = await this.createTxExecutionRequestFromPayloadAndFee(exec, opts.from, fee);
        const txProvingResult = await this.pxe.proveTx(txRequest, {
            scopes: this.scopesFrom(opts.from, opts.additionalScopes),
            senderForTags: this.senderForTagsFrom(opts.from, opts.sendMessagesAs)
        });
        return new ProvenTx(this.aztecNode, await txProvingResult.toTx(), txProvingResult.getOffchainEffects(), txProvingResult.stats);
    }
    getTxReceipt(txHash) {
        return this.aztecNode.getTxReceipt(txHash);
    }
    getNotes(filter) {
        return this.pxe.debug.getNotes(filter);
    }
    getSyncedBlockHeader() {
        return this.pxe.getSyncedBlockHeader();
    }
    sync() {
        return this.pxe.debug.sync();
    }
    stop() {
        return this.pxe.stop();
    }
}
