import { AccountShapeInfo } from "@ledgerhq/ledger-wallet-framework/bridge/jsHelpers";
import { TokenAccount } from "@ledgerhq/types-live";
import BigNumber from "bignumber.js";
import * as accountBalance from "../common-logic/account/getBalance";
import * as config from "../config";
import * as gateway from "../network/gateway";
import { OperationInfo } from "../network/gateway";
import resolver from "../signer";
import { createMockCantonCurrency } from "../test/fixtures";
import { CantonAccount } from "../types";
import * as onboard from "./onboard";
import { makeGetAccountShape, filterDisabledTokenAccounts } from "./sync";

jest.mock("../network/gateway", () => ({
  ...jest.requireActual("../network/gateway"),
  getLedgerEnd: jest.fn(),
  getOperations: jest.fn(),
  getPartyById: jest.fn(),
  getPendingTransferProposals: jest.fn(),
  getCalTokensCached: jest.fn(),
  getEnabledInstrumentsCached: jest.fn(),
}));
jest.mock("../signer");
jest.mock("../config");
jest.mock("./onboard");
jest.mock("../common-logic/account/getBalance");
const mockFindTokenByAddressInCurrency = jest.fn().mockResolvedValue(undefined);
const mockFindTokenById = jest.fn().mockResolvedValue(undefined);

jest.mock("@ledgerhq/ledger-wallet-framework/cryptoAssetsStore", () => ({
  getCryptoAssetsStore: jest.fn(() => ({
    findTokenByAddressInCurrency: mockFindTokenByAddressInCurrency,
    findTokenById: mockFindTokenById,
  })),
}));

const mockedGetBalance = accountBalance.getBalance as jest.Mock;
const mockedGetLedgerEnd = gateway.getLedgerEnd as jest.Mock;
const mockedGetOperations = gateway.getOperations as jest.Mock;
const mockedGetPendingTransferProposals = gateway.getPendingTransferProposals as jest.Mock;
const mockedGetPartyById = gateway.getPartyById as jest.Mock;
const mockedGetCalTokensCached = gateway.getCalTokensCached as unknown as jest.Mock;
const mockedGetEnabledInstrumentsCached =
  gateway.getEnabledInstrumentsCached as unknown as jest.Mock;
const mockedResolver = resolver as jest.Mock;
const mockedIsOnboarded = onboard.isAccountOnboarded as jest.Mock;
const mockedIsAuthorized = onboard.isCantonCoinPreapproved as jest.Mock;
const mockedCoinConfig = config.default.getCoinConfig as jest.Mock;

const sampleCurrency = createMockCantonCurrency();

type CantonBalance = {
  value: bigint;
  locked: bigint;
  asset: { type: "Amulet" } | { type: "token"; assetReference: string };
  utxoCount: number;
  instrumentId: string;
  adminId: string;
};

const createMockNativeBalance = (amount: string, locked = false): CantonBalance => ({
  value: BigInt(amount),
  locked: locked ? BigInt(amount) : BigInt(0),
  asset: { type: "Amulet" },
  utxoCount: 1,
  instrumentId: "Amulet",
  adminId: "native-admin",
});

const createMockOperationView = (
  overrides: {
    instrumentId?: string;
    instrumentAdmin?: string | null;
    txHash?: string;
    uid?: string;
    type?: string;
    value?: string;
    operationType?: string;
  } = {},
): OperationInfo =>
  ({
    transaction_hash: overrides.txHash ?? "tx-test",
    uid: overrides.uid ?? "uid-test",
    type: overrides.type ?? "Send",
    status: "Success",
    fee: {
      value: "5",
      asset: {
        type: "native" as const,
        instrumentAdmin: "AmuletAdmin",
        instrumentId: "Amulet",
      },
      details: {
        type: "fee",
      },
    },
    transfers: [
      {
        address: "party123",
        type: "Send",
        value: overrides.value ?? "100",
        asset: overrides.instrumentId ?? "Amulet",
        details: {
          operationType: overrides.operationType ?? "transfer",
          metadata: {
            reason: "test transfer",
          },
        },
      },
    ],
    transaction_timestamp: new Date().toISOString(),
    senders: ["party123"],
    recipients: ["party456"],
    block: {
      height: 1,
      time: new Date().toISOString(),
      hash: "blockhash1",
    },
    asset: {
      type: "native",
      instrumentId: overrides.instrumentId ?? "Amulet",
      instrumentAdmin: overrides.instrumentAdmin ?? "AmuletAdmin",
    },
    details: {
      operationType: overrides.operationType ?? "transfer",
    },
  }) as OperationInfo;

const createMockCantonAccountShapeInfo = (
  overrides: Partial<AccountShapeInfo<CantonAccount>> = {},
): AccountShapeInfo<CantonAccount> => {
  const currency = createMockCantonCurrency();
  return {
    address: "addr1",
    currency,
    derivationMode: "",
    derivationPath: "44'/0'/0'/0/0",
    deviceId: "fakeDevice",
    index: 0,
    initialAccount: undefined,
    ...overrides,
  };
};

describe("makeGetAccountShape", () => {
  const fakeSignerContext = {} as any;

  const defaultInfo: AccountShapeInfo<CantonAccount> = {
    address: "addr1",
    currency: sampleCurrency,
    derivationMode: "",
    derivationPath: "44'/0'/0'/0/0",
    deviceId: "fakeDevice",
    index: 0,
    initialAccount: undefined,
  };

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

    mockedResolver.mockReturnValue(async () => ({
      publicKey: "FAKE_PUBLIC_KEY",
    }));

    mockedIsOnboarded.mockResolvedValue({
      isOnboarded: true,
      partyId: "party123",
    });

    mockedCoinConfig.mockReturnValue({
      nativeInstrumentId: "Amulet",
      minReserve: "0",
      useGateway: true,
    });

    mockedIsAuthorized.mockResolvedValue(true);
    mockedGetLedgerEnd.mockResolvedValue(12345);
    mockedGetPendingTransferProposals.mockResolvedValue([]);
    mockedGetPartyById.mockResolvedValue({ party_id: "test-party-id", public_key: "" });
    mockedGetCalTokensCached.mockResolvedValue(new Map());
    mockFindTokenByAddressInCurrency.mockResolvedValue(undefined);
    mockFindTokenById.mockResolvedValue(undefined);
  });

  it("should return a valid account shape with correct balances and operations", async () => {
    mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]);
    mockedGetOperations.mockResolvedValue({
      operations: [
        createMockOperationView({
          txHash: "tx1",
          uid: "uid1",
          type: "Send",
          value: "100",
        }),
      ],
    });
    const getAccountShape = makeGetAccountShape(fakeSignerContext);
    const shape = await getAccountShape(defaultInfo, {
      paginationConfig: {},
    });

    expect(shape).toHaveProperty("id");
    expect(shape.balance).toEqual(BigNumber(1000));
    expect(shape.operations?.length).toBe(1);
    expect((shape.operations as any)[0].type).toBe("OUT");
    expect((shape.operations as any)[0].value).toEqual(BigNumber(105)); // 100 + 5 fee
    expect(shape.spendableBalance).toEqual(BigNumber(1000));
    expect(shape.used).toBe(true);
  });

  it("should handle locked balances correctly", async () => {
    mockedGetBalance.mockResolvedValue([
      createMockNativeBalance("1000", true),
      createMockNativeBalance("10", false),
    ]);

    const getAccountShape = makeGetAccountShape(fakeSignerContext);
    const shape = await getAccountShape(defaultInfo, {
      paginationConfig: {},
    });

    expect(shape).toMatchObject({
      balance: BigNumber(1010),
      spendableBalance: BigNumber(10),
    });
  });

  it("should handle empty balances correctly", async () => {
    mockedGetBalance.mockResolvedValue([]);

    const getAccountShape = makeGetAccountShape(fakeSignerContext);
    const shape = await getAccountShape(defaultInfo, {
      paginationConfig: {},
    });

    expect(shape).toMatchObject({
      balance: BigNumber(0),
      spendableBalance: BigNumber(0),
    });
  });

  it("should default to FEES operation type when transferValue is 0", async () => {
    mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]);
    mockedGetOperations.mockResolvedValue({
      operations: [
        createMockOperationView({
          txHash: "tx2",
          uid: "uid2",
          type: "Send",
          value: "0",
        }),
      ],
    });

    const getAccountShape = makeGetAccountShape(fakeSignerContext);
    const shape = await getAccountShape(defaultInfo, {
      paginationConfig: {},
    });
    expect(shape).toMatchObject({
      operations: [
        expect.objectContaining({
          type: "FEES",
          // In this case, value should equal the fee
          value: BigNumber(5), // fee is 5 in createMockOperationView
        }),
      ],
    });
  });

  it("should set operation type to TRANSFER_PROPOSAL when operationType is transfer-proposal", async () => {
    mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]);
    mockedGetOperations.mockResolvedValue({
      operations: [
        createMockOperationView({
          txHash: "tx3",
          uid: "uid3",
          type: "Send",
          value: "200",
          operationType: "transfer-proposal",
        }),
      ],
    });

    const getAccountShape = makeGetAccountShape(fakeSignerContext);
    const shape = await getAccountShape(defaultInfo, {
      paginationConfig: {},
    });
    expect(shape).toMatchObject({
      operations: [
        expect.objectContaining({
          type: "TRANSFER_PROPOSAL",
          value: BigNumber(200), // transfer value only, fees not added for TRANSFER_PROPOSAL
        }),
      ],
    });
  });

  it("should set operation type to TRANSFER_REJECTED when operationType is transfer-rejected", async () => {
    mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]);
    mockedGetOperations.mockResolvedValue({
      operations: [
        createMockOperationView({
          txHash: "tx4",
          uid: "uid4",
          type: "Send",
          value: "150",
          operationType: "transfer-rejected",
        }),
      ],
    });

    const getAccountShape = makeGetAccountShape(fakeSignerContext);
    const shape: any = await getAccountShape(defaultInfo, {
      paginationConfig: {},
    });
    expect(shape).toMatchObject({
      operations: [
        expect.objectContaining({
          type: "TRANSFER_REJECTED",
          value: BigNumber(150), // transfer value only, fees not added for TRANSFER_REJECTED
        }),
      ],
    });
  });

  it("should set operation type to TRANSFER_WITHDRAWN when operationType is transfer-withdrawn", async () => {
    mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]);
    mockedGetOperations.mockResolvedValue({
      operations: [
        createMockOperationView({
          txHash: "tx5",
          uid: "uid5",
          type: "Send",
          value: "50",
          operationType: "transfer-withdrawn",
        }),
      ],
    });

    const getAccountShape = makeGetAccountShape(fakeSignerContext);
    const shape = await getAccountShape(defaultInfo, {
      paginationConfig: {},
    });
    expect(shape).toMatchObject({
      operations: [
        expect.objectContaining({
          type: "TRANSFER_WITHDRAWN",
          value: BigNumber(50), // transfer value only, fees not added for TRANSFER_WITHDRAWN
        }),
      ],
    });
  });

  it("should filter out operations that match pending transfer proposals", async () => {
    mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]);

    mockedGetPendingTransferProposals.mockResolvedValue([
      {
        contract_id: "pending-proposal-uid",
        sender: "sender-party",
        receiver: "test-party-id",
        amount: "100",
        instrument_id: "Amulet",
        instrument_admin: "native-admin",
        memo: "Test proposal",
        expires_at_micros: Date.now() + 100000,
        update_id: "tx-pending",
      },
    ]);

    mockedGetOperations.mockResolvedValue({
      operations: [
        createMockOperationView({
          txHash: "tx-pending",
          uid: "pending-proposal-uid",
          type: "Receive",
          value: "100",
        }),
        createMockOperationView({
          txHash: "tx-completed",
          uid: "completed-uid",
          type: "Receive",
          value: "200",
        }),
      ],
    });

    const getAccountShape = makeGetAccountShape(fakeSignerContext);
    const shape = await getAccountShape(defaultInfo, {
      paginationConfig: {},
    });

    expect(shape).toMatchObject({
      operations: [expect.objectContaining({ hash: "tx-completed", value: BigNumber(200) })],
      cantonResources: expect.objectContaining({
        pendingTransferProposals: [
          expect.objectContaining({ contract_id: "pending-proposal-uid" }),
        ],
      }),
    });
  });

  it("should expose token transfer proposals on parent account so the offer UI is reachable from the main view", async () => {
    // GIVEN
    // a pending token offer addressed to the user
    const tokenAdminId = "token-admin-party::1220abc";
    const tokenInstrumentId = "cbtc-instrument-id";
    const cbtcToken = {
      type: "TokenCurrency" as const,
      id: "canton_network/cip56/cbtc",
      contractAddress: tokenAdminId,
      parentCurrencyId: "canton_network",
      tokenType: "cip56",
      name: "CBTC",
      ticker: "CBTC",
      delisted: false,
      disableCountervalue: false,
      units: [{ name: "CBTC", code: "CBTC", magnitude: 8 }],
    };

    mockedGetCalTokensCached.mockResolvedValue(
      new Map([["canton_network/cip56/cbtc", tokenInstrumentId]]),
    );
    mockFindTokenById.mockImplementation(async (id: string) =>
      id === "canton_network/cip56/cbtc" ? cbtcToken : undefined,
    );
    mockedGetEnabledInstrumentsCached.mockResolvedValue(
      new Set([`${tokenInstrumentId}____${tokenAdminId}`]),
    );

    // No native balance, no token balance — the sub-account exists only because of the pending offer
    mockedGetBalance.mockResolvedValue([]);
    mockedGetOperations.mockResolvedValue({ operations: [] });
    mockedGetPendingTransferProposals.mockResolvedValue([
      {
        contract_id: "cbtc-offer-1",
        sender: "faucet-party",
        receiver: "test-party-id",
        amount: "1",
        instrument_id: tokenInstrumentId,
        instrument_admin: tokenAdminId,
        memo: "Faucet drop",
        expires_at_micros: (Date.now() + 100000) * 1000,
        update_id: "update-cbtc-1",
      },
    ]);

    // WHEN
    // we sync the account
    const getAccountShape = makeGetAccountShape(fakeSignerContext);
    const shape = await getAccountShape(defaultInfo, { paginationConfig: {} });

    // THEN
    // the proposal lands on both the parent account and the token sub-account
    expect(shape.cantonResources?.pendingTransferProposals).toEqual([
      expect.objectContaining({ contract_id: "cbtc-offer-1", instrument_id: tokenInstrumentId }),
    ]);
    const subAccount = (shape.subAccounts as TokenAccount[] | undefined)?.[0] as
      | (TokenAccount & { cantonResources?: { pendingTransferProposals: unknown[] } })
      | undefined;
    expect(subAccount?.token.ticker).toBe("CBTC");
    expect(subAccount?.cantonResources?.pendingTransferProposals).toEqual([
      expect.objectContaining({ contract_id: "cbtc-offer-1" }),
    ]);
  });

  it("should sync without device when account has xpub but no publicKey", async () => {
    mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]);
    mockedGetOperations.mockResolvedValue({
      operations: [createMockOperationView()],
    });

    const infoWithXpub = createMockCantonAccountShapeInfo({
      initialAccount: {
        xpub: "test-party-id",
        cantonResources: {
          instrumentUtxoCounts: {},
          pendingTransferProposals: [],
        },
      } as unknown as CantonAccount,
    });
    delete infoWithXpub.deviceId;

    const getAccountShape = makeGetAccountShape(fakeSignerContext);
    const shape = await getAccountShape(infoWithXpub, { paginationConfig: {} });

    expect(shape).toHaveProperty("id");
    expect(shape.xpub).toBe("test-party-id");
    // Should not call getAddress since we have xpub
    expect(mockedResolver).not.toHaveBeenCalled();
  });

  it("backfills publicKey from the gateway when account has xpub but no publicKey (LIVE-34585)", async () => {
    mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]);
    mockedGetOperations.mockResolvedValue({ operations: [createMockOperationView()] });
    mockedGetPartyById.mockResolvedValue({
      party_id: "test-party-id",
      public_key: "backfilled-public-key",
    });

    const infoWithXpub = createMockCantonAccountShapeInfo({
      initialAccount: {
        xpub: "test-party-id",
        cantonResources: {
          isOnboarded: true,
          instrumentUtxoCounts: {},
          pendingTransferProposals: [],
        },
      } as unknown as CantonAccount,
    });
    delete infoWithXpub.deviceId;

    const getAccountShape = makeGetAccountShape(fakeSignerContext);
    const shape = await getAccountShape(infoWithXpub, { paginationConfig: {} });

    // Deviceless backfill: publicKey now resolves, so validateTopology can run.
    expect(mockedResolver).not.toHaveBeenCalled();
    expect(mockedGetPartyById).toHaveBeenCalledWith(sampleCurrency, "test-party-id");
    expect(shape.cantonResources?.publicKey).toBe("backfilled-public-key");
  });

  it("should sync without device when account has publicKey but no xpub", async () => {
    mockedGetBalance.mockResolvedValue([]); // Empty balances since no xpub
    mockedGetOperations.mockResolvedValue({
      operations: [],
    });

    const infoWithPublicKey = createMockCantonAccountShapeInfo({
      initialAccount: {
        xpub: "", // Missing xpub
        cantonResources: {
          publicKey: "test-public-key",
          instrumentUtxoCounts: {},
          pendingTransferProposals: [],
        },
      } as unknown as CantonAccount,
    });
    delete infoWithPublicKey.deviceId;

    const getAccountShape = makeGetAccountShape(fakeSignerContext);
    const shape = await getAccountShape(infoWithPublicKey, { paginationConfig: {} });

    expect(shape).toHaveProperty("id");
    // Should not call getAddress since we have publicKey (even though xpub is missing)
    expect(mockedResolver).not.toHaveBeenCalled();
  });

  it("should sync without device when account has both xpub and publicKey", async () => {
    mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]);
    mockedGetOperations.mockResolvedValue({
      operations: [createMockOperationView()],
    });

    const infoWithBoth = createMockCantonAccountShapeInfo({
      initialAccount: {
        xpub: "test-party-id",
        cantonResources: {
          publicKey: "test-public-key",
          instrumentUtxoCounts: {},
          pendingTransferProposals: [],
        },
      } as unknown as CantonAccount,
    });
    delete infoWithBoth.deviceId;

    const getAccountShape = makeGetAccountShape(fakeSignerContext);
    const shape = await getAccountShape(infoWithBoth, { paginationConfig: {} });

    expect(shape).toHaveProperty("id");
    expect(shape.xpub).toBe("test-party-id");
    // Should not call getAddress since we have both values
    expect(mockedResolver).not.toHaveBeenCalled();
  });

  it("should correctly identify tokens with same adminId but different instrumentId", async () => {
    const sharedAdminId =
      "party-28dc4516-b5ca-44ff-86c7-2107e90a6807::1220b8301e18aa8a401d6e34e6c20f8b0243183c514373bca8f1b6b9270246341a9e";
    const sbcInstrumentId = "f29bdd7a-1469-498a-ba2a-796bf5387b31";
    const cusdInstrumentId = "481871d4-ca56-42a8-b2d3-4b7d28742946";

    // Create token mocks
    const sbcToken = {
      type: "TokenCurrency" as const,
      id: "canton_network/cip56/sbc",
      contractAddress: sharedAdminId,
      parentCurrencyId: "canton_network",
      tokenType: "cip56",
      name: "SBC",
      ticker: "SBC",
      delisted: false,
      disableCountervalue: false,
      units: [{ name: "SBC", code: "SBC", magnitude: 38 }],
    };

    const cusdToken = {
      type: "TokenCurrency" as const,
      id: "canton_network/cip56/cusd",
      contractAddress: sharedAdminId,
      parentCurrencyId: "canton_network",
      tokenType: "cip56",
      name: "CUSD",
      ticker: "CUSD",
      delisted: false,
      disableCountervalue: false,
      units: [{ name: "CUSD", code: "CUSD", magnitude: 38 }],
    };

    // Mock CAL tokens map (token_identifier -> token id)
    mockedGetCalTokensCached.mockResolvedValue(
      new Map([
        ["canton_network/cip56/sbc", sbcInstrumentId],
        ["canton_network/cip56/cusd", cusdInstrumentId],
      ]),
    );

    // Mock findTokenById to return correct tokens
    mockFindTokenById.mockImplementation(async (id: string) => {
      if (id === "canton_network/cip56/sbc") return sbcToken;
      if (id === "canton_network/cip56/cusd") return cusdToken;
      return undefined;
    });

    // Mock enabled instruments to include SBC (using SEPARATOR format)
    mockedGetEnabledInstrumentsCached.mockResolvedValue(
      new Set([`${sbcInstrumentId}____${sharedAdminId}`]),
    );

    // Mock balances with SBC token (NOT CUSD)
    mockedGetBalance.mockResolvedValue([
      createMockNativeBalance("1000"),
      {
        value: BigInt("990000000000000000000000000000000"),
        locked: BigInt(0),
        asset: { type: "token", assetReference: sbcInstrumentId },
        utxoCount: 1,
        instrumentId: sbcInstrumentId,
        adminId: sharedAdminId,
      },
    ]);

    mockedGetOperations.mockResolvedValue({
      operations: [],
    });

    const getAccountShape = makeGetAccountShape(fakeSignerContext);
    const shape = await getAccountShape(defaultInfo, {
      paginationConfig: {},
    });

    expect(shape).toMatchObject({
      subAccounts: [
        expect.objectContaining({
          balance: BigNumber("990000000000000000000000000000000"),
          type: "TokenAccount",
          token: expect.objectContaining({
            id: "canton_network/cip56/sbc",
            ticker: "SBC", // Should be SBC, not CUSD!
          }),
        }),
      ],
    });
  });
});

describe("filterDisabledTokenAccounts", () => {
  const currency = createMockCantonCurrency();

  const createMockTokenAccount = (contractAddress: string, tokenId?: string): TokenAccount => ({
    type: "TokenAccount",
    id: `token-account-${contractAddress}`,
    parentId: "parent-account-id",
    token: {
      type: "TokenCurrency",
      id: tokenId ?? `token-id-${contractAddress}`,
      contractAddress,
      name: "Test Token",
      ticker: "TEST",
      decimals: 18,
      parentCurrencyId: "canton_network",
    } as any,
    balance: new BigNumber(100),
    spendableBalance: new BigNumber(100),
    operationsCount: 0,
    operations: [],
    pendingOperations: [],
    balanceHistoryCache: {
      HOUR: { latestDate: null, balances: [] },
      DAY: { latestDate: null, balances: [] },
      WEEK: { latestDate: null, balances: [] },
    },
    swapHistory: [],
    creationDate: new Date(),
  });

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

  it("should return empty array when subAccounts is undefined", async () => {
    const calTokens = new Map<string, string>();
    const result = await filterDisabledTokenAccounts(currency, undefined, calTokens);
    expect(result).toEqual([]);
    expect(mockedGetEnabledInstrumentsCached).not.toHaveBeenCalled();
  });

  it("should return empty array when subAccounts is empty", async () => {
    const calTokens = new Map<string, string>();
    const result = await filterDisabledTokenAccounts(currency, [], calTokens);
    expect(result).toEqual([]);
    expect(mockedGetEnabledInstrumentsCached).not.toHaveBeenCalled();
  });

  it("should filter out disabled token accounts", async () => {
    const enabledAdminId = "0xenabled";
    const disabledAdminId = "0xdisabled";
    const enabledTokenId = "token-id-enabled";
    const disabledTokenId = "token-id-disabled";
    const enabledInstrumentId = "instrument-enabled";
    const disabledInstrumentId = "instrument-disabled";

    const enabledTokenAccount = createMockTokenAccount(enabledAdminId, enabledTokenId);
    const disabledTokenAccount = createMockTokenAccount(disabledAdminId, disabledTokenId);

    // calTokens maps token.id -> instrumentId
    const calTokens = new Map<string, string>([
      [enabledTokenId, enabledInstrumentId],
      [disabledTokenId, disabledInstrumentId],
    ]);

    // enabledInstruments is a Set of keys in format "instrumentId____adminId"
    mockedGetEnabledInstrumentsCached.mockResolvedValue(
      new Set([`${enabledInstrumentId}____${enabledAdminId}`]),
    );

    const result = await filterDisabledTokenAccounts(
      currency,
      [enabledTokenAccount, disabledTokenAccount],
      calTokens,
    );

    expect(result).toHaveLength(1);
    expect(result[0]).toBe(enabledTokenAccount);
    expect(mockedGetEnabledInstrumentsCached).toHaveBeenCalledWith(currency);
  });

  it("should keep enabled token accounts", async () => {
    const adminId1 = "0xenabled1";
    const adminId2 = "0xenabled2";
    const tokenId1 = "token-id-1";
    const tokenId2 = "token-id-2";
    const instrumentId1 = "instrument-1";
    const instrumentId2 = "instrument-2";

    const enabledTokenAccount1 = createMockTokenAccount(adminId1, tokenId1);
    const enabledTokenAccount2 = createMockTokenAccount(adminId2, tokenId2);

    const calTokens = new Map<string, string>([
      [tokenId1, instrumentId1],
      [tokenId2, instrumentId2],
    ]);

    mockedGetEnabledInstrumentsCached.mockResolvedValue(
      new Set([`${instrumentId1}____${adminId1}`, `${instrumentId2}____${adminId2}`]),
    );

    const result = await filterDisabledTokenAccounts(
      currency,
      [enabledTokenAccount1, enabledTokenAccount2],
      calTokens,
    );

    expect(result).toHaveLength(2);
    expect(result).toContain(enabledTokenAccount1);
    expect(result).toContain(enabledTokenAccount2);
  });

  it("should not keep token accounts without contractAddress", async () => {
    const tokenAccountWithoutAddress = {
      ...createMockTokenAccount("0xtest", "token-id-noaddr"),
      token: {
        ...createMockTokenAccount("0xtest", "token-id-noaddr").token,
        contractAddress: "",
      },
    };
    const enabledAdminId = "0xenabled";
    const enabledTokenId = "token-id-enabled";
    const enabledInstrumentId = "instrument-enabled";
    const enabledTokenAccount = createMockTokenAccount(enabledAdminId, enabledTokenId);

    const calTokens = new Map<string, string>([
      ["token-id-noaddr", "instrument-noaddr"],
      [enabledTokenId, enabledInstrumentId],
    ]);

    mockedGetEnabledInstrumentsCached.mockResolvedValue(
      new Set([`${enabledInstrumentId}____${enabledAdminId}`, "instrument-noaddr____"]),
    );

    const result = await filterDisabledTokenAccounts(
      currency,
      [tokenAccountWithoutAddress, enabledTokenAccount],
      calTokens,
    );

    expect(result).toHaveLength(1);
    expect(result[0]).toBe(enabledTokenAccount);
    expect(result).not.toContain(tokenAccountWithoutAddress);
  });

  it("should handle empty enabled instruments list", async () => {
    const adminId1 = "0xtoken1";
    const adminId2 = "0xtoken2";
    const tokenId1 = "token-id-1";
    const tokenId2 = "token-id-2";

    const tokenAccount1 = createMockTokenAccount(adminId1, tokenId1);
    const tokenAccount2 = createMockTokenAccount(adminId2, tokenId2);

    const calTokens = new Map<string, string>([
      [tokenId1, "instrument-1"],
      [tokenId2, "instrument-2"],
    ]);

    mockedGetEnabledInstrumentsCached.mockResolvedValue(new Set());

    const result = await filterDisabledTokenAccounts(
      currency,
      [tokenAccount1, tokenAccount2],
      calTokens,
    );

    expect(result).toEqual([]);
  });
});
