import { Transaction as EvmTransaction } from "@ledgerhq/coin-evm/types/index";
import { TokenCurrency } from "@ledgerhq/types-cryptoassets";
import {
  OperationType,
  SignedOperation,
  SignedOperationRaw,
  TokenAccount,
} from "@ledgerhq/types-live";
import BigNumber from "bignumber.js";
import { setSupportedCurrencies } from "../currencies";
import * as signMessage from "../hw/signMessage/index";
import {
  createFixtureAccount,
  createFixtureCryptoCurrency,
  createFixtureTokenAccount,
} from "../mock/fixtures/cryptoCurrencies";
import * as converters from "./converters";

jest.mock("../hw/signMessage/index", () => ({
  ...jest.requireActual("../hw/signMessage/index"),
  prepareMessageToSign: jest.fn(),
}));

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

jest.mock("./serializers", () => ({
  ...jest.requireActual("./serializers"),
  deserializePlatformSignedTransaction: jest.fn(),
}));
import {
  broadcastTransactionLogic,
  completeExchangeLogic,
  receiveOnAccountLogic,
  signMessageLogic,
  WebPlatformContext,
} from "./logic";
import { RawPlatformTransaction } from "./rawTypes";
import * as serializers from "./serializers";
import { LiveAppManifest } from "./types";
import { initialState } from "@ledgerhq/live-wallet/store";
import { TrackingAPI } from "./tracking";

describe("receiveOnAccountLogic", () => {
  const walletState = initialState;
  // Given
  const mockPlatformReceiveRequested = jest.fn();
  const mockPlatformReceiveFail = jest.fn();
  const context = createContextContainingAccountId(
    {
      platformReceiveRequested: mockPlatformReceiveRequested,
      platformReceiveFail: mockPlatformReceiveFail,
    },
    "11",
    "12",
  );
  const uiNavigation = jest.fn();

  beforeEach(() => {
    mockPlatformReceiveRequested.mockClear();
    mockPlatformReceiveFail.mockClear();
    uiNavigation.mockClear();
    mockedAccountToPlatformAccount.mockClear();
  });

  describe("when nominal case", () => {
    // Given
    const accountId = "js:2:ethereum:0x012:";
    const expectedResult = "Function called";

    beforeEach(() => uiNavigation.mockResolvedValueOnce(expectedResult));

    it("calls uiNavigation callback with an accountAddress", async () => {
      // Given
      const convertedAccount = {
        ...createPlatformAccount(),
        address: "Converted address",
      };
      mockedAccountToPlatformAccount.mockReturnValueOnce(convertedAccount);

      // When
      const result = await receiveOnAccountLogic(walletState, context, accountId, uiNavigation);

      // Then
      expect(uiNavigation).toHaveBeenCalledTimes(1);
      expect(uiNavigation.mock.calls[0][2]).toEqual("Converted address");
      expect(result).toEqual(expectedResult);
    });

    it("calls the tracking for success", async () => {
      // Given
      const convertedAccount = {
        ...createPlatformAccount(),
        address: "Converted address",
      };
      mockedAccountToPlatformAccount.mockReturnValueOnce(convertedAccount);

      // When
      await receiveOnAccountLogic(walletState, context, accountId, uiNavigation);

      // Then
      expect(mockPlatformReceiveRequested).toHaveBeenCalledTimes(1);
      expect(mockPlatformReceiveFail).toHaveBeenCalledTimes(0);
    });
  });

  describe("when account cannot be found", () => {
    // Given
    const nonFoundAccountId = "js:2:ethereum:0x010:";

    it("returns an error", async () => {
      // When
      await expect(async () => {
        await receiveOnAccountLogic(walletState, context, nonFoundAccountId, uiNavigation);
      }).rejects.toThrow("Account required");

      // Then
      expect(uiNavigation).toHaveBeenCalledTimes(0);
    });

    it("calls the tracking for error", async () => {
      // When
      await expect(async () => {
        await receiveOnAccountLogic(walletState, context, nonFoundAccountId, uiNavigation);
      }).rejects.toThrow();

      // Then
      expect(mockPlatformReceiveRequested).toHaveBeenCalledTimes(1);
      expect(mockPlatformReceiveFail).toHaveBeenCalledTimes(1);
    });
  });
});

describe("completeExchangeLogic", () => {
  // Given
  const mockPlatformCompleteExchangeRequested = jest.fn();
  const context = createContextContainingAccountId(
    {
      platformCompleteExchangeRequested: mockPlatformCompleteExchangeRequested,
    },
    "11",
    "12",
  );
  const uiNavigation = jest.fn();

  beforeAll(() => {
    setSupportedCurrencies(["bitcoin", "ethereum"]);
  });
  afterAll(() => {
    setSupportedCurrencies([]);
  });

  beforeEach(() => {
    mockPlatformCompleteExchangeRequested.mockClear();
    uiNavigation.mockClear();
  });

  describe("when nominal case", () => {
    // Given
    const expectedResult = "Function called";

    beforeEach(() => uiNavigation.mockResolvedValueOnce(expectedResult));

    it("calls uiNavigation callback (token)", async () => {
      // Given
      const fromAccount = createFixtureTokenAccount("16");
      const fromParentAccount = createFixtureAccount("16");
      context.accounts = [...context.accounts, fromAccount, fromParentAccount];
      const rawPlatformTransaction = createRawEtherumTransaction();
      const completeExchangeRequest = {
        provider: "provider",
        fromAccountId: "js:2:ethereum:0x16:+ethereum%2Ferc20%2Fusd_tether__erc20_",
        toAccountId: "js:2:ethereum:0x042:",
        transaction: rawPlatformTransaction,
        binaryPayload: "binaryPayload",
        signature: "signature",
        feesStrategy: "medium",
        exchangeType: 8,
      };

      const expectedTransaction: EvmTransaction = {
        family: "evm",
        amount: new BigNumber("1000000000"),
        subAccountId: "js:2:ethereum:0x16:+ethereum%2Ferc20%2Fusd_tether__erc20_",
        recipient: "0x0123456",
        nonce: 8,
        data: Buffer.from("Some data...", "hex"),
        type: 0,
        gasPrice: new BigNumber("700000"),
        maxFeePerGas: undefined,
        maxPriorityFeePerGas: undefined,
        gasLimit: new BigNumber("1200000"),
        customGasLimit: new BigNumber("1200000"),
        feesStrategy: "medium",
        mode: "send",
        useAllAmount: false,
        chainId: 1,
      };

      // When
      const result = await completeExchangeLogic(context, completeExchangeRequest, uiNavigation);

      // Then
      expect(uiNavigation).toHaveBeenCalledTimes(1);
      expect(uiNavigation.mock.calls[0][0]).toEqual({
        provider: "provider",
        exchange: {
          fromAccount,
          fromParentAccount,
          fromCurrency: fromAccount.token,
          toAccount: undefined,
          toParentAccount: undefined,
          toCurrency: undefined,
        },
        transaction: expectedTransaction,
        binaryPayload: "binaryPayload",
        signature: "signature",
        feesStrategy: "medium",
        exchangeType: 8,
      });
      expect(result).toEqual(expectedResult);
    });

    it("calls uiNavigation callback (coin)", async () => {
      // Given
      const fromAccount = createFixtureAccount("17");
      context.accounts = [...context.accounts, fromAccount];
      const rawPlatformTransaction = createRawEtherumTransaction();
      const completeExchangeRequest = {
        provider: "provider",
        fromAccountId: "js:2:ethereum:0x017:",
        toAccountId: "js:2:ethereum:0x042:",
        transaction: rawPlatformTransaction,
        binaryPayload: "binaryPayload",
        signature: "signature",
        feesStrategy: "medium",
        exchangeType: 8,
      };

      const expectedTransaction: EvmTransaction = {
        family: "evm",
        amount: new BigNumber("1000000000"),
        recipient: "0x0123456",
        nonce: 8,
        data: Buffer.from("Some data...", "hex"),
        gasPrice: new BigNumber("700000"),
        gasLimit: new BigNumber("1200000"),
        customGasLimit: new BigNumber("1200000"),
        feesStrategy: "medium",
        mode: "send",
        useAllAmount: false,
        chainId: 1,
        subAccountId: undefined,
        type: 0,
        maxFeePerGas: undefined,
        maxPriorityFeePerGas: undefined,
      };

      // When
      const result = await completeExchangeLogic(context, completeExchangeRequest, uiNavigation);

      // Then
      expect(uiNavigation).toHaveBeenCalledTimes(1);
      expect(uiNavigation.mock.calls[0][0]).toEqual({
        provider: "provider",
        exchange: {
          fromAccount,
          fromParentAccount: undefined,
          fromCurrency: fromAccount.currency,
          toAccount: undefined,
          toParentAccount: undefined,
          toCurrency: undefined,
        },
        transaction: expectedTransaction,
        binaryPayload: "binaryPayload",
        signature: "signature",
        feesStrategy: "medium",
        exchangeType: 8,
      });
      expect(result).toEqual(expectedResult);
    });

    it.each(["slow", "medium", "fast", "custom"])(
      "calls uiNavigation with a transaction that has the %s feeStrategy",
      async expectedFeeStrategy => {
        // Given
        const fromAccount = createFixtureAccount("17");
        context.accounts = [...context.accounts, fromAccount];
        const rawPlatformTransaction = createRawEtherumTransaction();
        const completeExchangeRequest = {
          provider: "provider",
          fromAccountId: "js:2:ethereum:0x017:",
          toAccountId: "js:2:ethereum:0x042:",
          transaction: rawPlatformTransaction,
          binaryPayload: "binaryPayload",
          signature: "signature",
          feesStrategy: expectedFeeStrategy,
          exchangeType: 8,
        };

        // When
        await completeExchangeLogic(context, completeExchangeRequest, uiNavigation);

        // Then
        expect(uiNavigation).toHaveBeenCalledTimes(1);
        expect(uiNavigation.mock.calls[0][0]["transaction"].feesStrategy).toEqual(
          expectedFeeStrategy,
        );
      },
    );

    it("calls the tracking for success", async () => {
      // Given
      const completeExchangeRequest = {
        provider: "provider",
        fromAccountId: "js:2:ethereum:0x012:",
        toAccountId: "js:2:ethereum:0x042:",
        transaction: createRawEtherumTransaction(),
        binaryPayload: "binaryPayload",
        signature: "signature",
        feesStrategy: "feeStrategy",
        exchangeType: 8,
      };

      // When
      await completeExchangeLogic(context, completeExchangeRequest, uiNavigation);

      // Then
      expect(mockPlatformCompleteExchangeRequested).toHaveBeenCalledTimes(1);
    });
  });

  describe("when Account is from a different family than the transaction", () => {
    // Given
    const expectedResult = "Function called";

    beforeEach(() => uiNavigation.mockResolvedValueOnce(expectedResult));

    it("returns an error", async () => {
      // Given
      const fromAccount = createFixtureAccount("17");
      context.accounts = [...context.accounts, fromAccount];
      const rawPlatformTransaction = createRawBitcoinTransaction();
      const completeExchangeRequest = {
        provider: "provider",
        fromAccountId: "js:2:ethereum:0x017:",
        toAccountId: "js:2:ethereum:0x042:",
        transaction: rawPlatformTransaction,
        binaryPayload: "binaryPayload",
        signature: "signature",
        feesStrategy: "feeStrategy",
        exchangeType: 8,
      };

      // When
      await expect(async () => {
        await completeExchangeLogic(context, completeExchangeRequest, uiNavigation);
      }).rejects.toThrow("Account and transaction must be from the same family");

      // Then
      expect(uiNavigation).toHaveBeenCalledTimes(0);
    });
  });
});

describe("broadcastTransactionLogic", () => {
  // Given
  const mockplatformBroadcastFail = jest.fn();
  const context = createContextContainingAccountId(
    {
      platformBroadcastFail: mockplatformBroadcastFail,
    },
    "11",
    "12",
  );
  const uiNavigation = jest.fn();

  beforeEach(() => {
    mockplatformBroadcastFail.mockClear();
    uiNavigation.mockClear();
    mockedDeserializePlatformSignedTransaction.mockClear();
  });

  describe("when nominal case", () => {
    // Given
    const accountId = "js:2:ethereum:0x012:";
    const rawSignedTransaction = createSignedOperationRaw();

    it("calls uiNavigation callback with a signedOperation", async () => {
      // Given
      const expectedResult = "Function called";
      const signedOperation = createSignedOperation();
      mockedDeserializePlatformSignedTransaction.mockReturnValueOnce(signedOperation);
      uiNavigation.mockResolvedValueOnce(expectedResult);

      // When
      const result = await broadcastTransactionLogic(
        context,
        accountId,
        rawSignedTransaction,
        uiNavigation,
      );

      // Then
      expect(uiNavigation).toHaveBeenCalledTimes(1);
      expect(uiNavigation.mock.calls[0][2]).toEqual(signedOperation);
      expect(result).toEqual(expectedResult);
    });

    it("calls the tracking for success", async () => {
      // When
      await broadcastTransactionLogic(context, accountId, rawSignedTransaction, uiNavigation);

      // Then
      expect(mockplatformBroadcastFail).toHaveBeenCalledTimes(0);
    });
  });

  describe("when account cannot be found", () => {
    // Given
    const nonFoundAccountId = "js:2:ethereum:0x010:";
    const rawSignedTransaction = createSignedOperationRaw();

    it("returns an error", async () => {
      // Given
      const expectedResult = "Function called";
      const signedOperation = createSignedOperation();
      mockedDeserializePlatformSignedTransaction.mockReturnValueOnce(signedOperation);
      uiNavigation.mockResolvedValueOnce(expectedResult);

      // When
      await expect(async () => {
        await broadcastTransactionLogic(
          context,
          nonFoundAccountId,
          rawSignedTransaction,
          uiNavigation,
        );
      }).rejects.toThrow("Account required");

      // Then
      expect(uiNavigation).toHaveBeenCalledTimes(0);
    });

    it("calls the tracking for error", async () => {
      // When
      await expect(async () => {
        await broadcastTransactionLogic(
          context,
          nonFoundAccountId,
          rawSignedTransaction,
          uiNavigation,
        );
      }).rejects.toThrow();

      // Then
      expect(mockplatformBroadcastFail).toHaveBeenCalledTimes(1);
    });
  });
});

const mockedPrepareMessageToSign = jest.mocked(signMessage.prepareMessageToSign);
const mockedAccountToPlatformAccount = jest.mocked(converters.accountToPlatformAccount);
const mockedDeserializePlatformSignedTransaction = jest.mocked(
  serializers.deserializePlatformSignedTransaction,
);

describe("signMessageLogic", () => {
  // Given
  const mockPlatformSignMessageRequested = jest.fn();
  const mockPlatformSignMessageFail = jest.fn();
  const context = createContextContainingAccountId(
    {
      platformSignMessageRequested: mockPlatformSignMessageRequested,
      platformSignMessageFail: mockPlatformSignMessageFail,
    },
    "11",
    "12",
  );
  const uiNavigation = jest.fn();

  beforeEach(() => {
    mockPlatformSignMessageRequested.mockClear();
    mockPlatformSignMessageFail.mockClear();
    uiNavigation.mockClear();
    mockedPrepareMessageToSign.mockClear();
  });

  describe("when nominal case", () => {
    // Given
    const accountId = "js:2:ethereum:0x012:";
    const messageToSign = "Message to sign";

    it("calls uiNavigation callback with a signedOperation", async () => {
      // Given
      const expectedResult = "Function called";
      const formattedMessage = createMessageData();
      mockedPrepareMessageToSign.mockReturnValueOnce(formattedMessage);
      uiNavigation.mockResolvedValueOnce(expectedResult);

      // When
      const result = await signMessageLogic(context, accountId, messageToSign, uiNavigation);

      // Then
      expect(uiNavigation).toHaveBeenCalledTimes(1);
      expect(uiNavigation.mock.calls[0][1]).toEqual(formattedMessage);
      expect(result).toEqual(expectedResult);
    });

    it("calls the tracking for success", async () => {
      // When
      await signMessageLogic(context, accountId, messageToSign, uiNavigation);

      // Then
      expect(mockPlatformSignMessageRequested).toHaveBeenCalledTimes(1);
      expect(mockPlatformSignMessageFail).toHaveBeenCalledTimes(0);
    });
  });

  describe("when account cannot be found", () => {
    // Given
    const nonFoundAccountId = "js:2:ethereum:0x010:";
    const messageToSign = "Message to sign";

    it("returns an error", async () => {
      // When
      await expect(async () => {
        await signMessageLogic(context, nonFoundAccountId, messageToSign, uiNavigation);
      }).rejects.toThrow(`account with id "${nonFoundAccountId}" not found`);

      // Then
      expect(uiNavigation).toHaveBeenCalledTimes(0);
    });

    it("calls the tracking for error", async () => {
      // When
      await expect(async () => {
        await signMessageLogic(context, nonFoundAccountId, messageToSign, uiNavigation);
      }).rejects.toThrow();

      // Then
      expect(mockPlatformSignMessageRequested).toHaveBeenCalledTimes(1);
      expect(mockPlatformSignMessageFail).toHaveBeenCalledTimes(1);
    });
  });

  describe("when account found is not of type 'Account'", () => {
    // Given
    const tokenAccountId = "15";
    const messageToSign = "Message to sign";
    context.accounts = [createTokenAccount(tokenAccountId), ...context.accounts];

    it("returns an error", async () => {
      // When
      await expect(async () => {
        await signMessageLogic(context, tokenAccountId, messageToSign, uiNavigation);
      }).rejects.toThrow("account provided should be the main one");

      // Then
      expect(uiNavigation).toHaveBeenCalledTimes(0);
    });

    it("calls the tracking for error", async () => {
      // When
      await expect(async () => {
        await signMessageLogic(context, tokenAccountId, messageToSign, uiNavigation);
      }).rejects.toThrow();

      // Then
      expect(mockPlatformSignMessageRequested).toHaveBeenCalledTimes(1);
      expect(mockPlatformSignMessageFail).toHaveBeenCalledTimes(1);
    });
  });

  describe("when inner call prepareMessageToSign raise an error", () => {
    // Given
    const accountId = "js:2:ethereum:0x012:";
    const messageToSign = "Message to sign";

    it("returns an error", async () => {
      // Given
      mockedPrepareMessageToSign.mockImplementationOnce(() => {
        throw new Error("Some error");
      });

      // When
      await expect(async () => {
        await signMessageLogic(context, accountId, messageToSign, uiNavigation);
      }).rejects.toThrow("Some error");

      // Then
      expect(uiNavigation).toHaveBeenCalledTimes(0);
    });

    it("calls the tracking for error", async () => {
      // Given
      mockedPrepareMessageToSign.mockImplementationOnce(() => {
        throw new Error("Some error");
      });

      // When
      await expect(async () => {
        await signMessageLogic(context, accountId, messageToSign, uiNavigation);
      }).rejects.toThrow();

      // Then
      expect(mockPlatformSignMessageRequested).toHaveBeenCalledTimes(1);
      expect(mockPlatformSignMessageFail).toHaveBeenCalledTimes(1);
    });
  });
});

function createAppManifest(id = "1"): LiveAppManifest {
  return {
    id,
    private: false,
    name: "New App Manifest",
    url: "https://www.ledger.com",
    homepageUrl: "https://www.ledger.com",
    supportUrl: "https://www.ledger.com",
    icon: null,
    platforms: ["ios", "android", "desktop"],
    apiVersion: "1.0.0",
    manifestVersion: "1.0.0",
    branch: "debug",
    params: undefined,
    categories: [],
    currencies: "*",
    content: {
      shortDescription: {
        en: "short description",
      },
      description: {
        en: "description",
      },
    },
    permissions: [],
    domains: [],
    visibility: "complete",
  };
}

function createContextContainingAccountId(
  tracking: Partial<TrackingAPI>,
  ...accountIds: string[]
): WebPlatformContext {
  return {
    manifest: createAppManifest(),
    accounts: [...accountIds.map(val => createFixtureAccount(val)), createFixtureAccount()],
    tracking: tracking as TrackingAPI,
  };
}

function createSignedOperation(): SignedOperation {
  const operation = {
    id: "42",
    hash: "hashed",
    type: "IN" as OperationType,
    value: new BigNumber(0),
    fee: new BigNumber(0),
    senders: [],
    recipients: [],
    blockHeight: null,
    blockHash: null,
    accountId: "14",
    date: new Date(),
    extra: {},
  };
  return {
    operation,
    signature: "Signature",
  };
}

function createSignedOperationRaw(): SignedOperationRaw {
  const rawOperation = {
    id: "12",
    hash: "123456",
    type: "CREATE" as OperationType,
    value: "0",
    fee: "0",
    senders: [],
    recipients: [],
    blockHeight: null,
    blockHash: null,
    accountId: "12",
    date: "01/01/1970",
    extra: {},
  };
  return {
    operation: rawOperation,
    signature: "Signature",
  };
}

function createPlatformAccount() {
  return {
    id: "12",
    name: "",
    address: "",
    currency: "",
    balance: new BigNumber(0),
    spendableBalance: new BigNumber(0),
    blockHeight: 0,
    lastSyncDate: new Date(),
  };
}

function createMessageData() {
  return {
    account: createFixtureAccount("17"),
    message: "default message",
  };
}

function createTokenAccount(id = "32"): TokenAccount {
  return {
    type: "TokenAccount",
    id,
    parentId: "whatever",
    token: createTokenCurrency(),
    balance: new BigNumber(0),
    spendableBalance: new BigNumber(0),
    creationDate: new Date(),
    operationsCount: 0,
    operations: [],
    pendingOperations: [],
    balanceHistoryCache: {
      WEEK: { latestDate: null, balances: [] },
      HOUR: { latestDate: null, balances: [] },
      DAY: { latestDate: null, balances: [] },
    },
    swapHistory: [],
  };
}

function createTokenCurrency(): TokenCurrency {
  return {
    type: "TokenCurrency",
    id: "3",
    contractAddress: "",
    parentCurrency: createFixtureCryptoCurrency("eth"),
    tokenType: "",
    //-- CurrencyCommon
    name: "",
    ticker: "",
    units: [],
  };
}

function createRawEtherumTransaction(): RawPlatformTransaction {
  return {
    family: "ethereum" as any,
    amount: "1000000000",
    recipient: "0x0123456",
    nonce: 8,
    data: "Some data...",
    gasPrice: "700000",
    gasLimit: "1200000",
  };
}

function createRawBitcoinTransaction(): RawPlatformTransaction {
  return {
    family: "bitcoin" as any,
    amount: "1000000000",
    recipient: "0x0123456",
    feePerByte: "900000",
  };
}
