/*
 This file is part of GNU Taler
 (C) 2022 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 
 */
import {
  AbsoluteTime,
  AgeRestriction,
  AmountJson,
  Amounts,
  Duration,
  TransactionAmountMode,
} from "@gnu-taler/taler-util";
import test, { ExecutionContext } from "ava";
import {
  CoinInfo,
  convertDepositAmountForAvailableCoins,
  convertWithdrawalAmountFromAvailableCoins,
  getMaxDepositAmountForAvailableCoins,
} from "./coinSelection.js";
function makeCurrencyHelper(currency: string) {
  return (sx: TemplateStringsArray, ...vx: any[]) => {
    const s = String.raw({ raw: sx }, ...vx);
    return Amounts.parseOrThrow(`${currency}:${s}`);
  };
}
const kudos = makeCurrencyHelper("kudos");
function defaultFeeConfig(value: AmountJson, totalAvailable: number): CoinInfo {
  return {
    id: Amounts.stringify(value),
    denomDeposit: kudos`0.01`,
    denomRefresh: kudos`0.01`,
    denomWithdraw: kudos`0.01`,
    exchangeBaseUrl: "1",
    duration: Duration.getForever(),
    exchangePurse: undefined,
    exchangeWire: undefined,
    maxAge: AgeRestriction.AGE_UNRESTRICTED,
    totalAvailable,
    value,
  };
}
type Coin = [AmountJson, number];
/**
 * Making a deposit with effective amount
 *
 */
test("deposit effective 2", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`2`,
    TransactionAmountMode.Effective,
  );
  t.is(Amounts.stringifyValue(result.effective), "2");
  t.is(Amounts.stringifyValue(result.raw), "1.99");
});
test("deposit effective 10", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`10`,
    TransactionAmountMode.Effective,
  );
  t.is(Amounts.stringifyValue(result.effective), "10");
  t.is(Amounts.stringifyValue(result.raw), "9.98");
});
test("deposit effective 24", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`24`,
    TransactionAmountMode.Effective,
  );
  t.is(Amounts.stringifyValue(result.effective), "24");
  t.is(Amounts.stringifyValue(result.raw), "23.94");
});
test("deposit effective 40", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`40`,
    TransactionAmountMode.Effective,
  );
  t.is(Amounts.stringifyValue(result.effective), "35");
  t.is(Amounts.stringifyValue(result.raw), "34.9");
});
test("deposit with wire fee effective 2", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {
        one: {
          wireFee: kudos`0.1`,
          purseFee: kudos`0.00`,
          creditDeadline: AbsoluteTime.never(),
          debitDeadline: AbsoluteTime.never(),
        },
      },
    },
    kudos`2`,
    TransactionAmountMode.Effective,
  );
  t.is(Amounts.stringifyValue(result.effective), "2");
  t.is(Amounts.stringifyValue(result.raw), "1.89");
});
/**
 * Making a deposit with raw amount, using the result from effective
 *
 */
test("deposit raw 1.99 (effective 2)", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`1.99`,
    TransactionAmountMode.Raw,
  );
  t.is(Amounts.stringifyValue(result.effective), "2");
  t.is(Amounts.stringifyValue(result.raw), "1.99");
});
test("deposit raw 9.98 (effective 10)", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`9.98`,
    TransactionAmountMode.Raw,
  );
  t.is(Amounts.stringifyValue(result.effective), "10");
  t.is(Amounts.stringifyValue(result.raw), "9.98");
});
test("deposit raw 23.94 (effective 24)", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`23.94`,
    TransactionAmountMode.Raw,
  );
  t.is(Amounts.stringifyValue(result.effective), "24");
  t.is(Amounts.stringifyValue(result.raw), "23.94");
});
test("deposit raw 34.9 (effective 40)", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`34.9`,
    TransactionAmountMode.Raw,
  );
  t.is(Amounts.stringifyValue(result.effective), "35");
  t.is(Amounts.stringifyValue(result.raw), "34.9");
});
test("deposit with wire fee raw 2", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {
        one: {
          wireFee: kudos`0.1`,
          purseFee: kudos`0.00`,
          creditDeadline: AbsoluteTime.never(),
          debitDeadline: AbsoluteTime.never(),
        },
      },
    },
    kudos`2`,
    TransactionAmountMode.Effective,
  );
  t.is(Amounts.stringifyValue(result.effective), "2");
  t.is(Amounts.stringifyValue(result.raw), "1.89");
});
/**
 * Calculating the max amount possible to deposit
 *
 */
test("deposit max 35", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = getMaxDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {
        "2": {
          wireFee: kudos`0.00`,
          purseFee: kudos`0.00`,
          creditDeadline: AbsoluteTime.never(),
          debitDeadline: AbsoluteTime.never(),
        },
      },
    },
    "KUDOS",
  );
  t.is(Amounts.stringifyValue(result.raw), "34.9");
  t.is(Amounts.stringifyValue(result.effective), "35");
});
test("deposit max 35 with wirefee", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = getMaxDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {
        "2": {
          wireFee: kudos`1`,
          purseFee: kudos`0.00`,
          creditDeadline: AbsoluteTime.never(),
          debitDeadline: AbsoluteTime.never(),
        },
      },
    },
    "KUDOS",
  );
  t.is(Amounts.stringifyValue(result.raw), "33.9");
  t.is(Amounts.stringifyValue(result.effective), "35");
});
test("deposit max repeated denom", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 1],
    [kudos`2`, 1],
    [kudos`5`, 1],
  ];
  const result = getMaxDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {
        "2": {
          wireFee: kudos`0.00`,
          purseFee: kudos`0.00`,
          creditDeadline: AbsoluteTime.never(),
          debitDeadline: AbsoluteTime.never(),
        },
      },
    },
    "KUDOS",
  );
  t.is(Amounts.stringifyValue(result.raw), "8.97");
  t.is(Amounts.stringifyValue(result.effective), "9");
});
/**
 * Making a withdrawal with effective amount
 *
 */
test("withdraw effective 2", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertWithdrawalAmountFromAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`2`,
    TransactionAmountMode.Effective,
  );
  t.is(Amounts.stringifyValue(result.effective), "2");
  t.is(Amounts.stringifyValue(result.raw), "2.01");
});
test("withdraw effective 10", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertWithdrawalAmountFromAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`10`,
    TransactionAmountMode.Effective,
  );
  t.is(Amounts.stringifyValue(result.effective), "10");
  t.is(Amounts.stringifyValue(result.raw), "10.02");
});
test("withdraw effective 24", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertWithdrawalAmountFromAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`24`,
    TransactionAmountMode.Effective,
  );
  t.is(Amounts.stringifyValue(result.effective), "24");
  t.is(Amounts.stringifyValue(result.raw), "24.06");
});
test("withdraw effective 40", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertWithdrawalAmountFromAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`40`,
    TransactionAmountMode.Effective,
  );
  t.is(Amounts.stringifyValue(result.effective), "40");
  t.is(Amounts.stringifyValue(result.raw), "40.08");
});
/**
 * Making a deposit with raw amount, using the result from effective
 *
 */
test("withdraw raw 2.01 (effective 2)", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertWithdrawalAmountFromAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`2.01`,
    TransactionAmountMode.Raw,
  );
  t.is(Amounts.stringifyValue(result.effective), "2");
  t.is(Amounts.stringifyValue(result.raw), "2.01");
});
test("withdraw raw 10.02 (effective 10)", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertWithdrawalAmountFromAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`10.02`,
    TransactionAmountMode.Raw,
  );
  t.is(Amounts.stringifyValue(result.effective), "10");
  t.is(Amounts.stringifyValue(result.raw), "10.02");
});
test("withdraw raw 24.06 (effective 24)", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertWithdrawalAmountFromAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`24.06`,
    TransactionAmountMode.Raw,
  );
  t.is(Amounts.stringifyValue(result.effective), "24");
  t.is(Amounts.stringifyValue(result.raw), "24.06");
});
test("withdraw raw 40.08 (effective 40)", (t) => {
  const coinList: Coin[] = [
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertWithdrawalAmountFromAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`40.08`,
    TransactionAmountMode.Raw,
  );
  t.is(Amounts.stringifyValue(result.effective), "40");
  t.is(Amounts.stringifyValue(result.raw), "40.08");
});
test("withdraw raw 25", (t) => {
  const coinList: Coin[] = [
    [kudos`0.1`, 0],
    [kudos`1`, 0],
    [kudos`2`, 0],
    [kudos`5`, 0],
  ];
  const result = convertWithdrawalAmountFromAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`25`,
    TransactionAmountMode.Raw,
  );
  t.is(Amounts.stringifyValue(result.effective), "24.8");
  t.is(Amounts.stringifyValue(result.raw), "24.94");
});
test("withdraw effective 24.8 (raw 25)", (t) => {
  const coinList: Coin[] = [
    [kudos`0.1`, 0],
    [kudos`1`, 0],
    [kudos`2`, 0],
    [kudos`5`, 0],
  ];
  const result = convertWithdrawalAmountFromAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`24.8`,
    TransactionAmountMode.Effective,
  );
  t.is(Amounts.stringifyValue(result.effective), "24.8");
  t.is(Amounts.stringifyValue(result.raw), "24.94");
});
/**
 * Making a deposit with refresh
 *
 */
test("deposit with refresh: effective 3", (t) => {
  const coinList: Coin[] = [
    [kudos`0.1`, 0],
    [kudos`1`, 0],
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`3`,
    TransactionAmountMode.Effective,
  );
  t.is(Amounts.stringifyValue(result.effective), "3.1");
  t.is(Amounts.stringifyValue(result.raw), "2.98");
  expectDefined(t, result.refresh);
  //FEES
  //deposit  2 x 0.01
  //refresh  1 x 0.01
  //withdraw 9 x 0.01
  //-----------------
  //op           0.12
  //coins sent  2 x 2.0
  //coins recv  9 x 0.1
  //-------------------
  //effective       3.10
  //raw             2.98
  t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
  t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 9]]);
});
test("deposit with refresh: raw 2.98 (effective 3)", (t) => {
  const coinList: Coin[] = [
    [kudos`0.1`, 0],
    [kudos`1`, 0],
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`2.98`,
    TransactionAmountMode.Raw,
  );
  t.is(Amounts.stringifyValue(result.effective), "3.2");
  t.is(Amounts.stringifyValue(result.raw), "3.09");
  expectDefined(t, result.refresh);
  //FEES
  //deposit  1 x 0.01
  //refresh  1 x 0.01
  //withdraw 8 x 0.01
  //-----------------
  //op           0.10
  //coins sent  1 x 2.0
  //coins recv  8 x 0.1
  //-------------------
  //effective       3.20
  //raw             3.09
  t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
  t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 8]]);
});
test("deposit with refresh: effective 3.2 (raw 2.98)", (t) => {
  const coinList: Coin[] = [
    [kudos`0.1`, 0],
    [kudos`1`, 0],
    [kudos`2`, 5],
    [kudos`5`, 5],
  ];
  const result = convertDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`3.2`,
    TransactionAmountMode.Effective,
  );
  t.is(Amounts.stringifyValue(result.effective), "3.3");
  t.is(Amounts.stringifyValue(result.raw), "3.2");
  expectDefined(t, result.refresh);
  //FEES
  //deposit  2 x 0.01
  //refresh  1 x 0.01
  //withdraw 7 x 0.01
  //-----------------
  //op           0.10
  //coins sent  2 x 2.0
  //coins recv  7 x 0.1
  //-------------------
  //effective       3.30
  //raw             3.20
  t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
  t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 7]]);
});
function expectDefined(
  t: ExecutionContext,
  v: T | undefined,
): asserts v is T {
  t.assert(v !== undefined);
}
function asCoinList(v: { info: CoinInfo; size: number }[]): any {
  return v.map((c) => {
    return [c.info.value, c.size];
  });
}
/**
 * regression tests
 */
test("demo: withdraw raw 25", (t) => {
  const coinList: Coin[] = [
    [kudos`0.1`, 0],
    [kudos`1`, 0],
    [kudos`2`, 0],
    [kudos`5`, 0],
    [kudos`10`, 0],
  ];
  const result = convertWithdrawalAmountFromAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`25`,
    TransactionAmountMode.Raw,
  );
  t.is(Amounts.stringifyValue(result.effective), "24.8");
  t.is(Amounts.stringifyValue(result.raw), "24.92");
  // coins received
  // 8 x  0.1
  // 2 x  0.2
  // 2 x 10.0
  // total effective 24.8
  // fee 12 x 0.01 = 0.12
  // total raw 24.92
  // left in reserve 25 - 24.92 == 0.08
  //current wallet impl: hides the left in reserve fee
  //shows fee = 0.2
});
test("demo: deposit max after withdraw raw 25", (t) => {
  const coinList: Coin[] = [
    [kudos`0.1`, 8],
    [kudos`1`, 0],
    [kudos`2`, 2],
    [kudos`5`, 0],
    [kudos`10`, 2],
  ];
  const result = getMaxDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {
        one: {
          wireFee: kudos`0.01`,
          purseFee: kudos`0.00`,
          creditDeadline: AbsoluteTime.never(),
          debitDeadline: AbsoluteTime.never(),
        },
      },
    },
    "KUDOS",
  );
  t.is(Amounts.stringifyValue(result.effective), "24.8");
  t.is(Amounts.stringifyValue(result.raw), "24.67");
  // 8 x  0.1
  // 2 x  0.2
  // 2 x 10.0
  // total effective 24.8
  // deposit fee 12 x 0.01 = 0.12
  // wire fee 0.01
  // total raw: 24.8 - 0.13 = 24.67
  // current wallet impl fee 0.14
});
test("demo: withdraw raw 13", (t) => {
  const coinList: Coin[] = [
    [kudos`0.1`, 0],
    [kudos`1`, 0],
    [kudos`2`, 0],
    [kudos`5`, 0],
    [kudos`10`, 0],
  ];
  const result = convertWithdrawalAmountFromAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {},
    },
    kudos`13`,
    TransactionAmountMode.Raw,
  );
  t.is(Amounts.stringifyValue(result.effective), "12.8");
  t.is(Amounts.stringifyValue(result.raw), "12.9");
  // coins received
  // 8 x  0.1
  // 1 x  0.2
  // 1 x 10.0
  // total effective 12.8
  // fee 10 x 0.01 = 0.10
  // total raw 12.9
  // left in reserve 13 - 12.9 == 0.1
  //current wallet impl: hides the left in reserve fee
  //shows fee = 0.2
});
test("demo: deposit max after withdraw raw 13", (t) => {
  const coinList: Coin[] = [
    [kudos`0.1`, 8],
    [kudos`1`, 0],
    [kudos`2`, 1],
    [kudos`5`, 0],
    [kudos`10`, 1],
  ];
  const result = getMaxDepositAmountForAvailableCoins(
    {
      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
      exchanges: {
        one: {
          wireFee: kudos`0.01`,
          purseFee: kudos`0.00`,
          creditDeadline: AbsoluteTime.never(),
          debitDeadline: AbsoluteTime.never(),
        },
      },
    },
    "KUDOS",
  );
  t.is(Amounts.stringifyValue(result.effective), "12.8");
  t.is(Amounts.stringifyValue(result.raw), "12.69");
  // 8 x  0.1
  // 1 x  0.2
  // 1 x 10.0
  // total effective 12.8
  // deposit fee 10 x 0.01 = 0.10
  // wire fee 0.01
  // total raw: 12.8 - 0.11 = 12.69
  // current wallet impl fee 0.14
});