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

import type { TezosCoinConfig, TezosContext } from '../config'
import type { TransactionIntent } from '@ledgerhq/coin-module-framework/api/types'
import {
  RecipientRequired,
  InvalidAddress,
  InvalidAddressBecauseDestinationIsAlsoSource,
  AmountRequired,
  NotEnoughBalance,
} from '@ledgerhq/coin-module-framework/errors'
import {
  InvalidAddressBecauseAlreadyDelegated,
  MustDelegateBeforeStaking,
  NotEnoughBalanceToDelegate,
  TezosNotEnoughStaked,
  TezosStakeBlockedByPendingUnstake,
} from '../types/errors'
import { STAKE_USE_ALL_RESERVE_MUTEZ } from '../utils'
import { validateIntent } from './validateIntent'

const mockEstimateFees = jest.fn()
jest.mock('./estimateFees', () => ({
  estimateFees: (...args: unknown[]) => mockEstimateFees(...args),
}))

const mockGetAccountByAddress = jest.fn()
const mockGetTokensBalances = jest.fn()
const mockGetUnstakeRequestsFinalizable = jest.fn()
jest.mock('../network/tzkt', () => ({
  __esModule: true,
  createTzktApi: () => ({
    getAccountByAddress: (...args: unknown[]) => mockGetAccountByAddress(...args),
    getTokensBalances: (...args: unknown[]) => mockGetTokensBalances(...args),
    getUnstakeRequestsFinalizable: (...args: unknown[]) =>
      mockGetUnstakeRequestsFinalizable(...args),
  }),
}))

describe('validateIntent', () => {
  const senderAddress = 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oSM8'
  const validRecipient = 'tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx'

  const makeUserAccount = (overrides: Record<string, unknown> = {}) => ({
    type: 'user' as const,
    address: senderAddress,
    publicKey: 'edpk...',
    balance: 5000000,
    revealed: true,
    counter: 0,
    delegationLevel: 0,
    delegationTime: '2021-01-01T00:00:00Z',
    numTransactions: 0,
    firstActivityTime: '2021-01-01T00:00:00Z',
    ...overrides,
  })

  const context: TezosContext = {
    config: async () =>
      ({
        status: { type: 'active' },
        baker: { url: 'http://baker.example.com' },
        explorer: { url: 'http://tezos.explorer.com', maxTxQuery: 100 },
        node: { url: 'http://tezos.node.com' },
        fees: {
          minGasLimit: 600,
          minRevealGasLimit: 300,
          minStorageLimit: 0,
          minFees: 500,
          minEstimatedFees: 500,
        },
      }) as unknown as TezosCoinConfig,
    logger: () => undefined,
  }

  beforeEach(() => {
    jest.clearAllMocks()

    mockEstimateFees.mockResolvedValue({
      fees: 1000n,
      gasLimit: 10000n,
      storageLimit: 0n,
      estimatedFees: 1000n,
    })

    mockGetAccountByAddress.mockResolvedValue(makeUserAccount())
    mockGetTokensBalances.mockResolvedValue([])
    mockGetUnstakeRequestsFinalizable.mockResolvedValue(0n)
  })

  describe('recipient validation', () => {
    it('should return RecipientRequired error when recipient is missing', async () => {
      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: '',
        amount: 1000n,
      })

      expect(result.errors.recipient).toBeInstanceOf(RecipientRequired)
    })

    it('should return InvalidAddress error for invalid recipient', async () => {
      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: 'invalid_address',
        amount: 1000n,
      })

      expect(result.errors.recipient).toBeInstanceOf(InvalidAddress)
    })

    it('should return InvalidAddressBecauseDestinationIsAlsoSource when sender equals recipient', async () => {
      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: senderAddress,
        amount: 1000n,
      })

      expect(result.errors.recipient).toBeInstanceOf(InvalidAddressBecauseDestinationIsAlsoSource)
    })
  })

  describe('amount validation', () => {
    it('should return AmountRequired error when amount is zero and not useAllAmount', async () => {
      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 0n,
        useAllAmount: false,
      })

      expect(result.errors.amount).toBeInstanceOf(AmountRequired)
    })

    it('should not return AmountRequired error when useAllAmount is true', async () => {
      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 0n,
        useAllAmount: true,
      })

      expect(result.errors.amount).toBeUndefined()
    })

    it('should return NotEnoughBalance when amount is negative', async () => {
      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: -1n,
      })

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
    })

    it('uses the staked amount as amount and amount + fees as totalSpent for stake', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({
          delegate: { alias: 'baker', address: validRecipient, active: true },
          delegationLevel: 1,
        })
      )

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'stake',
        sender: senderAddress,
        recipient: '',
        amount: 2500n,
      })

      expect(result).toEqual({
        errors: {},
        warnings: {},
        estimatedFees: 1000n,
        amount: 2500n,
        totalSpent: 3500n,
      })
    })

    it('uses only fees as totalSpent for unstake', async () => {
      mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ stakedBalance: 4000 }))

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'unstake',
        sender: senderAddress,
        recipient: '',
        amount: 2500n,
      })

      expect(result).toEqual({
        errors: {},
        warnings: {},
        estimatedFees: 1000n,
        amount: 2500n,
        totalSpent: 1000n,
      })
    })

    it('uses amount 0 and only fees as totalSpent for finalize_unstake', async () => {
      mockGetUnstakeRequestsFinalizable.mockResolvedValue(1234n)

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'finalize_unstake',
        sender: senderAddress,
        recipient: '',
        amount: 0n,
      } satisfies TransactionIntent)

      expect(result).toEqual({
        errors: {},
        warnings: {},
        estimatedFees: 1000n,
        amount: 0n,
        totalSpent: 1000n,
      })
    })
  })

  describe('transaction constraints', () => {
    it('allows send-max on a delegated account with no error or warning and resolves the max amount', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({
          delegate: { alias: 'baker', address: validRecipient, active: true },
          delegationLevel: 1,
        })
      )

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 0n,
        useAllAmount: true,
      })

      expect(result.errors).toEqual({})
      expect(result.warnings).toEqual({})
      // balance 5_000_000 - fees 1_000
      expect(result.amount).toBe(4999000n)
      expect(result.totalSpent).toBe(5000000n)
    })

    it('should return MustDelegateBeforeStaking when stake intent has no delegate', async () => {
      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'stake',
        sender: senderAddress,
        recipient: '',
        amount: 1n,
      })

      expect(result.errors.amount).toBeInstanceOf(MustDelegateBeforeStaking)
      expect(mockEstimateFees).not.toHaveBeenCalled()
    })

    it('allows a registered baker (delegate account) to stake without MustDelegateBeforeStaking', async () => {
      // A baker is self-delegated: tzkt returns type "delegate" with no `delegate` field, but it
      // must still be allowed to stake. Regression for LIVE-34256.
      mockGetAccountByAddress.mockResolvedValue({
        type: 'delegate',
        address: senderAddress,
        publicKey: 'edpk...',
        balance: 5000000,
        revealed: true,
        counter: 0,
      })

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'stake',
        sender: senderAddress,
        recipient: '',
        amount: 1000n,
      })

      // No validation errors at all, and fee estimation was reached — a MustDelegateBeforeStaking
      // short-circuit would return before estimation (see the "no delegate" case above).
      expect(result.errors).toEqual({})
      expect(mockEstimateFees).toHaveBeenCalled()
    })

    it('should return AmountRequired when stake amount is zero', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({
          delegate: { alias: 'baker', address: validRecipient, active: true },
          delegationLevel: 1,
        })
      )

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'stake',
        sender: senderAddress,
        recipient: '',
        amount: 0n,
      })

      expect(result.errors.amount).toBeInstanceOf(AmountRequired)
      expect(mockEstimateFees).not.toHaveBeenCalled()
    })

    it('should skip AmountRequired and resolve max amount when stake useAllAmount is true', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({
          delegate: { alias: 'baker', address: validRecipient, active: true },
          delegationLevel: 1,
        })
      )
      mockEstimateFees.mockResolvedValueOnce({
        fees: 1000n,
        gasLimit: 10000n,
        storageLimit: 0n,
        estimatedFees: 1000n,
        amount: 4_500_000n,
      })

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'stake',
        sender: senderAddress,
        recipient: '',
        amount: 0n,
        useAllAmount: true,
      })

      expect(result.errors.amount).toBeUndefined()
      expect(mockEstimateFees).toHaveBeenCalledTimes(1)
      expect(result.amount).toBe(4_500_000n)
    })

    it('should return NotEnoughBalance when stake useAllAmount resolves max to 0n', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({
          delegate: { alias: 'baker', address: validRecipient, active: true },
          delegationLevel: 1,
        })
      )
      mockEstimateFees.mockResolvedValueOnce({
        fees: 1000n,
        gasLimit: 10000n,
        storageLimit: 0n,
        estimatedFees: 1000n,
        amount: 0n,
      })

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'stake',
        sender: senderAddress,
        recipient: '',
        amount: 0n,
        useAllAmount: true,
      })

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
      expect(result.amount).toBe(0n)
    })

    it('excludes already-staked funds when stake useAllAmount falls back to balance computation', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({
          balance: 5_000_000,
          stakedBalance: 1_000_000,
          delegate: { alias: 'baker', address: validRecipient, active: true },
          delegationLevel: 1,
        })
      )
      // No `amount` field => estimatedAmount is undefined, exercising the fallback path.
      mockEstimateFees.mockResolvedValueOnce({
        fees: 1000n,
        gasLimit: 10000n,
        storageLimit: 0n,
        estimatedFees: 1000n,
      })

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'stake',
        sender: senderAddress,
        recipient: '',
        amount: 0n,
        useAllAmount: true,
      })

      expect(result.errors.amount).toBeUndefined()
      expect(result.amount).toBe(4_000_000n - 1000n - STAKE_USE_ALL_RESERVE_MUTEZ)
    })

    it('excludes unstaked-frozen funds when stake useAllAmount falls back to balance computation', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({
          balance: 5_000_000,
          stakedBalance: 1_000_000,
          unstakedBalance: 500_000,
          delegate: { alias: 'baker', address: validRecipient, active: true },
          delegationLevel: 1,
        })
      )
      // No `amount` field => estimatedAmount is undefined, exercising the fallback path.
      mockEstimateFees.mockResolvedValueOnce({
        fees: 1000n,
        gasLimit: 10000n,
        storageLimit: 0n,
        estimatedFees: 1000n,
      })

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'stake',
        sender: senderAddress,
        recipient: '',
        amount: 0n,
        useAllAmount: true,
      })

      expect(result.errors.amount).toBeUndefined()
      expect(result.amount).toBe(3_500_000n - 1000n - STAKE_USE_ALL_RESERVE_MUTEZ)
    })

    it('should return TezosNotEnoughStaked when unstake has no staked balance', async () => {
      mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ stakedBalance: 0 }))

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'unstake',
        sender: senderAddress,
        recipient: '',
        amount: 1n,
      })

      expect(result.errors.amount).toBeInstanceOf(TezosNotEnoughStaked)
      expect(mockEstimateFees).not.toHaveBeenCalled()
    })

    it('should return AmountRequired when unstake amount is zero', async () => {
      mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ stakedBalance: 4000 }))

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'unstake',
        sender: senderAddress,
        recipient: '',
        amount: 0n,
      })

      expect(result.errors.amount).toBeInstanceOf(AmountRequired)
      expect(mockEstimateFees).not.toHaveBeenCalled()
    })

    it('should return TezosNotEnoughStaked when unstake amount exceeds staked balance', async () => {
      mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ stakedBalance: 4000 }))

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'unstake',
        sender: senderAddress,
        recipient: '',
        amount: 5000n,
      })

      expect(result.errors.amount).toBeInstanceOf(TezosNotEnoughStaked)
      expect(result.amount).toBe(5000n)
      expect(mockEstimateFees).not.toHaveBeenCalled()
    })

    it('should resolve unstake useAllAmount to full stakedBalance', async () => {
      mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ stakedBalance: 4000 }))

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'unstake',
        sender: senderAddress,
        recipient: '',
        amount: 0n,
        useAllAmount: true,
      })

      expect(result.errors).toEqual({})
      expect(result.amount).toBe(4000n)
      expect(result.totalSpent).toBe(1000n)
    })

    it('should return TezosNotEnoughStaked when unstake useAllAmount with zero stakedBalance', async () => {
      mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ stakedBalance: 0 }))

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'unstake',
        sender: senderAddress,
        recipient: '',
        amount: 0n,
        useAllAmount: true,
      })

      expect(result.errors.amount).toBeInstanceOf(TezosNotEnoughStaked)
      expect(mockEstimateFees).not.toHaveBeenCalled()
    })

    it("should return NotEnoughBalance when unstake useAllAmount but liquid balance can't cover fees", async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({ balance: 500, stakedBalance: 4000 })
      )
      mockEstimateFees.mockResolvedValueOnce({
        fees: 1000n,
        gasLimit: 10000n,
        storageLimit: 0n,
        estimatedFees: 1000n,
      })

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'unstake',
        sender: senderAddress,
        recipient: '',
        amount: 0n,
        useAllAmount: true,
      })

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
    })

    it('should return NotEnoughBalance when finalize_unstake has nothing finalizable', async () => {
      mockGetUnstakeRequestsFinalizable.mockResolvedValue(0n)

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'finalize_unstake',
        sender: senderAddress,
        recipient: '',
        amount: 0n,
      } satisfies TransactionIntent)

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
      expect(mockEstimateFees).not.toHaveBeenCalled()
    })
  })

  describe('balance validation', () => {
    it('should return NotEnoughBalance error when amount exceeds balance', async () => {
      const balance = 1000000
      const amount = 2000000

      mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ balance }))

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: BigInt(amount),
      })

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
    })

    it('should pass validation when amount is within balance', async () => {
      const amount = 1000000n
      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount,
      })

      expect(result.errors.amount).toBeUndefined()
      expect(result.amount).toBe(amount)
    })

    it("returns NotEnoughBalance when funds are staked/frozen and spendable can't cover fees", async () => {
      // balance includes the staked funds (total > fees), but spendable (total - staked) is 0
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({ balance: 5_000_000, stakedBalance: 5_000_000 })
      )

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'undelegate',
        sender: senderAddress,
        recipient: '',
        amount: 0n,
      })

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
    })

    it('passes delegate when spendable covers fees despite most funds being staked', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({ balance: 5_000_000, stakedBalance: 4_000_000 })
      )

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'delegate',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 0n,
      })

      expect(result.errors.amount).toBeUndefined()
    })

    it('passes a send that spends exactly the spendable balance (totalSpent === spendable boundary)', async () => {
      // spendable = balance - staked = 1_000_000; amount (999_000) + mocked fees (1_000) lands exactly
      // on it. The coverage check is strictly-greater, so the boundary must NOT error.
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({ balance: 2_000_000, stakedBalance: 1_000_000 })
      )

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 999_000n,
      })

      expect(result.errors.amount).toBeUndefined()
      expect(result.totalSpent).toBe(1_000_000n)
    })

    it('returns NotEnoughBalance for a send when unstaked-frozen funds leave too little liquid', async () => {
      // Reproduces the consecutive-send failure: unstaked funds are pending withdrawal (frozen),
      // so liquid spendable is only 220 mutez — far below amount + fees.
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({ balance: 300_322, unstakedBalance: 300_102 })
      )

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 100n,
      })

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
    })
  })

  describe('transaction types', () => {
    it('should pass validation for delegate transaction', async () => {
      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'delegate',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 0n,
      })

      expect(result.errors).toEqual({})
    })

    it('should pass validation for undelegate transaction', async () => {
      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'undelegate',
        sender: senderAddress,
        recipient: '',
        amount: 0n,
      })

      expect(result.errors).toEqual({})
    })

    it('should pass validation for stake transaction when already delegated', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({
          delegate: { alias: 'baker', address: validRecipient, active: true },
          delegationLevel: 1,
        })
      )

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'stake',
        sender: senderAddress,
        recipient: '',
        amount: 1000n,
      })

      expect(result.errors).toEqual({})
    })

    it('should pass validation for unstake transaction', async () => {
      mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ stakedBalance: 4000 }))

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'unstake',
        sender: senderAddress,
        recipient: '',
        amount: 1000n,
      })

      expect(result.errors).toEqual({})
    })

    it('should pass validation for finalize_unstake transaction', async () => {
      mockGetUnstakeRequestsFinalizable.mockResolvedValue(4000n)

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'finalize_unstake',
        sender: senderAddress,
        recipient: '',
        amount: 0n,
      } satisfies TransactionIntent)

      expect(result.errors).toEqual({})
    })
  })

  describe('taquito error mapping', () => {
    it('maps balance_too_low to NotEnoughBalance for send', async () => {
      mockEstimateFees.mockResolvedValue({
        fees: 0n,
        gasLimit: 0n,
        storageLimit: 0n,
        estimatedFees: 500n,
        taquitoError: 'proto.001-PtAtLas.contract.balance_too_low',
      })

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 1000n,
      })

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
    })

    it('maps balance_too_low to NotEnoughBalance for stake', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({
          delegate: { alias: 'baker', address: validRecipient, active: true },
          delegationLevel: 1,
        })
      )

      mockEstimateFees.mockResolvedValue({
        fees: 0n,
        gasLimit: 0n,
        storageLimit: 0n,
        estimatedFees: 500n,
        taquitoError: 'proto.001-PtAtLas.contract.balance_too_low',
      })

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'stake',
        sender: senderAddress,
        recipient: '',
        amount: 1n,
      })

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
    })

    it('maps subtraction_underflow to NotEnoughBalance for non-stake', async () => {
      mockEstimateFees.mockResolvedValue({
        fees: 0n,
        gasLimit: 0n,
        storageLimit: 0n,
        estimatedFees: 500n,
        taquitoError: 'some.path.subtraction_underflow',
      })

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 1000n,
      })

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
    })

    it('maps delegate.unchanged to InvalidAddressBecauseAlreadyDelegated for stake', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({
          delegate: { alias: 'baker', address: validRecipient, active: true },
          delegationLevel: 1,
        })
      )

      mockEstimateFees.mockResolvedValue({
        fees: 0n,
        gasLimit: 0n,
        storageLimit: 0n,
        estimatedFees: 500n,
        taquitoError: 'proto.024-PtTALLiN.delegate.unchanged',
      })

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'stake',
        sender: senderAddress,
        recipient: '',
        amount: 1n,
      })

      expect(result.errors.recipient).toBeInstanceOf(InvalidAddressBecauseAlreadyDelegated)
    })

    it('maps delegate.unchanged to InvalidAddressBecauseAlreadyDelegated for delegate', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({
          delegate: { alias: 'baker', address: validRecipient, active: true },
          delegationLevel: 1,
        })
      )

      mockEstimateFees.mockResolvedValue({
        fees: 0n,
        gasLimit: 0n,
        storageLimit: 0n,
        estimatedFees: 500n,
        taquitoError: 'proto.024-PtTALLiN.delegate.unchanged',
      })

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'delegate',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 0n,
      })

      expect(result.errors.recipient).toBeInstanceOf(InvalidAddressBecauseAlreadyDelegated)
    })

    it('maps empty_implicit_contract to NotEnoughBalanceToDelegate', async () => {
      mockEstimateFees.mockResolvedValue({
        fees: 0n,
        gasLimit: 0n,
        storageLimit: 0n,
        estimatedFees: 500n,
        taquitoError: 'proto.empty_implicit_contract',
      })

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'delegate',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 0n,
      })

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalanceToDelegate)
    })

    it('maps empty_implicit_contract to NotEnoughBalance for stake', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({
          delegate: { alias: 'baker', address: validRecipient, active: true },
          delegationLevel: 1,
        })
      )

      mockEstimateFees.mockResolvedValue({
        fees: 0n,
        gasLimit: 0n,
        storageLimit: 0n,
        estimatedFees: 500n,
        taquitoError: 'proto.empty_implicit_contract',
      })

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'stake',
        sender: senderAddress,
        recipient: '',
        amount: 1n,
      })

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
    })

    it('maps staking.too_much_unstaked to TezosNotEnoughStaked for unstake', async () => {
      mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ stakedBalance: 4000 }))
      mockEstimateFees.mockResolvedValue({
        fees: 0n,
        gasLimit: 0n,
        storageLimit: 0n,
        estimatedFees: 500n,
        taquitoError: 'proto.alpha.staking.too_much_unstaked',
      })

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'unstake',
        sender: senderAddress,
        recipient: '',
        amount: 1000n,
      })

      expect(result.errors.amount).toBeInstanceOf(TezosNotEnoughStaked)
    })

    it('maps contract.must_be_delegated_to_stake to MustDelegateBeforeStaking', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({
          delegate: { alias: 'baker', address: validRecipient, active: true },
          delegationLevel: 1,
        })
      )
      mockEstimateFees.mockResolvedValue({
        fees: 0n,
        gasLimit: 0n,
        storageLimit: 0n,
        estimatedFees: 500n,
        taquitoError: 'proto.alpha.contract.must_be_delegated_to_stake',
      })

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'stake',
        sender: senderAddress,
        recipient: '',
        amount: 1000n,
      })

      expect(result.errors.amount).toBeInstanceOf(MustDelegateBeforeStaking)
    })

    it('maps cannot_stake_with_unfinalizable_unstake_requests_to_another_delegate to TezosStakeBlockedByPendingUnstake for stake', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({
          delegate: { alias: 'baker', address: validRecipient, active: true },
          delegationLevel: 1,
        })
      )
      mockEstimateFees.mockResolvedValue({
        fees: 0n,
        gasLimit: 0n,
        storageLimit: 0n,
        estimatedFees: 500n,
        taquitoError:
          'proto.alpha.cannot_stake_with_unfinalizable_unstake_requests_to_another_delegate',
      })

      const result = await validateIntent(context, {
        intentType: 'staking',
        asset: { type: 'native' },
        type: 'stake',
        sender: senderAddress,
        recipient: '',
        amount: 1000n,
      })

      expect(result.errors.amount).toBeInstanceOf(TezosStakeBlockedByPendingUnstake)
      expect(result.errors.amount?.name).toBe('TezosStakeBlockedByPendingUnstake')
    })

    it('maps unknown taquito errors to a generic Error on amount', async () => {
      mockEstimateFees.mockResolvedValue({
        fees: 0n,
        gasLimit: 0n,
        storageLimit: 0n,
        estimatedFees: 500n,
        taquitoError: 'proto.unknown_rpc_failure',
      })

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 1000n,
      })

      expect(result.errors.amount).toBeInstanceOf(Error)
      expect(result.errors.amount?.message).toBe('proto.unknown_rpc_failure')
    })
  })

  describe('account fetch and reveal', () => {
    it('uses fixed estimated fees when account is not revealed and skips estimateFees', async () => {
      mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ revealed: false }))

      const amount = 1000000n
      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount,
      })

      expect(mockEstimateFees).not.toHaveBeenCalled()
      expect(result.estimatedFees).toBe(2000n)
      expect(result.amount).toBe(amount)
      expect(result.totalSpent).toBe(amount + 2000n)
    })

    it('sets estimation error when getAccountByAddress rejects', async () => {
      mockGetAccountByAddress.mockRejectedValue(new Error('network failure'))

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 1000n,
      })

      expect(result.errors.estimation).toBeInstanceOf(Error)
      expect(result.errors.estimation?.message).toBe('network failure')
      expect(result.estimatedFees).toBe(0n)
      expect(result.amount).toBe(1000n)
      expect(result.totalSpent).toBe(1000n)
    })

    it('sets estimation error when account type is not user', async () => {
      mockGetAccountByAddress.mockResolvedValue({
        type: 'empty',
        address: senderAddress,
        counter: 0,
      })

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 1000n,
      })

      expect(result.errors.estimation).toBeInstanceOf(Error)
      expect(result.estimatedFees).toBe(0n)
    })
  })

  describe('FA2 token validation', () => {
    it('should set amount to full FA2 token balance when useAllAmount is true', async () => {
      const contract = 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU'
      const tokenBalance = '1234567890'

      mockGetTokensBalances.mockResolvedValue([
        {
          id: 1,
          account: { address: senderAddress },
          token: {
            id: 1,
            contract: { address: contract },
            tokenId: '0',
            standard: 'fa2' as const,
            metadata: { symbol: 'TK', decimals: '6' },
          },
          balance: tokenBalance,
          transfersCount: 0,
          firstLevel: 0,
          firstTime: '',
          lastLevel: 0,
          lastTime: '',
        },
      ])

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'token', assetReference: `${contract}:0` },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 0n,
        useAllAmount: true,
      })

      expect(mockGetTokensBalances).toHaveBeenCalledWith(senderAddress, {
        contractAddress: contract,
        tokenId: 0,
      })
      expect(result.errors.amount).toBeUndefined()
      expect(result.amount).toBe(BigInt(tokenBalance))
      expect(result.totalSpent).toBe(1000n)
    })

    it('should return NotEnoughBalance when estimation reports script_rejected (FA2_INSUFFICIENT_BALANCE)', async () => {
      mockEstimateFees.mockResolvedValue({
        fees: 0n,
        gasLimit: 0n,
        storageLimit: 0n,
        estimatedFees: 0n,
        taquitoError: 'proto.024-PtTALLiN.michelson_v1.script_rejected',
      })

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'token', assetReference: 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU:0' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 1000000n,
      })

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
    })

    it('should set amount to 0n when useAllAmount and token balance row is missing', async () => {
      const contract = 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU'
      mockGetTokensBalances.mockResolvedValue([])

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'token', assetReference: `${contract}:0` },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 0n,
        useAllAmount: true,
      })

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

    it('fixed-amount TKEY (18 dec): no error when user has enough token and XTZ for fees [LIVE-35976]', async () => {
      // 0.01 TKEY = 10^16 base units — vastly exceeds any XTZ balance in mutez, was the exact failure case
      const contract = 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU'
      const tkeyAmount = 10n ** 16n

      mockGetTokensBalances.mockResolvedValue([
        {
          id: 1,
          account: { address: senderAddress },
          token: {
            id: 1,
            contract: { address: contract },
            tokenId: '0',
            standard: 'fa2' as const,
            metadata: { symbol: 'TKEY', decimals: '18' },
          },
          balance: tkeyAmount.toString(),
          transfersCount: 0,
          firstLevel: 0,
          firstTime: '',
          lastLevel: 0,
          lastTime: '',
        },
      ])

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'token', assetReference: `${contract}:0` },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: tkeyAmount,
      })

      expect(result.errors.amount).toBeUndefined()
      expect(result.amount).toBe(tkeyAmount)
      expect(result.totalSpent).toBe(1000n) // only XTZ fees; token amount not included
    })

    it('fixed-amount token send: NotEnoughBalance when XTZ is insufficient for fees [LIVE-35976]', async () => {
      const contract = 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU'
      const tkeyAmount = 10n ** 16n

      mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ balance: 500 })) // 500 mutez < 1000 fees
      // Token balance is sufficient — this test must isolate the XTZ coverage branch only.
      mockGetTokensBalances.mockResolvedValue([
        {
          id: 1,
          account: { address: senderAddress },
          token: {
            id: 1,
            contract: { address: contract },
            tokenId: '0',
            standard: 'fa2' as const,
            metadata: { symbol: 'TKEY', decimals: '18' },
          },
          balance: tkeyAmount.toString(),
          transfersCount: 0,
          firstLevel: 0,
          firstTime: '',
          lastLevel: 0,
          lastTime: '',
        },
      ])

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'token', assetReference: `${contract}:0` },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: tkeyAmount,
      })

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
    })

    it('fixed-amount token send: NotEnoughBalance when token balance is insufficient [LIVE-35976]', async () => {
      const contract = 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU'
      const tkeyAmount = 10n ** 16n

      mockGetTokensBalances.mockResolvedValue([
        {
          id: 1,
          account: { address: senderAddress },
          token: {
            id: 1,
            contract: { address: contract },
            tokenId: '0',
            standard: 'fa2' as const,
            metadata: { symbol: 'TKEY', decimals: '18' },
          },
          balance: (tkeyAmount - 1n).toString(), // one unit short
          transfersCount: 0,
          firstLevel: 0,
          firstTime: '',
          lastLevel: 0,
          lastTime: '',
        },
      ])

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'token', assetReference: `${contract}:0` },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: tkeyAmount,
      })

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
    })

    it('fixed-amount USDt (6 dec): passes correctly, not just accidentally, after fix [LIVE-35976]', async () => {
      // 0.01 USDt = 10_000 base units — previously passed only because 10_000 < XTZ balance by coincidence
      const contract = 'KT1XnTn74bagnxYoFErbDH5QRAULUDtpXkmV'
      const usdtAmount = 10_000n

      mockGetTokensBalances.mockResolvedValue([
        {
          id: 2,
          account: { address: senderAddress },
          token: {
            id: 2,
            contract: { address: contract },
            tokenId: '0',
            standard: 'fa2' as const,
            metadata: { symbol: 'USDt', decimals: '6' },
          },
          balance: usdtAmount.toString(),
          transfersCount: 0,
          firstLevel: 0,
          firstTime: '',
          lastLevel: 0,
          lastTime: '',
        },
      ])

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'token', assetReference: `${contract}:0` },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: usdtAmount,
      })

      expect(result.errors.amount).toBeUndefined()
      expect(result.totalSpent).toBe(1000n) // only fees in XTZ
    })

    it('unrevealed account + fixed-amount FA2 token send: no error and totalSpent equals reveal fees only [LIVE-35976]', async () => {
      // Unrevealed accounts skip taquito estimation and get a fixed 2000n fee.
      // Before the fix, calculateAmounts fell through to `amount + estimatedFees`, mixing token
      // base units with mutez. After the fix it correctly returns `totalSpent: estimatedFees`.
      const contract = 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU'
      const tkeyAmount = 10n ** 16n

      mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ revealed: false }))
      mockGetTokensBalances.mockResolvedValue([
        {
          id: 1,
          account: { address: senderAddress },
          token: {
            id: 1,
            contract: { address: contract },
            tokenId: '0',
            standard: 'fa2' as const,
            metadata: { symbol: 'TKEY', decimals: '18' },
          },
          balance: tkeyAmount.toString(),
          transfersCount: 0,
          firstLevel: 0,
          firstTime: '',
          lastLevel: 0,
          lastTime: '',
        },
      ])

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'token', assetReference: `${contract}:0` },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: tkeyAmount,
      })

      expect(mockEstimateFees).not.toHaveBeenCalled()
      expect(result.errors.amount).toBeUndefined()
      expect(result.amount).toBe(tkeyAmount)
      expect(result.totalSpent).toBe(2000n) // only XTZ reveal+tx fees; no token units mixed in
    })

    it('unrevealed account + fixed-amount FA2 token send: NotEnoughBalance when token balance is insufficient [LIVE-35976]', async () => {
      const contract = 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU'
      const tkeyAmount = 10n ** 16n

      mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ revealed: false }))
      mockGetTokensBalances.mockResolvedValue([
        {
          id: 1,
          account: { address: senderAddress },
          token: {
            id: 1,
            contract: { address: contract },
            tokenId: '0',
            standard: 'fa2' as const,
            metadata: { symbol: 'TKEY', decimals: '18' },
          },
          balance: (tkeyAmount - 1n).toString(), // one unit short
          transfersCount: 0,
          firstLevel: 0,
          firstTime: '',
          lastLevel: 0,
          lastTime: '',
        },
      ])

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'token', assetReference: `${contract}:0` },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: tkeyAmount,
      })

      expect(mockEstimateFees).not.toHaveBeenCalled()
      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
    })

    it('send-max token: amount stays in token units when taquito reports balance_too_low', async () => {
      // Regression guard for the optimization that skips fetchTokenBalance when errors.amount is set.
      // For send-max, tokenBalance is also used as the returned amount — skipping it would cause
      // calculateAmounts to fall back to the native XTZ path and return mutez instead of token units.
      const contract = 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU'
      const tokenBalance = 5_000_000n

      mockEstimateFees.mockResolvedValue({
        fees: 0n,
        gasLimit: 0n,
        storageLimit: 0n,
        estimatedFees: 0n,
        taquitoError: 'proto.024-PtTALLiN.contract.balance_too_low',
      })
      mockGetTokensBalances.mockResolvedValue([
        {
          id: 1,
          account: { address: senderAddress },
          token: {
            id: 1,
            contract: { address: contract },
            tokenId: '0',
            standard: 'fa2' as const,
            metadata: { symbol: 'TK', decimals: '6' },
          },
          balance: tokenBalance.toString(),
          transfersCount: 0,
          firstLevel: 0,
          firstTime: '',
          lastLevel: 0,
          lastTime: '',
        },
      ])

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'token', assetReference: `${contract}:0` },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 0n,
        useAllAmount: true,
      })

      expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
      expect(mockGetTokensBalances).toHaveBeenCalled()
      expect(result.amount).toBe(tokenBalance)
    })
  })

  describe('native XTZ send max', () => {
    it('should fall back to balance minus fees when estimateFees returns amount 0n', async () => {
      mockEstimateFees.mockResolvedValue({
        fees: 1000n,
        gasLimit: 10000n,
        storageLimit: 0n,
        estimatedFees: 1000n,
        amount: 0n,
      })

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 0n,
        useAllAmount: true,
      })

      expect(mockGetTokensBalances).not.toHaveBeenCalled()
      expect(result.amount).toBe(4999000n)
      expect(result.totalSpent).toBe(5000000n)
    })

    it('should prefer positive estimatedAmount from estimateFees over balance minus fees', async () => {
      const estimatedFromTaquito = 3000000n
      mockEstimateFees.mockResolvedValue({
        fees: 1000n,
        gasLimit: 10000n,
        storageLimit: 0n,
        estimatedFees: 1000n,
        amount: estimatedFromTaquito,
      })

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 0n,
        useAllAmount: true,
      })

      expect(result.amount).toBe(estimatedFromTaquito)
      expect(result.totalSpent).toBe(estimatedFromTaquito + 1000n)
    })

    it('excludes staked and unstaked funds from the send-max fallback', async () => {
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({ balance: 5_000_000, stakedBalance: 1_000_000, unstakedBalance: 500_000 })
      )
      mockEstimateFees.mockResolvedValue({
        fees: 1000n,
        gasLimit: 10000n,
        storageLimit: 0n,
        estimatedFees: 1000n,
        amount: 0n,
      })

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 0n,
        useAllAmount: true,
      })

      expect(result.amount).toBe(3_499_000n)
      expect(result.totalSpent).toBe(3_500_000n)
    })

    it('resolves a positive send-max for a delegated account with staked funds (LIVE-28506)', async () => {
      // Regression for LIVE-28506: a staking account (necessarily delegated, since you must
      // delegate before staking) doing Send Max used to report max spendable = 0 — a soft
      // "don't empty a delegated account" error short-circuited the amount computation. Send Max
      // must now surface the liquid balance (total minus staked) minus fees: a positive amount
      // whenever that liquid balance exceeds fees (as it does here), instead of short-circuiting
      // to 0. (It can still be 0 when fees exceed the liquid balance — not the case tested here.)
      mockEstimateFees.mockResolvedValue({
        fees: 1000n,
        gasLimit: 10000n,
        storageLimit: 0n,
        estimatedFees: 1000n,
        amount: 0n,
      })
      mockGetAccountByAddress.mockResolvedValue(
        makeUserAccount({
          balance: 5_000_000,
          stakedBalance: 3_000_000,
          delegate: { alias: 'baker', address: validRecipient, active: true },
          delegationLevel: 1,
        })
      )

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount: 0n,
        useAllAmount: true,
      })

      expect(result.errors).toEqual({})
      expect(result.warnings).toEqual({})
      // spendable = balance 5_000_000 - staked 3_000_000 = 2_000_000; minus fees 1_000
      expect(result.amount).toBe(1_999_000n)
      expect(result.totalSpent).toBe(2_000_000n)
    })
  })

  describe('successful validation', () => {
    it('should return valid result with correct values', async () => {
      const amount = 1000000n
      const estimatedFees = 1500n

      mockEstimateFees.mockResolvedValue({
        fees: 1000n,
        gasLimit: 10000n,
        storageLimit: 0n,
        estimatedFees,
      })

      const result = await validateIntent(context, {
        intentType: 'transaction',
        asset: { type: 'native' },
        type: 'send',
        sender: senderAddress,
        recipient: validRecipient,
        amount,
      })

      expect(result).toMatchObject({
        errors: {},
        warnings: {},
        estimatedFees,
        amount,
        totalSpent: amount + estimatedFees,
      })
    })
  })
})
