import { setupMockCryptoAssetsStore } from "@ledgerhq/cryptoassets/cal-client/test-helpers";
import { encodeTokenAccountId } from "@ledgerhq/ledger-wallet-framework/account/accountId";
import { encodeOperationId } from "@ledgerhq/ledger-wallet-framework/operation";
import { getEnv } from "@ledgerhq/live-env";
import BigNumber from "bignumber.js";
import { apiClient } from "../network/api";
import { getMockedCurrency } from "../test/fixtures/currency.fixture";
import type { HederaMirrorTransaction } from "../types";
import { listOperations } from "./listOperations";
import * as utils from "./utils";

setupMockCryptoAssetsStore();
jest.mock("@ledgerhq/ledger-wallet-framework/account/accountId");
jest.mock("@ledgerhq/ledger-wallet-framework/operation");
jest.mock("../network/api");
jest.mock("./utils");

describe("listOperations", () => {
  beforeEach(() => {
    jest.clearAllMocks();
    (utils.extractFeesPayer as jest.Mock).mockImplementation(input =>
      typeof input === "string"
        ? input.split("-")[0]
        : (input.transaction_id?.split("-")[0] ?? "0.0.0"),
    );
    (utils.getMemoFromBase64 as jest.Mock).mockImplementation(memo =>
      memo ? `decoded-${memo}` : null,
    );
    (utils.createStakingRewardOperationHash as jest.Mock).mockImplementation(
      hash => `${hash}-reward`,
    );
    (encodeOperationId as jest.Mock).mockImplementation(
      (accountId, hash, type) => `${accountId}-${hash}-${type}`,
    );
    (encodeTokenAccountId as jest.Mock).mockImplementation(
      (accountId, token) => `${accountId}-${token.id}`,
    );
  });

  it("should return empty arrays when no transactions are found", async () => {
    const address = "0.0.12345";
    const mockCurrency = getMockedCurrency();

    (apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
      transactions: [],
      nextCursor: null,
    });

    const result = await listOperations({
      currency: mockCurrency,
      address,
      limit: 10,
      order: "asc",
      mirrorTokens: [],
      fetchAllPages: true,
      skipFeesForTokenOperations: false,
      useEncodedHash: false,
      useSyntheticBlocks: false,
    });

    expect(apiClient.getAccountTransactions).toHaveBeenCalledTimes(1);
    expect(apiClient.getAccountTransactions).toHaveBeenCalledWith({
      address,
      fetchAllPages: true,
      pagingToken: null,
      order: "asc",
      limit: 10,
    });
    expect(result.coinOperations).toEqual([]);
    expect(result.tokenOperations).toEqual([]);
  });

  it("should parse HBAR transfer transactions correctly", async () => {
    const address = "0.0.1234567";
    const mockCurrency = getMockedCurrency();

    const mockTransactions: Partial<HederaMirrorTransaction>[] = [
      {
        consensus_timestamp: "1625097600.000000000",
        transaction_hash: "hash1",
        transaction_id: "0.0.1234567-1625097600-000000000",
        charged_tx_fee: 500000,
        result: "SUCCESS",
        memo_base64: "test-memo",
        token_transfers: [],
        staking_reward_transfers: [],
        transfers: [
          { account: address, amount: -1000000 },
          { account: "0.0.67890", amount: 1000000 },
        ],
        name: "CRYPTOTRANSFER",
      },
    ];

    (apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
      transactions: mockTransactions,
      nextCursor: null,
    });

    const result = await listOperations({
      currency: mockCurrency,
      address,
      limit: 10,
      order: "desc",
      mirrorTokens: [],
      fetchAllPages: true,
      skipFeesForTokenOperations: false,
      useEncodedHash: false,
      useSyntheticBlocks: false,
    });

    expect(result.tokenOperations).toEqual([]);
    expect(result.coinOperations).toHaveLength(1);
    expect(result.coinOperations).toMatchObject([
      {
        type: "OUT",
        value: expect.any(Object),
        hash: "hash1",
        fee: expect.any(Object),
        date: expect.any(Date),
        senders: [address],
        recipients: ["0.0.67890"],
        extra: {
          pagingToken: "1625097600.000000000",
          consensusTimestamp: "1625097600.000000000",
          transactionId: "0.0.1234567-1625097600-000000000", // <-- the transactionId is used in upstream layers to identify the fees payer
          memo: "decoded-test-memo",
        },
      },
    ]);
  });

  it("should parse token transfer transactions correctly", async () => {
    const address = "0.0.12345";
    const mockCurrency = getMockedCurrency();
    const tokenId = "0.0.7890";

    const mockToken = {
      id: "token1",
      contractAddress: tokenId,
      standard: "hts",
      name: "Test Token",
      units: [{ name: "TT", code: "tt", magnitude: 6 }],
    };

    const mockTransactions: Partial<HederaMirrorTransaction>[] = [
      {
        consensus_timestamp: "1625097600.000000000",
        transaction_hash: "hash1",
        transaction_id: "0.0.12345-1625097600-000000000",
        charged_tx_fee: 500000,
        result: "SUCCESS",
        token_transfers: [
          { token_id: tokenId, account: address, amount: -1000 },
          { token_id: tokenId, account: "0.0.67890", amount: 1000 },
        ],
        staking_reward_transfers: [],
        transfers: [],
        name: "CRYPTOTRANSFER",
      },
    ];

    (apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
      transactions: mockTransactions,
      nextCursor: null,
    });

    const findTokenByAddressInCurrencyMock = jest.fn().mockResolvedValue(mockToken);
    setupMockCryptoAssetsStore({
      findTokenByAddressInCurrency: findTokenByAddressInCurrencyMock,
    });

    const result = await listOperations({
      currency: mockCurrency,
      address,
      limit: 10,
      order: "desc",
      mirrorTokens: [],
      fetchAllPages: true,
      skipFeesForTokenOperations: false,
      useEncodedHash: false,
      useSyntheticBlocks: false,
    });

    expect(result.coinOperations).toMatchObject([
      {
        type: "FEES",
        fee: expect.any(Object),
      },
    ]);
    expect(result.tokenOperations).toMatchObject([
      {
        type: "OUT",
        value: expect.any(Object),
        hash: "hash1",
        contract: tokenId,
        standard: "hts",
        senders: [address],
        recipients: ["0.0.67890"],
        extra: {
          pagingToken: "1625097600.000000000",
          consensusTimestamp: "1625097600.000000000",
          transactionId: "0.0.12345-1625097600-000000000", // <-- the transactionId is used in upstream layers to identify the fees payer
        },
      },
    ]);
  });

  it("should parse token associate transactions correctly", async () => {
    const address = "0.0.12345";
    const mockCurrency = getMockedCurrency();

    const mockTransactions: Partial<HederaMirrorTransaction>[] = [
      {
        consensus_timestamp: "1625097600.000000000",
        transaction_hash: "hash1",
        transaction_id: "0.0.12345-1625097600-000000000",
        charged_tx_fee: 500000,
        result: "SUCCESS",
        token_transfers: [],
        staking_reward_transfers: [],
        transfers: [{ account: address, amount: -500000 }],
        name: "TOKENASSOCIATE",
      },
    ];

    (apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
      transactions: mockTransactions,
      nextCursor: null,
    });

    const result = await listOperations({
      currency: mockCurrency,
      address,
      limit: 10,
      order: "desc",
      mirrorTokens: [],
      fetchAllPages: true,
      skipFeesForTokenOperations: false,
      useEncodedHash: false,
      useSyntheticBlocks: false,
    });

    expect(result.tokenOperations).toEqual([]);
    expect(result.coinOperations).toMatchObject([
      {
        type: "ASSOCIATE_TOKEN",
        value: expect.any(Object),
        hash: "hash1",
        fee: expect.any(Object),
        senders: [address],
        recipients: [],
        extra: {
          pagingToken: "1625097600.000000000",
          consensusTimestamp: "1625097600.000000000",
          transactionId: "0.0.12345-1625097600-000000000", // <-- the transactionId is used in upstream layers to identify the fees payer
        },
      },
    ]);
  });

  it("should skip token operations when token is not found in cryptoassets", async () => {
    const address = "0.0.12345";
    const mockCurrency = getMockedCurrency();
    const tokenId = "0.0.7890";

    const mockTransactions: Partial<HederaMirrorTransaction>[] = [
      {
        consensus_timestamp: "1625097600.000000000",
        transaction_hash: "hash1",
        charged_tx_fee: 500000,
        result: "SUCCESS",
        token_transfers: [
          { token_id: tokenId, account: address, amount: -1000 },
          { token_id: tokenId, account: "0.0.67890", amount: 1000 },
        ],
        staking_reward_transfers: [],
        transfers: [],
        name: "CRYPTOTRANSFER",
      },
    ];

    (apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
      transactions: mockTransactions,
      nextCursor: null,
    });

    const findTokenByAddressInCurrencyMock = jest.fn().mockResolvedValue(null);
    setupMockCryptoAssetsStore({
      findTokenByAddressInCurrency: findTokenByAddressInCurrencyMock,
    });

    const result = await listOperations({
      currency: mockCurrency,
      address,
      limit: 10,
      order: "desc",
      mirrorTokens: [],
      fetchAllPages: true,
      skipFeesForTokenOperations: false,
      useEncodedHash: false,
      useSyntheticBlocks: false,
    });

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

  it("should use pagination parameters correctly", async () => {
    const address = "0.0.12345";
    const mockCurrency = getMockedCurrency();

    (apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
      transactions: [],
      nextCursor: null,
    });

    await listOperations({
      currency: mockCurrency,
      address,
      limit: 20,
      order: "asc",
      cursor: "1625097500.000000000",
      mirrorTokens: [],
      fetchAllPages: true,
      skipFeesForTokenOperations: false,
      useEncodedHash: false,
      useSyntheticBlocks: false,
    });

    expect(apiClient.getAccountTransactions).toHaveBeenCalledTimes(1);
    expect(apiClient.getAccountTransactions).toHaveBeenCalledWith({
      address,
      fetchAllPages: true,
      pagingToken: "1625097500.000000000",
      order: "asc",
      limit: 20,
    });
  });

  it("should handle failed transactions", async () => {
    const address = "0.0.12345";
    const mockCurrency = getMockedCurrency();

    const mockTransactions: Partial<HederaMirrorTransaction>[] = [
      {
        consensus_timestamp: "1625097600.000000000",
        transaction_hash: "hash1",
        transaction_id: "0.0.12345-1625097600-000000000",
        charged_tx_fee: 500000,
        result: "INVALID_SIGNATURE",
        memo_base64: "",
        token_transfers: [],
        staking_reward_transfers: [],
        transfers: [
          { account: address, amount: -1000000 },
          { account: "0.0.67890", amount: 1000000 },
        ],
        name: "CRYPTOTRANSFER",
      },
    ];

    (apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
      transactions: mockTransactions,
      nextCursor: null,
    });

    const result = await listOperations({
      currency: mockCurrency,
      address,
      limit: 10,
      order: "desc",
      mirrorTokens: [],
      fetchAllPages: true,
      skipFeesForTokenOperations: false,
      useEncodedHash: false,
      useSyntheticBlocks: false,
    });

    expect(result.coinOperations).toMatchObject([
      {
        hasFailed: true,
        extra: {
          transactionId: "0.0.12345-1625097600-000000000",
          feesPayer: "0.0.12345",
        },
      },
    ]);
  });

  it("should include inferred fees payer in operation extra", async () => {
    const address = "0.0.12345";
    const mockCurrency = getMockedCurrency();

    (utils.extractFeesPayer as jest.Mock).mockReturnValue("0.0.23");
    (apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
      transactions: [
        {
          consensus_timestamp: "1625097600.000000000",
          transaction_hash: "hash1",
          transaction_id: "0.0.10067173-1761755118-730000493",
          charged_tx_fee: 40743,
          result: "INSUFFICIENT_PAYER_BALANCE",
          memo_base64: "",
          token_transfers: [],
          staking_reward_transfers: [],
          transfers: [
            { account: "0.0.23", amount: -40743 },
            { account: "0.0.801", amount: 40743 },
          ],
          name: "CRYPTOTRANSFER",
        },
      ],
      nextCursor: null,
    });

    const result = await listOperations({
      currency: mockCurrency,
      address,
      limit: 10,
      order: "desc",
      mirrorTokens: [],
      fetchAllPages: true,
      skipFeesForTokenOperations: false,
      useEncodedHash: false,
      useSyntheticBlocks: false,
    });

    expect(result.coinOperations).toMatchObject([
      {
        extra: {
          transactionId: "0.0.10067173-1761755118-730000493",
          feesPayer: "0.0.23",
        },
      },
    ]);
  });

  it("should create REWARD operation when staking rewards are present", async () => {
    const address = "0.0.1234567";
    const mockCurrency = getMockedCurrency();
    const mockTransaction: Partial<HederaMirrorTransaction> = {
      consensus_timestamp: "1625097600.000000000",
      transaction_hash: "hash1",
      transaction_id: "0.0.1234567-1625097600-000000000",
      charged_tx_fee: 500000,
      result: "SUCCESS",
      memo_base64: "",
      token_transfers: [],
      staking_reward_transfers: [{ account: address, amount: 1000000 }],
      transfers: [{ account: address, amount: -500000 }],
      name: "CRYPTOTRANSFER",
    };

    (apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
      transactions: [mockTransaction],
      nextCursor: null,
    });

    const result = await listOperations({
      currency: mockCurrency,
      address,
      limit: 10,
      order: "desc",
      mirrorTokens: [],
      fetchAllPages: true,
      skipFeesForTokenOperations: false,
      useEncodedHash: false,
      useSyntheticBlocks: false,
    });

    const rewardTimestamp = result.coinOperations[0].date.getTime();
    const mainTimestamp = result.coinOperations[1].date.getTime();

    expect(result.tokenOperations).toEqual([]);
    expect(rewardTimestamp).toBe(mainTimestamp + 1);
    expect(result.coinOperations).toMatchObject([
      {
        type: "REWARD",
        hash: utils.createStakingRewardOperationHash(mockTransaction.transaction_hash ?? ""),
        value: new BigNumber(1000000),
        fee: new BigNumber(0),
        senders: [getEnv("HEDERA_STAKING_REWARD_ACCOUNT_ID")],
        recipients: [address],
        extra: { transactionId: "0.0.1234567-1625097600-000000000" }, // <-- the transactionId is used in upstream layers to identify the fees payer
      },
      {
        type: "OUT",
        hash: mockTransaction.transaction_hash,
        extra: { transactionId: "0.0.1234567-1625097600-000000000" }, // <-- the transactionId is used in upstream layers to identify the fees payer
      },
    ]);
  });
});
