#!/bin/bash
#
#  This file is part of TALER
#  Copyright (C) 2014-2026 Taler Systems SA
#
#  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.
#
#  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
#  TALER; see the file COPYING.  If not, If not, see <http://www.gnu.org/license>
#
#
# shellcheck disable=SC2317
# shellcheck disable=SC1091
#
#
# Setup database which was generated from a perfectly normal
# exchange-wallet-merchant interaction on a KYC-enabled exchange and run
# the auditor against it.
#
# The reference database (see generate-kyc-basedb.sh) contains two merchant
# instances that were paid the same amount and both proved that they own
# their bank account with a KYCAUTH wire transfer, but only one of them
# then passed the KYC form.  The exchange therefore wired the funds of the
# first one out and is still holding the funds of the second one.
#
# The auditor has almost no KYC-specific logic of its own -- see issues.txt
# for what it could check and does not -- so this test has two jobs:
#
#  1. the KYC artefacts (KYCAUTH credits, legitimization measures and
#     outcomes, an aggregation the exchange lawfully withheld) must not
#     make the auditor invent findings, and the KYCAUTH totals it does
#     track -- total_kycauth_in from the bank, total_kycauth_revenue from
#     the exchange -- must be exactly right;
#  2. fault detection must still work on a KYC-enabled exchange, so the
#     later tests inject the same kinds of faults test-auditor.sh does and
#     require the auditor to flag them.  Tests 2, 8 and 9 pull the two
#     KYCAUTH totals apart in each of the ways they can disagree, which is
#     what the reconciliation between them exists to catch.
#
# Requires 'jq' tool and Postgres superuser rights!
set -eu
#set -x

# Set of numbers for all the testcases.
# When adding new tests, increase the last number:
ALL_TESTS=$(seq 0 16)

# $TESTS determines which tests we should run.
# This construction is used to make it easy to
# only run a subset of the tests. To only run a subset,
# pass the numbers of the tests to run as the FIRST
# argument to test-kyc.sh, i.e.:
#
# $ test-kyc.sh "1 3"
#
# to run tests 1 and 3 only.  By default, all tests are run.
#
TESTS=${1:-$ALL_TESTS}

export TALER_AUDITOR_TOKEN="secret-token:D4CST1Z6AHN3RT03M0T9NSTF2QGHTB5ZD2D3RYZB4HAWG8SX0JEFWBXCKXZHMB7Y3Z7KVFW0B3XPXD5BHCFP8EB0R6CNH2KAWDWVET0"
export TALER_AUDITOR_SALT="64S36D1N6RVKGC9J6CT3ADHQ70RK4CSM6MV3EE1H68SK8D9P6WW32CHK6GTKCDSR64S36D1N6RVKGC9J6CT3ADHQ70RK4CSM6MV3EE0"

# Global variable to run the auditor processes under valgrind
# VALGRIND=valgrind
VALGRIND=""

. setup.sh


# Cleanup exchange and libeufin between runs.
function cleanup()
{
    if [ -n "${EPID:-}" ]
    then
        echo -n "Stopping exchange $EPID..."
        kill -TERM "$EPID"
        wait "$EPID" || true
        echo "DONE"
        unset EPID
    fi
    stop_libeufin &> /dev/null
}

# Cleanup to run whenever we exit
function exit_cleanup()
{
    jobs
    if [ -n "${POSTGRES_PATH:-}" ]
    then
        echo -n "Stopping Postgres at ${POSTGRES_PATH} ..."
        "${POSTGRES_PATH}/pg_ctl" \
                        -D "$TMPDIR" \
                        --log="${MY_TMP_DIR}/pg_ctl.log" \
                        stop \
            &> ${MY_TMP_DIR}/pg_ctl.out \
            || true
        echo "DONE"
    fi
    echo -n "Running exit-cleanup ..."
    cleanup
    for n in $(jobs -p)
    do
        kill "$n" 2> /dev/null || true
    done
    wait || true
    echo "DONE"
}

# Install cleanup handler (except for kill -9)
trap exit_cleanup EXIT


function await_bank () {
    for n in $(seq 1 80)
    do
        echo -n "."
        sleep 0.1
        OK=1
        wget http://localhost:8082/ \
             -o /dev/null \
             -O /dev/null \
             >/dev/null \
            && break
        OK=0
    done
    if [ 1 != "$OK" ]
    then
        exit_skip "Failed to launch libeufin-bank"
    fi
 }

# Operations to run before the actual audit
function pre_audit () {
    # Launch bank
    echo -n "Launching libeufin-bank"
    export CONF
    export MY_TMP_DIR
    launch_libeufin
    await_bank
    echo " DONE"

    if [ "${1:-no}" = "aggregator" ]
    then
        # NO '-y' here: that is --kyc-off, which makes the aggregator
        # wire funds out without consulting KYC at all and would defeat
        # the point of this test.
        echo -n "Running exchange aggregator ..."
        taler-exchange-aggregator \
            -L "INFO" \
            -t \
            -c "$CONF" \
            2> "${MY_TMP_DIR}/aggregator.log" \
            || exit_fail "FAIL"
        echo " DONE"
        echo -n "Running exchange closer ..."
        taler-exchange-closer \
            -L "INFO" \
            -t \
            -c "$CONF" \
            2> "${MY_TMP_DIR}/closer.log" \
            || exit_fail "FAIL"
        echo " DONE"
        echo -n "Running exchange transfer ..."
        taler-exchange-transfer \
            -L "INFO" \
            -t \
            -c "$CONF" \
            2> "${MY_TMP_DIR}/transfer.log" \
            || exit_fail "FAIL"
        echo " DONE"
    fi
}

# actual audit run
function audit_only () {
    # Run the auditor!
    echo -n "Running audit(s) ..."

    # Restart so that first run is always fresh, and second one is incremental
    taler-auditor-dbinit \
        -r \
        -c "$CONF"
    $VALGRIND taler-helper-auditor-aml \
              -i \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-aml.out" \
              2> "${MY_TMP_DIR}/test-audit-aml.err" \
        || exit_fail "aml audit failed (see ${MY_TMP_DIR}/test-audit-aml.*)"
    echo -n "."
    $VALGRIND taler-helper-auditor-aml \
              -i \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-aml-inc.out" \
              2> "${MY_TMP_DIR}/test-audit-aml-inc.err" \
        || exit_fail "incremental aml audit failed (see ${MY_TMP_DIR}/test-audit-aml-inc.*)"
    echo -n "."
    $VALGRIND taler-helper-auditor-aggregation \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-aggregation.out" \
              2> "${MY_TMP_DIR}/test-audit-aggregation.err" \
        || exit_fail "aggregation audit failed (see ${MY_TMP_DIR}/test-audit-aggregation.*)"
    echo -n "."
    $VALGRIND taler-helper-auditor-aggregation \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-aggregation-inc.out" \
              2> "${MY_TMP_DIR}/test-audit-aggregation-inc.err" \
        || exit_fail "incremental aggregation audit failed (see ${MY_TMP_DIR}/test-audit-aggregation-inc.*)"
    echo -n "."
    $VALGRIND taler-helper-auditor-coins \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-coins.out" \
              2> "${MY_TMP_DIR}/test-audit-coins.err" \
        || exit_fail "coin audit failed (see ${MY_TMP_DIR}/test-audit-coins.*)"
    echo -n "."
    $VALGRIND taler-helper-auditor-coins \
              -L DEBUG  \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-coins-inc.out" \
              2> "${MY_TMP_DIR}/test-audit-coins-inc.err" \
        || exit_fail "incremental coin audit failed (see ${MY_TMP_DIR}/test-audit-coins-inc.*)"
    echo -n "."
    $VALGRIND taler-helper-auditor-deposits \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-deposits.out" \
              2> "${MY_TMP_DIR}/test-audit-deposits.err" \
        || exit_fail "deposits audit failed (see ${MY_TMP_DIR}/test-audit-deposits.*)"
    echo -n "."
    $VALGRIND taler-helper-auditor-deposits \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-deposits-inc.out" \
              2> "${MY_TMP_DIR}/test-audit-deposits-inc.err" \
        || exit_fail "incremental deposits audit failed (see ${MY_TMP_DIR}/test-audit-deposits-inc.*)"
    echo -n "."
    $VALGRIND taler-helper-auditor-reserves \
              -i \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-reserves.out" \
              2> "${MY_TMP_DIR}/test-audit-reserves.err" \
        || exit_fail "reserves audit failed (see ${MY_TMP_DIR}/test-audit-reserves.*)"
    echo -n "."
    $VALGRIND taler-helper-auditor-reserves \
              -i \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-reserves-inc.out" \
              2> "${MY_TMP_DIR}/test-audit-reserves-inc.err" \
        || exit_fail "incremental reserves audit failed (see ${MY_TMP_DIR}/test-audit-reserves-inc.*)"
    echo -n "."
    $VALGRIND taler-helper-auditor-wire-credit \
              -i \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-wire-credit.out" \
              2> "${MY_TMP_DIR}/test-audit-wire-credit.err" \
        || exit_fail "wire credit audit failed (see ${MY_TMP_DIR}/test-audit-wire-credit.*)"
    echo -n "."
    $VALGRIND taler-helper-auditor-wire-credit \
              -i \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-wire-credit-inc.out" \
              2> "${MY_TMP_DIR}/test-audit-wire-credit-inc.err" \
        || exit_fail "wire credit audit inc failed (see ${MY_TMP_DIR}/test-audit-wire-credit-inc.*)"
    echo -n "."
    $VALGRIND taler-helper-auditor-wire-debit \
              -i \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-wire-debit.out" \
              2> "${MY_TMP_DIR}/test-audit-wire-debit.err" \
        || exit_fail "wire debit audit failed (see ${MY_TMP_DIR}/test-audit-wire-debit.*)"
    echo -n "."
    $VALGRIND taler-helper-auditor-wire-debit \
              -i \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-wire-debit-inc.out" \
              2> "${MY_TMP_DIR}/test-audit-wire-debit-inc.err" \
        || exit_fail "wire debit audit inc failed (see ${MY_TMP_DIR}/test-audit-wire-debit-inc.*)"
    echo -n "."
    $VALGRIND taler-helper-auditor-purses \
             -i \
             -L DEBUG \
             -c "$CONF" \
             -t \
             > "${MY_TMP_DIR}/test-audit-purses.out" \
             2> "${MY_TMP_DIR}/test-audit-purses.err" \
       || exit_fail "audit purses failed"
    echo -n "."
    $VALGRIND taler-helper-auditor-purses \
              -i \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-purses-inc.out" \
              2> "${MY_TMP_DIR}/test-audit-purses-inc.err" \
        || exit_fail "audit purses inc failed"
    echo -n "."
    $VALGRIND taler-helper-auditor-transfer \
             -i \
             -L DEBUG \
             -c "$CONF" \
             -t \
             > "${MY_TMP_DIR}/test-audit-transfer.out" \
             2> "${MY_TMP_DIR}/test-audit-transfer.err" \
       || exit_fail "audit transfer failed"
    echo -n "."
    $VALGRIND taler-helper-auditor-transfer \
              -i \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-transfer-inc.out" \
              2> "${MY_TMP_DIR}/test-audit-transfer-inc.err" \
        || exit_fail "audit transfer inc failed"
    echo -n "."

    echo " DONE"
}


# Cleanup to run after the auditor
function post_audit () {
    taler-exchange-dbinit \
        -c "$CONF" \
        -g \
        || exit_fail "exchange DB GC failed"
    cleanup
}


# Run audit process on current database, including report
# generation.  Pass "aggregator" as $1 to run
# $ taler-exchange-aggregator
# before auditor (to trigger pending wire transfers).
# Pass "drain" as $2 to run a drain operation as well.
function run_audit () {
    pre_audit "${1:-no}"
    audit_only
    post_audit
}


function stop_auditor_httpd() {
  if [ -n "${APID:-}" ]
  then
      echo -n "Stopping auditor $APID..."
      kill -TERM "$APID"
      wait "$APID" || true
      echo "DONE"
      unset APID
  fi
}


# Do a full reload of the (original) database
function full_reload()
{
    echo -n "Doing full reload of the database (loading ${BASEDB}.sql into $DB at ${PGHOST:-})... "
    dropdb -f "$DB" &>> ${MY_TMP_DIR}/drop.log || true
    createdb -T template0 "$DB" \
        || exit_skip "could not create database $DB (at ${PGHOST:-})"
    # Import pre-generated database, -q(ietly) using single (-1) transaction
    psql -Aqt "$DB" \
         -q \
         -1 \
         -f "${BASEDB}.sql" \
         &>> ${MY_TMP_DIR}/postgresql-reload.log \
        || exit_skip "Failed to load database $DB from ${BASEDB}.sql"
    echo "DONE"
    # Technically, this call shouldn't be needed as libeufin should already be stopped here...
    stop_libeufin
    stop_auditor_httpd
}

function run_auditor_httpd() {
  echo -n "Starting auditor..."
  $VALGRIND taler-auditor-httpd \
      -c "${CONF}" \
      -L INFO \
      2> "${MY_TMP_DIR}/auditor-httpd.err" &
  APID=$!

  # Wait for auditor service to be available
  for n in $(seq 1 50)
  do
      echo -n "."
      sleep 0.2
      OK=0
      # auditor
      wget "http://localhost:8083/config" \
           -o /dev/null \
           -O /dev/null \
           >/dev/null \
          || continue
      OK=1
      break
  done
  echo "... DONE."
}


function check_auditor_running() {
  ARUNSTATUS=$(curl -Is http://localhost:8083/config | head -1)
  if [ -n "${ARUNSTATUS:-}" ]
    then
      echo "Auditor running"
    else
      echo "Auditor not running, starting it"
      run_auditor_httpd
  fi
  unset ARUNSTATUS
}

function call_endpoint() {
    if [ -n "${2+x}" ]
    then
        curl -s -H "Accept: application/json" -H "Authorization: Bearer ${TALER_AUDITOR_TOKEN}" -o "${MY_TMP_DIR}/${2}.json" "localhost:8083/monitoring/${1}?limit=50&balance_key=${2}"
        echo "endpoint ${1} called (with balance_key)... "
    else
        curl -s -H "Accept: application/json" -H "Authorization: Bearer ${TALER_AUDITOR_TOKEN}" -o "${MY_TMP_DIR}/${1}.json" "localhost:8083/monitoring/${1}?limit=50"
        echo "endpoint ${1} called... "
    fi
}


function check_balance() {
    call_endpoint "balances" "$1"
    BAL=$(jq -r .balances[0].balance_value < "${MY_TMP_DIR}/${1}.json")
    if [ "$BAL" != "$2" ]
    then
        exit_fail "$3 (got $BAL, wanted $2)"
    fi
    echo "PASS"
}


function check_not_balance() {
    call_endpoint "balances" "$1"
    BAL=$(jq -r .balances[0].balance_value < "${MY_TMP_DIR}/${1}.json")
    if [ "$BAL" = "$2" ]
    then
        exit_fail "$3 (got $BAL, wanted NOT $2)"
    fi
    echo "PASS"
}


function check_report() {
    call_endpoint "$1"
    NAME=$(echo "$1" | tr '-' '_')
    # shellcheck disable=SC2086
    VAL=$(jq -r .\"${NAME}\"[0].\"$2\" < "${MY_TMP_DIR}/${1}.json")
    if [ "$VAL" != "$3" ]
    then
        exit_fail "$1::$2 (got $VAL, wanted $3)"
    fi
    echo "PASS"
}

# Check that at least one entry of report $1 has field $2 set to $3.
# Unlike check_report, this does not depend on the order in which the
# auditor inserted its findings: one fault can legitimately be reported
# against several operations (a coin with a bad denomination signature is
# flagged once per operation that used it).
function check_report_any() {
    call_endpoint "$1"
    NAME=$(echo "$1" | tr '-' '_')
    # shellcheck disable=SC2086
    jq -e --arg want "$3" "any(.\"${NAME}\"[]; .\"$2\" == \$want)" \
       < "${MY_TMP_DIR}/${1}.json" \
       > /dev/null \
        || exit_fail "$1::$2 (no entry with value $3)"
    echo "PASS"
}

# Check that report $1 has at least one entry, without pinning down any of
# its fields.  Useful when the injected fault is certain to be found but
# the exact amounts depend on how much of a reserve happened to be left.
function check_some_report() {
    call_endpoint "$1"
    NAME=$(echo "$1" | tr '-' '_')
    # shellcheck disable=SC2086
    jq -e .\"${NAME}\"[0] \
       < "${MY_TMP_DIR}/${1}.json" \
       > /dev/null \
        || exit_fail "Wanted a report for $1, but got none"
    echo "PASS"
}

function check_no_report() {
    call_endpoint "$1"
    NAME=$(echo "$1" | tr '-' '_')
    # shellcheck disable=SC2086
    jq -e .\"${NAME}\"[0] \
       < "${MY_TMP_DIR}/${1}.json" \
       > /dev/null \
       && exit_fail "Wanted empty report for $1, but got incidents"
    echo "PASS"
}

function check_report_neg() {
    call_endpoint "$1"
    NAME=$(echo "$1" | tr '-' '_')
    # shellcheck disable=SC2086
    VAL=$(jq -r .\"${NAME}\"[0].\"$2\" < "${MY_TMP_DIR}/${1}.json")
    if [ "$VAL" == "$3" ]
    then
        exit_fail "$1::$2 (got $VAL, wanted $3)"
    fi
    echo "PASS"
}

function check_row() {
    call_endpoint "$1"
    NAME=$(echo "$1" | tr '-' '_')
    if [ -n "${3+x}" ]
    then
        RID="$2"
        WANT="$3"
    else
        RID="row_id"
        WANT="$2"
    fi
    # shellcheck disable=SC2086
    ROW=$(jq -r .\"${NAME}\"[0].\"${RID}\" < "${MY_TMP_DIR}/${1}.json")
    if [ "$ROW" != "$WANT" ]
    then
        exit_fail "Row ${1} wrong (got ${ROW}, wanted ${WANT})"
    fi
    echo "PASS"
}



# Amount the exchange wired to the merchant that passed KYC.
CLEARED_WIRED="TESTKUDOS:8.02"
# Amount the exchange aggregated for the merchant that did not pass KYC
# and then parked in aggregation_transient instead of wiring it out.
WITHHELD="TESTKUDOS:8.03"
# The account of that merchant.  The IBAN libeufin derives from the account
# name is stable across regenerations of the reference database, so this can be
# hardcoded -- but the wire transfer identifier cannot: the aggregator draws it
# with GNUNET_CRYPTO_random_block(), so tests 14 and 15 read it out of the
# exchange's own tables instead of naming it.
BLOCKED_ACCOUNT="payto://iban/DE61109451?receiver-name=Blocked44"
# Two KYCAUTH wire transfers of TESTKUDOS:0.1, one per merchant instance.
KYCAUTH_IN="TESTKUDOS:0.2"
# Everything the exchange's bank account was credited with: TESTKUDOS:56 of
# reserve top-ups plus the two KYCAUTH transfers above.  The KYCAUTH part is
# revenue rather than a liability, but it is money in the account all the
# same, so it belongs in the balance the account is expected to have.
WIRE_IN="TESTKUDOS:56.2"


# Assert that all of the auditor's loss, delta and irregularity balances
# are zero, i.e. that nothing about this database looks wrong to it.
function check_all_clean() {
    echo -n "Test for emergencies... "
    check_no_report "emergency"
    echo -n "Test for emergencies by count... "
    check_no_report "emergency-by-count"
    echo -n "Test for deposit confirmation problems... "
    check_no_report "deposit-confirmation"
    echo -n "Test for denomination key validity problems... "
    check_no_report "denomination-key-validity-withdraw-inconsistency"
    echo -n "Test for denominations without signatures... "
    check_no_report "denominations-without-sigs"
    echo -n "Test for wire out inconsistencies... "
    check_no_report "wire-out-inconsistency"
    echo -n "Test for reserve in inconsistencies... "
    check_no_report "reserve-in-inconsistency"
    echo -n "Test for KYCAUTH in inconsistencies... "
    check_no_report "kycauth-in-inconsistency"
    echo -n "Test for misattribution inconsistencies... "
    check_no_report "misattribution-in-inconsistency"
    echo -n "Test for row inconsistencies... "
    check_no_report "row-inconsistency"
    echo -n "Test for minor row inconsistencies... "
    check_no_report "row-minor-inconsistencies"
    echo -n "Test for wire format inconsistencies... "
    check_no_report "wire-format-inconsistency"
    echo -n "Test for arithmetic inconsistencies... "
    check_no_report "amount-arithmetic-inconsistency"
    echo -n "Test for coin inconsistencies... "
    check_no_report "coin-inconsistency"
    echo -n "Test for bad signature losses... "
    check_no_report "bad-sig-losses"
    echo -n "Test for fee time inconsistencies... "
    check_no_report "fee-time-inconsistency"
    echo -n "Test for purses not closed... "
    check_no_report "purse-not-closed-inconsistencies"
    echo -n "Test for reserves not closed... "
    check_no_report "reserve-not-closed-inconsistency"
    echo -n "Test for insufficient reserve balances... "
    check_no_report "reserve-balance-insufficient-inconsistency"
    echo -n "Test for wrong reserve balance summaries... "
    check_no_report "reserve-balance-summary-wrong-inconsistency"
    echo -n "Test for closure lags... "
    check_no_report "closure-lags"

    # Just to test the endpoint and for logging ...
    call_endpoint "balances"

    echo -n "Testing aggregation bad sig loss balance... "
    check_balance \
        "aggregation_total_bad_sig_loss" \
        "TESTKUDOS:0" \
        "Wrong total bad sig loss from aggregation"
    echo -n "Testing coin irregular loss balance... "
    check_balance \
        "coin_irregular_loss" \
        "TESTKUDOS:0" \
        "Wrong total irregular loss from coins"
    echo -n "Testing reserves bad sig loss balance... "
    check_balance \
        "reserves_total_bad_sig_loss" \
        "TESTKUDOS:0" \
        "Wrong total bad sig loss from reserves"
    echo -n "Testing purse bad sig loss balance... "
    check_balance \
        "purse_total_bad_sig_loss" \
        "TESTKUDOS:0" \
        "Wrong total bad sig loss from purses"

    echo -n "Test for bad incoming delta plus... "
    check_balance \
        "total_bad_amount_in_plus" \
        "TESTKUDOS:0" \
        "Expected total wire in delta plus wrong"
    echo -n "Test for bad incoming delta minus... "
    check_balance \
        "total_bad_amount_in_minus" \
        "TESTKUDOS:0" \
        "Expected total wire in delta minus wrong"
    echo -n "Test for bad outgoing delta plus... "
    check_balance \
        "total_bad_amount_out_plus" \
        "TESTKUDOS:0" \
        "Expected total wire out delta plus wrong"
    echo -n "Test for bad outgoing delta minus... "
    check_balance \
        "total_bad_amount_out_minus" \
        "TESTKUDOS:0" \
        "Expected total wire out delta minus wrong"
    echo -n "Test for misattribution amounts... "
    check_balance \
        "total_misattribution_in" \
        "TESTKUDOS:0" \
        "Expected total misattribution in wrong"

    echo -n "Test for aggregation wire out delta plus... "
    check_balance \
        "aggregation_total_wire_out_delta_plus" \
        "TESTKUDOS:0" \
        "Expected total wire out delta plus wrong"
    echo -n "Test for aggregation wire out delta minus... "
    check_balance \
        "aggregation_total_wire_out_delta_minus" \
        "TESTKUDOS:0" \
        "Expected total wire out delta minus wrong"

    echo -n "Checking for unexpected aggregation delta plus... "
    check_balance \
        "aggregation_total_arithmetic_delta_plus" \
        "TESTKUDOS:0" \
        "Wrong arithmetic delta plus from aggregations"
    echo -n "Checking for unexpected aggregation delta minus... "
    check_balance \
        "aggregation_total_arithmetic_delta_minus" \
        "TESTKUDOS:0" \
        "Wrong arithmetic delta minus from aggregations"
    echo -n "Checking for unexpected coin delta plus... "
    check_balance \
        "coins_total_arithmetic_delta_plus" \
        "TESTKUDOS:0" \
        "Wrong arithmetic delta plus from coins"
    echo -n "Checking for unexpected coin delta minus... "
    check_balance \
        "coins_total_arithmetic_delta_minus" \
        "TESTKUDOS:0" \
        "Wrong arithmetic delta minus from coins"
    echo -n "Checking for unexpected reserves delta plus... "
    check_balance \
        "reserves_total_arithmetic_delta_plus" \
        "TESTKUDOS:0" \
        "Wrong arithmetic delta plus from reserves"
    echo -n "Checking for unexpected reserves delta minus... "
    check_balance \
        "reserves_total_arithmetic_delta_minus" \
        "TESTKUDOS:0" \
        "Wrong arithmetic delta minus from reserves"
}


# The KYC-specific assertions: how much arrived by KYCAUTH, how much left
# the exchange, and how much it is sitting on.
function check_kyc_state() {
    echo -n "Checking the KYCAUTH credits were accounted for... "
    check_balance \
        "total_kycauth_in" \
        "$KYCAUTH_IN" \
        "Wrong total for KYCAUTH wire transfers"

    # KYCAUTH money is not credited to a reserve and never paid back, so
    # the auditor books it as revenue of the exchange operator and counts
    # it towards the balance the bank account is expected to have.
    echo -n "Checking the KYCAUTH credits were booked as revenue... "
    check_balance \
        "total_kycauth_revenue" \
        "$KYCAUTH_IN" \
        "Wrong KYCAUTH revenue"
    echo -n "Checking the KYCAUTH credits reached the balance sheet... "
    check_balance \
        "total_wire_in" \
        "$WIRE_IN" \
        "Wrong total credited to the exchange"

    echo -n "Checking that only the cleared merchant was paid... "
    check_balance \
        "total_wire_out" \
        "$CLEARED_WIRED" \
        "Wrong total wired out"

    # The exchange aggregated the blocked merchant's coins, wrote the
    # aggregation_tracking rows and then parked the payout in
    # aggregation_transient rather than executing it.  Confirm from the
    # exchange's own database that the money really is still there, so
    # that the auditor-side assertion below is about the auditor and not
    # about the reference database having drifted.
    echo -n "Checking that the exchange withheld the blocked payout... "
    TRANSIENT=$(psql -Aqt "$DB" \
                     -c "SELECT COUNT(*) FROM exchange.aggregation_transient;")
    if [ "$TRANSIENT" != "1" ]
    then
        exit_fail "Expected one withheld aggregation, got ${TRANSIENT}"
    fi
    WIRED=$(psql -Aqt "$DB" -c "SELECT COUNT(*) FROM exchange.wire_out;")
    if [ "$WIRED" != "1" ]
    then
        exit_fail "Expected exactly one wire transfer, got ${WIRED}"
    fi
    echo "PASS"

    # The exchange also said *why*, in the append-only table that replication
    # can carry.  aggregation_transient above is the same claim, but an
    # external auditor never gets to see it, so this is the row the auditor's
    # classification actually rests on.  Reason 2 is
    # TALER_EXCHANGEDB_DR_KYC.
    #
    # Restrict this to transfers that have not gone out.  Both merchants were
    # withheld on the generator's first aggregator run -- neither had passed
    # KYC yet -- so the cleared one has a deferral of its own, made obsolete by
    # the wire_out row that followed.  Append-only means those stay on file,
    # which is the point: the record of a hold outlives the hold.
    echo -n "Checking that the exchange recorded why it withheld it... "
    DEFERRED=$(psql -Aqt "$DB" \
                    -c "SELECT COUNT(*) FROM exchange.aggregation_deferrals d
                         WHERE d.deferral_reason=2
                           AND d.legitimization_requirement_serial_id != 0
                           AND NOT EXISTS (
                                 SELECT 1 FROM exchange.wire_out w
                                  WHERE w.wtid_raw=d.wtid_raw);")
    if [ "$DEFERRED" != "1" ]
    then
        exit_fail "Expected one open KYC deferral on record, got ${DEFERRED}"
    fi
    echo "PASS"

    # ... and the auditor sees it.  The withheld deposits do carry
    # aggregation_tracking rows, so they are not "lagging" in the sense of
    # total_amount_lag, which counts deposits the exchange has not aggregated
    # at all; that balance is legitimately zero here.  What the exchange has
    # not done is the wire transfer, and that is what total_aml_hold counts.
    echo -n "Checking that nothing is waiting to be aggregated... "
    check_balance \
        "total_amount_lag" \
        "TESTKUDOS:0" \
        "Wrong total for deposits that were never aggregated"
    echo -n "Checking that the withheld payout is booked as an AML hold... "
    check_balance \
        "total_aml_hold" \
        "$WITHHELD" \
        "Wrong total withheld pending legitimization"
    # The blocked merchant is the *only* transfer the exchange owes, and its
    # KYC measure is open, so nothing may land in the unexplained bucket.
    # Were the two ever to swap, the exchange would be stalling a payout it
    # has no legal reason to stall, which is the case this split exists for.
    echo -n "Checking that no payout is unexplained... "
    check_balance \
        "total_transfer_lag" \
        "TESTKUDOS:0" \
        "Exchange is withholding funds it gave no reason for"
    # Every deposit here is aggregated into a payout worth making, so the
    # third bucket -- money parked only because the aggregate would not
    # cover its wire fee -- must be empty.  It exists so that such money
    # does not read as either of the two above, both of which say something
    # is wrong.
    echo -n "Checking that nothing is parked as a small aggregate... "
    check_balance \
        "total_small_aggregate" \
        "TESTKUDOS:0" \
        "Exchange parked an aggregate as too small to pay out"
    echo -n "Checking that the hold names the blocked account... "
    check_report_any \
        "aml-holds" \
        "account" \
        "$BLOCKED_ACCOUNT"
    echo -n "Checking that the hold names the amount... "
    check_report "aml-holds" "amount" "$WITHHELD"
    echo -n "Checking that the hold is attributed to KYC... "
    check_report "aml-holds" "deferral_reason" "KYC"
    echo -n "Test for pending deposits... "
    check_no_report "pending-deposits"
    echo -n "Test for early aggregations... "
    check_no_report "early-aggregation"
}


# The KYC artefacts must not make the auditor invent findings.
function test_0() {

    echo "===========0: normal run with aggregator==========="
    run_audit aggregator
    check_auditor_running

    echo "Checking output"
    check_all_clean
    check_kyc_state

    # cannot easily undo aggregator, hence full reload
    full_reload
    cleanup
}


# Same, but without letting the aggregator run first.  The aggregator
# cannot lawfully do anything more than it already did when the reference
# database was generated, so the auditor's view must be identical.
function test_1() {

    echo "===========1: normal run==========="
    run_audit
    check_auditor_running

    echo "Checking output"
    check_all_clean
    check_kyc_state

    # Database was unmodified, no need to undo
    echo "OK"
}


# A KYCAUTH credit that the bank does not agree is a KYCAUTH credit.
function test_2() {

    echo "===========2: KYCAUTH credit reclassified by the bank==========="
    # Modifying the bank's books needs libeufin to be restarted, it does
    # not re-read them.
    pre_audit
    stop_libeufin
    echo -n "Modifying database: "
    # libeufin records for every credit to the exchange whether it was a
    # reserve top-up or an account authorisation.  Turn one of the two
    # KYCAUTH credits into a reserve credit: the KYCAUTH total the auditor
    # tracks must drop by that amount, and because the 32 bytes of KYCAUTH
    # metadata are not a reserve public key the exchange knows, the credit
    # now also looks like an incoming transfer the exchange never booked.
    echo "UPDATE libeufin_bank.taler_exchange_incoming SET type='reserve' WHERE exchange_incoming_id=(SELECT MIN(exchange_incoming_id) FROM libeufin_bank.taler_exchange_incoming WHERE type='kyc')" \
        | psql -Aqt "$DB"
    echo "DONE"
    launch_libeufin
    await_bank

    audit_only
    check_auditor_running

    echo -n "Checking that the KYCAUTH total dropped... "
    check_balance \
        "total_kycauth_in" \
        "TESTKUDOS:0.1" \
        "Wrong total for KYCAUTH wire transfers"
    # Only the bank's books were touched, so the exchange still claims the
    # KYCAUTH revenue for a transfer the bank no longer reports as one.
    echo -n "Checking that the exchange still claims the revenue... "
    check_balance \
        "total_kycauth_revenue" \
        "$KYCAUTH_IN" \
        "Wrong KYCAUTH revenue"
    echo -n "Checking that the unbooked credit was flagged... "
    check_report_any \
        "reserve-in-inconsistency" \
        "diagnostic" "unknown to exchange"
    echo -n "Checking that it was counted as an excess credit... "
    check_balance \
        "total_bad_amount_in_plus" \
        "TESTKUDOS:0.1" \
        "Wrong total_bad_amount_in_plus"

    # ... and the other side of the same disagreement: the exchange has a
    # kycauths_in row for a transfer the bank now says was something else,
    # so the auditor must not take the exchange's word for it.
    echo -n "Checking that the unconfirmed KYCAUTH was flagged... "
    check_report_any \
        "kycauth-in-inconsistency" \
        "diagnostic" "KYCAUTH wire transfer claimed by exchange not found"
    echo -n "Checking the amount the exchange claims... "
    check_report_any \
        "kycauth-in-inconsistency" \
        "amount_exchange_expected" "TESTKUDOS:0.1"
    echo -n "Checking that it was counted as a missing credit... "
    check_balance \
        "total_bad_amount_in_minus" \
        "TESTKUDOS:0.1" \
        "Wrong total_bad_amount_in_minus"

    post_audit
    full_reload
    cleanup
}


# The rest of the tests inject the same kinds of faults test-auditor.sh
# injects, to show that KYC does not stop the auditor from finding them.


# Change the amount of an incoming wire transfer as recorded by the
# exchange.
function test_3() {

    echo "===========3: reserves_in inconsistency==========="
    echo -n "Modifying database: "
    echo "UPDATE exchange.reserves_in SET credit.val=5 WHERE reserve_in_serial_id=1" \
        | psql -Aqt "$DB"
    echo "DONE"

    run_audit
    check_auditor_running

    echo -n "Testing inconsistency detection... "
    check_report \
        "reserve-in-inconsistency" \
        "row_id" 1
    echo -n "Testing amount wired... "
    check_report \
        "reserve-in-inconsistency" \
        "amount_wired" "TESTKUDOS:10"
    echo -n "Testing amount expected... "
    check_report \
        "reserve-in-inconsistency" \
        "amount_exchange_expected" "TESTKUDOS:5"
    echo -n "Checking wire credit balance plus... "
    check_balance \
        "total_bad_amount_in_plus" \
        "TESTKUDOS:5" \
        "Expected total_bad_amount_in_plus wrong"

    full_reload
    cleanup
}


# Test where denom_sig in the known_coins table is wrong (=> bad signature)
function test_4() {

    echo "===========4: known_coins signature wrong==========="
    OLD_ROW=$(echo "SELECT known_coin_id FROM exchange.known_coins LIMIT 1;" | psql "$DB" -Aqt)
    COIN_PUB=$(echo "SELECT coin_pub FROM exchange.known_coins WHERE known_coin_id=$OLD_ROW;" | psql "$DB" -Aqt)
# shellcheck disable=SC2028
    echo "UPDATE exchange.known_coins SET denom_sig='\x0000000100000000287369672d76616c200a2028727361200a2020287320233542383731423743393036444643303442424430453039353246413642464132463537303139374131313437353746324632323332394644443146324643333445393939413336363430334233413133324444464239413833353833464536354442374335434445304441453035374438363336434541423834463843323843344446304144363030343430413038353435363039373833434431333239393736423642433437313041324632414132414435413833303432434346314139464635394244434346374436323238344143354544364131373739463430353032323241373838423837363535453434423145443831364244353638303232413123290a2020290a20290b' WHERE coin_pub='$COIN_PUB'" \
        | psql -Aqt "$DB"

    run_audit
    check_auditor_running

    echo -n "Checking bad-signature-loss detected... "
    check_report_neg \
        "bad-sig-losses" \
        "loss" "TESTKUDOS:0"
    echo -n "Checking bad-signature-loss balance update... "
    check_not_balance \
        "coin_irregular_loss" \
        "TESTKUDOS:0" \
        "Wrong total bad sig loss"

    full_reload
    cleanup
}


# Test where the amount the bank actually wired differs from what the
# exchange claims.
function test_5() {

    echo "===========5: incorrect wire_out amount==========="
    pre_audit
    stop_libeufin
    echo -n "Modifying database: "
    echo "UPDATE libeufin_bank.bank_account_transactions SET amount=(50,0) WHERE debtor_name='Exchange Company';" \
        | psql -Aqt "$DB"
    echo "DONE"
    launch_libeufin
    await_bank

    audit_only
    check_auditor_running

    echo -n "Testing wire-out-inconsistency expected... "
    check_report \
        "wire-out-inconsistency" \
        "expected" \
        "$CLEARED_WIRED"
    echo -n "Testing wire-out-inconsistency claimed... "
    check_report \
        "wire-out-inconsistency" \
        "claimed" \
        "TESTKUDOS:50"
    echo -n "Testing bad_amount_out_plus balance reporting... "
    check_not_balance \
        "total_bad_amount_out_plus" \
        "TESTKUDOS:0" \
        "reported total_bad_amount_out_plus wrong"

    post_audit
    full_reload
    cleanup
}


# Test where the exchange claims to have received more than the bank sent,
# which also throws its reserve balance summary off.
function test_6() {

    echo "===========6: reserve balance summary wrong==========="
    echo -n "Modifying database: "
    echo "UPDATE exchange.reserves_in SET credit.val=15 WHERE reserve_in_serial_id=1" \
        | psql -Aqt "$DB"
    echo "DONE"

    run_audit
    check_auditor_running

    echo -n "Checking reserve balance summary inconsistency detected... "
    check_some_report \
        "reserve-balance-summary-wrong-inconsistency"
    echo -n "Testing amount the exchange expected... "
    check_report \
        "reserve-in-inconsistency" \
        "amount_exchange_expected" "TESTKUDOS:15"
    echo -n "Checking wire credit balance minus... "
    check_balance \
        "total_bad_amount_in_minus" \
        "TESTKUDOS:5" \
        "Wrong total_bad_amount_in_minus"

    full_reload
    cleanup
}


# Test where the exchange forgot a deposit that a merchant can prove.
function test_7() {

    echo "===========7: deposit wire target malformed==========="
    SERIAL=$(echo "SELECT batch_deposit_serial_id FROM exchange.coin_deposits WHERE (amount_with_fee).val=3 ORDER BY batch_deposit_serial_id LIMIT 1" | psql "$DB" -Aqt)
# shellcheck disable=SC2028
    echo "INSERT INTO exchange.wire_targets (payto_uri, wire_target_h_payto) VALUES ('payto://x-taler-bank/localhost/testuser-xxlargtp', '\x1e8f31936b3cee8f8afd3aac9e38b5db42d45b721ffc4eb1e5b9ddaf1565660b');" \
        | psql "$DB" -Aqt
# shellcheck disable=SC2028
    echo "UPDATE exchange.batch_deposits SET wire_target_h_payto='\x1e8f31936b3cee8f8afd3aac9e38b5db42d45b721ffc4eb1e5b9ddaf1565660b' WHERE batch_deposit_serial_id=${SERIAL};" \
        | psql -Aqt "$DB"

    run_audit
    check_auditor_running

    echo -n "Checking correct operation of loss reported... "
    check_report \
        "bad-sig-losses" \
        "operation" "deposit"
    echo -n "Checking correct problem row ID reported... "
    check_report \
        "bad-sig-losses" \
        "problem_row_id" "$SERIAL"
    echo -n "Checking irregular coin loss... "
    check_not_balance \
        "coin_irregular_loss" \
        "TESTKUDOS:0" \
        "Wrong total irregular coin loss"

    full_reload
    cleanup
}


# The reverse of test 2: the bank reports a KYCAUTH transfer that the
# exchange has no record of.  An exchange that could make such a row
# disappear could also make the account authorisation it justified
# disappear, so this must be flagged even though the money involved is
# small.
function test_8() {

    echo "===========8: KYCAUTH credit missing from the exchange==========="
    echo -n "Modifying database: "
    echo "DELETE FROM exchange.kycauths_in WHERE kycauth_in_serial_id=1" \
        | psql -Aqt "$DB"
    echo "DONE"

    run_audit
    check_auditor_running

    echo -n "Checking that the bank's view is unchanged... "
    check_balance \
        "total_kycauth_in" \
        "$KYCAUTH_IN" \
        "Wrong total for KYCAUTH wire transfers"
    echo -n "Checking that the exchange claims less revenue... "
    check_balance \
        "total_kycauth_revenue" \
        "TESTKUDOS:0.1" \
        "Wrong KYCAUTH revenue"
    echo -n "Checking that the unbooked KYCAUTH was flagged... "
    check_report_any \
        "kycauth-in-inconsistency" \
        "diagnostic" "unknown to exchange"
    echo -n "Checking the amount the bank reports... "
    check_report_any \
        "kycauth-in-inconsistency" \
        "amount_wired" "TESTKUDOS:0.1"
    echo -n "Checking that it was counted as an excess credit... "
    check_balance \
        "total_bad_amount_in_plus" \
        "TESTKUDOS:0.1" \
        "Wrong total_bad_amount_in_plus"

    full_reload
    cleanup
}


# The exchange booked a KYCAUTH transfer, but for a different amount than
# the bank actually moved.
function test_9() {

    echo "===========9: KYCAUTH amount inconsistency==========="
    echo -n "Modifying database: "
    echo "UPDATE exchange.kycauths_in SET credit.val=5 WHERE kycauth_in_serial_id=1" \
        | psql -Aqt "$DB"
    echo "DONE"

    run_audit
    check_auditor_running

    echo -n "Checking that the mismatch was flagged... "
    check_report_any \
        "kycauth-in-inconsistency" \
        "diagnostic" "wire amount does not match"
    echo -n "Checking the amount the exchange claims... "
    check_report_any \
        "kycauth-in-inconsistency" \
        "amount_exchange_expected" "TESTKUDOS:5.1"
    echo -n "Checking the amount the bank reports... "
    check_report_any \
        "kycauth-in-inconsistency" \
        "amount_wired" "TESTKUDOS:0.1"
    echo -n "Checking that the shortfall was counted... "
    check_balance \
        "total_bad_amount_in_minus" \
        "TESTKUDOS:5" \
        "Wrong total_bad_amount_in_minus"
    echo -n "Checking that the inflated revenue reached the balance sheet... "
    check_balance \
        "total_kycauth_revenue" \
        "TESTKUDOS:5.2" \
        "Wrong KYCAUTH revenue"

    full_reload
    cleanup
}


# The auditor may well see a KYCAUTH transfer at the bank before the
# exchange has booked it.  It reports that, and must take the report back
# -- together with the excess credit it booked alongside -- once the
# exchange catches up.  Without the retraction of the balance, a purely
# transient disagreement would inflate total_bad_amount_in_plus forever.
function test_10() {

    echo "===========10: KYCAUTH finding retracted when the exchange catches up==========="
    echo -n "Modifying database: "
    # Hide the exchange's record of the *last* KYCAUTH transfer, keeping a
    # copy so it can be restored byte for byte.  It has to be the last one:
    # kycauth_in_cb() resumes from a progress point, so a row reinstated
    # below that point would never be looked at again.
    echo "CREATE TABLE stashed_kycauth AS
            SELECT * FROM exchange.kycauths_in
             WHERE kycauth_in_serial_id
                   = (SELECT MAX(kycauth_in_serial_id) FROM exchange.kycauths_in);
          DELETE FROM exchange.kycauths_in
             WHERE kycauth_in_serial_id
                   = (SELECT MAX(kycauth_in_serial_id) FROM exchange.kycauths_in);" \
        | psql -Aqt "$DB"
    echo "DONE"

    pre_audit
    audit_only
    check_auditor_running

    echo -n "Checking that the unbooked transfer was reported... "
    check_report_any \
        "kycauth-in-inconsistency" \
        "diagnostic" "unknown to exchange"
    echo -n "Checking that it was counted as an excess credit... "
    check_balance \
        "total_bad_amount_in_plus" \
        "TESTKUDOS:0.1" \
        "Wrong total_bad_amount_in_plus"

    # The exchange catches up.  No progress points are touched: the credit
    # audit is simply run once more, exactly as a resident helper would on
    # its next wake-up.
    echo -n "Letting the exchange catch up: "
    echo "INSERT INTO exchange.kycauths_in SELECT * FROM stashed_kycauth;
          DROP TABLE stashed_kycauth;" \
        | psql -Aqt "$DB"
    echo "DONE"

    echo -n "Re-running the credit audit ..."
    $VALGRIND taler-helper-auditor-wire-credit \
              -i \
              -L DEBUG \
              -c "$CONF" \
              -t \
              > "${MY_TMP_DIR}/test-audit-wire-credit-retract.out" \
              2> "${MY_TMP_DIR}/test-audit-wire-credit-retract.err" \
        || exit_fail "wire credit re-audit failed (see ${MY_TMP_DIR}/test-audit-wire-credit-retract.*)"
    echo " DONE"

    echo -n "Checking that the report was retracted... "
    check_no_report "kycauth-in-inconsistency"
    echo -n "Checking that the excess credit was retracted too... "
    check_balance \
        "total_bad_amount_in_plus" \
        "TESTKUDOS:0" \
        "Retracted finding left total_bad_amount_in_plus inflated"

    post_audit
    full_reload
    cleanup
}


# An AML staff appointment that the exchange's offline master key did not
# sign.  Without this check an exchange could appoint an AML officer, or
# quietly widen a read-only officer to read-write, with the offline key never
# having been involved.
function test_11() {

    echo "===========11: AML staff appointment with a bad master signature==========="
    echo -n "Modifying database: "
# shellcheck disable=SC2028
    echo "INSERT INTO exchange.aml_staff
            (decider_pub, master_sig, decider_name, is_active, read_only, last_change)
          VALUES
            ('\x1111111111111111111111111111111111111111111111111111111111111111',
             '\x22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222',
             'Unappointed Officer', TRUE, FALSE, 1000000000000000);" \
        | psql -Aqt "$DB"
    echo "DONE"

    run_audit
    check_auditor_running

    echo -n "Checking that the appointment was flagged... "
    check_report_any \
        "row-inconsistency" \
        "diagnostic" "invalid master key signature on staff status"
    echo -n "Checking that it was blamed on the right table... "
    check_report_any \
        "row-inconsistency" \
        "row_table" "aml_staff"

    # aml_staff is append-only, so the helper can and must resume from where
    # it stopped instead of re-reading the whole table on every wake-up.
    # Both halves have to hold: the cursor has to be stored at all, and the
    # second run has to pick it up.
    echo -n "Checking that the aml_staff cursor was stored... "
    AML_PP=$(psql -Aqt "$DB" \
        -c "SELECT progress_offset FROM auditor.auditor_progress \
            WHERE progress_key='aml_staff_uuid';")
    if [ -z "$AML_PP" ] || [ "$AML_PP" = "0" ]
    then
        exit_fail "aml_staff_uuid progress point not recorded (got '$AML_PP')"
    fi
    echo "PASS ($AML_PP)"
    echo -n "Checking that the second run resumed from it... "
    if ! grep -q "Resuming AML audit at $AML_PP/" \
         "${MY_TMP_DIR}/test-audit-aml-inc.err"
    then
        exit_fail "incremental AML audit did not resume at $AML_PP"
    fi
    echo "PASS"

    full_reload
    cleanup
}


# An AML decision that no valid officer signature backs, by somebody the
# exchange never appointed at all.  Both halves have to be reported: the
# signature is what makes the decision accountable, and the appointment is
# what makes the signature mean anything.
function test_12() {

    echo "===========12: AML decision by an unappointed officer==========="
    echo -n "Modifying database: "
# shellcheck disable=SC2028
    echo "INSERT INTO exchange.aml_history
            (h_payto, justification, decider_pub, decider_sig, outcome_serial_id)
          SELECT h_payto,
                 'injected by test 12',
                 '\x3333333333333333333333333333333333333333333333333333333333333333',
                 '\x44444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444',
                 outcome_serial_id
            FROM exchange.legitimization_outcomes
           ORDER BY outcome_serial_id
           LIMIT 1;" \
        | psql -Aqt "$DB"
    echo "DONE"

    run_audit
    check_auditor_running

    echo -n "Checking that the bad signature was flagged... "
    check_report_any \
        "row-inconsistency" \
        "diagnostic" "invalid officer signature on decision"
    echo -n "Checking that the missing appointment was flagged... "
    check_report_any \
        "row-inconsistency" \
        "diagnostic" "officer was not appointed when the decision was made"
    echo -n "Checking that it was blamed on the right table... "
    check_report_any \
        "row-inconsistency" \
        "row_table" "aml_history"

    full_reload
    cleanup
}


# `legitimization_outcomes' is the table that says which KYC rules apply to
# an account, so a row nothing produced is how an exchange would quietly
# exempt a customer from KYC.  The account here is fresh: it has no AML
# decision pointing at the outcome, no legitimization process, and no earlier
# outcome that expired, which is all three of the ways the exchange's own
# code creates one.
function test_13() {

    echo "===========13: legitimization outcome without a justification==========="
    echo -n "Modifying database: "
# shellcheck disable=SC2028
    echo "INSERT INTO exchange.kyc_targets
            (h_normalized_payto, is_wallet)
          VALUES
            ('\x5555555555555555555555555555555555555555555555555555555555555555',
             FALSE);
          INSERT INTO exchange.legitimization_outcomes
            (h_payto, decision_time, expiration_time, to_investigate)
          VALUES
            ('\x5555555555555555555555555555555555555555555555555555555555555555',
             1000000000000000, 2000000000000000, FALSE);" \
        | psql -Aqt "$DB"
    echo "DONE"

    run_audit
    check_auditor_running

    echo -n "Checking that the unjustified outcome was flagged... "
    check_report_any \
        "row-inconsistency" \
        "diagnostic" \
        "KYC rules in force without an AML decision, a legitimization process or an expired predecessor"
    echo -n "Checking that it was blamed on the right table... "
    check_report_any \
        "row-inconsistency" \
        "row_table" "legitimization_outcomes"

    # Only the injected row is bad.  The outcomes the reference database
    # earned honestly are each accounted for by a legitimization process or
    # an AML decision, and reporting those too would make the check useless.
    echo -n "Checking that the honest outcomes were left alone... "
    BAD=$(jq -r '[.row_inconsistency[]
                  | select(.row_table == "legitimization_outcomes")]
                 | length' \
             < "${MY_TMP_DIR}/row-inconsistency.json")
    if [ "$BAD" != "1" ]
    then
        exit_fail "wanted exactly 1 bad legitimization outcome, got $BAD"
    fi
    echo "PASS"

    full_reload
    cleanup
}


# Releasing the funds must retract the alert, not just stop adding to it.
# The exchange announces the release by writing the `wire_out' row, which is
# the only thing that distinguishes "paid" from "decided what to pay"; so
# that is what this test injects.  Nothing else changes: the deposits, the
# aggregation_tracking rows and the open KYC measure all stay exactly as they
# were, which is the point -- the hold must end because the money moved, and
# for no other reason.
function test_14() {

    echo "===========14: releasing a withheld payout==========="
    echo -n "Modifying database: "
    # Take the transfer straight from the transient the exchange parked it in,
    # so that the test does not have to know the randomly drawn wtid.
    echo "INSERT INTO exchange.wire_out
            (execution_date, wtid_raw, wire_target_h_payto,
             exchange_account_section, amount)
          SELECT 1785869200000000
                ,t.wtid_raw
                ,t.wire_target_h_payto
                ,t.exchange_account_section
                ,t.amount
            FROM exchange.aggregation_transient t;" \
        | psql -Aqt "$DB"
    echo "DONE"

    run_audit
    check_auditor_running

    echo -n "Checking that the hold was retracted... "
    check_no_report "aml-holds"
    echo -n "Checking that the AML total went back to zero... "
    check_balance \
        "total_aml_hold" \
        "TESTKUDOS:0" \
        "Released funds are still counted as withheld"
    echo -n "Checking that they did not turn into a lag instead... "
    check_balance \
        "total_transfer_lag" \
        "TESTKUDOS:0" \
        "Released funds were reclassified rather than dropped"
    echo -n "Checking that they were not parked as a small aggregate... "
    check_balance \
        "total_small_aggregate" \
        "TESTKUDOS:0" \
        "Released funds were reclassified rather than dropped"

    full_reload
    cleanup
}


# What the exchange *claims* decides which balance a hold lands in; whether the
# claim holds up is a separate question, and this is the test that they are
# separate.  Here the measure the exchange named is carried through to a
# decision while the exchange goes on withholding the money and goes on saying
# it is withholding it for that measure.
#
# The money must therefore stay in `total_aml_hold' -- the auditor does not get
# to overrule the exchange about its own reasons -- and the auditor must report
# a row_inconsistency against `aggregation_deferrals', because the one part of
# the claim it can check has stopped being true.  Deciding what to make of that
# is a job for the humans reading the finding.
function test_15() {

    echo "===========15: a hold whose KYC measure was satisfied==========="
    echo -n "Modifying database: "
    # Carry the measure that is blocking the payout through to a decision, the
    # way the exchange would have if the merchant had filled in the form: a
    # process referring to the measure, and an outcome for the account decided
    # no earlier than that process started.  `decision_time' is rounded up to a
    # whole second because the exchange stores it rounded and the AML helper
    # reads it as a timestamp.
    echo "INSERT INTO exchange.legitimization_processes
            (h_payto, start_time, expiration_time, provider_name,
             legitimization_measure_serial_id, measure_index)
          SELECT kt.h_normalized_payto
                ,lm.start_time
                ,0
                ,'full_name_and_birthdate'
                ,lm.legitimization_measure_serial_id
                ,0
            FROM exchange.aggregation_transient t
            JOIN exchange.wire_targets wt
              ON (wt.wire_target_h_payto=t.wire_target_h_payto)
            JOIN exchange.kyc_targets kt
              ON (kt.h_normalized_payto=wt.h_normalized_payto)
            JOIN exchange.legitimization_measures lm
              USING (access_token);
          INSERT INTO exchange.legitimization_outcomes
            (h_payto, decision_time, expiration_time, to_investigate)
          SELECT kt.h_normalized_payto
                ,(lm.start_time/1000000+1)*1000000
                ,2000000000000000
                ,FALSE
            FROM exchange.aggregation_transient t
            JOIN exchange.wire_targets wt
              ON (wt.wire_target_h_payto=t.wire_target_h_payto)
            JOIN exchange.kyc_targets kt
              ON (kt.h_normalized_payto=wt.h_normalized_payto)
            JOIN exchange.legitimization_measures lm
              USING (access_token);" \
        | psql -Aqt "$DB"
    echo "DONE"

    run_audit
    check_auditor_running

    echo -n "Checking that the exchange is still taken at its word... "
    check_balance \
        "total_aml_hold" \
        "$WITHHELD" \
        "Auditor reclassified a hold the exchange still attributes to KYC"
    echo -n "Checking that it did not become an unexplained lag... "
    check_balance \
        "total_transfer_lag" \
        "TESTKUDOS:0" \
        "Auditor overruled the exchange's stated reason"
    echo -n "Checking that the hold still names the claimed reason... "
    check_report "aml-holds" "deferral_reason" "KYC"
    echo -n "Checking that it still names the blocked account... "
    check_report_any \
        "aml-holds" \
        "account" \
        "$BLOCKED_ACCOUNT"
    echo -n "Checking that the stale claim was reported... "
    check_report_any \
        "row-inconsistency" \
        "row_table" "aggregation_deferrals"

    full_reload
    cleanup
}


# The other thing an exchange can lawfully be sitting on: an aggregate that
# would not survive its own wire fee.  That is not an irregularity and must not
# read as one, so it gets a balance of its own rather than being lumped in with
# payouts nobody has explained.
#
# The reference database has no such aggregate -- every payout in it is worth
# making -- so the test restates the reason on the one deferral it does have.
# Reason 1 is TALER_EXCHANGEDB_DR_AMOUNT_TOO_SMALL, 2 is ..._DR_KYC.  Note that
# this leaves the exchange's `aggregation_transient' still naming a
# legitimization requirement, so the -i cross-check must notice that the two
# no longer agree.
function test_16() {

    echo "===========16: an aggregate too small to pay out==========="
    echo -n "Modifying database: "
    # Only the transfer that is still outstanding; the cleared merchant's
    # deferral is on file too, but it was overtaken by its wire_out row.
    echo "UPDATE exchange.aggregation_deferrals d
             SET deferral_reason=1
                ,legitimization_requirement_serial_id=0
           WHERE NOT EXISTS (
                 SELECT 1 FROM exchange.wire_out w
                  WHERE w.wtid_raw=d.wtid_raw);" \
        | psql -Aqt "$DB"
    echo "DONE"

    run_audit
    check_auditor_running

    echo -n "Checking that the funds are booked as a small aggregate... "
    check_balance \
        "total_small_aggregate" \
        "$WITHHELD" \
        "Aggregate below the wire fee was not booked as such"
    echo -n "Checking that they are no longer an AML hold... "
    check_balance \
        "total_aml_hold" \
        "TESTKUDOS:0" \
        "Aggregate below the wire fee still counted as withheld for KYC"
    echo -n "Checking that they are not an unexplained lag either... "
    check_balance \
        "total_transfer_lag" \
        "TESTKUDOS:0" \
        "Aggregate below the wire fee read as an unexplained delay"
    echo -n "Checking that the hold names the new reason... "
    check_report "aml-holds" "deferral_reason" "AMOUNT_TOO_SMALL"
    echo -n "Checking that the contradiction with the transient was reported... "
    check_report_any \
        "row-inconsistency" \
        "row_table" "aggregation_transient"

    full_reload
    cleanup
}


# *************** Main test loop starts here **************


# Run all the tests against the database given in $1.
# Sets $fail to 0 on success, non-zero on failure.
function check_with_database()
{
    BASEDB="$1"
    CONF="$1.conf"
    export CONF
    echo "Running test suite with database $BASEDB using configuration $CONF"
    MASTER_PRIV_FILE="${BASEDB}.mpriv"
    taler-exchange-config \
        -f \
        -c "${CONF}" \
        -s exchange-offline \
        -o MASTER_PRIV_FILE \
        -V "${MASTER_PRIV_FILE}"

    # Load database
    full_reload

    # Run test suite
    fail=0
    for i in $TESTS
    do
        "test_$i"
        if test 0 != $fail
        then
            break
        fi
    done
    echo "Cleanup (disabled, leaving database $DB behind)"
    # dropdb $DB
}

# When the script is not run as root, setup a temporary directory for the
# postgres database.
# Sets PGHOST accordingly to the freshly created socket.
function perform_initdb() {
    # Available directly in path?
    INITDB_BIN=$(command -v initdb) || true
    if [[ -n "$INITDB_BIN" ]]; then
      echo " FOUND (in path) at $INITDB_BIN"
    else
        HAVE_INITDB=$(find /usr -name "initdb" 2> /dev/null \
                          | head -1 2> /dev/null \
                          | grep postgres) \
            || exit_skip " MISSING"
      echo " FOUND at $(dirname "$HAVE_INITDB")"
      INITDB_BIN=$(echo "$HAVE_INITDB" | grep bin/initdb | grep postgres | sort -n | tail -n1)
    fi
    POSTGRES_PATH=$(dirname "$INITDB_BIN")

    TMPDIR="$MY_TMP_DIR/postgres"
    mkdir -p "$TMPDIR"
    echo -n "Setting up Postgres DB at $TMPDIR ..."
    $INITDB_BIN \
        --no-sync \
        --auth=trust \
        -D "${TMPDIR}" \
        > "${MY_TMP_DIR}/postgres-dbinit.log" \
        2> "${MY_TMP_DIR}/postgres-dbinit.err" \
        || {
        echo "FAILED!"
        echo "Last entries in ${MY_TMP_DIR}/postgres-dbinit.err:"
        tail "${MY_TMP_DIR}/postgres-dbinit.err"
        exit 1
    }
    echo "DONE"

    # Once we move to PG16, we can use:
    #    --set listen_addresses='' \
    #    --set fsync=off \
    #    --set max_wal_senders=0 \
    #    --set synchronous_commit=off \
    #    --set wal_level=minimal \
    #    --set unix_socket_directories="${TMPDIR}/sockets" \


    SOCKETDIR="${TMPDIR}/sockets"
    mkdir "${SOCKETDIR}"

    echo -n "Launching Postgres service"

    cat - >> "$TMPDIR/postgresql.conf" <<EOF
unix_socket_directories='${TMPDIR}/sockets'
fsync=off
max_wal_senders=0
synchronous_commit=off
wal_level=minimal
listen_addresses=''
EOF

    grep -v host \
         < "$TMPDIR/pg_hba.conf" \
         > "$TMPDIR/pg_hba.conf.new"
    mv "$TMPDIR/pg_hba.conf.new" "$TMPDIR/pg_hba.conf"
    "${POSTGRES_PATH}/pg_ctl" \
        -D "$TMPDIR" \
        -l "${MY_TMP_DIR}/postgres.log" \
        start \
        > "${MY_TMP_DIR}/postgres-start.log" \
        2> "${MY_TMP_DIR}/postgres-start.err"
    echo " DONE"
    PGHOST="$TMPDIR/sockets"
    export PGHOST
}


# *************** Main logic starts here **************

# ####### Setup globals ######
# Postgres database to use (must match configuration file)
export DB="auditor-basedb"

# test required commands exist
echo "Testing for jq"
jq -h > /dev/null || exit_skip "jq required"
echo "Testing for taler-merchant-config"
taler-merchant-config -h > /dev/null || exit_skip "taler-merchant-config required"
echo "Testing for taler-merchant-httpd"
taler-merchant-httpd -h > /dev/null || exit_skip "taler-merchant-httpd required"
echo "Testing for faketime"
faketime -h > /dev/null || exit_skip "faketime required"
# NOTE: really check for all three libeufin commands?
echo "Testing for libeufin"
libeufin-bank --help >/dev/null 2> /dev/null </dev/null || exit_skip "libeufin required"
echo "Testing for taler-wallet-cli"
taler-wallet-cli -h >/dev/null </dev/null 2>/dev/null || exit_skip "taler-wallet-cli required"


echo -n "Testing for Postgres"

MY_TMP_DIR=$(mktemp -d /tmp/taler-auditor-basedbXXXXXX)
echo "Using $MY_TMP_DIR for logging and temporary data"

# If run as root, simply use the running postgres instance.
# Otherwise create a temporary storage space for postgres.
[ $(id -u) == 0 ] || perform_initdb

MYDIR="${MY_TMP_DIR}/basedb"
mkdir -p "${MYDIR}"

if [ -z ${REUSE_BASEDB_DIR+x} ]
then
    echo "Generating fresh database at $MYDIR"

    if faketime -f '-1 d' ./generate-kyc-basedb.sh -d "$MYDIR/$DB"
    then
        echo -n "Reset 'auditor-basedb' database at ${PGHOST:-} ..."
        dropdb --if-exists "auditor-basedb" > /dev/null 2> /dev/null || true
        createdb "auditor-basedb" || exit_skip "Could not create database '$BASEDB' at ${PGHOST:-}"
        echo " DONE"
    else
        echo "Generation failed"
        exit 1
    fi
    echo "To reuse this database in the future, use:"
    echo "export REUSE_BASEDB_DIR=$MY_TMP_DIR"
else
    echo "Reusing existing database from ${REUSE_BASEDB_DIR}"
    cp -r "${REUSE_BASEDB_DIR}/basedb"/* "${MYDIR}/"
fi

check_with_database "$MYDIR/$DB"
if [ "$fail" != "0" ]
then
    exit "$fail"
fi

if [ -z "${REUSE_BASEDB_DIR+x}" ]
then
    echo "Run 'export REUSE_BASEDB_DIR=${MY_TMP_DIR}' to re-run tests against the same database"
fi

exit 0
