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

import type { TezosCoinConfig, TezosContext } from '../config'
import type { APITokenBalance } from '../network/types'
import { getBalance } from './getBalance'

const mockedGetAccountByAddress = jest.fn()
const mockedGetTokensBalances = jest.fn()
const mockedGetUnstakeRequests = jest.fn()

jest.mock('../network/tzkt', () => {
  const originalModule = jest.requireActual('../network/tzkt')
  return {
    ...originalModule,
    createTzktApi: () => {
      return {
        ...originalModule.api,
        getAccountByAddress: mockedGetAccountByAddress,
        getTokensBalances: mockedGetTokensBalances,
        getUnstakeRequests: mockedGetUnstakeRequests,
      }
    },
  }
})

const mockTokensBalancesWithAllInfo: APITokenBalance[] = [
  {
    id: 2398642347442177,
    account: { address: 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oSM8' },
    token: {
      id: 113484097388545,
      contract: { alias: 'BTCtz', address: 'KT1T87QbpXEVgkwsNPzz8iRoah3SS3D1MDmh' },
      tokenId: '0',
      standard: 'fa2',
      metadata: {
        name: 'BTCtez',
        symbol: 'BTCtz',
        decimals: '8',
      },
    },
    balance: '2000',
    transfersCount: 2,
    firstLevel: 11909116,
    firstTime: '2026-02-12T14:16:16Z',
    lastLevel: 11969607,
    lastTime: '2026-02-16T20:05:40Z',
  },
]

const mockTokensBalancesWithMissingInfo: APITokenBalance[] = [
  {
    id: 2398642347442177,
    account: { address: 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oMMM' },
    token: {
      id: 113484097388545,
      contract: { alias: 'BTCtz', address: 'KT1T87QbpXEVgkwsNPzz8iRoah3SS3D1MDmh' },
      tokenId: '0',
      standard: 'fa2',
    },
    balance: '2000',
    transfersCount: 2,
    firstLevel: 11909116,
    firstTime: '2026-02-12T14:16:16Z',
    lastLevel: 11969607,
    lastTime: '2026-02-16T20:05:40Z',
  },
]

describe('getBalance', () => {
  beforeEach(() => {
    jest.resetAllMocks()
  })

  const context: TezosContext = {
    config: async () =>
      ({
        status: { type: 'active' },
        explorer: { url: 'http://tezos.explorer.com' },
      }) as unknown as TezosCoinConfig,
    logger: () => undefined,
  }
  it('gets the balance of a Tezos account', async () => {
    mockedGetAccountByAddress.mockImplementation((address: string) => {
      if (address === 'tz1WvvbEGpBXGeTVbLiR6DYBe1izmgiYuZbq') {
        return { type: 'empty' }
      } else if (address === 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oSM8') {
        return { type: 'user', balance: 25 }
      } else if (address === 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oMMM') {
        return {
          type: 'user',
          balance: 15,
          delegate: { address: 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oMMM' },
        }
      } else {
        throw new Error('address not handled by the test: ' + address)
      }
    })

    mockedGetTokensBalances.mockImplementation((address: string) => {
      if (address === 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oSM8') {
        return mockTokensBalancesWithAllInfo
      } else if (address === 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oMMM') {
        return mockTokensBalancesWithMissingInfo
      } else {
        return []
      }
    })

    expect(await getBalance(context, 'tz1WvvbEGpBXGeTVbLiR6DYBe1izmgiYuZbq')).toEqual([
      {
        value: 0n,
        asset: { type: 'native' },
      },
    ])
    expect(await getBalance(context, 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oSM8')).toEqual([
      {
        value: BigInt(25),
        asset: { type: 'native' },
      },
      {
        value: BigInt('2000'),
        asset: {
          type: 'fa2',
          assetReference: 'KT1T87QbpXEVgkwsNPzz8iRoah3SS3D1MDmh:0',
          assetOwner: 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oSM8',
          name: 'BTCtz',
          unit: { magnitude: 8, name: 'BTCtez', code: 'BTCtz' },
        },
      },
    ])
    expect(await getBalance(context, 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oMMM')).toEqual([
      {
        value: 15n,
        asset: { type: 'native' },
      },
      {
        value: 15n,
        asset: { type: 'native' },
        stake: {
          uid: 'delegation-tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oMMM',
          address: 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oMMM',
          delegate: 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oMMM',
          state: 'active',
          asset: {
            type: 'native',
          },
          amount: 15n,
          actions: [],
        },
      },
      {
        value: 2000n,
        asset: {
          type: 'fa2',
          assetReference: 'KT1T87QbpXEVgkwsNPzz8iRoah3SS3D1MDmh:0',
          assetOwner: 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oMMM',
          name: 'BTCtz',
          unit: {
            code: 'BTCtz',
            magnitude: 0,
            name: 'BTCtz',
          },
        },
      },
    ])
  })

  describe('staking positions (Paris upgrade)', () => {
    const address = 'tz1StakingAddr'
    const delegateAddress = 'tz1BakerAddr'

    function mockAccount(account: Record<string, unknown>) {
      mockedGetAccountByAddress.mockResolvedValue({ type: 'user', ...account })
      mockedGetTokensBalances.mockResolvedValue([])
      mockedGetUnstakeRequests.mockResolvedValue([])
    }

    it('attaches a delegation Stake when only delegate is set', async () => {
      mockAccount({ balance: 100, delegate: { address: delegateAddress } })

      expect(await getBalance(context, address)).toEqual([
        { value: 100n, asset: { type: 'native' } },
        {
          value: 100n,
          asset: {
            type: 'native',
          },
          stake: {
            uid: `delegation-${address}`,
            address,
            delegate: delegateAddress,
            state: 'active',
            asset: {
              type: 'native',
            },
            amount: 100n,
            actions: [],
          },
        },
      ])
    })

    it('attaches a stake Stake when stakedBalance > 0 (no delegate)', async () => {
      mockAccount({ balance: 100, stakedBalance: 30 })

      expect(await getBalance(context, address)).toEqual([
        { value: 100n, asset: { type: 'native' }, locked: 30n },
        {
          value: 30n,
          asset: {
            type: 'native',
          },
          stake: {
            uid: `stake-${address}`,
            address,
            state: 'active',
            asset: {
              type: 'native',
            },
            amount: 30n,
            actions: [],
          },
        },
      ])
    })

    it('splits delegation and stake amounts when both delegate and stakedBalance are set', async () => {
      mockAccount({
        balance: 100,
        stakedBalance: 30,
        delegate: { address: delegateAddress },
      })

      expect(await getBalance(context, address)).toEqual([
        { value: 100n, asset: { type: 'native' }, locked: 30n },
        {
          value: 70n,
          asset: {
            type: 'native',
          },
          stake: {
            uid: `delegation-${address}`,
            address,
            delegate: delegateAddress,
            state: 'active',
            asset: {
              type: 'native',
            },
            amount: 70n,
            actions: [],
          },
        },
        {
          value: 30n,
          asset: {
            type: 'native',
          },
          stake: {
            uid: `stake-${address}`,
            address,
            delegate: delegateAddress,
            state: 'active',
            asset: {
              type: 'native',
            },
            amount: 30n,
            actions: [],
          },
        },
      ])
    })

    it("attaches an unstaking Stake with state 'deactivating' when a pending request exists", async () => {
      mockedGetAccountByAddress.mockResolvedValue({
        type: 'user',
        balance: 100,
        stakedBalance: 30,
        unstakedBalance: 10,
        delegate: { address: delegateAddress },
      })
      mockedGetTokensBalances.mockResolvedValue([])
      mockedGetUnstakeRequests.mockResolvedValue([
        {
          id: 77,
          cycle: 100,
          baker: { address: delegateAddress },
          staker: { address },
          firstTime: '2026-05-01T00:00:00Z',
          status: 'pending',
          actualAmount: 10,
        },
      ])

      const result = await getBalance(context, address)

      expect(result).toHaveLength(4)
      expect(result[0]).toEqual({ value: 100n, asset: { type: 'native' }, locked: 40n })
      expect(result[3]).toEqual({
        value: 10n,
        asset: { type: 'native' },
        stake: {
          uid: 'unstaking-77',
          address,
          delegate: delegateAddress,
          state: 'deactivating',
          createdAt: new Date('2026-05-01T00:00:00Z'),
          asset: { type: 'native' },
          amount: 10n,
          actions: [],
        },
      })
    })

    it('emits per-request unstaking and finalizable Stakes when both statuses are present', async () => {
      mockedGetAccountByAddress.mockResolvedValue({
        type: 'user',
        balance: 100,
        unstakedBalance: 50,
        delegate: { address: delegateAddress },
      })
      mockedGetUnstakeRequests.mockResolvedValue([
        {
          id: 1,
          cycle: 100,
          baker: { address: delegateAddress },
          staker: { address },
          firstTime: '2026-05-01T00:00:00Z',
          status: 'pending',
          actualAmount: 20,
        },
        {
          id: 2,
          cycle: 99,
          baker: { address: delegateAddress },
          staker: { address },
          firstTime: '2026-04-25T00:00:00Z',
          status: 'finalizable',
          actualAmount: 30,
        },
      ])
      mockedGetTokensBalances.mockResolvedValue([])

      const result = await getBalance(context, address)

      expect(result[0]).toEqual({ value: 100n, asset: { type: 'native' }, locked: 50n })

      const stakes = result.filter((b) => b.stake).map((b) => b.stake)
      expect(stakes).toEqual([
        {
          uid: `delegation-${address}`,
          address,
          delegate: delegateAddress,
          state: 'active',
          asset: { type: 'native' },
          amount: 50n,
          actions: [],
        },
        {
          uid: `unstaking-1`,
          address,
          delegate: delegateAddress,
          state: 'deactivating',
          createdAt: new Date('2026-05-01T00:00:00Z'),
          asset: { type: 'native' },
          amount: 20n,
          actions: [],
        },
        {
          uid: `finalizable-2`,
          address,
          delegate: delegateAddress,
          state: 'inactive',
          createdAt: new Date('2026-04-25T00:00:00Z'),
          asset: { type: 'native' },
          amount: 30n,
          actions: [],
        },
      ])
    })

    it("returns the baker's own balance, self-stake and unstake positions for a delegate account (LIVE-34256)", async () => {
      // tzkt reports a registered baker with type "delegate" (not "user"). Before the fix this
      // fell through to a 0 native balance and no stakes; the account appeared empty in Ledger Live.
      // A delegate has no `delegate` field (it is its own baker), so there is no delegation position —
      // only its self-stake and any unstake requests. The unstakedBalance > 0 exercises the
      // delegate → fetchUnstakeRequests path.

      mockedGetAccountByAddress.mockResolvedValue({
        type: 'delegate',
        balance: 100,
        stakedBalance: 30,
        unstakedBalance: 10,
      })
      mockedGetTokensBalances.mockResolvedValue([])
      mockedGetUnstakeRequests.mockResolvedValue([
        {
          id: 77,
          cycle: 100,
          baker: { address },
          staker: { address },
          firstTime: '2026-05-01T00:00:00Z',
          status: 'pending',
          actualAmount: 10,
        },
      ])

      expect(await getBalance(context, address)).toEqual([
        { value: 100n, asset: { type: 'native' }, locked: 40n },
        {
          value: 30n,
          asset: { type: 'native' },
          stake: {
            uid: `stake-${address}`,
            address,
            state: 'active',
            asset: { type: 'native' },
            amount: 30n,
            actions: [],
          },
        },
        {
          value: 10n,
          asset: { type: 'native' },
          stake: {
            uid: 'unstaking-77',
            address,
            delegate: address,
            state: 'deactivating',
            createdAt: new Date('2026-05-01T00:00:00Z'),
            asset: { type: 'native' },
            amount: 10n,
            actions: [],
          },
        },
      ])
    })

    it('returns only the primary native Balance when no staking activity', async () => {
      mockAccount({ balance: 50 })

      expect(await getBalance(context, address)).toEqual([
        { value: 50n, asset: { type: 'native' } },
      ])
    })

    it('still excludes unstaked-frozen funds from spendable when the unstake_requests endpoint fails', async () => {
      mockedGetAccountByAddress.mockResolvedValue({
        type: 'user',
        balance: 100,
        unstakedBalance: 10,
        delegate: { address: delegateAddress },
      })
      mockedGetUnstakeRequests.mockRejectedValue(new Error('endpoint failure'))
      mockedGetTokensBalances.mockResolvedValue([])

      const result = await getBalance(context, address)

      // Breakdown rows are lost, but locked/delegated still derive from the account-level unstakedBalance.
      expect(result).toHaveLength(2)
      expect(result[0]).toEqual({ value: 100n, asset: { type: 'native' }, locked: 10n })
      expect(result[1]).toEqual({
        value: 90n,
        asset: { type: 'native' },
        stake: {
          uid: `delegation-${address}`,
          address,
          delegate: delegateAddress,
          state: 'active',
          asset: { type: 'native' },
          amount: 90n,
          actions: [],
        },
      })
    })
  })
})
