/*
 This file is part of GNU Taler
 (C) 2021 Taler Systems S.A.

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

/**
 * This file defines euFin test logic that needs state
 * and that depends on the main harness.ts.  The other
 * definitions - mainly helper functions to call RESTful
 * APIs - moved to libeufin-apis.ts.  That enables harness.ts
 * to depend on such API calls, in contrast to the previous
 * situation where harness.ts had to include this file causing
 * a circular dependency.  */

/**
 * Imports.
 */
import { AmountString, Logger } from "@gnu-taler/taler-util";
import {
  DbInfo,
  GlobalTestState,
  ProcessWrapper,
  getRandomIban,
  pingProc,
  runCommand,
  setupDb,
  sh,
} from "../harness/harness.js";
import {
  CreateAnastasisFacadeRequest,
  CreateEbicsBankAccountRequest,
  CreateEbicsBankConnectionRequest,
  CreateNexusUserRequest,
  CreateTalerWireGatewayFacadeRequest,
  LibeufinNexusApi,
  LibeufinSandboxApi,
  LibeufinSandboxServiceInterface,
  PostNexusPermissionRequest,
} from "../harness/libeufin-apis.js";

const logger = new Logger("libeufin.ts");

export { LibeufinNexusApi, LibeufinSandboxApi };

export interface LibeufinServices {
  libeufinSandbox: LibeufinSandboxService;
  libeufinNexus: LibeufinNexusService;
  commonDb: DbInfo;
}

export interface LibeufinSandboxConfig {
  httpPort: number;
  databaseJdbcUri: string;
}

export interface LibeufinNexusConfig {
  httpPort: number;
  databaseJdbcUri: string;
}

export interface LibeufinNexusMoneyMovement {
  amount: string;
  creditDebitIndicator: string;
  details: {
    debtor: {
      name: string;
    };
    debtorAccount: {
      iban: string;
    };
    debtorAgent: {
      bic: string;
    };
    creditor: {
      name: string;
    };
    creditorAccount: {
      iban: string;
    };
    creditorAgent: {
      bic: string;
    };
    endToEndId: string;
    unstructuredRemittanceInformation: string;
  };
}

export interface LibeufinNexusBatches {
  batchTransactions: Array<LibeufinNexusMoneyMovement>;
}

export interface LibeufinNexusTransaction {
  amount: string;
  creditDebitIndicator: string;
  status: string;
  bankTransactionCode: string;
  valueDate: string;
  bookingDate: string;
  accountServicerRef: string;
  batches: Array<LibeufinNexusBatches>;
}

export interface LibeufinNexusTransactions {
  transactions: Array<LibeufinNexusTransaction>;
}

export interface LibeufinCliDetails {
  nexusUrl: string;
  sandboxUrl: string;
  nexusDatabaseUri: string;
  sandboxDatabaseUri: string;
  nexusUser: LibeufinNexusUser;
}

export interface LibeufinEbicsSubscriberDetails {
  hostId: string;
  partnerId: string;
  userId: string;
}

export interface LibeufinEbicsConnectionDetails {
  subscriberDetails: LibeufinEbicsSubscriberDetails;
  ebicsUrl: string;
  connectionName: string;
}

export interface LibeufinBankAccountDetails {
  currency: string;
  iban: string;
  bic: string;
  personName: string;
  accountName: string;
}

export interface LibeufinNexusUser {
  username: string;
  password: string;
}

export interface LibeufinBackupFileDetails {
  passphrase: string;
  outputFile: string;
  connectionName: string;
}

export interface LibeufinKeyLetterDetails {
  outputFile: string;
  connectionName: string;
}

export interface LibeufinBankAccountImportDetails {
  offeredBankAccountName: string;
  nexusBankAccountName: string;
  connectionName: string;
}

export interface LibeufinPreparedPaymentDetails {
  creditorIban: string;
  creditorBic: string;
  creditorName: string;
  subject: string;
  amount: string;
  currency: string;
  nexusBankAccountName: string;
}

export interface NexusBankConnection {
  // connection type.  For example "ebics".
  type: string;

  // connection name as given by the user at
  // the moment of creation.
  name: string;
}

export interface NexusBankConnections {
  bankConnections: NexusBankConnection[];
}

export interface FacadeShowInfo {
  // Name of the facade, same as the "fcid" parameter.
  name: string;

  // Type of the facade.
  // For example, "taler-wire-gateway".
  type: string;

  // Bas URL of the facade.
  baseUrl: string;

  // details depending on the facade type.
  config: any;
}

export interface FetchParams {
  // Because transactions are delivered by banks in "batches",
  // then every batch can have different qualities.  This value
  // lets the request specify which type of batch ought to be
  // returned.  Currently, the following two type are supported:
  //
  // 'report': typically includes only non booked transactions.
  // 'statement': typically includes only booked transactions.
  level: "report" | "statement" | "all";

  // This type indicates the time range of the query.
  // It allows the following values:
  //
  // 'latest': retrieves the last transactions from the bank.
  //           If there are older unread transactions, those will *not*
  //           be downloaded.
  //
  // 'all': retrieves all the transactions from the bank,
  //        until the oldest.
  //
  // 'previous-days': currently *not* implemented, it will allow
  //                  the request to download transactions from
  //                  today until N days before.
  //
  // 'since-last': retrieves all the transactions since the last
  //               time one was downloaded.
  //
  rangeType: "latest" | "all" | "previous-days" | "since-last";
}

export interface NexusTask {
  // The resource being impacted by this operation.
  // Typically a (Nexus) bank account being fetched
  // or whose payments are submitted.  In this cases,
  // this value is the "bank-account" constant.
  resourceType: string;
  // Name of the resource.  In case of "bank-account", that
  // is the name under which the bank account was imported
  // from the bank.
  resourceId: string;
  // Task name, equals 'taskId'
  taskName: string;
  // Values allowed are "fetch" or "submit".
  taskType: string;
  // FIXME: describe.
  taskCronSpec: string;
  // Only meaningful for "fetch" types.
  taskParams: FetchParams;
  // Timestamp in secons when the next iteration will run.
  nextScheduledExecutionSec: number;
  // Timestamp in seconds when the previous iteration ran.
  prevScheduledExecutionSec: number;
}

export interface NexusNewTransactionsInfo {
  // How many transactions are new to Nexus.
  newTransactions: number;
  // How many transactions got downloaded by the request.
  // Note that a transaction can be downloaded multiple
  // times but only counts as new once.
  downloadedTransactions: number;
}


export interface NexusUserResponse {
  // User name
  username: string;

  // Is this a super user?
  superuser: boolean;
}

export interface NexusTaskShortInfo {
  cronspec: string;
  type: "fetch" | "submit";
  params: FetchParams;
}

export interface NexusTaskCollection {
  // This field can contain *multiple* objects of the type sampled below.
  schedule: {
    [taskName: string]: NexusTaskShortInfo;
  };
}

export interface NexusFacadeListResponse {
  facades: FacadeShowInfo[];
}

export interface LibeufinSandboxAdminBankAccountBalance {
  // Balance in the $currency:$amount format.
  balance: AmountString;
  // IBAN of the bank account identified by $accountLabel
  iban: string;
  // BIC of the bank account identified by $accountLabel
  bic: string;
  // Mentions $accountLabel
  label: string;
}

export interface LibeufinPermission {
  subjectType: string;
  subjectId: string;
  resourceType: string;
  resourceId: string;
  permissionName: string;
}

export interface NexusGetPermissionsResponse {
  permissions: LibeufinPermission[];
}

export class LibeufinSandboxService implements LibeufinSandboxServiceInterface {
  static async create(
    gc: GlobalTestState,
    sandboxConfig: LibeufinSandboxConfig,
  ): Promise<LibeufinSandboxService> {
    return new LibeufinSandboxService(gc, sandboxConfig);
  }

  sandboxProc: ProcessWrapper | undefined;
  globalTestState: GlobalTestState;

  constructor(
    gc: GlobalTestState,
    private sandboxConfig: LibeufinSandboxConfig,
  ) {
    this.globalTestState = gc;
  }

  get baseUrl(): string {
    return `http://localhost:${this.sandboxConfig.httpPort}/`;
  }

  async start(): Promise<void> {
    await sh(
      this.globalTestState,
      "libeufin-sandbox-config",
      "libeufin-sandbox config default",
      {
        ...process.env,
        LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri,
      },
    );

    this.sandboxProc = this.globalTestState.spawnService(
      "libeufin-sandbox",
      ["serve", "--port", `${this.sandboxConfig.httpPort}`],
      "libeufin-sandbox",
      {
        ...process.env,
        LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri,
        LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret",
      },
    );
  }

  async c53tick(): Promise<string> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-sandbox-c53tick",
      "libeufin-sandbox camt053tick",
      {
        ...process.env,
        LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri,
      },
    );
    return stdout;
  }

  async makeTransaction(
    debit: string,
    credit: string,
    amount: string, // $currency:x.y
    subject: string,
  ): Promise<string> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-sandbox-maketransfer",
      `libeufin-sandbox make-transaction --debit-account=${debit} --credit-account=${credit} ${amount} "${subject}"`,
      {
        ...process.env,
        LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri,
      },
    );
    return stdout;
  }

  async pingUntilAvailable(): Promise<void> {
    const url = this.baseUrl;
    await pingProc(this.sandboxProc, url, "libeufin-sandbox");
  }
}

export class LibeufinNexusService {
  static async create(
    gc: GlobalTestState,
    nexusConfig: LibeufinNexusConfig,
  ): Promise<LibeufinNexusService> {
    return new LibeufinNexusService(gc, nexusConfig);
  }

  nexusProc: ProcessWrapper | undefined;
  globalTestState: GlobalTestState;

  constructor(gc: GlobalTestState, private nexusConfig: LibeufinNexusConfig) {
    this.globalTestState = gc;
  }

  get baseUrl(): string {
    return `http://localhost:${this.nexusConfig.httpPort}/`;
  }

  async start(): Promise<void> {
    await runCommand(
      this.globalTestState,
      "libeufin-nexus-superuser",
      "libeufin-nexus",
      ["superuser", "admin", "--password", "test"],
      {
        ...process.env,
        LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri,
      },
    );

    this.nexusProc = this.globalTestState.spawnService(
      "libeufin-nexus",
      ["serve", "--port", `${this.nexusConfig.httpPort}`],
      "libeufin-nexus",
      {
        ...process.env,
        LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri,
      },
    );
  }

  async pingUntilAvailable(): Promise<void> {
    const url = `${this.baseUrl}config`;
    await pingProc(this.nexusProc, url, "libeufin-nexus");
  }

  async createNexusSuperuser(details: LibeufinNexusUser): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-nexus",
      `libeufin-nexus superuser ${details.username} --password=${details.password}`,
      {
        ...process.env,
        LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri,
      },
    );
    console.log(stdout);
  }
}

export interface TwgAddIncomingRequest {
  amount: string;
  reserve_pub: string;
  debit_account: string;
}

/**
 * The bundle aims at minimizing the amount of input
 * data that is required to initialize a new user + Ebics
 * connection.
 */
export class NexusUserBundle {
  userReq: CreateNexusUserRequest;
  connReq: CreateEbicsBankConnectionRequest;
  anastasisReq: CreateAnastasisFacadeRequest;
  twgReq: CreateTalerWireGatewayFacadeRequest;
  twgTransferPermission: PostNexusPermissionRequest;
  twgHistoryPermission: PostNexusPermissionRequest;
  twgAddIncomingPermission: PostNexusPermissionRequest;
  localAccountName: string;
  remoteAccountName: string;

  constructor(salt: string, ebicsURL: string) {
    this.userReq = {
      username: `username-${salt}`,
      password: `password-${salt}`,
    };

    this.connReq = {
      name: `connection-${salt}`,
      ebicsURL: ebicsURL,
      hostID: `ebicshost,${salt}`,
      partnerID: `ebicspartner,${salt}`,
      userID: `ebicsuser,${salt}`,
    };

    this.twgReq = {
      currency: "EUR",
      name: `twg-${salt}`,
      reserveTransferLevel: "report",
      accountName: `local-account-${salt}`,
      connectionName: `connection-${salt}`,
    };
    this.anastasisReq = {
      currency: "EUR",
      name: `anastasis-${salt}`,
      reserveTransferLevel: "report",
      accountName: `local-account-${salt}`,
      connectionName: `connection-${salt}`,
    };
    this.remoteAccountName = `remote-account-${salt}`;
    this.localAccountName = `local-account-${salt}`;
    this.twgTransferPermission = {
      action: "grant",
      permission: {
        subjectId: `username-${salt}`,
        subjectType: "user",
        resourceType: "facade",
        resourceId: `twg-${salt}`,
        permissionName: "facade.talerWireGateway.transfer",
      },
    };
    this.twgHistoryPermission = {
      action: "grant",
      permission: {
        subjectId: `username-${salt}`,
        subjectType: "user",
        resourceType: "facade",
        resourceId: `twg-${salt}`,
        permissionName: "facade.talerWireGateway.history",
      },
    };
  }
}

/**
 * The bundle aims at minimizing the amount of input
 * data that is required to initialize a new Sandbox
 * customer, associating their bank account with a Ebics
 * subscriber.
 */
export class SandboxUserBundle {
  ebicsBankAccount: CreateEbicsBankAccountRequest;
  constructor(salt: string) {
    this.ebicsBankAccount = {
      bic: "BELADEBEXXX",
      iban: getRandomIban(),
      label: `remote-account-${salt}`,
      name: `Taler Exchange: ${salt}`,
      subscriber: {
        hostID: `ebicshost,${salt}`,
        partnerID: `ebicspartner,${salt}`,
        userID: `ebicsuser,${salt}`,
      },
    };
  }
}

export class LibeufinCli {
  cliDetails: LibeufinCliDetails;
  globalTestState: GlobalTestState;

  constructor(gc: GlobalTestState, cd: LibeufinCliDetails) {
    this.globalTestState = gc;
    this.cliDetails = cd;
  }

  env(): any {
    return {
      ...process.env,
      LIBEUFIN_SANDBOX_URL: this.cliDetails.sandboxUrl,
      LIBEUFIN_SANDBOX_USERNAME: "admin",
      LIBEUFIN_SANDBOX_PASSWORD: "secret",
    };
  }

  async checkSandbox(): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-checksandbox",
      "libeufin-cli sandbox check",
      this.env(),
    );
  }

  async registerBankCustomer(
    username: string,
    password: string,
  ): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-registercustomer",
      "libeufin-cli sandbox demobank register --name='Test Customer'",
      {
        ...process.env,
        LIBEUFIN_SANDBOX_URL: this.cliDetails.sandboxUrl + "/demobanks/default",
        LIBEUFIN_SANDBOX_USERNAME: username,
        LIBEUFIN_SANDBOX_PASSWORD: password,
      },
    );
    console.log(stdout);
  }

  async createEbicsHost(hostId: string): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-createebicshost",
      `libeufin-cli sandbox ebicshost create --host-id=${hostId}`,
      this.env(),
    );
    console.log(stdout);
  }

  async createEbicsSubscriber(
    details: LibeufinEbicsSubscriberDetails,
  ): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-createebicssubscriber",
      "libeufin-cli sandbox ebicssubscriber create" +
        ` --host-id=${details.hostId}` +
        ` --partner-id=${details.partnerId}` +
        ` --user-id=${details.userId}`,
      this.env(),
    );
    console.log(stdout);
  }

  async createEbicsBankAccount(
    sd: LibeufinEbicsSubscriberDetails,
    bankAccountDetails: LibeufinBankAccountDetails,
  ): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-createebicsbankaccount",
      "libeufin-cli sandbox ebicsbankaccount create" +
        ` --iban=${bankAccountDetails.iban}` +
        ` --bic=${bankAccountDetails.bic}` +
        ` --person-name='${bankAccountDetails.personName}'` +
        ` --account-name=${bankAccountDetails.accountName}` +
        ` --ebics-host-id=${sd.hostId}` +
        ` --ebics-partner-id=${sd.partnerId}` +
        ` --ebics-user-id=${sd.userId}`,
      this.env(),
    );
    console.log(stdout);
  }

  async generateTransactions(accountName: string): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-generatetransactions",
      `libeufin-cli sandbox bankaccount generate-transactions ${accountName}`,
      this.env(),
    );
    console.log(stdout);
  }

  async showSandboxTransactions(accountName: string): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-showsandboxtransactions",
      `libeufin-cli sandbox bankaccount transactions ${accountName}`,
      this.env(),
    );
    console.log(stdout);
  }

  async createEbicsConnection(
    connectionDetails: LibeufinEbicsConnectionDetails,
  ): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-createebicsconnection",
      `libeufin-cli connections new-ebics-connection` +
        ` --ebics-url=${connectionDetails.ebicsUrl}` +
        ` --host-id=${connectionDetails.subscriberDetails.hostId}` +
        ` --partner-id=${connectionDetails.subscriberDetails.partnerId}` +
        ` --ebics-user-id=${connectionDetails.subscriberDetails.userId}` +
        ` ${connectionDetails.connectionName}`,
      {
        ...process.env,
        LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
        LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username,
        LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password,
      },
    );
    console.log(stdout);
  }

  async createBackupFile(details: LibeufinBackupFileDetails): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-createbackupfile",
      `libeufin-cli connections export-backup` +
        ` --passphrase=${details.passphrase}` +
        ` --output-file=${details.outputFile}` +
        ` ${details.connectionName}`,
      {
        ...process.env,
        LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
        LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username,
        LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password,
      },
    );
    console.log(stdout);
  }

  async createKeyLetter(details: LibeufinKeyLetterDetails): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-createkeyletter",
      `libeufin-cli connections get-key-letter` +
        ` ${details.connectionName} ${details.outputFile}`,
      {
        ...process.env,
        LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
        LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username,
        LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password,
      },
    );
    console.log(stdout);
  }

  async connect(connectionName: string): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-connect",
      `libeufin-cli connections connect ${connectionName}`,
      {
        ...process.env,
        LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
        LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username,
        LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password,
      },
    );
    console.log(stdout);
  }

  async downloadBankAccounts(connectionName: string): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-downloadbankaccounts",
      `libeufin-cli connections download-bank-accounts ${connectionName}`,
      {
        ...process.env,
        LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
        LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username,
        LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password,
      },
    );
    console.log(stdout);
  }

  async listOfferedBankAccounts(connectionName: string): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-listofferedbankaccounts",
      `libeufin-cli connections list-offered-bank-accounts ${connectionName}`,
      {
        ...process.env,
        LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
        LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username,
        LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password,
      },
    );
    console.log(stdout);
  }

  async importBankAccount(
    importDetails: LibeufinBankAccountImportDetails,
  ): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-importbankaccount",
      "libeufin-cli connections import-bank-account" +
        ` --offered-account-id=${importDetails.offeredBankAccountName}` +
        ` --nexus-bank-account-id=${importDetails.nexusBankAccountName}` +
        ` ${importDetails.connectionName}`,
      {
        ...process.env,
        LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
        LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username,
        LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password,
      },
    );
    console.log(stdout);
  }

  async fetchTransactions(bankAccountName: string): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-fetchtransactions",
      `libeufin-cli accounts fetch-transactions ${bankAccountName}`,
      {
        ...process.env,
        LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
        LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username,
        LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password,
      },
    );
    console.log(stdout);
  }

  async transactions(bankAccountName: string): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-transactions",
      `libeufin-cli accounts transactions ${bankAccountName}`,
      {
        ...process.env,
        LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
        LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username,
        LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password,
      },
    );
    console.log(stdout);
  }

  async preparePayment(details: LibeufinPreparedPaymentDetails): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-preparepayment",
      `libeufin-cli accounts prepare-payment` +
        ` --creditor-iban=${details.creditorIban}` +
        ` --creditor-bic=${details.creditorBic}` +
        ` --creditor-name='${details.creditorName}'` +
        ` --payment-subject='${details.subject}'` +
        ` --payment-amount=${details.currency}:${details.amount}` +
        ` ${details.nexusBankAccountName}`,
      {
        ...process.env,
        LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
        LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username,
        LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password,
      },
    );
    console.log(stdout);
  }

  async submitPayment(
    details: LibeufinPreparedPaymentDetails,
    paymentUuid: string,
  ): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-submitpayments",
      `libeufin-cli accounts submit-payments` +
        ` --payment-uuid=${paymentUuid}` +
        ` ${details.nexusBankAccountName}`,
      {
        ...process.env,
        LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
        LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username,
        LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password,
      },
    );
    console.log(stdout);
  }

  async newAnastasisFacade(req: NewAnastasisFacadeReq): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-new-anastasis-facade",
      `libeufin-cli facades new-anastasis-facade` +
        ` --currency ${req.currency}` +
        ` --facade-name ${req.facadeName}` +
        ` ${req.connectionName} ${req.accountName}`,
      {
        ...process.env,
        LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
        LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username,
        LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password,
      },
    );
    console.log(stdout);
  }

  async newTalerWireGatewayFacade(req: NewTalerWireGatewayReq): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-new-taler-wire-gateway-facade",
      `libeufin-cli facades new-taler-wire-gateway-facade` +
        ` --currency ${req.currency}` +
        ` --facade-name ${req.facadeName}` +
        ` ${req.connectionName} ${req.accountName}`,
      {
        ...process.env,
        LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
        LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username,
        LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password,
      },
    );
    console.log(stdout);
  }

  async listFacades(): Promise<void> {
    const stdout = await sh(
      this.globalTestState,
      "libeufin-cli-facades-list",
      `libeufin-cli facades list`,
      {
        ...process.env,
        LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
        LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username,
        LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password,
      },
    );
    console.log(stdout);
  }
}

interface NewAnastasisFacadeReq {
  facadeName: string;
  connectionName: string;
  accountName: string;
  currency: string;
}

interface NewTalerWireGatewayReq {
  facadeName: string;
  connectionName: string;
  accountName: string;
  currency: string;
}

/**
 * Launch Nexus and Sandbox AND creates users / facades / bank accounts /
 * .. all that's required to start making bank traffic.
 */
export async function launchLibeufinServices(
  t: GlobalTestState,
  nexusUserBundle: NexusUserBundle[],
  sandboxUserBundle: SandboxUserBundle[] = [],
  withFacades: string[] = [], // takes only "twg" and/or "anastasis"
): Promise<LibeufinServices> {
  const db = await setupDb(t);

  const libeufinSandbox = await LibeufinSandboxService.create(t, {
    httpPort: 5010,
    databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
  });

  await libeufinSandbox.start();
  await libeufinSandbox.pingUntilAvailable();

  const libeufinNexus = await LibeufinNexusService.create(t, {
    httpPort: 5011,
    databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
  });

  await libeufinNexus.start();
  await libeufinNexus.pingUntilAvailable();
  console.log("Libeufin services launched!");

  for (let sb of sandboxUserBundle) {
    await LibeufinSandboxApi.createEbicsHost(
      libeufinSandbox,
      sb.ebicsBankAccount.subscriber.hostID,
    );
    await LibeufinSandboxApi.createEbicsSubscriber(
      libeufinSandbox,
      sb.ebicsBankAccount.subscriber,
    );
    await LibeufinSandboxApi.createDemobankAccount(
      sb.ebicsBankAccount.label,
      "password-unused",
      { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/access-api/" },
    );
    await LibeufinSandboxApi.createDemobankEbicsSubscriber(
      sb.ebicsBankAccount.subscriber,
      sb.ebicsBankAccount.label,
      { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/" },
    );
  }
  console.log("Sandbox user(s) / account(s) / subscriber(s): created");

  for (let nb of nexusUserBundle) {
    await LibeufinNexusApi.createEbicsBankConnection(libeufinNexus, nb.connReq);
    await LibeufinNexusApi.connectBankConnection(
      libeufinNexus,
      nb.connReq.name,
    );
    await LibeufinNexusApi.fetchAccounts(libeufinNexus, nb.connReq.name);
    await LibeufinNexusApi.importConnectionAccount(
      libeufinNexus,
      nb.connReq.name,
      nb.remoteAccountName,
      nb.localAccountName,
    );
    await LibeufinNexusApi.createUser(libeufinNexus, nb.userReq);
    for (let facade of withFacades) {
      switch (facade) {
        case "twg":
          await LibeufinNexusApi.createTwgFacade(libeufinNexus, nb.twgReq);
          await LibeufinNexusApi.postPermission(
            libeufinNexus,
            nb.twgTransferPermission,
          );
          await LibeufinNexusApi.postPermission(
            libeufinNexus,
            nb.twgHistoryPermission,
          );
          break;
        case "anastasis":
          await LibeufinNexusApi.createAnastasisFacade(
            libeufinNexus,
            nb.anastasisReq,
          );
      }
    }
  }
  console.log(
    "Nexus user(s) / connection(s) / facade(s) / permission(s): created",
  );

  return {
    commonDb: db,
    libeufinNexus: libeufinNexus,
    libeufinSandbox: libeufinSandbox,
  };
}

/**
 * Helper function that searches a payment among
 * a list, as returned by Nexus.  The key is just
 * the payment subject.
 */
export function findNexusPayment(
  key: string,
  payments: LibeufinNexusTransactions,
): LibeufinNexusMoneyMovement | void {
  let transactions = payments["transactions"];
  for (let i = 0; i < transactions.length; i++) {
    //FIXME: last line won't compile with the current definition of the type
    //@ts-ignore
    let batches = transactions[i]["camtData"]["batches"];
    for (let y = 0; y < batches.length; y++) {
      let movements = batches[y]["batchTransactions"];
      for (let z = 0; z < movements.length; z++) {
        let movement = movements[z];
        if (movement["details"]["unstructuredRemittanceInformation"] == key)
          return movement;
      }
    }
  }
}