import { genericPrepareTransaction } from "../prepareTransaction";
import { getAlpacaApi } from "../alpaca";
import { getBridgeApi } from "../bridge";
import { transactionToIntent } from "../utils";
import BigNumber from "bignumber.js";
import { GenericTransaction } from "../types";
import { setupMockCryptoAssetsStore } from "@ledgerhq/cryptoassets/cal-client/test-helpers";
import { TokenCurrency } from "@ledgerhq/types-cryptoassets";
import { decodeTokenAccountId } from "@ledgerhq/ledger-wallet-framework/account/index";

jest.mock("../alpaca", () => ({
  getAlpacaApi: jest.fn(),
}));
jest.mock("../bridge", () => ({
  getBridgeApi: jest.fn(),
}));
jest.mock("@ledgerhq/ledger-wallet-framework/account/index", () => {
  const actual = jest.requireActual("@ledgerhq/ledger-wallet-framework/account/index");
  return {
    ...actual,
    decodeTokenAccountId: jest.fn(actual.decodeTokenAccountId),
  };
});

jest.mock("../utils", () => ({
  ...jest.requireActual("../utils"),
  transactionToIntent: jest.fn(),
  extractBalances: jest.fn(),
}));

describe("genericPrepareTransaction", () => {
  const account = {
    id: "test-account",
    address: "0xabc",
    currency: { id: "ethereum" },
  } as any;

  const baseTransaction = {
    amount: new BigNumber(100_000),
    fees: new BigNumber(500),
    recipient: "0xrecipient",
    family: "family",
  };

  beforeEach(() => {
    jest.clearAllMocks();
    setupMockCryptoAssetsStore({
      findTokenById: () => Promise.resolve(undefined),
    });
    (transactionToIntent as jest.Mock).mockReturnValue({ mock: "intent" });
    (getBridgeApi as jest.Mock).mockReturnValue({
      getAssetFromToken: jest.fn().mockReturnValue(undefined),
    });
  });

  it("updates fees if they differ", async () => {
    const newFee = new BigNumber(700);

    (getAlpacaApi as jest.Mock).mockReturnValue({
      estimateFees: jest.fn().mockResolvedValue({ value: newFee }),
    });

    const prepareTransaction = genericPrepareTransaction("testnet", "local");
    const result = await prepareTransaction(account, { ...baseTransaction });

    expect((result as any).fees.toString()).toBe(newFee.toString());
    expect(transactionToIntent).toHaveBeenCalledWith(
      account,
      expect.objectContaining(baseTransaction),
      undefined,
    );
  });

  it("returns original transaction if fees are the same", async () => {
    const sameFee = baseTransaction.fees;

    (getAlpacaApi as jest.Mock).mockReturnValue({
      estimateFees: jest.fn().mockResolvedValue({ value: sameFee }),
    });

    const prepareTransaction = genericPrepareTransaction("testnet", "local");
    const result = await prepareTransaction(account, baseTransaction);

    expect(result).toBe(baseTransaction);
  });

  it("sets fee if original fees are undefined", async () => {
    const newFee = new BigNumber(1234);
    (getAlpacaApi as jest.Mock).mockReturnValue({
      estimateFees: jest.fn().mockResolvedValue({ value: newFee }),
    });

    const txWithoutFees = { ...baseTransaction, fees: undefined as any };
    const prepareTransaction = genericPrepareTransaction("testnet", "local");
    const result = await prepareTransaction(account, txWithoutFees);

    expect((result as any).fees.toString()).toBe(newFee.toString());
    expect(result).not.toBe(txWithoutFees);
  });

  it("returns original if fees are BigNumber-equal but different instance", async () => {
    const sameValue = new BigNumber(baseTransaction.fees.toString()); // different instance
    (getAlpacaApi as jest.Mock).mockReturnValue({
      estimateFees: jest.fn().mockResolvedValue({ value: sameValue }),
    });

    const prepareTransaction = genericPrepareTransaction("testnet", "local");
    const result = await prepareTransaction(account, baseTransaction);

    expect(result).toBe(baseTransaction); // still same reference
  });

  it.each([
    ["type", 2, 2],
    ["storageLimit", 300n, new BigNumber(300)],
    ["gasLimit", 300n, new BigNumber(300)],
    ["gasPrice", 300n, new BigNumber(300)],
    ["maxFeePerGas", 300n, new BigNumber(300)],
    ["maxPriorityFeePerGas", 300n, new BigNumber(300)],
    ["additionalFees", 300n, new BigNumber(300)],
  ])(
    "propagates %s from estimation parameters",
    async (parameterName, parameterValue, expectedValue) => {
      (getAlpacaApi as jest.Mock).mockReturnValue({
        estimateFees: jest.fn().mockResolvedValue({
          value: new BigNumber(491),
          parameters: { [parameterName]: parameterValue },
        }),
      });

      const txWithoutCustomFees = { ...baseTransaction, customFees: undefined };
      const prepareTransaction = genericPrepareTransaction("testnet", "local");
      const result = await prepareTransaction(account, txWithoutCustomFees);

      expect(result).toEqual(
        expect.objectContaining({
          fees: new BigNumber(491),
          [parameterName]: expectedValue,
          customFees: {
            parameters: {
              fees: undefined,
            },
          },
        }),
      );
    },
  );

  it("does not propagate the custom gas limit", async () => {
    (getAlpacaApi as jest.Mock).mockReturnValue({
      estimateFees: jest.fn().mockResolvedValue({
        value: 100000n,
        parameters: { gasLimit: 22000n }, // custom gasLimit in parameter
      }),
    });

    const txWithoutCustomFees = {
      ...baseTransaction,
      gasLimit: new BigNumber(21000),
      customGasLimit: new BigNumber(22000),
    };
    const prepareTransaction = genericPrepareTransaction("testnet", "local");
    const result = await prepareTransaction(account, txWithoutCustomFees);

    expect(result).toEqual(
      expect.objectContaining({
        fees: new BigNumber(100000),
        gasLimit: new BigNumber(21000),
        customGasLimit: new BigNumber(22000),
      }),
    );
  });

  it("estimates using the token account spendable balance when sending all amount", async () => {
    (decodeTokenAccountId as jest.Mock).mockResolvedValueOnce({
      accountId: "test-sub-account",
      token: undefined,
    });
    const estimateFees = jest.fn().mockResolvedValue({ value: new BigNumber(50) });
    (transactionToIntent as jest.Mock).mockImplementation((_, transaction) => ({
      amount: BigInt(transaction.amount.toFixed()),
    }));
    (getAlpacaApi as jest.Mock).mockReturnValue({
      estimateFees,
      validateIntent: intent => Promise.resolve({ amount: intent.amount }),
    });
    const prepareTransaction = genericPrepareTransaction("testnet", "local");

    await prepareTransaction(
      {
        ...account,
        subAccounts: [{ id: "test-sub-account", spendableBalance: new BigNumber(100) }],
      },
      {
        subAccountId: "test-sub-account",
        useAllAmount: true,
        amount: new BigNumber(0),
      } as GenericTransaction,
    );

    expect(estimateFees).toHaveBeenCalledWith(expect.objectContaining({ amount: 100n }), {});
  });

  it("fills 'assetOwner' and 'assetReference' from 'subAccountId' for retro compatibility", async () => {
    setupMockCryptoAssetsStore({
      findTokenById: tokenId =>
        Promise.resolve(tokenId === "usdc" ? ({ id: tokenId } as TokenCurrency) : undefined),
    });
    (getAlpacaApi as jest.Mock).mockReturnValue({
      estimateFees: () => Promise.resolve({ value: 0n }),
    });
    (getBridgeApi as jest.Mock).mockReturnValue({
      getAssetFromToken: jest.fn().mockImplementation((token: TokenCurrency, owner: string) => ({
        assetOwner: owner,
        assetReference: token.id,
      })),
    });
    const prepareTransaction = genericPrepareTransaction("testnet", "local");

    await prepareTransaction(
      {
        ...account,
        freshAddress: "test-account-address",
        subAccounts: [{ id: "test-sub-account+usdc" }],
      },
      {
        subAccountId: "test-sub-account+usdc",
        amount: new BigNumber(10),
      } as GenericTransaction,
    );

    expect(transactionToIntent).toHaveBeenCalledWith(
      {
        ...account,
        freshAddress: "test-account-address",
        subAccounts: [{ id: "test-sub-account+usdc" }],
      },
      {
        subAccountId: "test-sub-account+usdc",
        amount: new BigNumber(10),
        assetOwner: "test-account-address",
        assetReference: "usdc",
      },
      undefined,
    );
  });
});
