import { setupMockCryptoAssetsStore } from "@ledgerhq/cryptoassets/cal-client/test-helpers";
import { InvalidAddress } from "@ledgerhq/errors";
import { getEnv } from "@ledgerhq/live-env";
import BigNumber from "bignumber.js";
import { SUPPORTED_ERC20_TOKENS } from "../constants";
import { HederaRecipientInvalidChecksum } from "../errors";
import { getMockedAccount } from "../test/fixtures/account.fixture";
import {
  getMockedCurrency,
  getMockedERC20TokenCurrency,
  getMockedHTSTokenCurrency,
} from "../test/fixtures/currency.fixture";
import {
  getMockedERC20TokenBalance,
  getMockedERC20TokenTransfer,
} from "../test/fixtures/hgraph.fixture";
import {
  createMirrorCoinTransfer,
  createMirrorTokenTransfer,
  getMockedMirrorTransaction,
  getMockedMirrorContractCallResult,
  getMockedMirrorAccount,
} from "../test/fixtures/mirror.fixture";
import { getMockedConfig } from "../test/fixtures/config.fixture";
import hederaCoinConfig from "../config";
import type { HederaMirrorCoinTransfer, HederaMirrorTransaction } from "../types";
import { apiClient } from "./api";
import { hgraphClient } from "./hgraph";
import { rpcClient } from "./rpc";
import {
  analyzeStakingOperation,
  calculateUncommittedBalanceChange,
  checkAccountTokenAssociationStatus,
  createTransactionId,
  enrichERC20Transfers,
  getERC20BalancesForAccountV2,
  parseTransfers,
  safeParseAccountId,
  toEVMAddress,
} from "./utils";

jest.mock("./api");
jest.mock("./hgraph");
jest.mock("./rpc", () => ({
  rpcClient: require("../test/fixtures/rpc.fixture").getMockedRpcClient(),
}));

describe("network utils", () => {
  const defaultConfig = getMockedConfig();
  const mockCurrency = getMockedCurrency();

  beforeAll(() => {
    hederaCoinConfig.setCoinConfig(getMockedConfig);
  });

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

  afterAll(async () => {
    await rpcClient._resetInstance();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  describe("createTransactionId", () => {
    it("should use mirror node timestamp when feature flag is enabled", async () => {
      (apiClient.getLatestBlock as jest.Mock).mockResolvedValue({
        timestamp: { from: "1758733200.632122898", to: null },
      });

      const result = await createTransactionId("0.0.54321", {
        ...defaultConfig,
        useNetworkTimestamp: true,
      });

      expect(apiClient.getLatestBlock).toHaveBeenCalledTimes(1);
      expect(result.validStart?.seconds.toString()).toEqual("1758733200");
      expect(result.validStart?.nanos.toString()).toEqual("632122898");
    });

    it("should fallback to system timestamp when latest block fetch fails", async () => {
      jest.useFakeTimers();
      jest.setSystemTime(new Date("2000-01-01T00:00:00.000Z"));
      (apiClient.getLatestBlock as jest.Mock).mockRejectedValue(new Error("network unavailable"));

      const result = await createTransactionId("0.0.54321", {
        ...defaultConfig,
        useNetworkTimestamp: true,
      });

      const localSkewSeconds = Number(result.validStart?.seconds.toString());
      expect(apiClient.getLatestBlock).toHaveBeenCalledTimes(1);
      expect(localSkewSeconds).toBeGreaterThanOrEqual(946684700);
      expect(localSkewSeconds).toBeLessThanOrEqual(946684800);
    });

    it("should use system timestamp when feature flag is disabled", async () => {
      jest.useFakeTimers();
      jest.setSystemTime(new Date("2000-01-01T00:00:00.000Z"));

      const result = await createTransactionId("0.0.54321", {
        ...defaultConfig,
        useNetworkTimestamp: false,
      });

      const localSkewSeconds = Number(result.validStart?.seconds.toString());
      expect(apiClient.getLatestBlock).not.toHaveBeenCalled();
      expect(localSkewSeconds).toBeGreaterThanOrEqual(946684700);
      expect(localSkewSeconds).toBeLessThanOrEqual(946684800);
    });
  });

  describe("parseTransfers", () => {
    const userAddress = "0.0.1234";
    const rewardPayer = getEnv("HEDERA_STAKING_REWARD_ACCOUNT_ID");

    it("should correctly identify an incoming transfer", () => {
      const transfers = [
        createMirrorCoinTransfer("0.0.5678", -100),
        createMirrorCoinTransfer(userAddress, 100),
      ];

      const result = parseTransfers(transfers, userAddress);

      expect(result.type).toBe("IN");
      expect(result.value).toEqual(new BigNumber(100));
      expect(result.senders).toEqual(["0.0.5678"]);
      expect(result.recipients).toEqual([userAddress]);
    });

    it("should correctly identify an outgoing transfer", () => {
      const transfers = [
        createMirrorCoinTransfer(userAddress, -100),
        createMirrorCoinTransfer("0.0.5678", 100),
      ];

      const result = parseTransfers(transfers, userAddress);

      expect(result.type).toBe("OUT");
      expect(result.value).toEqual(new BigNumber(100));
      expect(result.senders).toEqual([userAddress]);
      expect(result.recipients).toEqual(["0.0.5678"]);
    });

    it("should handle multiple senders and recipients", () => {
      const transfers = [
        createMirrorCoinTransfer("0.0.5678", -50),
        createMirrorCoinTransfer(userAddress, -50),
        createMirrorCoinTransfer("0.0.9999", 100),
      ];

      const result = parseTransfers(transfers, userAddress);

      expect(result.type).toBe("OUT");
      expect(result.value).toEqual(new BigNumber(50));
      expect(result.senders).toEqual(["0.0.1234", "0.0.5678"]);
      expect(result.recipients).toEqual(["0.0.9999"]);
    });

    it("should correctly process token transfers", () => {
      const tokenId = "0.0.7777";
      const transfers = [
        createMirrorTokenTransfer(userAddress, -10, tokenId),
        createMirrorTokenTransfer("0.0.5678", 10, tokenId),
      ];

      const result = parseTransfers(transfers, userAddress);

      expect(result.type).toBe("OUT");
      expect(result.value).toEqual(new BigNumber(10));
      expect(result.senders).toEqual([userAddress]);
      expect(result.recipients).toEqual(["0.0.5678"]);
    });

    it("should exclude system accounts that are not nodes from recipients", () => {
      const systemAccount = "0.0.500";
      const transfers = [
        createMirrorCoinTransfer(userAddress, -100),
        createMirrorCoinTransfer(systemAccount, 100),
      ];

      const result = parseTransfers(transfers, userAddress);

      expect(result.type).toBe("OUT");
      expect(result.value).toEqual(new BigNumber(100));
      expect(result.senders).toEqual([userAddress]);
      expect(result.recipients).toEqual([]);
    });

    it("should include node accounts as recipients only if no other recipients", () => {
      const nodeAccount = "0.0.3";
      const transfers = [
        createMirrorCoinTransfer(userAddress, -100),
        createMirrorCoinTransfer(nodeAccount, 100),
      ];

      const result = parseTransfers(transfers, userAddress);

      expect(result.type).toBe("OUT");
      expect(result.value).toEqual(new BigNumber(100));
      expect(result.senders).toEqual([userAddress]);
      expect(result.recipients).toEqual([nodeAccount]);
    });

    it("should exclude node accounts if there are other recipients", () => {
      const normalAccount = "0.0.5678";
      const nodeAccount = "0.0.3";
      const transfers = [
        createMirrorCoinTransfer(userAddress, -100),
        createMirrorCoinTransfer(normalAccount, 50),
        createMirrorCoinTransfer(nodeAccount, 50),
      ];

      const result = parseTransfers(transfers, userAddress);

      expect(result.type).toBe("OUT");
      expect(result.value).toEqual(new BigNumber(100));
      expect(result.senders).toEqual([userAddress]);
      expect(result.recipients).toEqual([normalAccount]);
    });

    it("should handle transactions where user is not involved", () => {
      const transfers = [
        createMirrorCoinTransfer("0.0.5678", -100),
        createMirrorCoinTransfer("0.0.9999", 100),
      ];

      const result = parseTransfers(transfers, userAddress);

      expect(result.type).toBe("NONE");
      expect(result.value).toEqual(new BigNumber(0));
      expect(result.senders).toEqual(["0.0.5678"]);
      expect(result.recipients).toEqual(["0.0.9999"]);
    });

    it("should handle empty transfers array", () => {
      const transfers: HederaMirrorCoinTransfer[] = [];

      const result = parseTransfers(transfers, userAddress);

      expect(result.type).toBe("NONE");
      expect(result.value).toEqual(new BigNumber(0));
      expect(result.senders).toEqual([]);
      expect(result.recipients).toEqual([]);
    });

    it("should reverse the order of senders and recipients", () => {
      const transfers = [
        createMirrorCoinTransfer("0.0.900", -5),
        createMirrorCoinTransfer("0.0.5678", -95),
        createMirrorCoinTransfer(userAddress, 100),
      ];

      const result = parseTransfers(transfers, userAddress);

      expect(result.type).toBe("IN");
      expect(result.value).toEqual(new BigNumber(100));
      expect(result.senders).toEqual(["0.0.5678", "0.0.900"]);
      expect(result.recipients).toEqual([userAddress]);
    });

    it("should subtract staking reward from amount", () => {
      const amount = new BigNumber(30);
      const stakingReward = new BigNumber(20);
      const transfers = [createMirrorCoinTransfer(userAddress, amount.toNumber())];

      const expectedAmountWithoutReward = amount.minus(stakingReward);
      const result = parseTransfers(transfers, userAddress, stakingReward);

      expect(result).toMatchObject({
        type: "IN",
        value: expectedAmountWithoutReward,
      });
    });

    it("excludes reward payer from senders when staking reward is present", () => {
      const stakingReward = new BigNumber(30000000);
      const transfers = [
        createMirrorCoinTransfer(rewardPayer, -30000000),
        createMirrorCoinTransfer("0.0.801", 1000),
        createMirrorCoinTransfer(userAddress, 30000000),
      ];

      const result = parseTransfers(transfers, userAddress, stakingReward);

      expect(result.senders).not.toContain(rewardPayer);
    });

    it("includes reward payer in senders when no staking reward", () => {
      const transfers = [
        createMirrorCoinTransfer(rewardPayer, -1000000),
        createMirrorCoinTransfer(userAddress, 1000000),
      ];

      const result = parseTransfers(transfers, userAddress);

      expect(result.senders).toContain(rewardPayer);
    });
  });

  describe("getERC20BalancesForAccountV2", () => {
    it("returns balances only for supported ERC20 tokens and calls hgraphClient.getERC20Balances accordingly", async () => {
      const mockAccount = getMockedAccount();
      const erc20Token = getMockedERC20TokenCurrency();

      (hgraphClient.getERC20Balances as jest.Mock).mockResolvedValue([
        getMockedERC20TokenBalance({ token_id: 0, balance: 100 }),
        getMockedERC20TokenBalance({ token_id: 2, balance: 200 }),
        // token id from SUPPORTED_ERC20_TOKENS
        getMockedERC20TokenBalance({ token_id: 9470869, balance: 300 }),
      ]);
      setupMockCryptoAssetsStore({
        findTokenById: jest.fn().mockReturnValue(erc20Token),
      });

      const res = await getERC20BalancesForAccountV2({
        configOrCurrencyId: mockCurrency.id,
        address: mockAccount.freshAddress,
      });

      expect(hgraphClient.getERC20Balances).toHaveBeenCalledTimes(1);
      expect(hgraphClient.getERC20Balances).toHaveBeenCalledWith({
        configOrCurrencyId: mockCurrency.id,
        address: mockAccount.freshAddress,
      });
      expect(res).toEqual([
        {
          balance: new BigNumber(300),
          token: expect.objectContaining({
            contractAddress: erc20Token.contractAddress,
          }),
        },
      ]);
    });
  });

  describe("enrichERC20Transfers", () => {
    const payerAccountId = 1234;
    const erc20Token = SUPPORTED_ERC20_TOKENS[0];
    const mockMirrorTransaction = getMockedMirrorTransaction({
      entity_id: erc20Token.tokenId,
      consensus_timestamp: "1704067200.000000000",
      transaction_id: `0.0.${payerAccountId}-1704067200-000000000`,
      transaction_hash: "hash123",
      name: "CONTRACTCALL",
    });
    const mockERC20Transfer = getMockedERC20TokenTransfer({
      token_id: Number(erc20Token.tokenId.split(".").pop()),
      token_evm_address: erc20Token.contractAddress,
      consensus_timestamp: Number(mockMirrorTransaction.consensus_timestamp) * 10 ** 9,
      payer_account_id: payerAccountId,
      transaction_hash: mockMirrorTransaction.transaction_hash,
    });
    const mockContractCallResult = getMockedMirrorContractCallResult({
      contract_id: erc20Token.tokenId,
      timestamp: mockMirrorTransaction.consensus_timestamp,
    });

    beforeEach(() => {
      (apiClient.getContractCallResult as jest.Mock).mockResolvedValue(mockContractCallResult);
      (apiClient.findTransactionByContractCallV2 as jest.Mock).mockResolvedValue(
        mockMirrorTransaction,
      );
    });

    it("should enrich supported ERC20 transfers with contract call result and mirror transaction", async () => {
      const result = await enrichERC20Transfers({
        configOrCurrencyId: mockCurrency.id,
        erc20Transfers: [mockERC20Transfer],
      });

      expect(result).toEqual([
        {
          transfers: [mockERC20Transfer],
          contractCallResult: mockContractCallResult,
          mirrorTransaction: mockMirrorTransaction,
        },
      ]);
      expect(apiClient.getContractCallResult).toHaveBeenCalledTimes(1);
      expect(apiClient.getContractCallResult).toHaveBeenCalledWith({
        configOrCurrencyId: mockCurrency.id,
        transactionHash: mockMirrorTransaction.transaction_hash,
      });
      expect(apiClient.findTransactionByContractCallV2).toHaveBeenCalledTimes(1);
      expect(apiClient.findTransactionByContractCallV2).toHaveBeenCalledWith({
        configOrCurrencyId: mockCurrency.id,
        timestamp: "1704067200.000000000",
        payerAddress: `0.0.${payerAccountId}`,
      });
    });

    it("should group multiple transfers with the same transaction hash into one enriched result", async () => {
      const transfer1 = { ...mockERC20Transfer, amount: 1000 };
      const transfer2 = { ...mockERC20Transfer, amount: 2000 }; // same transaction_hash

      const result = await enrichERC20Transfers({
        configOrCurrencyId: mockCurrency.id,
        erc20Transfers: [transfer1, transfer2],
      });

      expect(result).toEqual([
        expect.objectContaining({
          transfers: [transfer1, transfer2],
        }),
      ]);
    });

    it("should skip transfers where mirror transaction is not found", async () => {
      (apiClient.findTransactionByContractCallV2 as jest.Mock).mockResolvedValue(null);

      const result = await enrichERC20Transfers({
        configOrCurrencyId: mockCurrency.id,
        erc20Transfers: [mockERC20Transfer],
      });

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

    it("should handle multiple transfers", async () => {
      const transfers = [mockERC20Transfer, { ...mockERC20Transfer, transaction_hash: "hash456" }];

      (apiClient.findTransactionByContractCallV2 as jest.Mock).mockResolvedValue(
        mockMirrorTransaction,
      );

      const result = await enrichERC20Transfers({
        configOrCurrencyId: mockCurrency.id,
        erc20Transfers: transfers,
      });
      const txHashes = result.flatMap(r => r.transfers.map(t => t.transaction_hash));

      expect(txHashes).toEqual([mockERC20Transfer.transaction_hash, "hash456"]);
    });

    it("should correctly convert consensus timestamp to seconds format", async () => {
      const transferWithTimestamp = {
        ...mockERC20Transfer,
        consensus_timestamp: 1768092990 * 10 ** 9,
      };

      await enrichERC20Transfers({
        configOrCurrencyId: mockCurrency.id,
        erc20Transfers: [transferWithTimestamp],
      });

      expect(apiClient.findTransactionByContractCallV2).toHaveBeenCalledTimes(1);
      expect(apiClient.findTransactionByContractCallV2).toHaveBeenCalledWith({
        configOrCurrencyId: mockCurrency.id,
        timestamp: "1768092990.000000000",
        payerAddress: `0.0.${payerAccountId}`,
      });
    });

    it("should handle empty array", async () => {
      const result = await enrichERC20Transfers({
        configOrCurrencyId: mockCurrency.id,
        erc20Transfers: [],
      });

      expect(result).toEqual([]);
      expect(apiClient.getContractCallResult).not.toHaveBeenCalled();
      expect(apiClient.findTransactionByContractCallV2).not.toHaveBeenCalled();
    });
  });

  describe("checkAccountTokenAssociationStatus", () => {
    const accountId = "0.0.1234";
    const htsToken = getMockedHTSTokenCurrency({ contractAddress: "0.0.1234", tokenType: "hts" });
    const erc20Token = getMockedHTSTokenCurrency({
      contractAddress: "0.0.4321",
      tokenType: "erc20",
    });

    beforeEach(() => {
      jest.clearAllMocks();
      // reset LRU cache to make sure all tests receive correct mocks from mockedGetAccount
      checkAccountTokenAssociationStatus.clear(`${accountId}-${htsToken.contractAddress}`);
    });

    it("returns true if max_automatic_token_associations === -1", async () => {
      (apiClient.getAccount as jest.Mock).mockResolvedValueOnce({
        account: accountId,
        max_automatic_token_associations: -1,
        balance: {
          balance: 0,
          timestamp: "",
          tokens: [],
        },
      });

      const result = await checkAccountTokenAssociationStatus(accountId, htsToken);
      expect(result).toBe(true);
    });

    it("returns true if token is already associated", async () => {
      (apiClient.getAccount as jest.Mock).mockResolvedValueOnce({
        account: accountId,
        max_automatic_token_associations: 0,
        balance: {
          balance: 1,
          timestamp: "",
          tokens: [{ token_id: htsToken.contractAddress, balance: 1 }],
        },
      });

      const result = await checkAccountTokenAssociationStatus(accountId, htsToken);
      expect(result).toBe(true);
    });

    it("returns false if token is not associated", async () => {
      (apiClient.getAccount as jest.Mock).mockResolvedValueOnce({
        account: accountId,
        max_automatic_token_associations: 0,
        balance: {
          balance: 1,
          timestamp: "",
          tokens: [{ token_id: "0.1234", balance: 1 }],
        },
      });

      const result = await checkAccountTokenAssociationStatus(accountId, htsToken);
      expect(result).toBe(false);
    });

    it("returns true for erc20 tokens", async () => {
      const result = await checkAccountTokenAssociationStatus(accountId, erc20Token);
      expect(apiClient.getAccount as jest.Mock).not.toHaveBeenCalled();
      expect(result).toBe(true);
    });

    it("supports addresses with checksum", async () => {
      const addressWithChecksum = "0.0.9124531-xrxlv";

      (apiClient.getAccount as jest.Mock).mockResolvedValueOnce({
        account: accountId,
        max_automatic_token_associations: 0,
        balance: {
          balance: 1,
          timestamp: "",
          tokens: [{ token_id: htsToken.contractAddress, balance: 1 }],
        },
      });

      await checkAccountTokenAssociationStatus(addressWithChecksum, htsToken);
      expect(apiClient.getAccount).toHaveBeenCalledTimes(1);
      expect(apiClient.getAccount).toHaveBeenCalledWith({
        configOrCurrencyId: htsToken.parentCurrencyId,
        address: "0.0.9124531",
      });
    });
  });

  describe("safeParseAccountId", () => {
    it("returns account id and no checksum for valid address without checksum", async () => {
      const [error, result] = await safeParseAccountId({
        configOrCurrencyId: defaultConfig,
        address: "0.0.9124531",
      });

      expect(error).toBeNull();
      expect(result?.accountId).toBe("0.0.9124531");
      expect(result?.checksum).toBeNull();
    });

    it("returns account id and checksum for valid address with correct checksum", async () => {
      const [error, result] = await safeParseAccountId({
        configOrCurrencyId: defaultConfig,
        address: "0.0.9124531-xrxlv",
      });

      expect(error).toBeNull();
      expect(result?.accountId).toBe("0.0.9124531");
      expect(result?.checksum).toBe("xrxlv");
    });

    it("returns error for valid address with incorrect checksum", async () => {
      const [error, accountId] = await safeParseAccountId({
        configOrCurrencyId: defaultConfig,
        address: "0.0.9124531-invld",
      });

      expect(error).toBeInstanceOf(HederaRecipientInvalidChecksum);
      expect(accountId).toBeNull();
    });

    it("returns error for invalid address format", async () => {
      const [error, accountId] = await safeParseAccountId({
        configOrCurrencyId: defaultConfig,
        address: "not-a-valid-address",
      });

      expect(error).toBeInstanceOf(InvalidAddress);
      expect(accountId).toBeNull();
    });
  });

  describe("toEVMAddress", () => {
    const mockMirrorAccount = {
      account: "0.0.12345",
      evm_address: "0x0000000000000000000000000000000000003039",
    };

    it("returns correct EVM address for valid Hedera account ID", async () => {
      (apiClient.getAccount as jest.Mock).mockResolvedValueOnce(mockMirrorAccount);

      const evmAddress = await toEVMAddress({
        configOrCurrencyId: defaultConfig,
        accountId: mockMirrorAccount.account,
      });

      expect(apiClient.getAccount).toHaveBeenCalledTimes(1);
      expect(apiClient.getAccount).toHaveBeenCalledWith({
        configOrCurrencyId: defaultConfig,
        address: mockMirrorAccount.account,
      });
      expect(evmAddress).toBe(mockMirrorAccount.evm_address);
    });

    it("returns null when API call fails", async () => {
      (apiClient.getAccount as jest.Mock).mockRejectedValueOnce(new Error("API error"));

      const evmAddress = await toEVMAddress({
        configOrCurrencyId: defaultConfig,
        accountId: mockMirrorAccount.account,
      });

      expect(apiClient.getAccount).toHaveBeenCalledTimes(1);
      expect(evmAddress).toBeNull();
    });
  });

  describe("calculateUncommittedBalanceChange", () => {
    const mockAddress = "0.0.12345";
    const mockStartTimestamp = "1762202064.065172388";
    const mockEndTimestamp = "1762202074.065172388";

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

    it("should return 0 when there are no transactions in the time range", async () => {
      (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValueOnce([]);

      const result = await calculateUncommittedBalanceChange({
        configOrCurrencyId: defaultConfig,
        address: mockAddress,
        startTimestamp: mockStartTimestamp,
        endTimestamp: mockEndTimestamp,
      });

      expect(result).toEqual(new BigNumber(0));
      expect(apiClient.getTransactionsByTimestampRange).toHaveBeenCalledTimes(1);
      expect(apiClient.getTransactionsByTimestampRange).toHaveBeenCalledWith({
        configOrCurrencyId: defaultConfig,
        address: mockAddress,
        startTimestamp: `gt:${mockStartTimestamp}`,
        endTimestamp: `lte:${mockEndTimestamp}`,
      });
    });

    it("should calculate balance change with mixed incoming and outgoing transfers", async () => {
      const mockTransactions = [
        {
          consensus_timestamp: "1762202065.000000000",
          transfers: [
            { account: mockAddress, amount: 2000 },
            { account: "0.0.98", amount: -2000 },
          ],
        },
        {
          consensus_timestamp: "1762202070.000000000",
          transfers: [
            { account: mockAddress, amount: -500 },
            { account: "0.0.99", amount: 500 },
          ],
        },
        {
          consensus_timestamp: "1762202072.000000000",
          transfers: [
            { account: mockAddress, amount: 300 },
            { account: "0.0.100", amount: -300 },
          ],
        },
      ] as HederaMirrorTransaction[];

      (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValueOnce(
        mockTransactions,
      );

      const result = await calculateUncommittedBalanceChange({
        configOrCurrencyId: defaultConfig,
        address: mockAddress,
        startTimestamp: mockStartTimestamp,
        endTimestamp: mockEndTimestamp,
      });

      expect(result).toEqual(new BigNumber(1800)); // 2000 - 500 + 300
    });

    it("should ignore transfers for other accounts", async () => {
      const mockTransactions = [
        {
          consensus_timestamp: "1762202065.000000000",
          transfers: [
            { account: "0.0.98", amount: 5000 },
            { account: "0.0.99", amount: -5000 },
          ],
        },
        {
          consensus_timestamp: "1762202070.000000000",
          transfers: [
            { account: mockAddress, amount: 1000 },
            { account: "0.0.100", amount: -1000 },
          ],
        },
      ] as HederaMirrorTransaction[];

      (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValueOnce(
        mockTransactions,
      );

      const result = await calculateUncommittedBalanceChange({
        configOrCurrencyId: defaultConfig,
        address: mockAddress,
        startTimestamp: mockStartTimestamp,
        endTimestamp: mockEndTimestamp,
      });

      expect(result).toEqual(new BigNumber(1000));
    });

    it("should return 0 when timestamps are equal or invalid", async () => {
      (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValueOnce([]);

      const [resultEqual, resultInvalid] = await Promise.all([
        calculateUncommittedBalanceChange({
          configOrCurrencyId: defaultConfig,
          address: mockAddress,
          startTimestamp: mockStartTimestamp,
          endTimestamp: mockStartTimestamp,
        }),
        calculateUncommittedBalanceChange({
          configOrCurrencyId: defaultConfig,
          address: mockAddress,
          startTimestamp: mockEndTimestamp,
          endTimestamp: mockStartTimestamp,
        }),
      ]);

      expect(resultEqual).toEqual(new BigNumber(0));
      expect(resultInvalid).toEqual(new BigNumber(0));
    });
  });

  describe("analyzeStakingOperation", () => {
    const mockAddress = "0.0.12345";
    const mockTimestamp = "1762202064.065172388";
    const mockTx = {
      consensus_timestamp: mockTimestamp,
      name: "CRYPTOUPDATEACCOUNT",
    } as HederaMirrorTransaction;

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

    it("detects DELEGATE operation when staking starts", async () => {
      const accountBefore = getMockedMirrorAccount({ staked_node_id: null });
      const accountAfter = getMockedMirrorAccount({ staked_node_id: 5 });

      (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValueOnce([]);
      (apiClient.getAccount as jest.Mock)
        .mockResolvedValueOnce(accountBefore)
        .mockResolvedValueOnce(accountAfter);

      const result = await analyzeStakingOperation({
        configOrCurrencyId: defaultConfig,
        address: mockAddress,
        mirrorTx: mockTx,
      });

      expect(result).toEqual({
        operationType: "DELEGATE",
        previousStakingNodeId: null,
        targetStakingNodeId: 5,
        stakedAmount: BigInt(1000),
      });
      expect(apiClient.getAccount).toHaveBeenCalledTimes(2);
      expect(apiClient.getAccount).toHaveBeenCalledWith({
        configOrCurrencyId: defaultConfig,
        address: mockAddress,
        timestamp: `lt:${mockTimestamp}`,
      });
      expect(apiClient.getAccount).toHaveBeenCalledWith({
        configOrCurrencyId: defaultConfig,
        address: mockAddress,
        timestamp: `eq:${mockTimestamp}`,
      });
    });

    it("detects UNDELEGATE operation when staking stops", async () => {
      const accountBefore = getMockedMirrorAccount({ staked_node_id: 5 });
      const accountAfter = getMockedMirrorAccount({ staked_node_id: null });

      (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValueOnce([]);
      (apiClient.getAccount as jest.Mock)
        .mockResolvedValueOnce(accountBefore)
        .mockResolvedValueOnce(accountAfter);

      const result = await analyzeStakingOperation({
        configOrCurrencyId: defaultConfig,
        address: mockAddress,
        mirrorTx: mockTx,
      });

      expect(result).toEqual({
        operationType: "UNDELEGATE",
        previousStakingNodeId: 5,
        targetStakingNodeId: null,
        stakedAmount: BigInt(1000),
      });
    });

    it("detects REDELEGATE operation when changing nodes", async () => {
      const accountBefore = getMockedMirrorAccount({ staked_node_id: 3 });
      const accountAfter = getMockedMirrorAccount({ staked_node_id: 10 });

      (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValueOnce([]);
      (apiClient.getAccount as jest.Mock)
        .mockResolvedValueOnce(accountBefore)
        .mockResolvedValueOnce(accountAfter);

      const result = await analyzeStakingOperation({
        configOrCurrencyId: defaultConfig,
        address: mockAddress,
        mirrorTx: mockTx,
      });

      expect(result).toEqual({
        operationType: "REDELEGATE",
        previousStakingNodeId: 3,
        targetStakingNodeId: 10,
        stakedAmount: BigInt(1000),
      });
    });

    it("calculates correct staked amount with uncommitted transactions", async () => {
      const mockBalance = { balance: 1000000, timestamp: "1762202060.000000000", tokens: [] };
      const mockAccountBefore = getMockedMirrorAccount({
        account: mockAddress,
        staked_node_id: null,
        balance: mockBalance,
      });
      const mockAccountAfter = getMockedMirrorAccount({
        account: mockAddress,
        staked_node_id: 5,
        balance: mockBalance,
      });
      const mockTransactionsMissingInBalance = [
        {
          consensus_timestamp: `${Math.floor(Number(mockBalance.timestamp)) + 5}.000000000`,
          transfers: [
            { account: mockAddress, amount: -100000 },
            { account: "0.0.98", amount: 100000 },
          ],
        },
      ] as HederaMirrorTransaction[];

      (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValueOnce(
        mockTransactionsMissingInBalance,
      );
      (apiClient.getAccount as jest.Mock)
        .mockResolvedValueOnce(mockAccountBefore)
        .mockResolvedValueOnce(mockAccountAfter);

      const result = await analyzeStakingOperation({
        configOrCurrencyId: defaultConfig,
        address: mockAddress,
        mirrorTx: mockTx,
      });

      expect(apiClient.getTransactionsByTimestampRange).toHaveBeenCalledTimes(1);
      expect(apiClient.getTransactionsByTimestampRange).toHaveBeenCalledWith({
        configOrCurrencyId: defaultConfig,
        address: mockAddress,
        startTimestamp: `gt:${mockAccountBefore.balance.timestamp}`,
        endTimestamp: `lte:${mockTimestamp}`,
      });
      expect(result).toEqual({
        operationType: "DELEGATE",
        previousStakingNodeId: null,
        targetStakingNodeId: 5,
        stakedAmount: BigInt(900000),
      });
    });

    it("returns null for regular account update (both null)", async () => {
      const accountBefore = getMockedMirrorAccount({ staked_node_id: null });
      const accountAfter = getMockedMirrorAccount({ staked_node_id: null });

      (apiClient.getAccount as jest.Mock)
        .mockResolvedValueOnce(accountBefore)
        .mockResolvedValueOnce(accountAfter);

      const result = await analyzeStakingOperation({
        configOrCurrencyId: defaultConfig,
        address: mockAddress,
        mirrorTx: mockTx,
      });

      expect(result).toBeNull();
    });

    it("returns null when staked node doesn't change", async () => {
      const accountBefore = getMockedMirrorAccount({ staked_node_id: 5 });
      const accountAfter = getMockedMirrorAccount({ staked_node_id: 5 });

      (apiClient.getAccount as jest.Mock)
        .mockResolvedValueOnce(accountBefore)
        .mockResolvedValueOnce(accountAfter);

      const result = await analyzeStakingOperation({
        configOrCurrencyId: defaultConfig,
        address: mockAddress,
        mirrorTx: mockTx,
      });

      expect(result).toBeNull();
    });
  });
});
