/*******************************************************************************
***  This file is part of Pecunia.                                           ***
***                                                                          ***
***  Copyright (C) 2017, 2018, 2019, 2020, 2021, 2023                        ***
***  John Schneiderman <Licencing _AT_ Schneiderman _DOT_ me>                ***
***                                                                          ***
***  This program is free software: you can redistribute it and/or modify it ***
***  under the terms of the GNU Lesser General Public License as published   ***
***  by the Free Software Foundation, either version 3 of the License, or    ***
***  (at your option) any later version.                                     ***
***                                                                          ***
***  This program 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 Lesser General Public License for more details.             ***
***                                                                          ***
***  You should have received a copy of the GNU Lesser General Public License***
***  along with this program. If not, see <http://www.gnu.org/licenses/>.    ***
*******************************************************************************/
#include <QDebug>
// using qDebug
#include <QTest>
using ::QObject;
// using QTEST_MAIN

#include <cstdint>
using std::int16_t;
using std::uint16_t;
#include <cmath>
using std::pow;
#include <map>
using std::map;
#include <stdexcept>
using std::runtime_error;
#include <vector>
using std::vector;

#include <pecunia/Codes.h>
using pecunia::currency::Iso4217Codes;
#include <pecunia/FloatingPoint.h>
using pecunia::FloatingPoint;
#include <pecunia/Math.h>
using pecunia::math::absoluteValue;
using pecunia::math::sum;
#include <pecunia/Money.h>
using pecunia::currency::Money;
#include <pecunia/MoneyTypes.h>
using pecunia::converter;


namespace
{

FloatingPoint testConverter(const Iso4217Codes& from, const Iso4217Codes& to)
{
	/*
	 1 PLN = 0.238928 USD
	 1 USD = 4.18536 PLN

	 1 PLN = 0.22487 EUR
	 1 EUR = 4.44702 PLN

	 1 USD = 0.941161 EUR
	 1 EUR = 1.06252 USD
	*/

	switch (from)
	{
	case Iso4217Codes::EUR:
		switch (to)
		{
		case Iso4217Codes::XXX:
		case Iso4217Codes::EUR:
			return 1.0;
		case Iso4217Codes::PLN:
			qDebug() << "EUR->PLN" << 4.44702;
			return 4.44702;
		case Iso4217Codes::USD:
			qDebug() << "EUR->USD" << 1.06252;
			return 1.06252;
		default:
			throw runtime_error{"Unknown to currency."};
		};
	case Iso4217Codes::PLN:
		switch (to)
		{
		case Iso4217Codes::EUR:
			qDebug() << "PLN->EUR" << 0.22487;
			return 0.22487;
		case Iso4217Codes::XXX:
		case Iso4217Codes::PLN:
			return 1.0;
		case Iso4217Codes::USD:
			qDebug() << "PLN->USD" << 0.238928;
			return 0.238928;
		default:
			throw runtime_error{"Unknown to currency."};
		};
	case Iso4217Codes::USD:
		switch (to)
		{
		case Iso4217Codes::EUR:
			qDebug() << "USD->EUR" << 0.941161;
			return 0.941161;
		case Iso4217Codes::PLN:
			qDebug() << "USD->PLN" << 4.18536;
			return 4.18536;
		case Iso4217Codes::XXX:
		case Iso4217Codes::USD:
			return 1.0;
		default:
			throw runtime_error{"Unknown to currency."};
		};
	case Iso4217Codes::XXX:
		return 1.0;
	default:
		throw runtime_error{"Unknown from currency."};
	};
}

}

namespace pecunia
{
namespace math
{
namespace tests
{

struct MathUT : QObject
{
	Q_OBJECT

private slots:
	//{ absoluteValue Tests

	void absoluteValue_Positive_ShouldBeSame()
	{
		const Money sm{100, 23u, Iso4217Codes::XXX};
		const auto actual{absoluteValue(sm)};
		const Money expected{100, 23u, Iso4217Codes::XXX};
		QCOMPARE(actual, expected);
	}
	void absoluteValue_Negative_ShouldBePositive()
	{
		const Money sm{-100, 23u, Iso4217Codes::XXX};
		const auto actual{absoluteValue(sm)};
		const Money expected{100, 23u, Iso4217Codes::XXX};
		QCOMPARE(actual, expected);
	}

	//}

	//{ sum Tests

	void sum_Empty_ShouldBeTotal()
	{
		const vector<Money> monies{};
		const Money actual{sum(Iso4217Codes::XXX, monies)};
		QCOMPARE(actual, Money(0, 0u, Iso4217Codes::XXX));
		QCOMPARE(actual.code(), Iso4217Codes::XXX);
	}
	void sum_SameCurrencies_ShouldBeTotal()
	{
		const vector<Money> monies{
			{100, 15u, Iso4217Codes::XXX},
			{100, 15u, Iso4217Codes::XXX},
			{100, 15u, Iso4217Codes::XXX}
		};
		const Money actual{sum(Iso4217Codes::XXX, monies)};
		QCOMPARE(actual, Money(300, 45u, Iso4217Codes::XXX));
		QCOMPARE(actual.code(), Iso4217Codes::XXX);
	}
	void sum_SameCurrenciesWithDoublingFunctor_ShouldBeTotalDoubled()
	{
		const vector<Money> monies{
			{100, 15u, Iso4217Codes::XXX},
			{100, 15u, Iso4217Codes::XXX},
			{100, 15u, Iso4217Codes::XXX}
		};
		const Money actual{
			sum(Iso4217Codes::XXX, monies, [] (const auto& money) { return money * 2; })
		};
		QCOMPARE(actual, Money(600, 90u, Iso4217Codes::XXX));
		QCOMPARE(actual.code(), Iso4217Codes::XXX);
	}
	void sum_DifferentCurrenciesSharedOneAccumulator_ShouldBeTotal()
	{
		converter = testConverter;
		const vector<Money> monies{
			{100, 1500u, Iso4217Codes::USD},
			{100, 1500u, Iso4217Codes::EUR},
			{100, 1500u, Iso4217Codes::USD}
		};
		const Money actual{sum(Iso4217Codes::EUR, monies)};
		// 200,30 USD -> 188,5145483 EUR
		//            -> 100,1500 EUR
		QCOMPARE(actual, Money(288, 6645u, Iso4217Codes::EUR));
		QCOMPARE(actual.code(), Iso4217Codes::EUR);
	}
	void sum_DifferentCurrenciesDifferentAccumulator_ShouldBeTotal()
	{
		const vector<Money> monies{
			{100, 1500u, Iso4217Codes::USD},
			{100, 1500u, Iso4217Codes::EUR},
			{100, 1500u, Iso4217Codes::USD}
		};
		const Money actual{sum(Iso4217Codes::PLN, monies)};
		// 200,30 USD -> 838,327608 PLN
		// 100,15 USD -> 445,369053 PLN
		QCOMPARE(actual, Money(1283, 6966u, Iso4217Codes::PLN));
		QCOMPARE(actual.code(), Iso4217Codes::PLN);
	}
	void sum_AssociativeContainerEmpty_ShouldBeTotal()
	{
		const map<qint32, Money> monies{};
		const Money actual{sum(Iso4217Codes::XXX, monies)};
		QCOMPARE(actual, Money(0, 0u, Iso4217Codes::XXX));
		QCOMPARE(actual.code(), Iso4217Codes::XXX);
	}
	void sum_AssociativeContainerSameCurrencies_ShouldBeTotal()
	{
		const map<qint32, Money> monies{
			{1, {100, 15u, Iso4217Codes::XXX}},
			{2, {100, 15u, Iso4217Codes::XXX}},
			{3, {100, 15u, Iso4217Codes::XXX}}
		};
		const Money actual{sum(Iso4217Codes::XXX, monies)};
		QCOMPARE(actual, Money(300, 45u, Iso4217Codes::XXX));
		QCOMPARE(actual.code(), Iso4217Codes::XXX);
	}
	void sum_AssociativeContainerSameCurrenciesWithFunctor_ShouldBeTotalDoubled()
	{
		const map<qint32, Money> monies{
			{1, {100, 15u, Iso4217Codes::XXX}},
			{2, {100, 15u, Iso4217Codes::XXX}},
			{3, {100, 15u, Iso4217Codes::XXX}}
		};
		const Money actual{
			sum(Iso4217Codes::XXX, monies, [] (const auto& money) { return money * 2; })
		};
		QCOMPARE(actual, Money(600, 90u, Iso4217Codes::XXX));
		QCOMPARE(actual.code(), Iso4217Codes::XXX);
	}
	void sum_AssociativeContainerDifferentCurrenciesSharedOneAccumulator_ShouldBeTotal()
	{
		converter = testConverter;
		const map<qint32, Money> monies{
			{1, {100, 1500u, Iso4217Codes::USD}},
			{2, {100, 1500u, Iso4217Codes::EUR}},
			{3, {100, 1500u, Iso4217Codes::USD}}
		};
		const Money actual{sum(Iso4217Codes::EUR, monies)};
		// 200,30 USD -> 188,5145483 EUR
		//            -> 100,1500 EUR
		QCOMPARE(actual, Money(288, 6645u, Iso4217Codes::EUR));
		QCOMPARE(actual.code(), Iso4217Codes::EUR);
	}
	void sum_AssociativeContainerDifferentCurrenciesDifferentAccumulator_ShouldBeTotal()
	{
		const map<qint32, Money> monies{
			{1, {100, 1500u, Iso4217Codes::USD}},
			{2, {100, 1500u, Iso4217Codes::EUR}},
			{3, {100, 1500u, Iso4217Codes::USD}}
		};
		const Money actual{sum(Iso4217Codes::PLN, monies)};
		// 200,30 USD -> 838,327608 PLN
		// 100,15 USD -> 445,369053 PLN
		QCOMPARE(actual, Money(1283, 6966u, Iso4217Codes::PLN));
		QCOMPARE(actual.code(), Iso4217Codes::PLN);
	}

	//}
};

}}}

QTEST_MAIN(pecunia::math::tests::MathUT)
#include "math-unit-tests.moc"
