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

import type { TezosCoinConfig, TezosContext } from '../config'
import type { APIAccount } from '../network/types'
import type { BalanceOptions, Operation } from '@ledgerhq/coin-module-framework/api/types'
import type { TransactionIntent } from '@ledgerhq/coin-module-framework/api/types'
import { createApi } from './index'

// The tzkt factory `createTzktApi(config)` builds a fresh object on every call, so a spy on a
// single instance would not intercept the calls the source makes internally. We mock the module
// so `createTzktApi` always returns the SAME shared object, and expose its `getAccountByAddress`
// jest.fn as `mockGetAccountByAddress` for the assertions below (which used to target
// `mockGetAccountByAddress`).
const mockGetAccountByAddress = jest.fn()
jest.mock('../network/tzkt', () => ({
  createTzktApi: () => ({ getAccountByAddress: mockGetAccountByAddress }),
}))

const DEFAULT_ESTIMATED_FEES = 300n
const DEFAULT_GAS_LIMIT = 30n
const DEFAULT_STORAGE_LIMIT = 40n

const logicGetTransactions = jest.fn()
const logicEstimateFees = jest.fn()
const logicCraftTransactionMock = jest.fn(
  (_context: unknown, _account: unknown, _transaction: { fee: { fees: string } }) => {
    return { type: undefined, contents: undefined }
  }
)
const logicCraftRawOperationsMock = jest.fn()

jest.mock('../logic', () => ({
  listOperations: async () => logicGetTransactions(),
  estimateFees: (...args: unknown[]) => logicEstimateFees(...args),
  craftTransaction: (context: unknown, account: unknown, transaction: { fee: { fees: string } }) =>
    logicCraftTransactionMock(context, account, transaction),
  craftRawOperations: (...args: unknown[]) => logicCraftRawOperationsMock(...args),
  rawEncode: () => Promise.resolve('tz1heMGVHQnx7ALDcDKqez8fan64Eyicw4DJ'),
}))

const mockGetTezosToolkit = jest.fn()
jest.mock('../logic/tezosToolkit', () => ({
  getTezosToolkit: () => mockGetTezosToolkit(),
}))

mockGetAccountByAddress.mockResolvedValue({
  type: 'user',
  balance: 1000,
  revealed: true,
  address: 'tz1test',
  publicKey: 'edpktest',
  counter: 0,
  delegationLevel: 0,
  delegationTime: '2021-01-01T00:00:00Z',
  numTransactions: 0,
  firstActivityTime: '2021-01-01T00:00:00Z',
} as APIAccount)

const api = createApi()

/** Builds a TezosContext carrying the given coin config (see logic/getBalance.test.ts). */
function makeContext(config: unknown): TezosContext {
  return {
    config: async () => config as TezosCoinConfig,
    logger: () => undefined,
  }
}

const context = makeContext({
  baker: {
    url: 'https://baker.example.com',
  },
  explorer: {
    url: 'foo',
    maxTxQuery: 1,
  },
  node: {
    url: 'bar',
  },
  fees: {
    minGasLimit: 1,
    minRevealGasLimit: 1,
    minStorageLimit: 1,
    minFees: 1,
    minEstimatedFees: 2,
  },
})

describe('craftRawTransaction', () => {
  afterEach(() => {
    logicCraftRawOperationsMock.mockClear()
  })

  it('delegates to craftRawOperations and wraps the result', async () => {
    logicCraftRawOperationsMock.mockResolvedValue('03deadbeef')
    const rawTransaction = '[{"kind":"transaction","destination":"tz1recipient","amount":"1000"}]'

    const result = await api.craftRawTransaction(
      context,
      rawTransaction,
      'tz1sender',
      'edpkpublickey',
      5n
    )

    expect(logicCraftRawOperationsMock).toHaveBeenCalledWith(
      expect.anything(),
      rawTransaction,
      'tz1sender',
      'edpkpublickey',
      5n
    )
    expect(result).toEqual({ transaction: '03deadbeef' })
  })
})

describe('get operations', () => {
  afterEach(() => {
    logicGetTransactions.mockClear()
  })

  it('could return no operation', async () => {
    logicGetTransactions.mockResolvedValue([[], ''])
    const { items: operations, next: token } = await api.listOperations(context, 'addr', {
      minHeight: 100,
      order: 'asc',
    })
    expect(operations).toEqual([])
    expect(token).toBeUndefined()
  })

  const op: Operation = {
    id: 'blockhash',
    asset: { type: 'native' },
    tx: {
      hash: 'opHash',
      fees: BigInt(100),
      block: {
        hash: 'blockHash',
        height: 123456,
        time: new Date(),
      },
      date: new Date(),
      failed: false,
    },
    type: 'transaction',
    value: BigInt(1000),
    senders: ['tz1Sender'],
    recipients: ['tz1Recipient'],
  }

  it('only does 1 iteration', async () => {
    logicGetTransactions.mockResolvedValue([[op], '888'])
    const { items: operations, next: token } = await api.listOperations(context, 'addr', {
      minHeight: 100,
      order: 'asc',
    })
    expect(logicGetTransactions).toHaveBeenCalledTimes(1)
    expect(operations.length).toBe(1)
    expect(token).toEqual('888')
  })
})

describe('craftTransaction', () => {
  beforeEach(() => jest.clearAllMocks())

  describe('when suggested fee is above or below minFees floor', () => {
    it('craft transaction when default estimation is greater than minFees', async () => {
      const estimatedFees = 500n
      logicEstimateFees.mockResolvedValue({
        estimatedFees,
        gasLimit: DEFAULT_GAS_LIMIT,
        storageLimit: DEFAULT_STORAGE_LIMIT,
        parameters: {
          gasLimit: DEFAULT_GAS_LIMIT,
          storageLimit: DEFAULT_STORAGE_LIMIT,
          txFee: 500n,
        },
      })
      await api.craftTransaction(context, {
        intentType: 'transaction',
        type: 'send',
        sender: 'tz1test',
        recipient: 'tz1recipient',
        amount: 1000n,
      } as TransactionIntent)
      expect(logicEstimateFees).toHaveBeenCalledTimes(1)
      expect(logicCraftTransactionMock).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({ address: 'tz1test' }),
        expect.objectContaining({
          fee: expect.objectContaining({
            fees: estimatedFees.toString(),
            gasLimit: DEFAULT_GAS_LIMIT.toString(),
            storageLimit: DEFAULT_STORAGE_LIMIT.toString(),
          }),
        })
      )
    })

    it('craft transaction when default estimation is lesser than minFees', async () => {
      const estimatedFees = 50n
      logicEstimateFees.mockResolvedValue({
        estimatedFees,
        gasLimit: DEFAULT_GAS_LIMIT,
        storageLimit: DEFAULT_STORAGE_LIMIT,
        parameters: {
          gasLimit: DEFAULT_GAS_LIMIT,
          storageLimit: DEFAULT_STORAGE_LIMIT,
          txFee: 50n,
        },
      })
      await api.craftTransaction(context, {
        intentType: 'transaction',
        type: 'send',
        sender: 'tz1test',
        recipient: 'tz1recipient',
        amount: 1000n,
      } as TransactionIntent)
      expect(logicCraftTransactionMock).toHaveBeenCalledWith(
        expect.anything(),
        expect.any(Object),
        expect.objectContaining({
          fee: expect.objectContaining({ fees: estimatedFees.toString() }),
        })
      )
    })
  })

  describe('with customFee', () => {
    it('craft transaction with customFee when default estimation is greater than customFee', async () => {
      const defaultEstimatedFees = 500n
      const customFee = 100n
      logicEstimateFees.mockResolvedValue({
        estimatedFees: defaultEstimatedFees,
        gasLimit: DEFAULT_GAS_LIMIT,
        storageLimit: DEFAULT_STORAGE_LIMIT,
        parameters: {
          gasLimit: DEFAULT_GAS_LIMIT,
          storageLimit: DEFAULT_STORAGE_LIMIT,
        },
      })
      await api.craftTransaction(
        context,
        {
          intentType: 'transaction',
          type: 'send',
          sender: 'tz1test',
          recipient: 'tz1recipient',
          amount: 1000n,
        } as TransactionIntent,
        { customFees: { value: customFee } }
      )
      expect(logicCraftTransactionMock).toHaveBeenCalledWith(
        expect.anything(),
        expect.any(Object),
        expect.objectContaining({
          fee: expect.objectContaining({ fees: customFee.toString() }),
        })
      )
    })

    it('craft transaction with customFee when default estimation is lesser than customFee', async () => {
      const customFee = 500n
      logicEstimateFees.mockResolvedValue({
        estimatedFees: 100n,
        gasLimit: DEFAULT_GAS_LIMIT,
        storageLimit: DEFAULT_STORAGE_LIMIT,
        parameters: {
          gasLimit: DEFAULT_GAS_LIMIT,
          storageLimit: DEFAULT_STORAGE_LIMIT,
        },
      })
      await api.craftTransaction(
        context,
        {
          intentType: 'transaction',
          type: 'send',
          sender: 'tz1test',
          recipient: 'tz1recipient',
          amount: 1000n,
        } as TransactionIntent,
        { customFees: { value: customFee } }
      )
      expect(logicCraftTransactionMock).toHaveBeenCalledWith(
        expect.anything(),
        expect.any(Object),
        expect.objectContaining({
          fee: expect.objectContaining({ fees: customFee.toString() }),
        })
      )
    })

    it('craft transaction with customFee splits total for unrevealed delegate so total equals customFee', async () => {
      const minFees = 1000
      const unrevealedSender = 'tz2TaTpo31sAiX2HBJUTLLdUnqVJR4QjLy1V'
      const delegateRecipient = 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D'
      const totalCustomFee = BigInt(minFees * 2)

      const contextMinFees1000 = makeContext({
        baker: { url: 'https://baker.example.com' },
        explorer: { url: 'foo', maxTxQuery: 1 },
        node: { url: 'bar' },
        fees: {
          minGasLimit: 600,
          minRevealGasLimit: 300,
          minStorageLimit: 0,
          minFees,
          minEstimatedFees: minFees,
        },
      })

      const unrevealedAccount = {
        type: 'user' as const,
        balance: 1000000,
        revealed: false,
        address: unrevealedSender,
        publicKey: 'sppktest',
        counter: 0,
        delegationLevel: 0,
        delegationTime: '2021-01-01T00:00:00Z',
        numTransactions: 0,
        firstActivityTime: '2021-01-01T00:00:00Z',
      } as APIAccount
      mockGetAccountByAddress
        .mockResolvedValueOnce(unrevealedAccount)
        .mockResolvedValueOnce(unrevealedAccount)

      logicEstimateFees.mockResolvedValue({
        estimatedFees: totalCustomFee,
        fees: 1000n,
        gasLimit: 10000n,
        storageLimit: 0n,
        parameters: {
          gasLimit: 10000n,
          storageLimit: 0n,
          txFee: 1000n,
        },
      })

      await api.craftTransaction(
        contextMinFees1000,
        {
          intentType: 'staking',
          type: 'delegate',
          sender: unrevealedSender,
          senderPublicKey: '021bab48f41fc555e0fcf322a28e31b56f4369242f65324758ec8bbae3e84109a5',
          recipient: delegateRecipient,
          amount: 0n,
        } as TransactionIntent,
        { value: totalCustomFee }
      )

      expect(logicCraftTransactionMock).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({ address: unrevealedSender }),
        expect.objectContaining({
          fee: expect.objectContaining({
            fees: String(minFees),
            gasLimit: '10000',
            storageLimit: '0',
          }),
        })
      )
    })
  })
  it.each(['stake', 'unstake'] as const)(
    'passes %s mode and explicit amount through to craftTransaction',
    async (type) => {
      logicEstimateFees.mockResolvedValue({
        estimatedFees: DEFAULT_ESTIMATED_FEES,
        fees: DEFAULT_ESTIMATED_FEES,
        gasLimit: DEFAULT_GAS_LIMIT,
        storageLimit: DEFAULT_STORAGE_LIMIT,
      })

      await api.craftTransaction(context, {
        intentType: 'staking',
        type,
        sender: 'tz1test',
        recipient: '',
        amount: 1234n,
      } as TransactionIntent)

      expect(logicCraftTransactionMock).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({ address: 'tz1test' }),
        expect.objectContaining({
          type,
          amount: 1234n,
        })
      )
    }
  )

  it('passes finalize_unstake mode with zero amount through to craftTransaction', async () => {
    logicEstimateFees.mockResolvedValue({
      estimatedFees: DEFAULT_ESTIMATED_FEES,
      fees: DEFAULT_ESTIMATED_FEES,
      gasLimit: DEFAULT_GAS_LIMIT,
      storageLimit: DEFAULT_STORAGE_LIMIT,
    })

    await api.craftTransaction(context, {
      intentType: 'staking',
      type: 'finalize_unstake',
      sender: 'tz1test',
      recipient: '',
      amount: 999n,
    } as TransactionIntent)

    expect(logicCraftTransactionMock).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({ address: 'tz1test' }),
      expect.objectContaining({
        type: 'finalize_unstake',
        amount: 0n,
      })
    )
  })
})

describe('estimateFees', () => {
  beforeEach(() => jest.clearAllMocks())

  describe('when suggested fee is above or below minFees floor', () => {
    it('estimate fees when default estimation is greater than minFees', async () => {
      const estimatedFees = 500n
      logicEstimateFees.mockResolvedValue({
        estimatedFees,
        gasLimit: DEFAULT_GAS_LIMIT,
        storageLimit: DEFAULT_STORAGE_LIMIT,
      })
      const result = await api.estimateFees(context, {
        intentType: 'transaction',
        type: 'send',
        sender: 'tz1test',
        recipient: 'tz1recipient',
        amount: 1000n,
      } as TransactionIntent)
      expect(result.value).toBe(estimatedFees)
      expect(result.parameters?.gasLimit).toBe(DEFAULT_GAS_LIMIT)
      expect(result.parameters?.storageLimit).toBe(DEFAULT_STORAGE_LIMIT)
    })

    it('estimate fees when default estimation is lesser than minFees', async () => {
      const estimatedFees = 100n
      logicEstimateFees.mockResolvedValue({
        estimatedFees,
        gasLimit: DEFAULT_GAS_LIMIT,
        storageLimit: DEFAULT_STORAGE_LIMIT,
      })
      const result = await api.estimateFees(context, {
        intentType: 'transaction',
        type: 'send',
        sender: 'tz1test',
        recipient: 'tz1recipient',
        amount: 1000n,
      } as TransactionIntent)
      expect(result.value).toBe(estimatedFees)
      expect(result.parameters?.gasLimit).toBe(DEFAULT_GAS_LIMIT)
      expect(result.parameters?.storageLimit).toBe(DEFAULT_STORAGE_LIMIT)
    })
  })

  it('returns estimation from logic module', async () => {
    logicEstimateFees.mockResolvedValue({
      estimatedFees: DEFAULT_ESTIMATED_FEES,
      gasLimit: DEFAULT_GAS_LIMIT,
      storageLimit: DEFAULT_STORAGE_LIMIT,
    })
    const result = await api.estimateFees(context, {
      intentType: 'transaction',
      type: 'send',
      sender: 'tz1test',
      recipient: 'tz1recipient',
      amount: 1000n,
    } as TransactionIntent)
    expect(result).toEqual({
      value: DEFAULT_ESTIMATED_FEES,
      parameters: {
        gasLimit: DEFAULT_GAS_LIMIT,
        storageLimit: DEFAULT_STORAGE_LIMIT,
      },
    })
  })

  it('forwards stakedBalance and unstakedBalance to the logic estimate for send-max', async () => {
    mockGetAccountByAddress.mockResolvedValueOnce({
      type: 'user',
      balance: 1000,
      revealed: true,
      address: 'tz1test',
      publicKey: 'edpktest',
      counter: 0,
      delegationLevel: 0,
      delegationTime: '2021-01-01T00:00:00Z',
      numTransactions: 0,
      firstActivityTime: '2021-01-01T00:00:00Z',
      stakedBalance: 200,
      unstakedBalance: 300,
    })
    logicEstimateFees.mockResolvedValue({
      estimatedFees: DEFAULT_ESTIMATED_FEES,
      gasLimit: DEFAULT_GAS_LIMIT,
      storageLimit: DEFAULT_STORAGE_LIMIT,
      amount: 500n,
    })

    await api.estimateFees(context, {
      intentType: 'transaction',
      type: 'send',
      sender: 'tz1test',
      recipient: 'tz1recipient',
      amount: 0n,
      useAllAmount: true,
    } as TransactionIntent)

    expect(logicEstimateFees).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        account: expect.objectContaining({ stakedBalance: 200n, unstakedBalance: 300n }),
      })
    )
  })

  it('should throw taquito errors', async () => {
    logicEstimateFees.mockResolvedValue({
      estimatedFees: DEFAULT_ESTIMATED_FEES,
      gasLimit: DEFAULT_GAS_LIMIT,
      storageLimit: DEFAULT_STORAGE_LIMIT,
      taquitoError: 'test',
    })
    await expect(
      api.estimateFees(context, {
        intentType: 'transaction',
        type: 'send',
        sender: 'tz1test',
        recipient: 'tz1recipient',
        amount: 1000n,
      } as TransactionIntent)
    ).rejects.toThrow('Fees estimation failed: test')
  })

  it('should not throw for FA2 script_rejected errors (returns estimation)', async () => {
    logicEstimateFees.mockResolvedValue({
      estimatedFees: DEFAULT_ESTIMATED_FEES,
      gasLimit: DEFAULT_GAS_LIMIT,
      storageLimit: DEFAULT_STORAGE_LIMIT,
      taquitoError: 'proto.024-PtTALLiN.michelson_v1.script_rejected',
    })
    const result = await api.estimateFees(context, {
      intentType: 'transaction',
      type: 'send',
      sender: 'tz1test',
      recipient: 'tz1recipient',
      amount: 1000n,
      asset: { type: 'token', assetReference: 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU:0' },
    } as TransactionIntent)
    expect(result.value).toBe(DEFAULT_ESTIMATED_FEES)
    expect(result.parameters?.gasLimit).toBe(DEFAULT_GAS_LIMIT)
    expect(result.parameters?.storageLimit).toBe(DEFAULT_STORAGE_LIMIT)
  })

  it('should not throw for delegate.unchanged errors', async () => {
    logicEstimateFees.mockResolvedValue({
      estimatedFees: DEFAULT_ESTIMATED_FEES,
      gasLimit: DEFAULT_GAS_LIMIT,
      storageLimit: DEFAULT_STORAGE_LIMIT,
      taquitoError: 'proto.022-PsRiotum.delegate.unchanged',
    })
    const result = await api.estimateFees(context, {
      intentType: 'staking',
      type: 'delegate',
      sender: 'tz1test',
      recipient: 'tz1validator',
      amount: 0n,
    } as TransactionIntent)

    expect(result).toEqual({
      value: DEFAULT_ESTIMATED_FEES,
      parameters: {
        gasLimit: DEFAULT_GAS_LIMIT,
        storageLimit: DEFAULT_STORAGE_LIMIT,
      },
    })
  })

  it('should not throw for cannot_stake_with_unfinalizable_unstake errors', async () => {
    logicEstimateFees.mockResolvedValue({
      estimatedFees: DEFAULT_ESTIMATED_FEES,
      gasLimit: DEFAULT_GAS_LIMIT,
      storageLimit: DEFAULT_STORAGE_LIMIT,
      taquitoError:
        'proto.alpha.cannot_stake_with_unfinalizable_unstake_requests_to_another_delegate',
    })
    const result = await api.estimateFees(context, {
      intentType: 'staking',
      type: 'stake',
      sender: 'tz1test',
      recipient: 'tz1validator',
      amount: 1000n,
    } as TransactionIntent)

    expect(result).toEqual({
      value: DEFAULT_ESTIMATED_FEES,
      parameters: {
        gasLimit: DEFAULT_GAS_LIMIT,
        storageLimit: DEFAULT_STORAGE_LIMIT,
      },
    })
  })

  it('returns total estimation for unrevealed delegate when minFees applied (composite)', async () => {
    const unrevealedSender = 'tz2TaTpo31sAiX2HBJUTLLdUnqVJR4QjLy1V'
    mockGetAccountByAddress.mockResolvedValueOnce({
      type: 'user',
      balance: 1000000,
      revealed: false,
      address: unrevealedSender,
      publicKey: undefined,
      counter: 0,
      delegationLevel: 0,
      delegationTime: '2021-01-01T00:00:00Z',
      numTransactions: 0,
      firstActivityTime: '2021-01-01T00:00:00Z',
    } as APIAccount)

    const expectedTotalFees = 2000n
    logicEstimateFees.mockResolvedValue({
      estimatedFees: expectedTotalFees,
      fees: 1000n,
      gasLimit: DEFAULT_GAS_LIMIT,
      storageLimit: DEFAULT_STORAGE_LIMIT,
    })

    const result = await api.estimateFees(context, {
      intentType: 'staking',
      type: 'delegate',
      sender: unrevealedSender,
      recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
      amount: 0n,
    } as TransactionIntent)

    expect(result.value).toBe(expectedTotalFees)
    expect(result.parameters.gasLimit).toBe(DEFAULT_GAS_LIMIT)
    expect(result.parameters.storageLimit).toBe(DEFAULT_STORAGE_LIMIT)
  })

  it.each([
    ['stake', 1234n, 1234n],
    ['unstake', 1234n, 1234n],
    ['finalize_unstake', 999n, 0n],
  ] as const)('delegates %s estimation to the logic layer', async (type, intentAmount, amount) => {
    logicEstimateFees.mockResolvedValue({
      estimatedFees: DEFAULT_ESTIMATED_FEES,
      fees: DEFAULT_ESTIMATED_FEES,
      gasLimit: DEFAULT_GAS_LIMIT,
      storageLimit: DEFAULT_STORAGE_LIMIT,
    })

    const result = await api.estimateFees(context, {
      intentType: 'staking',
      type,
      sender: 'tz1test',
      recipient: '',
      amount: intentAmount,
    } as TransactionIntent)

    expect(result.value).toBe(DEFAULT_ESTIMATED_FEES)
    expect(logicEstimateFees).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        transaction: expect.objectContaining({
          mode: type,
          amount,
        }),
      })
    )
  })

  it('fallback when Taquito throws Public key not found returns total with minFees for reveal (unrevealed)', async () => {
    const unrevealedSender = 'tz2TaTpo31sAiX2HBJUTLLdUnqVJR4QjLy1V'
    const delegateRecipient = 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D'
    const minFees = 1000

    const contextMinFees1000 = makeContext({
      baker: { url: 'https://baker.example.com' },
      explorer: { url: 'foo', maxTxQuery: 1 },
      node: { url: 'bar' },
      fees: {
        minGasLimit: 600,
        minRevealGasLimit: 300,
        minStorageLimit: 0,
        minFees,
        minEstimatedFees: minFees,
      },
    })

    logicEstimateFees.mockImplementation(() => Promise.reject(new Error('Public key not found')))

    const defaultUserForTry = {
      type: 'user' as const,
      balance: 1000,
      revealed: true,
      address: unrevealedSender,
      publicKey: 'edpktest',
      counter: 0,
      delegationLevel: 0,
      delegationTime: '2021-01-01T00:00:00Z',
      numTransactions: 0,
      firstActivityTime: '2021-01-01T00:00:00Z',
    } as APIAccount
    const recipientAccount = {
      type: 'user' as const,
      balance: 1000000,
      address: delegateRecipient,
      counter: 0,
      delegationLevel: 0,
      delegationTime: '2021-01-01T00:00:00Z',
      numTransactions: 0,
      firstActivityTime: '2021-01-01T00:00:00Z',
    } as APIAccount
    const senderUnrevealedAccount = {
      type: 'user' as const,
      balance: 1000000,
      revealed: false,
      address: unrevealedSender,
      publicKey: undefined,
      counter: 0,
      delegationLevel: 0,
      delegationTime: '2021-01-01T00:00:00Z',
      numTransactions: 0,
      firstActivityTime: '2021-01-01T00:00:00Z',
    } as APIAccount
    mockGetAccountByAddress
      .mockImplementationOnce(() => Promise.resolve(defaultUserForTry))
      .mockImplementationOnce(() => Promise.resolve(recipientAccount))
      .mockImplementationOnce(() => Promise.resolve(senderUnrevealedAccount))

    mockGetTezosToolkit.mockReturnValueOnce({
      estimate: {
        transfer: jest.fn().mockResolvedValue({
          suggestedFeeMutez: 100,
          gasLimit: 10000,
          storageLimit: 0,
          burnFeeMutez: 0,
          opSize: 100,
        }),
      },
    })

    const result = await api.estimateFees(contextMinFees1000, {
      intentType: 'staking',
      type: 'delegate',
      sender: unrevealedSender,
      recipient: delegateRecipient,
      amount: 0n,
    } as TransactionIntent)

    expect(result.value).toBe(2000n)
    expect(result.parameters?.txFee).toBe(1000n)
    expect(result.parameters?.gasLimit).toBe(10000n)
    expect(result.parameters?.storageLimit).toBe(0n)

    logicEstimateFees.mockReset()
  })

  it('uses static fallback gas when both logicEstimate and toolkit.estimate.transfer throw (revealed sender)', async () => {
    // SAFE_FALLBACK_GAS = 2500, FALLBACK_OP_SIZE_BYTES = 154
    // baseTxFee = max(minFees=1, ceil(120 + 0.1*2500 + 154)) = max(1, 524) = 524
    // createApi here to reset the global coinConfig to minFees=1 (other tests may leave it at a higher value)
    const contextLowMinFees = makeContext({
      baker: { url: 'https://baker.example.com' },
      explorer: { url: 'foo', maxTxQuery: 1 },
      node: { url: 'bar' },
      fees: {
        minGasLimit: 1,
        minRevealGasLimit: 1,
        minStorageLimit: 1,
        minFees: 1,
        minEstimatedFees: 2,
      },
    })
    const revealedSender = 'tz1test'
    const recipient = 'tz2recipient'

    const revealedAccount = {
      type: 'user' as const,
      balance: 1000000,
      revealed: true,
      address: revealedSender,
      publicKey: 'edpktest',
      counter: 0,
      delegationLevel: 0,
      delegationTime: '2021-01-01T00:00:00Z',
      numTransactions: 0,
      firstActivityTime: '2021-01-01T00:00:00Z',
    } as APIAccount
    const recipientAccount = {
      type: 'user' as const,
      balance: 1000000,
      revealed: true,
      address: recipient,
      publicKey: 'edpkrecipient',
      counter: 0,
      delegationLevel: 0,
      delegationTime: '2021-01-01T00:00:00Z',
      numTransactions: 0,
      firstActivityTime: '2021-01-01T00:00:00Z',
    } as APIAccount

    mockGetAccountByAddress
      .mockResolvedValueOnce(revealedAccount) // initial sender lookup
      .mockResolvedValueOnce(recipientAccount) // recipient lookup in catch block
      .mockResolvedValueOnce(revealedAccount) // sender lookup for needsReveal in catch block

    logicEstimateFees.mockRejectedValue(new Error('Public key not found'))

    mockGetTezosToolkit.mockReturnValueOnce({
      estimate: {
        transfer: jest.fn().mockRejectedValue(new Error('Public key not found')),
      },
    })

    const result = await api.estimateFees(contextLowMinFees, {
      intentType: 'transaction',
      type: 'send',
      sender: revealedSender,
      recipient,
      amount: 1000n,
    } as TransactionIntent)

    // No reveal needed → revealFee = 0 → total = baseTxFee = 524
    expect(result.value).toBe(524n)
    expect(result.parameters?.txFee).toBe(524n)
    expect(result.parameters?.gasLimit).toBe(2500n)
    expect(result.parameters?.storageLimit).toBe(0n)

    logicEstimateFees.mockReset()
  })

  it('uses static fallback gas and adds reveal fee when sender is unrevealed', async () => {
    // baseTxFee = max(minFees=1, ceil(120 + 0.1*2500 + 154)) = 524
    // revealFee = max(0, getRevealFee(tz2address)) — tz2 addresses have a non-zero reveal fee
    // createApi here to reset the global coinConfig to minFees=1 (other tests may leave it at a higher value)
    const contextLowMinFees = makeContext({
      baker: { url: 'https://baker.example.com' },
      explorer: { url: 'foo', maxTxQuery: 1 },
      node: { url: 'bar' },
      fees: {
        minGasLimit: 1,
        minRevealGasLimit: 1,
        minStorageLimit: 1,
        minFees: 1,
        minEstimatedFees: 2,
      },
    })
    const unrevealedSender = 'tz2TaTpo31sAiX2HBJUTLLdUnqVJR4QjLy1V'
    const recipient = 'tz1recipient'

    const unrevealedAccount = {
      type: 'user' as const,
      balance: 1000000,
      revealed: false,
      address: unrevealedSender,
      publicKey: undefined,
      counter: 0,
      delegationLevel: 0,
      delegationTime: '2021-01-01T00:00:00Z',
      numTransactions: 0,
      firstActivityTime: '2021-01-01T00:00:00Z',
    } as APIAccount
    const recipientAccount = {
      type: 'user' as const,
      balance: 1000000,
      revealed: true,
      address: recipient,
      publicKey: 'edpkrecipient',
      counter: 0,
      delegationLevel: 0,
      delegationTime: '2021-01-01T00:00:00Z',
      numTransactions: 0,
      firstActivityTime: '2021-01-01T00:00:00Z',
    } as APIAccount

    mockGetAccountByAddress
      .mockResolvedValueOnce(unrevealedAccount) // initial sender lookup
      .mockResolvedValueOnce(recipientAccount) // recipient lookup in catch block
      .mockResolvedValueOnce(unrevealedAccount) // sender lookup for needsReveal in catch block

    logicEstimateFees.mockRejectedValue(new Error('Public key not found'))

    mockGetTezosToolkit.mockReturnValueOnce({
      estimate: {
        transfer: jest.fn().mockRejectedValue(new Error('Public key not found')),
      },
    })

    const result = await api.estimateFees(contextLowMinFees, {
      intentType: 'transaction',
      type: 'send',
      sender: unrevealedSender,
      recipient,
      amount: 1000n,
    } as TransactionIntent)

    // baseTxFee = 524n; revealFee > 0 (tz2 secp256k1 address)
    // total > 524n and txFee stays at 524n
    expect(result.parameters?.txFee).toBe(524n)
    expect(result.parameters?.gasLimit).toBe(2500n)
    expect(result.parameters?.storageLimit).toBe(0n)
    expect(result.value).toBeGreaterThan(524n)

    logicEstimateFees.mockReset()
  })

  it('uses minFees floor when it exceeds the static fallback calculation', async () => {
    // With minFees = 2000 > ceil(120 + 0.1*2500 + 154) = 524 → baseTxFee = 2000
    const minFees = 2000
    const contextHighMinFees = makeContext({
      baker: { url: 'https://baker.example.com' },
      explorer: { url: 'foo', maxTxQuery: 1 },
      node: { url: 'bar' },
      fees: {
        minGasLimit: 1,
        minRevealGasLimit: 1,
        minStorageLimit: 1,
        minFees,
        minEstimatedFees: minFees,
      },
    })

    const revealedAccount = {
      type: 'user' as const,
      balance: 1000000,
      revealed: true,
      address: 'tz1test',
      publicKey: 'edpktest',
      counter: 0,
      delegationLevel: 0,
      delegationTime: '2021-01-01T00:00:00Z',
      numTransactions: 0,
      firstActivityTime: '2021-01-01T00:00:00Z',
    } as APIAccount
    const recipientAccount = {
      type: 'user' as const,
      balance: 1000000,
      revealed: true,
      address: 'tz2recipient',
      publicKey: 'edpkrecipient',
      counter: 0,
      delegationLevel: 0,
      delegationTime: '2021-01-01T00:00:00Z',
      numTransactions: 0,
      firstActivityTime: '2021-01-01T00:00:00Z',
    } as APIAccount

    mockGetAccountByAddress
      .mockResolvedValueOnce(revealedAccount)
      .mockResolvedValueOnce(recipientAccount)
      .mockResolvedValueOnce(revealedAccount)

    logicEstimateFees.mockRejectedValue(new Error('Public key not found'))

    mockGetTezosToolkit.mockReturnValueOnce({
      estimate: {
        transfer: jest.fn().mockRejectedValue(new Error('Public key not found')),
      },
    })

    const result = await api.estimateFees(contextHighMinFees, {
      intentType: 'transaction',
      type: 'send',
      sender: 'tz1test',
      recipient: 'tz2recipient',
      amount: 1000n,
    } as TransactionIntent)

    expect(result.value).toBe(BigInt(minFees))
    expect(result.parameters?.txFee).toBe(BigInt(minFees))
    expect(result.parameters?.gasLimit).toBe(2500n)

    logicEstimateFees.mockReset()
  })
})

describe('getBalance', () => {
  it('should throw an exception when options is provided', async () => {
    await expect(
      api.getBalance(context, 'random address', {} as unknown as BalanceOptions)
    ).rejects.toMatchObject({ name: 'InvalidParameterError' })
  })
})

describe('getAccountInfo', () => {
  // `getAccountInfo` is optional on the generic CoinModuleApi (ADR-045 / LIVE-33431); coin-tezos
  // always implements it, so the non-null assertions below are safe.
  afterEach(() => {
    mockGetAccountByAddress.mockClear()
  })

  it('returns revealed:true for a revealed user account', async () => {
    mockGetAccountByAddress.mockResolvedValueOnce({
      type: 'user',
      balance: 1000,
      revealed: true,
      address: 'tz1test',
      publicKey: 'edpktest',
      counter: 0,
      delegationLevel: 0,
      delegationTime: '2021-01-01T00:00:00Z',
      numTransactions: 0,
      firstActivityTime: '2021-01-01T00:00:00Z',
    } as APIAccount)

    await expect(api.getAccountInfo!(context, 'tz1test')).resolves.toEqual({
      type: 'tezos',
      revealed: true,
    })
    expect(mockGetAccountByAddress).toHaveBeenCalledWith('tz1test')
  })

  it('returns revealed:false for an unrevealed user account', async () => {
    mockGetAccountByAddress.mockResolvedValueOnce({
      type: 'user',
      balance: 1000,
      revealed: false,
      address: 'tz2unrevealed',
      publicKey: '',
      counter: 0,
      delegationLevel: 0,
      delegationTime: '2021-01-01T00:00:00Z',
      numTransactions: 0,
      firstActivityTime: '2021-01-01T00:00:00Z',
    } as APIAccount)

    await expect(api.getAccountInfo!(context, 'tz2unrevealed')).resolves.toEqual({
      type: 'tezos',
      revealed: false,
    })
    expect(mockGetAccountByAddress).toHaveBeenCalledWith('tz2unrevealed')
  })

  it('returns revealed:true for a registered baker (delegate) account', async () => {
    // tzkt reports bakers with type "delegate" (not "user"), still carrying revealed:true.
    // Regression test for LIVE-34256: bakers must not be reported as unrevealed.
    mockGetAccountByAddress.mockResolvedValueOnce({
      type: 'delegate',
      balance: 616109,
      revealed: true,
      address: 'tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9',
      publicKey: 'p2pktest',
      counter: 18,
    } as APIAccount)

    await expect(
      api.getAccountInfo!(context, 'tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9')
    ).resolves.toEqual({
      type: 'tezos',
      revealed: true,
    })
    expect(mockGetAccountByAddress).toHaveBeenCalledWith('tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9')
  })

  it('returns revealed:false for an empty / non-existent account', async () => {
    mockGetAccountByAddress.mockResolvedValueOnce({
      type: 'empty',
      address: 'tz1empty',
      counter: 0,
    } as APIAccount)

    await expect(api.getAccountInfo!(context, 'tz1empty')).resolves.toEqual({
      type: 'tezos',
      revealed: false,
    })
    expect(mockGetAccountByAddress).toHaveBeenCalledWith('tz1empty')
  })

  it('propagates errors from getAccountByAddress', async () => {
    mockGetAccountByAddress.mockRejectedValueOnce(new Error('boom'))

    await expect(api.getAccountInfo!(context, 'tz1boom')).rejects.toThrow('boom')
  })
})
