// docs:start:cross_chain_test_harness
import { EthAddress, Fr, L1TokenPortalManager, retryUntil } from '@aztec/aztec.js';
import { deployL1Contract } from '@aztec/ethereum';
import { TestERC20Abi, TokenPortalAbi, TokenPortalBytecode } from '@aztec/l1-artifacts';
import { TokenContract } from '@aztec/noir-contracts.js/Token';
import { TokenBridgeContract } from '@aztec/noir-contracts.js/TokenBridge';
import { getContract } from 'viem';
import { mintTokensToPrivate } from '../fixtures/token_utils.js';
// docs:start:deployAndInitializeTokenAndBridgeContracts
/**
 * Deploy L1 token and portal, initialize portal, deploy a non native l2 token contract, its L2 bridge contract and attach is to the portal.
 * @param wallet - the wallet instance
 * @param walletClient - A viem WalletClient.
 * @param publicClient - A viem PublicClient.
 * @param rollupRegistryAddress - address of rollup registry to pass to initialize the token portal
 * @param owner - owner of the L2 contract
 * @param underlyingERC20Address - address of the underlying ERC20 contract to use (if none supplied, it deploys one)
 * @returns l2 contract instance, bridge contract instance, token portal instance, token portal address and the underlying ERC20 instance
 */ export async function deployAndInitializeTokenAndBridgeContracts(wallet, l1Client, rollupRegistryAddress, owner, underlyingERC20Address) {
    // deploy the token portal
    const { address: tokenPortalAddress } = await deployL1Contract(l1Client, TokenPortalAbi, TokenPortalBytecode);
    const tokenPortal = getContract({
        address: tokenPortalAddress.toString(),
        abi: TokenPortalAbi,
        client: l1Client
    });
    // deploy l2 token
    const token = await TokenContract.deploy(wallet, owner, 'TokenName', 'TokenSymbol', 18).send({
        from: owner
    }).deployed();
    // deploy l2 token bridge and attach to the portal
    const bridge = await TokenBridgeContract.deploy(wallet, token.address, tokenPortalAddress).send({
        from: owner
    }).deployed();
    if (await token.methods.get_admin().simulate({
        from: owner
    }) !== owner.toBigInt()) {
        throw new Error(`Token admin is not ${owner}`);
    }
    if (!(await bridge.methods.get_config().simulate({
        from: owner
    })).token.equals(token.address)) {
        throw new Error(`Bridge token is not ${token.address}`);
    }
    // make the bridge a minter on the token:
    await token.methods.set_minter(bridge.address, true).send({
        from: owner
    }).wait();
    if (await token.methods.is_minter(bridge.address).simulate({
        from: owner
    }) === 1n) {
        throw new Error(`Bridge is not a minter`);
    }
    // initialize portal
    await tokenPortal.write.initialize([
        rollupRegistryAddress.toString(),
        underlyingERC20Address.toString(),
        bridge.address.toString()
    ], {});
    const underlyingERC20 = getContract({
        address: underlyingERC20Address.toString(),
        abi: TestERC20Abi,
        client: l1Client
    });
    return {
        token,
        bridge,
        tokenPortalAddress,
        tokenPortal,
        underlyingERC20
    };
}
/**
 * A Class for testing cross chain interactions, contains common interactions
 * shared between cross chain tests.
 */ export class CrossChainTestHarness {
    aztecNode;
    pxeService;
    logger;
    l2Token;
    l2Bridge;
    ethAccount;
    tokenPortalAddress;
    underlyingERC20Address;
    l1Client;
    l1ContractAddresses;
    ownerWallet;
    ownerAddress;
    static async new(aztecNode, pxeService, l1Client, wallet, ownerAddress, logger, underlyingERC20Address) {
        const ethAccount = EthAddress.fromString((await l1Client.getAddresses())[0]);
        const l1ContractAddresses = (await pxeService.getNodeInfo()).l1ContractAddresses;
        // Deploy and initialize all required contracts
        logger.info('Deploying and initializing token, portal and its bridge...');
        const { token, bridge, tokenPortalAddress, underlyingERC20 } = await deployAndInitializeTokenAndBridgeContracts(wallet, l1Client, l1ContractAddresses.registryAddress, wallet.getAddress(), underlyingERC20Address);
        logger.info('Deployed and initialized token, portal and its bridge.');
        return new CrossChainTestHarness(aztecNode, pxeService, logger, token, bridge, ethAccount, tokenPortalAddress, underlyingERC20.address, l1Client, l1ContractAddresses, wallet, ownerAddress);
    }
    l1TokenManager;
    l1TokenPortalManager;
    constructor(/** Aztec node instance. */ aztecNode, /** Private eXecution Environment (PXE). */ pxeService, /** Logger. */ logger, /** L2 Token contract. */ l2Token, /** L2 Token bridge contract. */ l2Bridge, /** Eth account to interact with. */ ethAccount, /** Portal address. */ tokenPortalAddress, /** Underlying token for portal tests. */ underlyingERC20Address, /** Viem Extended client instance. */ l1Client, /** Deployment addresses for all L1 contracts */ l1ContractAddresses, /** Wallet of the owner. */ ownerWallet, /** Owner of the l2 token and bridge */ ownerAddress){
        this.aztecNode = aztecNode;
        this.pxeService = pxeService;
        this.logger = logger;
        this.l2Token = l2Token;
        this.l2Bridge = l2Bridge;
        this.ethAccount = ethAccount;
        this.tokenPortalAddress = tokenPortalAddress;
        this.underlyingERC20Address = underlyingERC20Address;
        this.l1Client = l1Client;
        this.l1ContractAddresses = l1ContractAddresses;
        this.ownerWallet = ownerWallet;
        this.ownerAddress = ownerAddress;
        this.l1TokenPortalManager = new L1TokenPortalManager(this.tokenPortalAddress, this.underlyingERC20Address, this.l1ContractAddresses.feeAssetHandlerAddress, this.l1ContractAddresses.outboxAddress, this.l1Client, this.logger);
        this.l1TokenManager = this.l1TokenPortalManager.getTokenManager();
        this.ownerAddress = this.ownerWallet.getAddress();
    }
    async mintTokensOnL1(amount) {
        const contract = getContract({
            abi: TestERC20Abi,
            address: this.l1TokenManager.tokenAddress.toString(),
            client: this.l1Client
        });
        const balanceBefore = await this.l1TokenManager.getL1TokenBalance(this.ethAccount.toString());
        const hash = await contract.write.mint([
            this.ethAccount.toString(),
            amount
        ]);
        await this.l1Client.waitForTransactionReceipt({
            hash
        });
        expect(await this.l1TokenManager.getL1TokenBalance(this.ethAccount.toString())).toEqual(balanceBefore + amount);
    }
    getL1BalanceOf(address) {
        return this.l1TokenManager.getL1TokenBalance(address.toString());
    }
    sendTokensToPortalPublic(bridgeAmount, mint = false) {
        return this.l1TokenPortalManager.bridgeTokensPublic(this.ownerAddress, bridgeAmount, mint);
    }
    sendTokensToPortalPrivate(bridgeAmount, mint = false) {
        return this.l1TokenPortalManager.bridgeTokensPrivate(this.ownerAddress, bridgeAmount, mint);
    }
    async mintTokensPublicOnL2(amount) {
        this.logger.info('Minting tokens on L2 publicly');
        await this.l2Token.methods.mint_to_public(this.ownerAddress, amount).send({
            from: this.ownerAddress
        }).wait();
    }
    async mintTokensPrivateOnL2(amount) {
        await mintTokensToPrivate(this.l2Token, this.ownerAddress, this.ownerWallet, this.ownerAddress, amount);
    }
    async sendL2PublicTransfer(transferAmount, receiverAddress) {
        // send a transfer tx to force through rollup with the message included
        await this.l2Token.methods.transfer_in_public(this.ownerAddress, receiverAddress, transferAmount, 0).send({
            from: this.ownerAddress
        }).wait();
    }
    async consumeMessageOnAztecAndMintPrivately(claim) {
        this.logger.info('Consuming messages on L2 privately');
        const { recipient, claimAmount, claimSecret: secretForL2MessageConsumption, messageLeafIndex } = claim;
        await this.l2Bridge.methods.claim_private(recipient, claimAmount, secretForL2MessageConsumption, messageLeafIndex).send({
            from: this.ownerAddress
        }).wait();
    }
    async consumeMessageOnAztecAndMintPublicly(claim) {
        this.logger.info('Consuming messages on L2 Publicly');
        const { claimAmount, claimSecret, messageLeafIndex } = claim;
        await this.l2Bridge.methods.claim_public(this.ownerAddress, claimAmount, claimSecret, messageLeafIndex).send({
            from: this.ownerAddress
        }).wait();
    }
    async withdrawPrivateFromAztecToL1(withdrawAmount, authwitNonce = Fr.ZERO, authWitness) {
        const withdrawReceipt = await this.l2Bridge.methods.exit_to_l1_private(this.l2Token.address, this.ethAccount, withdrawAmount, EthAddress.ZERO, authwitNonce).send({
            authWitnesses: [
                authWitness
            ],
            from: this.ownerAddress
        }).wait();
        return withdrawReceipt;
    }
    async withdrawPublicFromAztecToL1(withdrawAmount, authwitNonce = Fr.ZERO) {
        const withdrawReceipt = await this.l2Bridge.methods.exit_to_l1_public(this.ethAccount, withdrawAmount, EthAddress.ZERO, authwitNonce).send({
            from: this.ownerAddress
        }).wait();
        return withdrawReceipt;
    }
    async getL2PrivateBalanceOf(owner) {
        return await this.l2Token.methods.balance_of_private(owner).simulate({
            from: owner
        });
    }
    async expectPrivateBalanceOnL2(owner, expectedBalance) {
        const balance = await this.getL2PrivateBalanceOf(owner);
        this.logger.info(`Account ${owner} balance: ${balance}`);
        expect(balance).toBe(expectedBalance);
    }
    async getL2PublicBalanceOf(owner) {
        return await this.l2Token.methods.balance_of_public(owner).simulate({
            from: this.ownerAddress
        });
    }
    async expectPublicBalanceOnL2(owner, expectedBalance) {
        const balance = await this.getL2PublicBalanceOf(owner);
        expect(balance).toBe(expectedBalance);
    }
    async getL2ToL1MessageLeaf(withdrawAmount, callerOnL1 = EthAddress.ZERO) {
        return await this.l1TokenPortalManager.getL2ToL1MessageLeaf(withdrawAmount, this.ethAccount, this.l2Bridge.address, callerOnL1);
    }
    withdrawFundsFromBridgeOnL1(amount, blockNumber, messageIndex, siblingPath) {
        return this.l1TokenPortalManager.withdrawFunds(amount, this.ethAccount, BigInt(blockNumber), messageIndex, siblingPath);
    }
    async transferToPrivateOnL2(shieldAmount) {
        this.logger.info('Transferring to private on L2');
        await this.l2Token.methods.transfer_to_private(this.ownerAddress, shieldAmount).send({
            from: this.ownerAddress
        }).wait();
    }
    async transferToPublicOnL2(amount, authwitNonce = Fr.ZERO) {
        this.logger.info('Transferring tokens to public');
        await this.l2Token.methods.transfer_to_public(this.ownerAddress, this.ownerAddress, amount, authwitNonce).send({
            from: this.ownerAddress
        }).wait();
    }
    /**
   * Makes message available for consumption.
   * @dev Does that by performing 2 unrelated transactions on L2 to progress the rollup by 2 blocks and then waits for
   * message to be processed by archiver. We need to progress by 2 because there is a 1 block lag between when
   * the message is sent to Inbox and when the subtree containing the message is included in the block and then when
   * it's included it becomes available for consumption in the next block because the l1 to l2 message tree.
   */ async makeMessageConsumable(msgHash) {
        const frMsgHash = typeof msgHash === 'string' ? Fr.fromHexString(msgHash) : msgHash;
        // We poll isL1ToL2MessageSynced endpoint until the message is available
        await retryUntil(async ()=>await this.aztecNode.isL1ToL2MessageSynced(frMsgHash), 'message sync', 10);
        await this.mintTokensPublicOnL2(0n);
        await this.mintTokensPublicOnL2(0n);
    }
    toCrossChainContext() {
        return {
            l2Token: this.l2Token.address,
            l2Bridge: this.l2Bridge.address,
            tokenPortal: this.tokenPortalAddress,
            underlying: this.underlyingERC20Address,
            ethAccount: this.ethAccount,
            ownerAddress: this.ownerAddress,
            inbox: this.l1ContractAddresses.inboxAddress,
            outbox: this.l1ContractAddresses.outboxAddress
        };
    }
} // docs:end:cross_chain_test_harness
