/*
 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 <http://www.gnu.org/licenses/>
 */
import {
  AbsoluteTime,
  AgeRestriction,
  AmountJson,
  Amounts,
  Duration,
  TransactionAmountMode,
} from "@gnu-taler/taler-util";
import test, { ExecutionContext } from "ava";
import {
  CoinInfo,
  convertDepositAmountForAvailableCoins,
  convertWithdrawalAmountFromAvailableCoins,
} from "./instructedAmountConversion.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
 *
 */

/**
 * 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>(
  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: 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
});
