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

import type { RawOperation } from '../types'
import type { Horizon } from '@stellar/stellar-sdk'
import BigNumber from 'bignumber.js'
import * as network from '../network'
import { parseAPIValue } from './common'
import { getBlock } from './getBlock'

jest.mock('../network', () => ({
  ...jest.requireActual<typeof import('../network')>('../network'),
  fetchLedgerRecord: jest.fn(),
  fetchAllLedgerOperations: jest.fn(),
}))

const fetchLedgerRecordMock = network.fetchLedgerRecord as jest.MockedFunction<
  typeof network.fetchLedgerRecord
>
const fetchAllLedgerOperationsMock = network.fetchAllLedgerOperations as jest.MockedFunction<
  typeof network.fetchAllLedgerOperations
>

function txRecord(
  overrides: Partial<Horizon.ServerApi.TransactionRecord> = {}
): Horizon.ServerApi.TransactionRecord {
  return {
    fee_charged: '100',
    source_account: 'GSOURCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
    fee_account: undefined,
    ledger_attr: 42,
    memo_type: 'none',
    ...overrides,
  } as Horizon.ServerApi.TransactionRecord
}

function rawOp(
  base: Record<string, unknown> & {
    type: string
    transaction_hash: string
    transactionRecord?: Horizon.ServerApi.TransactionRecord
  }
): RawOperation {
  const tr = base.transactionRecord ?? txRecord()
  const { transactionRecord: _t, ...rest } = base
  return {
    id: '1',
    paging_token: 'pt',
    source_account: 'GSOURCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
    transaction_successful: true,
    created_at: '2020-01-01T00:00:00Z',
    ...rest,
    transaction: () => Promise.resolve(tr),
  } as RawOperation
}

describe('getBlock', () => {
  beforeEach(() => {
    jest.clearAllMocks()
    fetchLedgerRecordMock.mockImplementation(
      async (seq: number) =>
        ({
          sequence: seq,
          hash: `LEDGER-HASH-${seq}`,
          closed_at: '2018-09-15T15:40:05Z',
          ...(seq > 1 && { prev_hash: `LEDGER-HASH-${seq - 1}` }),
        }) as Awaited<ReturnType<typeof network.fetchLedgerRecord>>
    )
  })

  describe('height validation', () => {
    it('rejects height 0', async () => {
      await expect(getBlock(0)).rejects.toThrow(
        'getBlock: height must be a positive integer, got 0'
      )
    })

    it('rejects negative height', async () => {
      await expect(getBlock(-1)).rejects.toThrow(
        'getBlock: height must be a positive integer, got -1'
      )
    })

    it('rejects non-integer height', async () => {
      await expect(getBlock(2.5)).rejects.toThrow(
        'getBlock: height must be a positive integer, got 2.5'
      )
    })

    it('rejects NaN', async () => {
      await expect(getBlock(Number.NaN)).rejects.toThrow(
        'getBlock: height must be a positive integer, got NaN'
      )
    })

    it('rejects Infinity', async () => {
      await expect(getBlock(Number.POSITIVE_INFINITY)).rejects.toThrow(
        'getBlock: height must be a positive integer, got Infinity'
      )
    })
  })

  it('maps a native payment with two transfer operations', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'payment',
        transaction_hash: 'txpay1',
        from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '1.0000000',
        asset_type: 'native',
        transactionRecord: txRecord({ fee_charged: '200' }),
      }),
    ])

    const block = await getBlock(20000000)

    expect(block.info.height).toBe(20000000)
    expect(block.info.hash).toBe('LEDGER-HASH-20000000')
    expect(block.info.parent).toEqual({
      height: 19999999,
      hash: 'LEDGER-HASH-19999999',
    })

    expect(block.transactions).toHaveLength(1)
    const tx0 = block.transactions[0]
    expect(tx0.hash).toBe('txpay1')
    expect(tx0.failed).toBe(false)
    expect(tx0.fees).toBe(200n)
    expect(tx0.feesPayer).toBe('GSOURCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
    expect(tx0.operations).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          type: 'transfer',
          address: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
          amount: -10000000n,
        }),
        expect.objectContaining({
          type: 'transfer',
          address: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
          amount: 10000000n,
        }),
      ])
    )
  })

  it('uses fee_account as feesPayer when present (fee bump)', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'payment',
        transaction_hash: 'txfeebump',
        from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '2.0000000',
        asset_type: 'native',
        transactionRecord: txRecord({
          fee_charged: '500',
          source_account: 'GINNERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
          fee_account: 'GFEEPAYERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        }),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions[0].feesPayer).toBe('GFEEPAYERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
  })

  it('marks failed transactions with empty operations but keeps fees', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'payment',
        transaction_hash: 'txfail',
        from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '1.0000000',
        asset_type: 'native',
        transaction_successful: false,
        transactionRecord: txRecord({ fee_charged: '400' }),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions[0]).toMatchObject({
      hash: 'txfail',
      failed: true,
      fees: 400n,
      operations: [],
    })
  })

  it('groups multiple allowlisted operations under the same transaction hash', async () => {
    const sharedTx = txRecord({ fee_charged: '300' })
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        id: '1',
        type: 'payment',
        transaction_hash: 'txmulti',
        from: 'GA',
        to: 'GB',
        amount: '1.0000000',
        asset_type: 'native',
        transactionRecord: sharedTx,
      }),
      rawOp({
        id: '2',
        type: 'payment',
        transaction_hash: 'txmulti',
        from: 'GB',
        to: 'GC',
        amount: '0.5000000',
        asset_type: 'native',
        transactionRecord: sharedTx,
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions).toHaveLength(1)
    expect(block.transactions[0].operations.length).toBeGreaterThanOrEqual(2)
  })

  it('maps token payments using assetReference and assetOwner', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'payment',
        transaction_hash: 'txtok',
        from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '10.0000000',
        asset_type: 'credit_alphanum4',
        asset_code: 'USD',
        asset_issuer: 'GISSUERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        transactionRecord: txRecord(),
      }),
    ])

    const block = await getBlock(10)
    const transfers = block.transactions[0].operations.filter((o) => o.type === 'transfer')
    expect(transfers[0]).toMatchObject({
      asset: {
        type: 'token',
        assetReference: 'USD',
        assetOwner: 'GISSUERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
      },
    })
  })

  it('maps create_account to two native transfers', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'create_account',
        transaction_hash: 'txcreate',
        funder: 'GFUNDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        account: 'GNEWACCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        starting_balance: '2.0000000',
        transactionRecord: txRecord({ fee_charged: '100' }),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions[0].operations).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          type: 'transfer',
          address: 'GFUNDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
          amount: -20000000n,
        }),
        expect.objectContaining({
          type: 'transfer',
          address: 'GNEWACCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
          amount: 20000000n,
        }),
      ])
    )
  })

  it('maps change_trust to other operations', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'change_trust',
        transaction_hash: 'txtrust',
        trustor: 'GTRUSTORAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        asset_code: 'ABC',
        asset_issuer: 'GISSUERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        limit: '1000.0000000',
        transactionRecord: txRecord({ source_account_sequence: '12345' }),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions[0].operations[0]).toMatchObject({
      type: 'other',
      ledgerOpType: 'OPT_IN',
      assetAmount: '100',
      memo: { type: 'NO_MEMO' },
      sequence: '12345',
    })
  })

  it('decodes MEMO_HASH from base64 to hex in change_trust details', async () => {
    const base64Hash = Buffer.from('abcdef1234567890', 'hex').toString('base64')
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'change_trust',
        transaction_hash: 'txhash',
        asset_code: 'USD',
        asset_issuer: 'GISSUERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        limit: '500.0000000',
        transactionRecord: txRecord({ memo_type: 'hash', memo: base64Hash }),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions[0].operations[0]).toMatchObject({
      memo: { type: 'MEMO_HASH', value: 'abcdef1234567890' },
    })
  })

  it('decodes MEMO_RETURN from base64 to hex in change_trust details', async () => {
    const base64Hash = Buffer.from('deadbeef', 'hex').toString('base64')
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'change_trust',
        transaction_hash: 'txreturn',
        asset_code: 'EUR',
        asset_issuer: 'GISSUERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        limit: '100.0000000',
        transactionRecord: txRecord({ memo_type: 'return', memo: base64Hash }),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions[0].operations[0]).toMatchObject({
      memo: { type: 'MEMO_RETURN', value: 'deadbeef' },
    })
  })

  it('keeps successful transactions with only unsupported operation types for fee tracking', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'manage_sell_offer',
        transaction_hash: 'txoffer',
        transactionRecord: txRecord(),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions).toHaveLength(1)
    expect(block.transactions[0].operations).toHaveLength(0)
    expect(block.transactions[0].fees).toBe(100n)
  })

  it('maps path_payment_strict_send with distinct source and destination assets', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'path_payment_strict_send',
        transaction_hash: 'txpathsend',
        from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '0.0033586',
        asset_type: 'credit_alphanum12',
        asset_code: 'Shitcoinz',
        asset_issuer: 'GBVYKIM4QGW7NK3Q5S76GWMZMLAWYB6FB43JQ4J7ZOXHAA3LEZZGYHSD',
        source_amount: '0.0130225',
        source_asset_type: 'native',
        transactionRecord: txRecord(),
      }),
    ])

    const block = await getBlock(10)
    const ops = block.transactions[0].operations
    expect(ops).toHaveLength(2)
    expect(ops[0]).toMatchObject({ type: 'transfer', asset: { type: 'native' } })
    expect(typeof ops[0].amount).toBe('bigint')
    expect(ops[1]).toMatchObject({
      type: 'transfer',
      asset: { type: 'token', assetReference: 'Shitcoinz' },
    })
    expect(typeof ops[1].amount).toBe('bigint')
  })

  it('path_payment_strict_receive debits source_amount when both send_max and source_amount are present', async () => {
    const sourceAmountStr = '0.0100000'
    const sendMaxStr = '5.0000000'
    const expectedDebit = BigInt(
      parseAPIValue(sourceAmountStr).integerValue(BigNumber.ROUND_FLOOR).toFixed(0)
    )

    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'path_payment_strict_receive',
        transaction_hash: 'txpathrecv',
        from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '0.0033586',
        asset_type: 'credit_alphanum12',
        asset_code: 'Shitcoinz',
        asset_issuer: 'GBVYKIM4QGW7NK3Q5S76GWMZMLAWYB6FB43JQ4J7ZOXHAA3LEZZGYHSD',
        send_max: sendMaxStr,
        source_amount: sourceAmountStr,
        source_asset_type: 'native',
        transactionRecord: txRecord(),
      }),
    ])

    const block = await getBlock(10)
    const ops = block.transactions[0].operations
    expect(ops).toHaveLength(2)
    expect(ops[0]).toMatchObject({
      type: 'transfer',
      address: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
      asset: { type: 'native' },
      amount: -expectedDebit,
    })
    const wrongDebit = BigInt(
      parseAPIValue(sendMaxStr).integerValue(BigNumber.ROUND_FLOOR).toFixed(0)
    )
    expect(ops[0].amount).not.toBe(-wrongDebit)
  })

  it('omits parent in block info for ledger height 1', async () => {
    fetchLedgerRecordMock.mockImplementation(
      async (seq: number) =>
        ({
          sequence: seq,
          hash: `LEDGER-HASH-${seq}`,
          closed_at: '2018-09-15T15:40:05Z',
        }) as Awaited<ReturnType<typeof network.fetchLedgerRecord>>
    )
    fetchAllLedgerOperationsMock.mockResolvedValue([])

    const block = await getBlock(1)
    expect(block.info.height).toBe(1)
    expect(block.info.parent).toBeUndefined()
  })

  it('omits feesPayer when fee_account and source_account are absent', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'payment',
        transaction_hash: 'txnopayer',
        from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '1.0000000',
        asset_type: 'native',
        transactionRecord: txRecord({
          fee_charged: '100',
          source_account: '',
        }),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions[0]).not.toHaveProperty('feesPayer')
  })

  it('maps create_account with zero starting balance to empty operations but keeps tx for fees', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'create_account',
        transaction_hash: 'txcreatezero',
        funder: 'GFUNDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        account: 'GNEWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        starting_balance: '0.0000000',
        transactionRecord: txRecord(),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions).toHaveLength(1)
    expect(block.transactions[0].operations).toHaveLength(0)
    expect(block.transactions[0].fees).toBe(100n)
  })

  it('maps payment using to_muxed when to is absent', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'payment',
        transaction_hash: 'txmuxed',
        from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        to: undefined,
        to_muxed: 'GTO_MUXEDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '0.5000000',
        asset_type: 'native',
        transactionRecord: txRecord(),
      }),
    ])

    const block = await getBlock(10)
    const transfers = block.transactions[0].operations.filter((o) => o.type === 'transfer')
    expect(
      transfers.some(
        (o) => 'peer' in o && o.peer === 'GTO_MUXEDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
      )
    ).toBe(true)
  })

  it('maps change_trust with zero limit as OPT_OUT', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'change_trust',
        transaction_hash: 'txoptout',
        trustor: 'GTRUSTORAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        asset_code: 'ABC',
        asset_issuer: 'GISSUERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        limit: '0.0000000',
        transactionRecord: txRecord(),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions[0].operations[0]).toMatchObject({
      type: 'other',
      ledgerOpType: 'OPT_OUT',
    })
  })

  it('path_payment_strict_send with zero amounts keeps tx for fees', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'path_payment_strict_send',
        transaction_hash: 'txpathzero',
        from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '0.0000000',
        asset_type: 'native',
        source_amount: '0.0000000',
        source_asset_type: 'native',
        transactionRecord: txRecord(),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions).toHaveLength(1)
    expect(block.transactions[0].operations).toHaveLength(0)
    expect(block.transactions[0].fees).toBe(100n)
  })

  it('path_payment_strict_receive uses send_max when source_amount is absent', async () => {
    const sendMaxStr = '0.0200000'
    const expected = BigInt(
      parseAPIValue(sendMaxStr).integerValue(BigNumber.ROUND_FLOOR).toFixed(0)
    )
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'path_payment_strict_receive',
        transaction_hash: 'txrecvmax',
        from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '0.0010000',
        asset_type: 'native',
        send_max: sendMaxStr,
        source_asset_type: 'native',
        transactionRecord: txRecord(),
      }),
    ])

    const block = await getBlock(10)
    const sender = block.transactions[0].operations.find(
      (o) =>
        o.type === 'transfer' && o.address === 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    )
    expect(sender?.amount).toBe(-expected)
  })

  it('path_payment_strict_receive with no source_amount nor send_max only maps destination leg', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'path_payment_strict_receive',
        transaction_hash: 'txrecvnodebit',
        from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '0.5000000',
        asset_type: 'native',
        source_asset_type: 'native',
        transactionRecord: txRecord(),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions[0].operations).toHaveLength(1)
    expect(block.transactions[0].operations[0]).toMatchObject({
      type: 'transfer',
      address: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
      asset: { type: 'native' },
      amount: 5000000n,
    })
  })

  it('path_payment_strict_send with zero source_amount maps only destination leg', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'path_payment_strict_send',
        transaction_hash: 'txpathsenddestonly',
        from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '0.2500000',
        asset_type: 'native',
        source_amount: '0.0000000',
        source_asset_type: 'native',
        transactionRecord: txRecord(),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions[0].operations).toHaveLength(1)
    expect(block.transactions[0].operations[0]).toMatchObject({
      type: 'transfer',
      address: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
      amount: 2500000n,
    })
  })

  it('path_payment_strict_send with zero amount maps only source leg', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'path_payment_strict_send',
        transaction_hash: 'txpathsendsourceonly',
        from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '0.0000000',
        asset_type: 'native',
        source_amount: '1.0000000',
        source_asset_type: 'native',
        transactionRecord: txRecord(),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions[0].operations).toHaveLength(1)
    expect(block.transactions[0].operations[0]).toMatchObject({
      type: 'transfer',
      address: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
      amount: -10000000n,
    })
  })

  it('maps payment with only to address as single incoming transfer', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'payment',
        transaction_hash: 'txtoonly',
        to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '3.0000000',
        asset_type: 'native',
        transactionRecord: txRecord(),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions[0].operations).toEqual([
      expect.objectContaining({
        type: 'transfer',
        address: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: 30000000n,
      }),
    ])
  })

  it('maps payment with only from address as single outgoing transfer', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'payment',
        transaction_hash: 'txfromonly',
        from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '0.2500000',
        asset_type: 'native',
        transactionRecord: txRecord(),
      }),
    ])

    const block = await getBlock(10)
    expect(block.transactions[0].operations).toEqual([
      expect.objectContaining({
        type: 'transfer',
        address: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: -2500000n,
      }),
    ])
  })

  it('treats payment with missing asset_type as native when mapping asset', async () => {
    fetchAllLedgerOperationsMock.mockResolvedValue([
      rawOp({
        type: 'payment',
        transaction_hash: 'txnoassettype',
        from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        amount: '1.0000000',
        asset_type: undefined,
        asset_code: 'USD',
        asset_issuer: 'GISSUERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        transactionRecord: txRecord(),
      }),
    ])

    const block = await getBlock(10)
    const transfers = block.transactions[0].operations.filter((o) => o.type === 'transfer')
    expect(transfers.every((t) => t.type === 'transfer' && t.asset.type === 'native')).toBe(true)
  })
})
