import invariant from "invariant";
import { BigNumber } from "bignumber.js";
import { reduce, filter, map } from "rxjs/operators";
import flatMap from "lodash/flatMap";
import omit from "lodash/omit";
import { InvalidAddress, RecipientRequired, AmountRequired } from "@ledgerhq/errors";
import {
  fromAccountRaw,
  toAccountRaw,
  decodeAccountId,
  encodeAccountId,
  flattenAccounts,
  isAccountBalanceUnconfirmed,
} from "../../account";
import { getCryptoCurrencyById } from "../../currencies";
import { getOperationAmountNumber } from "../../operation";
import { fromTransactionRaw, toTransactionRaw, toTransactionStatusRaw } from "../../transaction";
import { getAccountBridge, getCurrencyBridge } from "../../bridge";
import { mockDeviceWithAPDUs, releaseMockDevice } from "./mockDevice";
import type {
  Account,
  AccountBridge,
  AccountLike,
  AccountRawLike,
  SyncConfig,
  DatasetTest,
  CurrenciesData,
  TokenAccount,
  TransactionCommon,
  TransactionStatusCommon,
} from "@ledgerhq/types-live";
import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
import { firstValueFrom } from "rxjs";

const warnDev = process.env.CI ? (..._args) => {} : (...msg) => console.warn(...msg);
// FIXME move out into DatasetTest to be defined in
const blacklistOpsSumEq = {
  currencies: ["ripple", "ethereum", "tezos", "assethub_polkadot"],
  impls: ["mock"],
};

function expectBalanceIsOpsSum(a) {
  expect(a.balance).toEqual(
    a.operations.reduce((sum, op) => sum.plus(getOperationAmountNumber(op)), new BigNumber(0)),
  );
}

const defaultSyncConfig = {
  paginationConfig: {},
  blacklistedTokenIds: ["ethereum/erc20/ampleforth", "ethereum/erc20/steth"],
};
export function syncAccount<
  T extends TransactionCommon,
  A extends Account = Account,
  U extends TransactionStatusCommon = TransactionStatusCommon,
>(
  bridge: AccountBridge<T, A, U>,
  account: A,
  syncConfig: SyncConfig = defaultSyncConfig,
): Promise<A> {
  return firstValueFrom(
    bridge.sync(account, syncConfig).pipe(reduce((a, f: (arg0: A) => A) => f(a), account)),
  );
}

export function testBridge<T extends TransactionCommon>(data: DatasetTest<T>): void {
  // covers all bridges through many different accounts
  // to test the common shared properties of bridges.
  const accountsRelated: Array<{
    accountRaw: any;
    currencyData: CurrenciesData<T>;
    accountData: any;
    impl: string;
    currency: CryptoCurrency;
  }> = [];
  const currenciesRelated: Array<{
    currencyData: CurrenciesData<T>;
    currency: CryptoCurrency;
  }> = [];
  const { implementations, currencies } = data;

  for (const currencyId of Object.keys(currencies)) {
    const currencyData = currencies[currencyId];
    const currency = getCryptoCurrencyById(currencyId);
    currenciesRelated.push({
      currencyData,
      currency,
    });

    const accounts = currencyData.accounts || [];
    for (const accountData of accounts) {
      for (const impl of implementations) {
        if (accountData.implementations && !accountData.implementations.includes(impl)) {
          continue;
        }

        const accountRaw = {
          ...accountData.raw,
          id: encodeAccountId({
            ...decodeAccountId(accountData.raw.id),
            type: impl,
          }),
        };
        accountsRelated.push({
          currencyData,
          accountData,
          accountRaw,
          impl,
          currency,
        });
      }
    }
  }
  const accountsFoundInScanAccountsMap = {};

  currenciesRelated.forEach(({ currencyData, currency }) => {
    const bridge = getCurrencyBridge(currency);

    const scanAccounts = async apdus => {
      const deviceId = await mockDeviceWithAPDUs(apdus, currencyData.mockDeviceOptions);
      try {
        const accounts = await firstValueFrom(
          bridge
            .scanAccounts({
              currency,
              deviceId,
              syncConfig: defaultSyncConfig,
            })
            .pipe(
              filter(e => e.type === "discovered"),
              map(e => e.account),
              reduce((all, a) => all.concat(a), [] as Account[]),
            ),
        );
        return accounts;
      } catch (e: any) {
        console.error(e.message);
        throw e;
      } finally {
        releaseMockDevice(deviceId);
      }
    };

    const scanAccountsCaches = {};

    const scanAccountsCached = apdus =>
      scanAccountsCaches[apdus] || (scanAccountsCaches[apdus] = scanAccounts(apdus));

    describe(currency.id + " currency bridge", () => {
      const {
        scanAccounts,
        FIXME_ignoreAccountFields,
        FIXME_ignoreOperationFields,
        FIXME_ignorePreloadFields,
      } = currencyData;
      test("functions are defined", () => {
        expect(typeof bridge.scanAccounts).toBe("function");
        expect(typeof bridge.preload).toBe("function");
        expect(typeof bridge.hydrate).toBe("function");
      });
      if (FIXME_ignorePreloadFields !== true) {
        test("preload and rehydrate", async () => {
          const data1 = (await bridge.preload(currency)) || {};
          const data1filtered = omit(data1, FIXME_ignorePreloadFields || []);

          bridge.hydrate(data1filtered, currency);

          if (data1filtered) {
            const serialized1 = JSON.parse(JSON.stringify(data1filtered));
            bridge.hydrate(serialized1, currency);
            expect(serialized1).toBeDefined();
            const data2 = (await bridge.preload(currency)) || {};
            const data2filtered = omit(data2, FIXME_ignorePreloadFields || []);

            if (data2filtered) {
              bridge.hydrate(data2filtered, currency);
              expect(data1filtered).toMatchObject(data2filtered);
              const serialized2 = JSON.parse(JSON.stringify(data2filtered));
              expect(serialized1).toMatchObject(serialized2);
              bridge.hydrate(serialized2, currency);
            }
          }
        });
      }

      if (scanAccounts) {
        if (FIXME_ignoreOperationFields && FIXME_ignoreOperationFields.length) {
          warnDev(
            currency.id +
              " is ignoring operation fields: " +
              FIXME_ignoreOperationFields.join(", "),
          );
        }

        if (FIXME_ignoreAccountFields && FIXME_ignoreAccountFields.length) {
          warnDev(
            currency.id + " is ignoring account fields: " + FIXME_ignoreAccountFields.join(", "),
          );
        }

        describe("scanAccounts", () => {
          scanAccounts.forEach(sa => {
            // we start running the scan accounts in parallel!
            test(sa.name, async () => {
              const accounts = await scanAccountsCached(sa.apdus);
              accounts.forEach(a => {
                accountsFoundInScanAccountsMap[a.id] = a;
              });

              if (!sa.unstableAccounts) {
                const raws: AccountRawLike[] = flatMap(accounts, a => {
                  const main = toAccountRaw(a);
                  if (!main.subAccounts) return [main];
                  return [{ ...main, subAccounts: [] }, ...main.subAccounts] as AccountRawLike[];
                });
                const heads = raws.map(a => {
                  const copy = omit(
                    a,
                    [
                      "operations",
                      "lastSyncDate",
                      "creationDate",
                      "blockHeight",
                      "balanceHistory",
                      "balanceHistoryCache",
                    ].concat(FIXME_ignoreAccountFields || []),
                  );
                  return copy;
                });
                const ops = raws.map(({ operations }) =>
                  operations
                    .slice(0)
                    .sort((a, b) => a.id.localeCompare(b.id))
                    .map(op => {
                      const copy = omit(op, ["date"].concat(FIXME_ignoreOperationFields || []));
                      return copy;
                    }),
                );
                expect(heads).toMatchSnapshot();
                expect(ops).toMatchSnapshot();
              }

              const testFn = sa.test;

              if (testFn) {
                await testFn(expect, accounts, bridge);
              }
            });
            test("estimateMaxSpendable is between 0 and account balance", async () => {
              const accounts = await scanAccountsCached(sa.apdus);

              for (const account of accounts) {
                const accountBridge = getAccountBridge(account);
                const estimation = await accountBridge.estimateMaxSpendable({
                  account,
                });
                expect(estimation.gte(0)).toBe(true);
                if (!(account.spendableBalance.lt(0) && estimation.eq(0))) {
                  expect(estimation.lte(account.spendableBalance)).toBe(true);
                }

                for (const sub of account.subAccounts || []) {
                  const estimation = await accountBridge.estimateMaxSpendable({
                    parentAccount: account,
                    account: sub,
                  });
                  expect(estimation.gte(0)).toBe(true);
                  expect(estimation.lte(sub.balance)).toBe(true);
                }
              }
            });
            test("no unconfirmed account", async () => {
              const accounts = await scanAccountsCached(sa.apdus);

              for (const account of flattenAccounts(accounts)) {
                expect({
                  id: account.id,
                  unconfirmed: isAccountBalanceUnconfirmed(account),
                }).toEqual({
                  id: account.id,
                  unconfirmed: false,
                });
              }
            });
            test("creationDate is correct", async () => {
              const accounts = await scanAccountsCached(sa.apdus);

              for (const account of flattenAccounts(accounts)) {
                if (account.operations.length) {
                  const op = account.operations[account.operations.length - 1];

                  if (account.creationDate.getTime() > op.date.getTime()) {
                    warnDev(
                      `OP ${
                        op.id
                      } have date=${op.date.toISOString()} older than account.creationDate=${account.creationDate.toISOString()}`,
                    );
                  }

                  expect(account.creationDate.getTime()).not.toBeGreaterThan(op.date.getTime());
                }
              }
            });
          });
        });
      }

      const currencyDataTest = currencyData.test;

      if (currencyDataTest) {
        test(currency.id + " specific test", () => currencyDataTest(expect, bridge));
      }
    });
    const accounts = currencyData.accounts || [];

    if (accounts.length) {
      const accountsInScan: string[] = [];
      const accountsNotInScan: string[] = [];
      accounts.forEach(({ raw }) => {
        if (accountsFoundInScanAccountsMap[raw.id]) {
          accountsInScan.push(raw.id);
        } else {
          accountsNotInScan.push(raw.id);
        }
      });

      if (accountsInScan.length === 0) {
        warnDev(
          `/!\\ CURRENCY '${currency.id}' define accounts that are NOT in scanAccounts. please add at least one account that is from scanAccounts. This helps testing scanned accounts are fine and it also help performance.`,
        );
      }

      if (accountsNotInScan.length === 0) {
        warnDev(
          `/!\\ CURRENCY '${currency.id}' define accounts that are ONLY in scanAccounts. please add one account that is NOT from scanAccounts. This helps covering the "recovering from xpub" mecanism.`,
        );
      }
    }
  });

  accountsRelated
    .map(({ accountRaw, ...rest }) => {
      let accountPromise;
      let bridgePromise;
      let accountSyncedPromise;

      // lazy eval so we don't run this yet
      const getAccount = () => accountPromise || (accountPromise = fromAccountRaw(accountRaw));

      const getBridge = async () => {
        if (!bridgePromise) {
          bridgePromise = getAccount().then(account => {
            const bridge = getAccountBridge(account, null);
            if (!bridge) throw new Error("no bridge for " + account.id);
            return bridge;
          });
        }
        return bridgePromise;
      };

      const getSynced = async () => {
        if (!accountSyncedPromise) {
          const account = await getAccount();
          const bridge = await getBridge();
          accountSyncedPromise = syncAccount(bridge, account);
        }
        return accountSyncedPromise;
      };

      const currency = rest.currency;

      return {
        getSynced,
        getBridge,
        getAccount,
        initialAccountRaw: accountRaw,
        initialAccountId: accountRaw.id,
        initialAccountCurrency: currency,
        ...rest,
      };
    })
    .forEach(arg => {
      const {
        getSynced,
        getBridge,
        getAccount,
        initialAccountId,
        initialAccountCurrency,
        accountData,
        impl,
      } = arg;

      const makeTest = (name, fn) => {
        if (accountData.FIXME_tests && accountData.FIXME_tests.some(r => name.match(r))) {
          warnDev(
            "FIXME test was skipped. " +
              name +
              " for " +
              initialAccountCurrency.name +
              " (" +
              initialAccountId +
              ")",
          );
          return;
        }

        test(name, fn);
      };

      describe(
        impl + " bridge on account " + initialAccountCurrency.name + " (" + initialAccountId + ")",
        () => {
          describe("sync", () => {
            makeTest("succeed", async () => {
              const account = await getSynced();
              expect(fromAccountRaw(toAccountRaw(account))).toBeDefined();
            });

            if (impl !== "mock") {
              const accFromScanAccounts = accountsFoundInScanAccountsMap[initialAccountId];

              if (accFromScanAccounts) {
                makeTest("matches the same account from scanAccounts", async () => {
                  const acc = await getSynced();
                  expect(acc).toMatchObject(accFromScanAccounts);
                });
              }
            }
            makeTest("account have no NaN values", async () => {
              const account = await getSynced();
              [account, ...(account.subAccounts || [])].forEach(a => {
                expect(a.balance.isNaN()).toBe(false);
                expect(a.operations.find(op => op.value.isNaN())).toBe(undefined);
                expect(a.operations.find(op => op.fee.isNaN())).toBe(undefined);
              });
            });

            if (
              !blacklistOpsSumEq.currencies.includes(initialAccountCurrency.id) &&
              !blacklistOpsSumEq.impls.includes(impl)
            ) {
              makeTest("balance is sum of ops", async () => {
                const account = await getSynced();
                expectBalanceIsOpsSum(account);

                if (account.subAccounts) {
                  account.subAccounts.forEach(expectBalanceIsOpsSum);
                }
              });
              makeTest("balance and spendableBalance boundaries", async () => {
                const account = await getSynced();
                expect(account.balance).toBeInstanceOf(BigNumber);
                expect(account.spendableBalance).toBeInstanceOf(BigNumber);
                expect(account.balance.lt(0)).toBe(false);
                expect(account.spendableBalance.lt(0)).toBe(false);
                expect(account.spendableBalance.lte(account.balance)).toBe(true);
              });
            }

            makeTest("existing operations object refs are preserved", async () => {
              const account = await getSynced();
              const count = Math.floor(account.operations.length / 2);
              const operations = account.operations.slice(count);
              const copy = {
                ...account,
                operations,
                blockHeight: 0,
              };
              const bridge = await getBridge();
              const synced = await syncAccount(bridge, copy);
              expect(synced.operations.length).toBe(account.operations.length);
              // same ops are restored
              expect(synced.operations).toEqual(account.operations);
              if (initialAccountId.startsWith("ethereumjs")) return; // ethereumjs seems to have a bug on this, we ignore because the impl will be dropped.

              // existing ops are keeping refs
              synced.operations.slice(count).forEach((op, i) => {
                expect(op).toStrictEqual(operations[i]);
              });
            });
            makeTest("pendingOperations are cleaned up", async () => {
              const account = await getSynced();

              if (account.operations.length) {
                const operations = account.operations.slice(1);
                const pendingOperations = [account.operations[0]];
                const copy = {
                  ...account,
                  operations,
                  pendingOperations,
                  blockHeight: 0,
                };
                const bridge = await getBridge();
                const synced = await syncAccount(bridge, copy);
                // same ops are restored
                expect(synced.operations).toEqual(account.operations);
                // pendingOperations is empty
                expect(synced.pendingOperations).toEqual([]);
              }
            });
            makeTest("there are no Operation dups (by id)", async () => {
              const account = await getSynced();
              const seen = {};
              account.operations.forEach(op => {
                expect(seen[op.id]).toBeUndefined();
                seen[op.id] = op.id;
              });
            });
          });

          describe("createTransaction", () => {
            makeTest(
              "empty transaction is an object with empty recipient and zero amount",
              async () => {
                const account = await getAccount();
                const bridge = await getBridge();
                expect(bridge.createTransaction(account)).toMatchObject({
                  amount: new BigNumber(0),
                  recipient: "",
                });
              },
            );
            makeTest("empty transaction is equals to itself", async () => {
              const account = await getAccount();
              const bridge = await getBridge();
              expect(bridge.createTransaction(account)).toEqual(bridge.createTransaction(account));
            });
            makeTest("empty transaction correctly serialize", async () => {
              const account = await getAccount();
              const bridge = await getBridge();
              const t = bridge.createTransaction(account);
              expect(fromTransactionRaw(toTransactionRaw(t))).toEqual(t);
            });
            makeTest("transaction with amount and recipient correctly serialize", async () => {
              const account = await getSynced();
              const bridge = await getBridge();
              const t = {
                ...bridge.createTransaction(account),
                amount: new BigNumber(1000),
                recipient: account.freshAddress,
              };
              expect(fromTransactionRaw(toTransactionRaw(t))).toEqual(t);
            });
          });

          describe("updateTransaction", () => {
            // stability: function called twice will return the same object reference
            // (=== convergence so we can stop looping, typically because transaction will be a hook effect dependency of prepareTransaction)
            async function expectStability(t, patch) {
              const bridge = await getBridge();
              const t2 = bridge.updateTransaction(t, patch);
              const t3 = bridge.updateTransaction(t2, patch);
              expect(t2).toBe(t3);
            }

            makeTest("ref stability on empty transaction", async () => {
              const account = await getSynced();
              const bridge = await getBridge();
              const tx = bridge.createTransaction(account);
              await expectStability(tx, {});
            });

            makeTest("ref stability on self transaction", async () => {
              const account = await getSynced();
              const bridge = await getBridge();
              const tx = bridge.createTransaction(account);
              await expectStability(tx, {
                amount: new BigNumber(1000),
                recipient: account.freshAddress,
              });
            });
          });

          describe("prepareTransaction", () => {
            // stability: function called twice will return the same object reference
            // (=== convergence so we can stop looping, typically because transaction will be a hook effect dependency of prepareTransaction)
            async function expectStability(account, t) {
              const bridge = await getBridge();
              let t2 = await bridge.prepareTransaction(account, t);
              let t3 = await bridge.prepareTransaction(account, t2);
              t2 = omit(t2, arg.currencyData.IgnorePrepareTransactionFields || []);
              t3 = omit(t3, arg.currencyData.IgnorePrepareTransactionFields || []);
              expect(t2).toStrictEqual(t3);
            }

            makeTest("ref stability on empty transaction", async () => {
              const account = await getSynced();
              const bridge = await getBridge();
              await expectStability(account, bridge.createTransaction(account));
            });
            makeTest("ref stability on self transaction", async () => {
              const account = await getSynced();
              const bridge = await getBridge();
              await expectStability(account, {
                ...bridge.createTransaction(account),
                amount: new BigNumber(1000),
                recipient: account.freshAddress,
              });
            });
            makeTest("can be run in parallel and all yield same results", async () => {
              const account = await getSynced();
              const bridge = await getBridge();
              const t = {
                ...bridge.createTransaction(account),
                amount: new BigNumber(1000),
                recipient: account.freshAddress,
              };
              const stable = await bridge.prepareTransaction(account, t);
              const first = omit(
                await bridge.prepareTransaction(account, stable),
                arg.currencyData.IgnorePrepareTransactionFields || [],
              );
              const concur = await Promise.all(
                Array(3)
                  .fill(null)
                  .map(() => bridge.prepareTransaction(account, stable)),
              );
              concur.forEach(r => {
                r = omit(r, arg.currencyData.IgnorePrepareTransactionFields || []);
                expect(r).toEqual(first);
              });
            });
          });

          describe("getTransactionStatus", () => {
            makeTest("can be called on an empty transaction", async () => {
              const account = await getSynced();
              const bridge = await getBridge();
              const t = {
                ...bridge.createTransaction(account),
                feePerByte: new BigNumber(0.0001),
              };
              const s = await bridge.getTransactionStatus(account, t);
              expect(s).toBeDefined();
              expect(s.errors).toHaveProperty("recipient");
              expect(s).toHaveProperty("totalSpent");
              expect(s.totalSpent).toBeInstanceOf(BigNumber);
              expect(s).toHaveProperty("estimatedFees");
              expect(s.estimatedFees).toBeInstanceOf(BigNumber);
              expect(s).toHaveProperty("amount");
              expect(s.amount).toBeInstanceOf(BigNumber);
              expect(s.amount).toEqual(new BigNumber(0));
            });
            makeTest("can be called on an empty prepared transaction", async () => {
              const bridge = await getBridge();
              const account = await getSynced();
              const t = await bridge.prepareTransaction(account, {
                ...bridge.createTransaction(account),
                feePerByte: new BigNumber(0.0001),
              });
              const s = await bridge.getTransactionStatus(account, t);
              expect(s).toBeDefined(); // FIXME i'm not sure if we can establish more shared properties
            });
            makeTest("Default empty recipient have a recipientError", async () => {
              const bridge = await getBridge();
              const account = await getSynced();
              const t = {
                ...bridge.createTransaction(account),
                feePerByte: new BigNumber(0.0001),
              };
              const status = await bridge.getTransactionStatus(account, t);
              expect(status.errors.recipient).toBeInstanceOf(RecipientRequired);
            });
            makeTest("invalid recipient have a recipientError", async () => {
              const bridge = await getBridge();
              const account = await getSynced();
              const t = {
                ...bridge.createTransaction(account),
                feePerByte: new BigNumber(0.0001),
                recipient: "invalidADDRESS",
              };
              const status = await bridge.getTransactionStatus(account, t);
              expect(status.errors.recipient).toBeInstanceOf(InvalidAddress);
            });
            makeTest("Default empty amount has an amount error", async () => {
              const bridge = await getBridge();
              const account = await getSynced();
              const t = await bridge.prepareTransaction(account, {
                ...bridge.createTransaction(account),
                feePerByte: new BigNumber(0.0001),
              });
              const status = await bridge.getTransactionStatus(account, t);
              expect(status.errors.amount).toBeInstanceOf(AmountRequired);
            });

            const accountDataTest = accountData.test;

            if (accountDataTest) {
              makeTest("account specific test", async () => {
                const bridge = await getBridge();
                return accountDataTest(expect, await getSynced(), bridge);
              });
            }

            (accountData.transactions || []).forEach(
              ({ name, transaction, expectedStatus, apdus, testSignedOperation, test: testFn }) => {
                makeTest("transaction " + name, async () => {
                  const bridge = await getBridge();
                  const account: Account = await getSynced();
                  let t =
                    typeof transaction === "function"
                      ? transaction(bridge.createTransaction(account), account, bridge)
                      : transaction;
                  t = await bridge.prepareTransaction(account, {
                    feePerByte: new BigNumber(0.0001),
                    ...t,
                  });
                  const s = await bridge.getTransactionStatus(account, t);

                  if (expectedStatus) {
                    const es =
                      typeof expectedStatus === "function"
                        ? expectedStatus(account, t, s)
                        : expectedStatus;
                    const { errors, warnings } = es;

                    // we match errors and warnings
                    errors && expect(s.errors).toMatchObject(errors);
                    warnings && expect(s.warnings).toMatchObject(warnings);
                    // now we match rest of fields but using the raw version for better readability
                    const restRaw: Record<string, any> = toTransactionStatusRaw(
                      {
                        ...s,
                        ...es,
                      },
                      account.currency.family,
                    );
                    delete restRaw.errors;
                    delete restRaw.warnings;

                    for (const k in restRaw) {
                      if (!(k in es)) {
                        delete restRaw[k];
                      }
                    }

                    expect(
                      toTransactionStatusRaw(s as TransactionStatusCommon, account.currency.family),
                    ).toMatchObject(restRaw);
                  }

                  if (testFn) {
                    await testFn(expect, t, s, bridge);
                  }

                  if (Object.keys(s.errors).length === 0) {
                    const { subAccountId } = t;
                    const { subAccounts } = account;

                    const inferSubAccount = () => {
                      invariant(subAccounts, "sub accounts available");
                      const a = (subAccounts as TokenAccount[]).find(a => a.id === subAccountId);
                      invariant(a, "sub account not found");
                      return a;
                    };

                    const obj = subAccountId
                      ? {
                          transaction: t as TransactionCommon,
                          account: inferSubAccount() as AccountLike,
                          parentAccount: account,
                        }
                      : {
                          transaction: t as TransactionCommon,
                          account: account as AccountLike,
                        };

                    if (
                      (typeof t.mode !== "string" || t.mode === "send") &&
                      t.model &&
                      t.model.kind !== "stake.createAccount"
                    ) {
                      const estimation = await bridge.estimateMaxSpendable(obj);
                      expect(estimation.gte(0)).toBe(true);
                      expect(estimation.lte(obj.account.balance)).toBe(true);

                      if (t.useAllAmount) {
                        expect(estimation.toString()).toBe(s.amount.toString());
                      }
                    }
                  }

                  if (apdus && impl !== "mock") {
                    const deviceId = await mockDeviceWithAPDUs(apdus);

                    try {
                      const signedOperation = await firstValueFrom(
                        bridge
                          .signOperation({
                            account,
                            deviceId,
                            transaction: t,
                          })
                          .pipe(
                            filter((e: any) => e.type === "signed"),
                            map((e: any) => e.signedOperation),
                          ),
                      );

                      if (testSignedOperation) {
                        await testSignedOperation(expect, signedOperation, account, t, s, bridge);
                      }
                    } finally {
                      releaseMockDevice(deviceId);
                    }
                  }
                });
              },
            );
          });
          describe("signOperation and broadcast", () => {
            makeTest("method is available on bridge", async () => {
              const bridge = await getBridge();
              expect(typeof bridge.signOperation).toBe("function");
              expect(typeof bridge.broadcast).toBe("function");
            }); // NB for now we are not going farther because most is covered by bash tests
          });
        },
      );
    });
}
