import {
  adaptCoreOperationToLiveOperation,
  applyMemoToIntent,
  bigNumberToBigIntDeep,
  buildOptimisticOperation,
  cleanedOperation,
  extractBalance,
  extractBalances,
  findCryptoCurrencyByNetwork,
  transactionToIntent,
} from "./utils";
import BigNumber from "bignumber.js";
import { Operation as CoreOperation, TransactionIntent } from "@ledgerhq/coin-framework/api/types";
import { Account } from "@ledgerhq/types-live";
import { GenericTransaction, OperationCommon } from "./types";

describe("Alpaca utils", () => {
  describe("applyMemoToIntent", () => {
    it("does not apply any memo", () => {
      const intent = applyMemoToIntent(
        {} as unknown as TransactionIntent,
        {} as GenericTransaction,
      );

      expect(intent).toEqual({});
    });

    it.each([
      [0, "0"],
      [1, "1"],
    ])("applies '%s' as the destination tag", (tag, expectedTag) => {
      const intent = applyMemoToIntent(
        {} as unknown as TransactionIntent,
        { tag } as GenericTransaction,
      );

      expect(intent).toEqual({
        memo: { type: "map", memos: new Map([["destinationTag", expectedTag]]) },
      });
    });

    it("applies a custom memo type", () => {
      const intent = applyMemoToIntent(
        {} as unknown as TransactionIntent,
        { memoType: "memo-type", memoValue: "memo-value" } as GenericTransaction,
      );

      expect(intent).toEqual({
        memo: { type: "memo-type", value: "memo-value" },
      });
    });
  });

  describe("bigNumberToBigIntDeep", () => {
    it.each([
      [undefined, undefined],
      [null, null],
      ["", ""],
      ["str", "str"],
      [0, 0],
      [1, 1],
      [true, true],
      [false, false],
      [new BigNumber(0), 0n],
      [new BigNumber(1), 1n],
      [[], []],
      [
        ["str", 1],
        ["str", 1],
      ],
      [
        ["str", BigNumber(1)],
        ["str", 1n],
      ],
      [
        [new BigNumber(0), new BigNumber(1)],
        [0n, 1n],
      ],
      [{}, {}],
      [
        { a: "str", b: 0, c: true },
        { a: "str", b: 0, c: true },
      ],
      [
        { a: "str", b: new BigNumber(1), c: true },
        { a: "str", b: 1n, c: true },
      ],
      [
        { a: "str", b: new BigNumber(1), c: { ca: new BigNumber(2), cb: 4 } },
        { a: "str", b: 1n, c: { ca: 2n, cb: 4 } },
      ],
      [
        { a: "str", b: new BigNumber(1), c: { ca: new BigNumber(2), cb: null } },
        { a: "str", b: 1n, c: { ca: 2n, cb: null } },
      ],
      [
        { a: "str", b: new BigNumber(1), c: { ca: new BigNumber(2), cb: undefined } },
        { a: "str", b: 1n, c: { ca: 2n } },
      ],
    ])("replaces BigNumbers with BigInts (%j)", (input, output) => {
      expect(bigNumberToBigIntDeep(input)).toStrictEqual(output);
    });
  });

  describe("buildOptimisticOperation", () => {
    it.each([
      [
        "coin",
        "changeTrust",
        {},
        {
          parentType: "OPT_IN",
          subType: undefined,
          parentValue: new BigNumber(50),
          parentRecipient: "recipient-address",
        },
      ],
      [
        "coin",
        "delegate",
        {},
        {
          parentType: "DELEGATE",
          subType: undefined,
          parentValue: new BigNumber(50),
          parentRecipient: "recipient-address",
        },
      ],
      [
        "coin",
        "stake",
        {},
        {
          parentType: "DELEGATE",
          subType: undefined,
          parentValue: new BigNumber(50),
          parentRecipient: "recipient-address",
        },
      ],
      [
        "coin",
        "undelegate",
        {},
        {
          parentType: "UNDELEGATE",
          subType: undefined,
          parentValue: new BigNumber(50),
          parentRecipient: "recipient-address",
        },
      ],
      [
        "coin",
        "unstake",
        {},
        {
          parentType: "UNDELEGATE",
          subType: undefined,
          parentValue: new BigNumber(50),
          parentRecipient: "recipient-address",
        },
      ],
      [
        "coin",
        "send",
        {},
        {
          parentType: "OUT",
          subType: undefined,
          parentValue: new BigNumber(50),
          parentRecipient: "recipient-address",
        },
      ],
      [
        "token",
        "changeTrust",
        { subAccountId: "sub-account-id" },
        {
          parentType: "FEES",
          subType: "OPT_IN",
          parentValue: new BigNumber(12),
          parentRecipient: "contract-address",
        },
      ],
      [
        "token",
        "delegate",
        { subAccountId: "sub-account-id" },
        {
          parentType: "FEES",
          subType: "DELEGATE",
          parentValue: new BigNumber(12),
          parentRecipient: "contract-address",
        },
      ],
      [
        "token",
        "stake",
        { subAccountId: "sub-account-id" },
        {
          parentType: "FEES",
          subType: "DELEGATE",
          parentValue: new BigNumber(12),
          parentRecipient: "contract-address",
        },
      ],
      [
        "token",
        "undelegate",
        { subAccountId: "sub-account-id" },
        {
          parentType: "FEES",
          subType: "UNDELEGATE",
          parentValue: new BigNumber(12),
          parentRecipient: "contract-address",
        },
      ],
      [
        "token",
        "unstake",
        { subAccountId: "sub-account-id" },
        {
          parentType: "FEES",
          subType: "UNDELEGATE",
          parentValue: new BigNumber(12),
          parentRecipient: "contract-address",
        },
      ],
      [
        "token",
        "send",
        { subAccountId: "sub-account-id" },
        {
          parentType: "FEES",
          subType: "OUT",
          parentValue: new BigNumber(12),
          parentRecipient: "contract-address",
        },
      ],
    ])("builds an optimistic %s operation with %s mode", (_s, mode, params, expected) => {
      const operation = buildOptimisticOperation(
        {
          id: "parent-account-id",
          freshAddress: "account-address",
          subAccounts: [{ id: "sub-account-id", token: { contractAddress: "contract-address" } }],
        } as Account,
        {
          mode,
          amount: new BigNumber(50),
          fees: new BigNumber(12),
          recipient: "recipient-address",
          recipientDomain: {
            registry: "ens",
            domain: "recipient.eth",
            address: "recipient-address",
            type: "forward",
          },
          ...params,
        } as GenericTransaction,
        3n,
      );

      expect(operation).toMatchObject({
        id: `parent-account-id--${expected.parentType}`,
        transactionSequenceNumber: new BigNumber(3),
        type: expected.parentType,
        value: expected.parentValue,
        accountId: "parent-account-id",
        senders: ["account-address"],
        recipients: ["recipient-address"],
        fee: new BigNumber(12),
        blockHash: null,
        blockHeight: null,
        transactionRaw: {
          amount: expected.subType ? "0" : expected.parentValue.toFixed(),
          fees: "12",
          recipient: expected.parentRecipient,
          recipientDomain: {
            registry: "ens",
            domain: "recipient.eth",
            address: "recipient-address",
            type: "forward",
          },
        },
        ...(expected.subType
          ? {
              subOperations: [
                {
                  id: `sub-account-id--${expected.subType}`,
                  transactionSequenceNumber: new BigNumber(3),
                  accountId: "sub-account-id",
                  type: expected.subType,
                  senders: ["account-address"],
                  recipients: ["recipient-address"],
                  fee: new BigNumber(12),
                  value: new BigNumber(50),
                  blockHash: null,
                  blockHeight: null,
                  transactionRaw: {
                    amount: "50",
                    fees: "12",
                    recipient: "recipient-address",
                  },
                },
              ],
            }
          : {}),
      });
    });
  });

  describe("cleanedOperation", () => {
    it("creates a cleaned version of an operation without mutating it", () => {
      const dirty = {
        id: "id",
        hash: "hash",
        senders: ["sender"],
        recipients: ["recipient"],
        extra: { assetAmount: 5, assetReference: "USDC", paginationToken: "pagination" },
      } as unknown as OperationCommon;

      const clean = cleanedOperation(dirty);

      expect(clean).toEqual({
        id: "id",
        hash: "hash",
        senders: ["sender"],
        recipients: ["recipient"],
        extra: { paginationToken: "pagination" },
      });
      expect(dirty).toEqual({
        id: "id",
        hash: "hash",
        senders: ["sender"],
        recipients: ["recipient"],
        extra: { assetAmount: 5, assetReference: "USDC", paginationToken: "pagination" },
      });
    });
  });

  describe("transactionToIntent", () => {
    describe("type", () => {
      it("fallbacks to 'Payment' without a transaction mode", () => {
        expect(
          transactionToIntent(
            { currency: { name: "ethereum", units: [{}] } } as Account,
            { mode: undefined } as GenericTransaction,
          ),
        ).toMatchObject({
          type: "Payment",
        });
      });

      it.each([
        ["changeTrust", "changeTrust"],
        ["send", "send"],
        ["send-legacy", "send-legacy"],
        ["send-eip1559", "send-eip1559"],
        ["stake", "stake"],
        ["unstake", "unstake"],
        ["delegate", "stake"],
        ["undelegate", "unstake"],
      ])(
        "by default, associates '%s' transaction mode to '%s' intent type",
        (mode, expectedType) => {
          expect(
            transactionToIntent(
              { currency: { name: "ethereum", units: [{}] } } as Account,
              { mode } as GenericTransaction,
            ),
          ).toMatchObject({
            type: expectedType,
          });
        },
      );

      it("rejects other modes", () => {
        expect(() =>
          transactionToIntent(
            { currency: { name: "ethereum", units: [{}] } } as Account,
            { mode: "any" as unknown } as GenericTransaction,
          ),
        ).toThrow("Unsupported transaction mode: any");
      });

      it("supersedes the logic with a custom function", () => {
        const computeIntentType = (transaction: GenericTransaction) =>
          transaction.mode === "send" && transaction.type === 2 ? "send-eip1559" : "send-legacy";

        expect(
          transactionToIntent(
            { currency: { name: "ethereum", units: [{}] } } as Account,
            { mode: "send", type: 2 } as GenericTransaction,
            computeIntentType,
          ),
        ).toMatchObject({
          type: "send-eip1559",
        });
      });
    });
  });

  describe("findCryptoCurrencyByNetwork", () => {
    it("finds a crypto currency by id", () => {
      expect(findCryptoCurrencyByNetwork("ethereum")).toMatchObject({
        id: "ethereum",
        family: "evm",
      });
    });

    it("takes currency remapping into account", () => {
      expect(findCryptoCurrencyByNetwork("ripple")).toMatchObject({
        id: "ripple",
        family: "xrp",
      });
      expect(findCryptoCurrencyByNetwork("xrp")).toMatchObject({
        id: "ripple",
        family: "xrp",
      });
    });

    it("does not find non existing currencies", () => {
      expect(findCryptoCurrencyByNetwork("non_existing_currency")).toBeUndefined();
    });
  });

  describe("extractBalances", () => {
    it("extracts native balance only", () => {
      expect(
        extractBalances({
          spendableBalance: BigNumber(10),
          balance: BigNumber(10),
        } as unknown as Account),
      ).toEqual([{ value: 10n, locked: 0n, asset: { type: "native" } }]);

      expect(
        extractBalances({
          spendableBalance: BigNumber(8),
          balance: BigNumber(10),
        } as unknown as Account),
      ).toEqual([{ value: 10n, locked: 2n, asset: { type: "native" } }]);
    });

    it("extracts native and token balances", () => {
      expect(
        extractBalances(
          {
            spendableBalance: BigNumber(10),
            balance: BigNumber(10),
            subAccounts: [
              {
                spendableBalance: BigNumber(11),
                balance: BigNumber(20),
                token: {
                  tokenType: "erc20",
                  contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
                },
              },
            ],
          } as unknown as Account,
          token => ({
            type: token.tokenType,
            assetReference: token.contractAddress,
          }),
        ),
      ).toEqual([
        { value: 10n, locked: 0n, asset: { type: "native" } },
        {
          asset: {
            assetReference: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
            type: "erc20",
          },
          locked: 9n,
          value: 20n,
        },
      ]);
    });
  });

  describe("extractBalance", () => {
    it("extracts an existing balance", () => {
      expect(extractBalance([{ value: 4n, asset: { type: "type1" } }], "type1")).toEqual({
        value: 4n,
        asset: { type: "type1" },
      });
    });

    it("generates an empty balance for a missing type", () => {
      expect(extractBalance([{ value: 4n, asset: { type: "type1" } }], "type2")).toEqual({
        value: 0n,
        asset: { type: "type2" },
      });
    });
  });

  jest.mock("@ledgerhq/ledger-wallet-framework/operation", () => ({
    encodeOperationId: jest.fn((accountId, txHash, opType) => `${accountId}-${txHash}-${opType}`),
  }));

  describe("adaptCoreOperationToLiveOperation", () => {
    const accountId = "acc_123";
    const baseOp: CoreOperation = {
      id: "op_123",
      asset: { type: "native" },
      type: "OUT",
      value: BigInt(100),
      tx: {
        hash: "txhash123",
        fees: BigInt(10),
        block: {
          hash: "blockhash123",
          height: 123456,
          time: new Date("2025-08-29T12:00:00Z"),
        },
        date: new Date("2025-08-29T12:00:00Z"),
        failed: false,
      },
      senders: ["sender1"],
      recipients: ["recipient1"],
    };

    it("does not include fees in non native asset value", () => {
      expect(
        adaptCoreOperationToLiveOperation("account", {
          id: "operation",
          asset: { type: "token", assetOwner: "owner", assetReference: "reference" },
          type: "OUT",
          value: BigInt(100),
          tx: {
            hash: "hash",
            fees: BigInt(10),
            block: {
              hash: "block_hash",
              height: 123456,
              time: new Date("2025-08-29T12:00:00Z"),
            },
            date: new Date("2025-08-29T12:00:00Z"),
            failed: false,
          },
          senders: ["sender"],
          recipients: ["recipient"],
        }),
      ).toEqual({
        id: "account-hash-OUT",
        hash: "hash",
        accountId: "account",
        type: "OUT",
        value: new BigNumber(100), // value only
        fee: new BigNumber(10),
        extra: {
          assetOwner: "owner",
          assetReference: "reference",
        },
        blockHash: "block_hash",
        blockHeight: 123456,
        senders: ["sender"],
        recipients: ["recipient"],
        date: new Date("2025-08-29T12:00:00Z"),
        transactionSequenceNumber: undefined,
        hasFailed: false,
      });
    });

    it("adapts a basic OUT operation", () => {
      const result = adaptCoreOperationToLiveOperation(accountId, baseOp);

      expect(result).toEqual({
        id: "acc_123-txhash123-OUT",
        hash: "txhash123",
        accountId,
        type: "OUT",
        value: new BigNumber(110), // value + fee
        fee: new BigNumber(10),
        blockHash: "blockhash123",
        blockHeight: 123456,
        senders: ["sender1"],
        recipients: ["recipient1"],
        date: new Date("2025-08-29T12:00:00Z"),
        transactionSequenceNumber: undefined,
        hasFailed: false,
        extra: {},
      });
    });

    it.each([["FEES"], ["DELEGATE"], ["UNDELEGATE"]])(
      "handles %s operation where value = value + fees",
      operationType => {
        const op = {
          ...baseOp,
          type: operationType,
          value: BigInt(5),
          tx: { ...baseOp.tx, fees: BigInt(2) },
        };

        const result = adaptCoreOperationToLiveOperation(accountId, op);

        expect(result.value.toString()).toEqual("7");
      },
    );

    it("handles non-FEES/OUT operation where value = value only", () => {
      const op = {
        ...baseOp,
        type: "IN",
        value: BigInt(50),
        tx: { ...baseOp.tx, fees: BigInt(2) },
      };

      const result = adaptCoreOperationToLiveOperation(accountId, op);

      expect(result.value.toString()).toEqual("50");
    });

    it("shows fees in value when transaction has failed", () => {
      const failedOp = {
        ...baseOp,
        type: "OUT",
        value: BigInt(100),
        tx: { ...baseOp.tx, fees: BigInt(25), failed: true },
      };

      const result = adaptCoreOperationToLiveOperation(accountId, failedOp);

      expect(result).toMatchObject({
        hasFailed: true,
        value: new BigNumber(25),
        fee: new BigNumber(25),
      });
    });
  });
});
