// SPDX-FileCopyrightText: © 2026 LEDGER SAS
// SPDX-License-Identifier: Apache-2.0

import type { TezosCoinConfig, TezosContext } from '../config'
import { getRevealFee } from '@taquito/taquito'
import { normalizePublicKeyForAddress, STAKE_USE_ALL_RESERVE_MUTEZ } from '../utils'
import { estimateFees } from './estimateFees'
import { getTezosToolkit } from './tezosToolkit'

jest.mock('./tezosToolkit')

const defaultFeesConfig = {
  minGasLimit: 600,
  minRevealGasLimit: 300,
  minStorageLimit: 0,
  minFees: 0,
  minEstimatedFees: 1000,
}

const unrevealedAccount = {
  address: 'tz2TaTpo31sAiX2HBJUTLLdUnqVJR4QjLy1V',
  balance: BigInt(1000000),
  revealed: false,
  xpub: '021bab48f41fc555e0fcf322a28e31b56f4369242f65324758ec8bbae3e84109a5',
}

const revealedAccount = {
  ...unrevealedAccount,
  revealed: true,
}

function setupFa2ContractMock() {
  const toTransferParams = jest.fn().mockReturnValue({
    parameter: { entrypoint: 'transfer', value: [] },
    amount: 0,
    mutez: true,
  })
  const transfer = jest.fn().mockReturnValue({ toTransferParams })
  return { transfer, toTransferParams }
}

describe('estimateFees', () => {
  const mockTezosToolkit = {
    estimate: {
      finalizeUnstake: jest.fn(),
      setDelegate: jest.fn(),
      stake: jest.fn(),
      transfer: jest.fn(),
      unstake: jest.fn(),
    },
    contract: {
      at: jest.fn(),
    },
    setProvider: jest.fn(),
  }

  let currentConfig: TezosCoinConfig
  const context: TezosContext = {
    config: async () => currentConfig,
    logger: () => undefined,
  }
  const mkConfig = (cfg: unknown): TezosCoinConfig => cfg as unknown as TezosCoinConfig

  beforeEach(() => {
    jest.clearAllMocks()
    ;(getTezosToolkit as jest.Mock).mockReturnValue(mockTezosToolkit)
    currentConfig = {
      fees: defaultFeesConfig,
    } as unknown as TezosCoinConfig
  })

  describe('signer public key from account.xpub', () => {
    it('builds the mock signer from the account public key so reveal estimation is enabled', async () => {
      // xpub holds the account public key (hex or base58; estimateFees normalizes it).
      mockTezosToolkit.estimate.setDelegate.mockResolvedValue({
        suggestedFeeMutez: 100,
        gasLimit: 10000,
        storageLimit: 0,
        burnFeeMutez: 0,
        opSize: 100,
      })

      await estimateFees(context, {
        account: { ...unrevealedAccount },
        transaction: {
          mode: 'delegate',
          recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
          amount: BigInt(0),
        },
      })

      const signer = mockTezosToolkit.setProvider.mock.calls.at(-1)?.[0]?.signer
      // Signer carries the account's public key (normalized from xpub) so the reveal fee is
      // accounted for; an empty signer key would mean reveal estimation is skipped.
      const expectedPublicKey = normalizePublicKeyForAddress(
        unrevealedAccount.xpub,
        unrevealedAccount.address
      )
      expect(expectedPublicKey?.length).toBeGreaterThan(0)
      await expect(signer.publicKey()).resolves.toBe(expectedPublicKey)
    })
  })

  describe('when suggested fee is above or below minFees floor', () => {
    it('estimate fees when default estimation is greater than minFees', async () => {
      const suggestedFee = 2000
      const minFees = 1000
      mockTezosToolkit.estimate.setDelegate.mockResolvedValue({
        suggestedFeeMutez: suggestedFee,
        gasLimit: 10000,
        storageLimit: 0,
        burnFeeMutez: 0,
        opSize: 100,
      })
      currentConfig = mkConfig({
        fees: { ...defaultFeesConfig, minFees },
      })

      const result = await estimateFees(context, {
        account: revealedAccount,
        transaction: {
          mode: 'delegate',
          recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
          amount: BigInt(0),
        },
      })

      expect(result.fees).toBe(BigInt(suggestedFee))
      expect(result.estimatedFees).toBe(BigInt(suggestedFee))
    })

    it('estimate fees when default estimation is lesser than minFees', async () => {
      const minFees = 1000
      mockTezosToolkit.estimate.setDelegate.mockResolvedValue({
        suggestedFeeMutez: 100,
        gasLimit: 10000,
        storageLimit: 0,
        burnFeeMutez: 0,
        opSize: 100,
      })
      currentConfig = mkConfig({
        fees: { ...defaultFeesConfig, minFees },
      })

      const result = await estimateFees(context, {
        account: revealedAccount,
        transaction: {
          mode: 'delegate',
          recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
          amount: BigInt(0),
        },
      })

      expect(result.fees).toBe(BigInt(minFees))
      expect(result.estimatedFees).toBe(BigInt(minFees))
    })

    it('estimate fees when default estimation is lesser than minFees (unrevealed composite)', async () => {
      const minFees = 1000
      mockTezosToolkit.estimate.setDelegate.mockResolvedValue({
        suggestedFeeMutez: 100,
        gasLimit: 10000,
        storageLimit: 0,
        burnFeeMutez: 0,
        opSize: 100,
      })
      currentConfig = mkConfig({
        fees: { ...defaultFeesConfig, minFees },
      })

      const account = { ...unrevealedAccount }
      const transaction = {
        mode: 'delegate' as const,
        recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
        amount: BigInt(0),
      }

      const result = await estimateFees(context, { account, transaction })

      expect(result.fees).toBe(BigInt(minFees))
      expect(result.estimatedFees).toBe(BigInt(minFees * 2))
    })
  })

  it('uses getRevealFee for unrevealed account when minFees is 0', async () => {
    const delegationFee = 500
    mockTezosToolkit.estimate.setDelegate.mockResolvedValue({
      suggestedFeeMutez: delegationFee,
      gasLimit: 10000,
      storageLimit: 0,
      burnFeeMutez: 0,
      opSize: 100,
    })
    currentConfig = mkConfig({
      fees: { ...defaultFeesConfig, minFees: 0, minEstimatedFees: 0 },
    })

    const account = { ...unrevealedAccount }
    const transaction = {
      mode: 'delegate' as const,
      recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
      amount: BigInt(0),
    }

    const result = await estimateFees(context, { account, transaction })

    const expectedRevealFee = getRevealFee(account.address)
    expect(result.estimatedFees).toBe(BigInt(delegationFee + expectedRevealFee))
  })

  it('undelegate uses same fee logic as delegate', async () => {
    const suggestedFee = 800
    mockTezosToolkit.estimate.setDelegate.mockResolvedValue({
      suggestedFeeMutez: suggestedFee,
      gasLimit: 10000,
      storageLimit: 0,
      burnFeeMutez: 0,
      opSize: 100,
    })

    const result = await estimateFees(context, {
      account: revealedAccount,
      transaction: {
        mode: 'undelegate',
        recipient: '',
        amount: BigInt(0),
      },
    })

    expect(result.fees).toBe(BigInt(suggestedFee))
    expect(result.estimatedFees).toBe(BigInt(suggestedFee))
    expect(mockTezosToolkit.estimate.setDelegate).toHaveBeenCalledWith({
      source: revealedAccount.address,
    })
  })

  it('stake uses Taquito stake estimation and maps fee fields', async () => {
    mockTezosToolkit.estimate.stake.mockResolvedValue({
      suggestedFeeMutez: 700,
      gasLimit: 1100,
      storageLimit: 5,
      burnFeeMutez: 0,
      opSize: 100,
    })

    const result = await estimateFees(context, {
      account: revealedAccount,
      transaction: {
        mode: 'stake',
        recipient: '',
        amount: 1234n,
      },
    })

    expect(mockTezosToolkit.estimate.stake).toHaveBeenCalledWith({
      amount: 1234,
      mutez: true,
    })
    expect(result.fees).toBe(700n)
    expect(result.gasLimit).toBe(1100n)
    expect(result.storageLimit).toBe(5n)
    expect(result.estimatedFees).toBe(700n)
    expect(result.amount).toBe(1234n)
  })

  it('unstake uses Taquito unstake estimation and maps fee fields', async () => {
    mockTezosToolkit.estimate.unstake.mockResolvedValue({
      suggestedFeeMutez: 710,
      gasLimit: 1200,
      storageLimit: 6,
      burnFeeMutez: 0,
      opSize: 100,
    })

    const result = await estimateFees(context, {
      account: revealedAccount,
      transaction: {
        mode: 'unstake',
        recipient: '',
        amount: 4321n,
      },
    })

    expect(mockTezosToolkit.estimate.unstake).toHaveBeenCalledWith({
      amount: 4321,
      mutez: true,
    })
    expect(result.fees).toBe(710n)
    expect(result.gasLimit).toBe(1200n)
    expect(result.storageLimit).toBe(6n)
    expect(result.estimatedFees).toBe(710n)
    expect(result.amount).toBe(4321n)
  })

  it('useAllAmount unstake resolves max to the staked balance', async () => {
    mockTezosToolkit.estimate.unstake.mockResolvedValue({
      suggestedFeeMutez: 710,
      gasLimit: 1200,
      storageLimit: 6,
      burnFeeMutez: 0,
      opSize: 100,
    })

    const result = await estimateFees(context, {
      account: { ...revealedAccount, stakedBalance: 5000n },
      transaction: {
        mode: 'unstake',
        recipient: '',
        amount: 0n,
        useAllAmount: true,
      },
    })

    expect(result.amount).toBe(5000n)
  })

  it('useAllAmount stake coerces amount to 1 for estimation and resolves max to balance minus fees and reserve', async () => {
    const suggestedFee = 700
    const balance = 1_000_000n
    mockTezosToolkit.estimate.stake.mockResolvedValue({
      suggestedFeeMutez: suggestedFee,
      gasLimit: 1100,
      storageLimit: 5,
      burnFeeMutez: 0,
      opSize: 100,
    })

    const result = await estimateFees(context, {
      account: { ...revealedAccount, balance },
      transaction: {
        mode: 'stake',
        recipient: '',
        amount: 0n,
        useAllAmount: true,
      },
    })

    expect(mockTezosToolkit.estimate.stake).toHaveBeenCalledWith({
      amount: 1,
      mutez: true,
    })
    expect(result.fees).toBe(BigInt(suggestedFee))
    expect(result.amount).toBe(balance - BigInt(suggestedFee) - 10_000n)
  })

  it('useAllAmount stake subtracts reveal fee from maxStakable on unrevealed accounts', async () => {
    const suggestedFee = 700
    const balance = 1_000_000n
    mockTezosToolkit.estimate.stake.mockResolvedValue({
      suggestedFeeMutez: suggestedFee,
      gasLimit: 1100,
      storageLimit: 5,
      burnFeeMutez: 0,
      opSize: 100,
    })

    const result = await estimateFees(context, {
      account: { ...unrevealedAccount, balance },
      transaction: {
        mode: 'stake',
        recipient: '',
        amount: 0n,
        useAllAmount: true,
      },
    })

    const expectedRevealFee = BigInt(getRevealFee(unrevealedAccount.address))
    expect(result.amount).toBe(balance - BigInt(suggestedFee) - expectedRevealFee - 10_000n)
    expect(result.amount! + result.estimatedFees).toBeLessThanOrEqual(balance)
  })

  it('useAllAmount stake clamps amount to 0 when balance cannot cover fees and reserve', async () => {
    const suggestedFee = 700
    const balance = 5_000n
    mockTezosToolkit.estimate.stake.mockResolvedValue({
      suggestedFeeMutez: suggestedFee,
      gasLimit: 1100,
      storageLimit: 5,
      burnFeeMutez: 0,
      opSize: 100,
    })

    const result = await estimateFees(context, {
      account: { ...revealedAccount, balance },
      transaction: {
        mode: 'stake',
        recipient: '',
        amount: 0n,
        useAllAmount: true,
      },
    })

    expect(result.amount).toBe(0n)
  })

  it('useAllAmount stake excludes already-staked funds from maxStakable', async () => {
    const suggestedFee = 700
    const balance = 1_000_000n
    const stakedBalance = 300_000n
    mockTezosToolkit.estimate.stake.mockResolvedValue({
      suggestedFeeMutez: suggestedFee,
      gasLimit: 1100,
      storageLimit: 5,
      burnFeeMutez: 0,
      opSize: 100,
    })

    const result = await estimateFees(context, {
      account: { ...revealedAccount, balance, stakedBalance },
      transaction: {
        mode: 'stake',
        recipient: '',
        amount: 0n,
        useAllAmount: true,
      },
    })

    expect(result.amount).toBe(
      balance - stakedBalance - BigInt(suggestedFee) - STAKE_USE_ALL_RESERVE_MUTEZ
    )
  })

  it('useAllAmount stake excludes unstaked-frozen funds from maxStakable', async () => {
    const suggestedFee = 700
    const balance = 1_000_000n
    const stakedBalance = 300_000n
    const unstakedBalance = 200_000n
    mockTezosToolkit.estimate.stake.mockResolvedValue({
      suggestedFeeMutez: suggestedFee,
      gasLimit: 1100,
      storageLimit: 5,
      burnFeeMutez: 0,
      opSize: 100,
    })

    const result = await estimateFees(context, {
      account: { ...revealedAccount, balance, stakedBalance, unstakedBalance },
      transaction: {
        mode: 'stake',
        recipient: '',
        amount: 0n,
        useAllAmount: true,
      },
    })

    expect(result.amount).toBe(
      balance - stakedBalance - unstakedBalance - BigInt(suggestedFee) - STAKE_USE_ALL_RESERVE_MUTEZ
    )
  })

  it('useAllAmount unstake coerces amount to 1 for Taquito estimation', async () => {
    mockTezosToolkit.estimate.unstake.mockResolvedValue({
      suggestedFeeMutez: 710,
      gasLimit: 1200,
      storageLimit: 6,
      burnFeeMutez: 0,
      opSize: 100,
    })

    const result = await estimateFees(context, {
      account: revealedAccount,
      transaction: {
        mode: 'unstake',
        recipient: '',
        amount: 0n,
        useAllAmount: true,
      },
    })

    expect(mockTezosToolkit.estimate.unstake).toHaveBeenCalledWith({
      amount: 1,
      mutez: true,
    })
    expect(result.fees).toBe(710n)
    expect(result.amount).toBe(0n)
  })

  it('finalize_unstake uses Taquito finalizeUnstake estimation and maps fee fields', async () => {
    mockTezosToolkit.estimate.finalizeUnstake.mockResolvedValue({
      suggestedFeeMutez: 720,
      gasLimit: 1300,
      storageLimit: 7,
      burnFeeMutez: 0,
      opSize: 100,
    })

    const result = await estimateFees(context, {
      account: revealedAccount,
      transaction: {
        mode: 'finalize_unstake',
        recipient: '',
        amount: 0n,
      },
    })

    expect(mockTezosToolkit.estimate.finalizeUnstake).toHaveBeenCalledWith({})
    expect(result.fees).toBe(720n)
    expect(result.gasLimit).toBe(1300n)
    expect(result.storageLimit).toBe(7n)
    expect(result.estimatedFees).toBe(720n)
    expect(result.amount).toBe(0n)
  })

  it('transfer (send) applies minFees and returns gas/storage from estimate', async () => {
    const suggestedFee = 350
    const minFees = 500
    mockTezosToolkit.estimate.transfer.mockResolvedValue({
      suggestedFeeMutez: suggestedFee,
      gasLimit: 1500,
      storageLimit: 0,
      burnFeeMutez: 0,
      opSize: 200,
    })
    currentConfig = mkConfig({
      fees: { ...defaultFeesConfig, minFees },
    })

    const result = await estimateFees(context, {
      account: revealedAccount,
      transaction: {
        mode: 'send',
        recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
        amount: BigInt(1000),
      },
    })

    expect(result.fees).toBe(BigInt(minFees))
    expect(result.estimatedFees).toBe(BigInt(minFees))
    expect(result.gasLimit).toBe(1500n)
    expect(result.storageLimit).toBe(0n)
    expect(result.amount).toBe(1000n)
  })

  it('transfer (send) unrevealed adds reveal fee to estimatedFees', async () => {
    const transferFee = 400
    mockTezosToolkit.estimate.transfer.mockResolvedValue({
      suggestedFeeMutez: transferFee,
      gasLimit: 1500,
      storageLimit: 0,
      burnFeeMutez: 0,
      opSize: 200,
    })

    const result = await estimateFees(context, {
      account: unrevealedAccount,
      transaction: {
        mode: 'send',
        recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
        amount: BigInt(1000),
      },
    })

    const expectedRevealFee = getRevealFee(unrevealedAccount.address)
    expect(result.fees).toBe(BigInt(transferFee))
    expect(result.estimatedFees).toBe(BigInt(transferFee + expectedRevealFee))
  })

  it('returns zero estimation when balance is 0 and not useAllAmount', async () => {
    const result = await estimateFees(context, {
      account: { ...revealedAccount, balance: 0n },
      transaction: {
        mode: 'delegate',
        recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
        amount: BigInt(0),
      },
    })

    expect(result.fees).toBe(0n)
    expect(result.estimatedFees).toBe(0n)
    expect(mockTezosToolkit.estimate.setDelegate).not.toHaveBeenCalled()
  })

  it('returns estimation with amount 0 when balance is 0 and useAllAmount', async () => {
    const result = await estimateFees(context, {
      account: { ...revealedAccount, balance: 0n },
      transaction: {
        mode: 'send',
        recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
        amount: BigInt(0),
        useAllAmount: true,
      },
    })

    expect(result.amount).toBe(0n)
    expect(mockTezosToolkit.estimate.transfer).not.toHaveBeenCalled()
  })

  it('returns fallback estimation when send has no recipient', async () => {
    currentConfig = mkConfig({
      fees: {
        ...defaultFeesConfig,
        minEstimatedFees: 300,
        minGasLimit: 600,
        minStorageLimit: 0,
      },
    })

    const result = await estimateFees(context, {
      account: revealedAccount,
      transaction: {
        mode: 'send',
        recipient: '',
        amount: BigInt(100),
        useAllAmount: false,
      },
    })

    expect(result.fees).toBe(300n)
    expect(result.estimatedFees).toBe(300n)
    expect(result.gasLimit).toBe(600n)
    expect(mockTezosToolkit.estimate.transfer).not.toHaveBeenCalled()
  })

  it('uses fallback estimation and adds reveal fee when Taquito throws Public key not found (unrevealed)', async () => {
    const minEstimatedFees = 400
    currentConfig = mkConfig({
      fees: {
        ...defaultFeesConfig,
        minEstimatedFees,
        minGasLimit: 600,
        minStorageLimit: 0,
      },
    })
    mockTezosToolkit.estimate.transfer.mockRejectedValue(
      Object.assign(new Error('Fail'), { status: 404, message: 'Public key not found' })
    )

    const result = await estimateFees(context, {
      account: unrevealedAccount,
      transaction: {
        mode: 'send',
        recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
        amount: BigInt(100),
      },
    })

    const expectedRevealFee = getRevealFee(unrevealedAccount.address)
    expect(result.fees).toBe(BigInt(minEstimatedFees))
    expect(result.estimatedFees).toBe(BigInt(minEstimatedFees + expectedRevealFee))
  })

  it('send_token (FA2) revealed applies estimation from contract transfer', async () => {
    const { transfer } = setupFa2ContractMock()
    mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
    mockTezosToolkit.estimate.transfer.mockResolvedValue({
      suggestedFeeMutez: 600,
      gasLimit: 5000,
      storageLimit: 10,
      burnFeeMutez: 0,
      opSize: 250,
    })

    const contractAddress = 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU'
    const result = await estimateFees(context, {
      account: revealedAccount,
      transaction: {
        mode: 'send_token',
        recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
        amount: BigInt(100),
        contractAddress,
        tokenId: 0,
      },
    })

    expect(mockTezosToolkit.contract.at).toHaveBeenCalledWith(contractAddress)
    expect(transfer).toHaveBeenCalledWith([
      {
        from_: revealedAccount.address,
        txs: [{ to_: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr', token_id: 0, amount: 100n }],
      },
    ])
    expect(result.fees).toBe(600n)
    expect(result.gasLimit).toBe(5000n)
    expect(result.storageLimit).toBe(10n)
    expect(result.estimatedFees).toBe(600n)
    expect(result.amount).toBe(100n)
  })

  it('send_token unrevealed adds reveal fee to estimatedFees', async () => {
    const { transfer } = setupFa2ContractMock()
    mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
    mockTezosToolkit.estimate.transfer.mockResolvedValue({
      suggestedFeeMutez: 400,
      gasLimit: 4000,
      storageLimit: 0,
      burnFeeMutez: 0,
      opSize: 200,
    })

    const contractAddress = 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU'
    const result = await estimateFees(context, {
      account: unrevealedAccount,
      transaction: {
        mode: 'send_token',
        recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
        amount: BigInt(50),
        contractAddress,
        tokenId: 3,
      },
    })

    const expectedRevealFee = getRevealFee(unrevealedAccount.address)
    expect(result.fees).toBe(400n)
    expect(result.estimatedFees).toBe(BigInt(400 + expectedRevealFee))
  })

  it('send_token uses fallback when Taquito throws Public key not found', async () => {
    const minEstimatedFees = 400
    currentConfig = mkConfig({
      fees: {
        ...defaultFeesConfig,
        minEstimatedFees,
        minGasLimit: 600,
        minStorageLimit: 0,
      },
    })
    const { transfer } = setupFa2ContractMock()
    mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
    mockTezosToolkit.estimate.transfer.mockRejectedValue(
      Object.assign(new Error('Fail'), { status: 404, message: 'Public key not found' })
    )

    const contractAddress = 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU'
    const result = await estimateFees(context, {
      account: unrevealedAccount,
      transaction: {
        mode: 'send_token',
        recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
        amount: BigInt(10),
        contractAddress,
        tokenId: 0,
      },
    })

    const expectedRevealFee = getRevealFee(unrevealedAccount.address)
    expect(result.fees).toBe(BigInt(minEstimatedFees))
    expect(result.estimatedFees).toBe(BigInt(minEstimatedFees + expectedRevealFee))
    expect(result.amount).toBe(10n)
  })

  it('send_token with empty recipient returns fallback without calling the contract', async () => {
    const { transfer } = setupFa2ContractMock()
    mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })

    const result = await estimateFees(context, {
      account: revealedAccount,
      transaction: {
        mode: 'send_token',
        recipient: '',
        amount: BigInt(100),
        contractAddress: 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU',
        tokenId: 0,
      },
    })

    expect(mockTezosToolkit.contract.at).not.toHaveBeenCalled()
    expect(result.fees).toBe(BigInt(defaultFeesConfig.minEstimatedFees))
    expect(transfer).not.toHaveBeenCalled()
  })

  it('send_token propagates error when contract.at fails', async () => {
    mockTezosToolkit.contract.at.mockRejectedValue(new Error('Invalid contract'))

    await expect(
      estimateFees(context, {
        account: revealedAccount,
        transaction: {
          mode: 'send_token',
          recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
          amount: BigInt(1),
          contractAddress: 'KT1Bad',
          tokenId: 0,
        },
      })
    ).rejects.toThrow('Invalid contract')
  })

  it('send_token throws when contractAddress or tokenId is missing', async () => {
    await expect(
      estimateFees(context, {
        account: revealedAccount,
        transaction: {
          mode: 'send_token',
          recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
          amount: BigInt(1),
          tokenId: 0,
        },
      })
    ).rejects.toThrow('FA2 transfer requires contractAddress and tokenId')
  })

  it('useAllAmount send computes max amount and applies minFees to main op', async () => {
    const minFees = 500
    const balance = 10000
    mockTezosToolkit.estimate.transfer.mockResolvedValue({
      suggestedFeeMutez: 100,
      gasLimit: 1500,
      storageLimit: 0,
      burnFeeMutez: 0,
      opSize: 200,
    })
    currentConfig = mkConfig({
      fees: { ...defaultFeesConfig, minFees },
    })

    const result = await estimateFees(context, {
      account: { ...revealedAccount, balance: BigInt(balance) },
      transaction: {
        mode: 'send',
        recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
        amount: BigInt(0),
        useAllAmount: true,
      },
    })

    expect(result.fees).toBe(BigInt(minFees))
    // max = spendable(10000) - mainOpFee(500) - (DUST_MARGIN(500) - (DUST_MARGIN*0.1 + opSize(200)))
    //     = 9500 - (500 - 250) = 9250
    expect(result.amount).toBe(9250n)
  })

  // Regression for LIVE-28506: the max must reserve the fee that is actually reported, so that
  // `amount + estimatedFees` always fits the spendable balance. Reserving the raw taquito
  // suggestion instead made every send-max fail validation once `minFees` exceeded it by more
  // than the dust margin — at any balance.
  it.each([
    // balance, minFees, suggestedFeeMutez, burnFeeMutez, expected amount
    [10000n, 2000, 100, 0, 7750n],
    [10000n, 100, 2000, 0, 7750n],
    // recipient allocation burn (277 bytes * COST_PER_BYTE), minus taquito's 20-byte pad
    [100000n, 2000, 100, 69250, 33500n],
  ])(
    'useAllAmount send reserves the reported fee (balance=%i, minFees=%i, suggested=%i, burn=%i)',
    async (balance, minFees, suggestedFeeMutez, burnFeeMutez, expectedAmount) => {
      mockTezosToolkit.estimate.transfer.mockResolvedValue({
        suggestedFeeMutez,
        gasLimit: 1500,
        storageLimit: 0,
        burnFeeMutez,
        opSize: 200,
      })
      currentConfig = mkConfig({
        fees: { ...defaultFeesConfig, minFees },
      })

      const result = await estimateFees(context, {
        account: { ...revealedAccount, balance },
        transaction: {
          mode: 'send',
          recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
          amount: 0n,
          useAllAmount: true,
        },
      })

      expect(result.amount).toBe(expectedAmount)
      // the invariant validateIntent's balance-coverage check relies on
      expect(result.amount! + result.estimatedFees).toBeLessThanOrEqual(balance)
    }
  )

  it('useAllAmount send excludes staked and unstaked funds from the max', async () => {
    const balance = 10000n
    const stakedBalance = 3000n
    const unstakedBalance = 1000n
    mockTezosToolkit.estimate.transfer.mockResolvedValue({
      suggestedFeeMutez: 100,
      gasLimit: 1500,
      storageLimit: 0,
      burnFeeMutez: 0,
      opSize: 200,
    })
    currentConfig = mkConfig({
      fees: { ...defaultFeesConfig, minFees: 0 },
    })

    const result = await estimateFees(context, {
      account: { ...revealedAccount, balance, stakedBalance, unstakedBalance },
      transaction: {
        mode: 'send',
        recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
        amount: 0n,
        useAllAmount: true,
      },
    })

    // spendable = balance(10000) - staked(3000) - unstaked(1000) = 6000
    // max = 6000 - suggestedFee(100) - (DUST_MARGIN(500) - (DUST_MARGIN*0.1 + opSize(200)))
    //     = 5900 - (500 - 250) = 5650
    expect(result.amount).toBe(5650n)
  })
})
