import BigNumber from "bignumber.js";
import { HEDERA_OPERATION_TYPES, HEDERA_TRANSACTION_MODES } from "../constants";
import { estimateFees } from "../logic/estimateFees";
import { getMockedAccount, getMockedTokenAccount } from "../test/fixtures/account.fixture";
import {
  getMockedERC20TokenCurrency,
  getMockedHTSTokenCurrency,
} from "../test/fixtures/currency.fixture";
import { getMockedTransaction } from "../test/fixtures/transaction.fixture";
import type { EstimateFeesResult } from "../types";
import { prepareTransaction } from "./prepareTransaction";

jest.mock("../logic/estimateFees");

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

import * as utils from "./utils";
const mockCalculateAmount = utils.calculateAmount as jest.Mock;

describe("prepareTransaction", () => {
  const mockAccount = getMockedAccount();
  const mockTx = getMockedTransaction();
  const mockFeeEstimation: EstimateFeesResult = {
    tinybars: new BigNumber(10),
    gas: new BigNumber(5),
  };
  const mockCalculatedAmount = {
    amount: new BigNumber(100),
    totalSpent: new BigNumber(100),
  };

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

    (estimateFees as jest.Mock).mockResolvedValue(mockFeeEstimation);
    mockCalculateAmount.mockResolvedValue(mockCalculatedAmount);
  });

  it("should set amount and maxFee from utils", async () => {
    const result = await prepareTransaction(mockAccount, mockTx);

    expect(result.amount).toStrictEqual(new BigNumber(100));
    expect(result.maxFee).toStrictEqual(new BigNumber(10));
  });

  it("should build ContractCall estimation params with txIntent", async () => {
    const mockTokenERC20 = getMockedERC20TokenCurrency();
    const tokenAccount = getMockedTokenAccount(mockTokenERC20);
    const accountWithToken = getMockedAccount({ subAccounts: [tokenAccount] });
    const transaction = getMockedTransaction({
      mode: HEDERA_TRANSACTION_MODES.Send,
      subAccountId: tokenAccount.id,
      amount: new BigNumber(5000),
      recipient: "0.0.9999",
    });

    await prepareTransaction(accountWithToken, transaction);

    expect(estimateFees).toHaveBeenCalledWith({
      operationType: HEDERA_OPERATION_TYPES.ContractCall,
      txIntent: {
        intentType: "transaction",
        type: HEDERA_TRANSACTION_MODES.Send,
        asset: {
          type: mockTokenERC20.tokenType,
          assetReference: mockTokenERC20.contractAddress,
          assetOwner: accountWithToken.freshAddress,
        },
        amount: BigInt(5000),
        sender: accountWithToken.freshAddress,
        recipient: transaction.recipient,
      },
    });
  });

  it("should set gasLimit for ERC20 transactions when gas is estimated", async () => {
    const mockTokenERC20 = getMockedERC20TokenCurrency();
    const tokenAccount = getMockedTokenAccount(mockTokenERC20);
    const accountWithToken = getMockedAccount({ subAccounts: [tokenAccount] });
    const transaction = getMockedTransaction({
      mode: HEDERA_TRANSACTION_MODES.Send,
      subAccountId: tokenAccount.id,
    });

    const result = await prepareTransaction(accountWithToken, transaction);

    expect(result).toMatchObject({
      gasLimit: mockFeeEstimation.gas,
    });
  });

  it("should use TokenTransfer operation type for HTS tokens", async () => {
    const mockTokenHTS = getMockedHTSTokenCurrency();
    const tokenAccount = getMockedTokenAccount(mockTokenHTS);
    const accountWithToken = getMockedAccount({ subAccounts: [tokenAccount] });
    const transaction = getMockedTransaction({
      mode: HEDERA_TRANSACTION_MODES.Send,
      subAccountId: tokenAccount.id,
    });

    await prepareTransaction(accountWithToken, transaction);

    expect(estimateFees).toHaveBeenCalledWith({
      currency: accountWithToken.currency,
      operationType: HEDERA_OPERATION_TYPES.TokenTransfer,
    });
  });

  it("should use TokenAssociate operation type", async () => {
    const mockTokenHTS = getMockedHTSTokenCurrency();
    const transaction = getMockedTransaction({
      mode: HEDERA_TRANSACTION_MODES.TokenAssociate,
      properties: {
        token: mockTokenHTS,
      },
    });

    await prepareTransaction(mockAccount, transaction);

    expect(estimateFees).toHaveBeenCalledWith({
      currency: mockAccount.currency,
      operationType: HEDERA_OPERATION_TYPES.TokenAssociate,
    });
  });

  it("should use CryptoTransfer operation type for native transfers", async () => {
    const transaction = getMockedTransaction({
      mode: HEDERA_TRANSACTION_MODES.Send,
      amount: new BigNumber(1000000),
      recipient: "0.0.12345",
    });

    await prepareTransaction(mockAccount, transaction);

    expect(estimateFees).toHaveBeenCalledWith({
      currency: mockAccount.currency,
      operationType: HEDERA_OPERATION_TYPES.CryptoTransfer,
    });
  });
});
