import { isAccountEmpty } from "@ledgerhq/coin-framework/account";
import { botTest, genericTestDestination, pickSiblings } from "@ledgerhq/coin-framework/bot/specs";
import type { AppSpec } from "@ledgerhq/coin-framework/bot/types";
import { getCryptoCurrencyById, listTokensForCryptoCurrency } from "@ledgerhq/cryptoassets/index";
import { parseCurrencyUnit } from "@ledgerhq/coin-framework/currencies";
import { DeviceModelId } from "@ledgerhq/devices";
import { Account, AccountLike, TokenAccount } from "@ledgerhq/types-live";
import { BigNumber } from "bignumber.js";
import expect from "expect";
import invariant from "invariant";
import sample from "lodash/sample";
import { acceptTransaction } from "./speculos-deviceActions";
import { extractTokenId } from "./tokens";
import type { AlgorandAccount, AlgorandTransaction } from "./types";

const currency = getCryptoCurrencyById("algorand");
// Minimum balance required for a new non-ASA account
const minBalanceNewAccount = parseCurrencyUnit(currency.units[0], "0.1");

// Ensure that, when the recipient corresponds to an empty account,
// the amount to send is greater or equal to the required minimum
// balance for such a recipient
const checkSendableToEmptyAccount = (amount: BigNumber, recipient: AccountLike) => {
  if (isAccountEmpty(recipient) && amount.lte(minBalanceNewAccount)) {
    invariant(amount.gt(minBalanceNewAccount), "not enough funds to send to new account");
  }
};

// Get list of ASAs associated with the account
const getAssetsWithBalance = (account: Account): TokenAccount[] => {
  return account.subAccounts
    ? account.subAccounts.filter(a => a.type === "TokenAccount" && a.balance.gt(0))
    : [];
};

const pickSiblingsOptedIn = (siblings: Account[], assetId: string): Account | undefined => {
  return sample(
    siblings.filter(a => {
      return a.subAccounts?.some(sa => sa.type === "TokenAccount" && sa.token.id.endsWith(assetId));
    }),
  );
};

// TODO: rework to perform _difference_ between
// array of valid ASAs and array of ASAs currently
// being opted-in by an account

const getRandomAssetId = (account: Account): string | undefined => {
  const optedInASA = account.subAccounts?.reduce((old: string[], current: TokenAccount) => {
    if (current.type === "TokenAccount") {
      return [...old, current.token.id];
    }

    return old;
  }, []);
  const ASAs = listTokensForCryptoCurrency(account.currency).map(asa => asa.id);
  const diff = ASAs?.filter(asa => !optedInASA?.includes(asa));
  invariant(diff && diff.length > 0, "already got all optin");
  return sample(diff);
};

const algorand: AppSpec<AlgorandTransaction> = {
  name: "Algorand",
  currency,
  appQuery: {
    model: DeviceModelId.nanoS,
    appName: "Algorand",
  },
  genericDeviceAction: acceptTransaction,
  mutations: [
    {
      name: "move ~50%",
      feature: "send",
      maxRun: 1,
      testDestination: genericTestDestination,
      transaction: ({ account, siblings, bridge, maxSpendable }) => {
        invariant(maxSpendable.gt(0), "Spendable balance is too low");
        const sibling = pickSiblings(siblings, 4);
        const recipient = sibling.freshAddress;
        const transaction = bridge.createTransaction(account);
        const amount = maxSpendable.div(1.9 + 0.2 * Math.random()).integerValue();
        checkSendableToEmptyAccount(amount, sibling);
        const updates = [
          {
            amount,
          },
          {
            recipient,
          },
        ];
        return {
          transaction,
          updates,
        };
      },
      test: ({ account, accountBeforeTransaction, operation }) => {
        const rewards =
          (accountBeforeTransaction as AlgorandAccount).algorandResources?.rewards || 0;
        botTest("account balance moved with the operation value", () =>
          expect(account.balance.plus(rewards).toString()).toBe(
            accountBeforeTransaction.balance.minus(operation.value).toString(),
          ),
        );
      },
    },
    {
      name: "send max",
      feature: "sendMax",
      maxRun: 1,
      testDestination: genericTestDestination,
      transaction: ({ account, siblings, bridge, maxSpendable }) => {
        invariant(maxSpendable.gt(0), "Spendable balance is too low");
        const sibling = pickSiblings(siblings, 4);
        // Send the full spendable balance
        const amount = maxSpendable;
        checkSendableToEmptyAccount(amount, sibling);
        return {
          transaction: bridge.createTransaction(account),
          updates: [
            {
              recipient: sibling.freshAddress,
            },
            {
              useAllAmount: true,
            },
          ],
        };
      },
      test: ({ account }) => {
        // Ensure that there is no more than 20 μALGOs (discretionary value)
        // between the actual balance and the expected one to take into account
        // the eventual pending rewards added _after_ the transaction
        botTest("account spendable balance is very low", () =>
          expect(account.spendableBalance.lt(20)).toBe(true),
        );
      },
    },
    {
      name: "send ASA ~50%",
      feature: "tokens",
      maxRun: 1,
      transaction: ({ account, siblings, bridge, maxSpendable }) => {
        invariant(maxSpendable.gt(0), "Spendable balance is too low");
        const subAccount = sample(getAssetsWithBalance(account));
        invariant(subAccount && subAccount.type === "TokenAccount", "no subAccount with ASA");
        const assetId = subAccount.token.id;
        const sibling = pickSiblingsOptedIn(siblings, assetId);
        const transaction = bridge.createTransaction(account);
        const recipient = (sibling as Account).freshAddress;
        const mode = "send";
        const amount = subAccount.balance.div(1.9 + 0.2 * Math.random()).integerValue();
        const updates: Array<Partial<AlgorandTransaction>> = [
          {
            mode,
            subAccountId: subAccount.id,
          },
          {
            recipient,
          },
          {
            amount,
          },
        ];
        return {
          transaction,
          updates,
        };
      },
      test: ({ account, accountBeforeTransaction, transaction, status }) => {
        const subAccountId = transaction.subAccountId;
        const subAccount = account.subAccounts?.find(sa => sa.id === subAccountId);
        const subAccountBeforeTransaction = accountBeforeTransaction.subAccounts?.find(
          sa => sa.id === subAccountId,
        );
        botTest("subAccount balance moved with the tx status amount", () =>
          expect(subAccount?.balance.toString()).toBe(
            subAccountBeforeTransaction?.balance.minus(status.amount).toString(),
          ),
        );
      },
    },
    {
      name: "opt-In ASA available",
      feature: "tokens",
      maxRun: 1,
      transaction: ({ account, bridge, maxSpendable }) => {
        // maxSpendable is expected to be greater than 100,000 micro-Algos
        // corresponding to the requirement that the main account will have
        // one more ASA after the opt-in; its minimum balance is updated accordingly
        invariant(maxSpendable.gt(new BigNumber(100000)), "Spendable balance is too low");
        const transaction = bridge.createTransaction(account);
        const mode = "optIn";
        const assetId = getRandomAssetId(account);
        const subAccount = account.subAccounts
          ? account.subAccounts.find(a => a.id.includes(assetId as string))
          : null;
        invariant(!subAccount, "already opt-in");
        const updates: Array<Partial<AlgorandTransaction>> = [
          {
            mode,
          },
          {
            assetId,
          },
        ];
        return {
          transaction,
          updates,
        };
      },
      // eslint-disable-next-line no-unused-vars
      test: ({ account, transaction }) => {
        invariant(transaction.assetId, "should have an assetId");
        const assetId = extractTokenId(transaction.assetId as string);
        botTest("have sub account with asset id", () =>
          expect(account.subAccounts && account.subAccounts.some(a => a.id.endsWith(assetId))).toBe(
            true,
          ),
        );
      },
    },
    {
      name: "claim rewards",
      feature: "staking",
      maxRun: 1,
      transaction: ({ account, bridge, maxSpendable }) => {
        const rewards = (account as AlgorandAccount).algorandResources?.rewards;
        invariant(rewards && rewards.gt(0), "No pending rewards");
        // Ensure that the rewards can effectively be claimed
        // (fees have to be paid in order to claim the rewards)
        invariant(maxSpendable.gt(0), "Spendable balance is too low");
        const transaction = bridge.createTransaction(account);
        const mode = "claimReward";
        const updates: Array<Partial<AlgorandTransaction>> = [
          {
            mode,
          },
        ];
        return {
          transaction,
          updates,
        };
      },
      test: ({ account }) => {
        botTest("algoResources rewards is zero", () =>
          expect(
            (account as AlgorandAccount).algorandResources &&
              (account as AlgorandAccount).algorandResources.rewards.eq(0),
          ).toBe(true),
        );
      },
    },
  ],
};
export default {
  algorand,
};
