/*******************************************************************************
***  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 <cfenv>
using std::feclearexcept;
#include <cmath>
#include <cstdint>
#include <iostream>
#include <limits>
#include <locale>
#include <sstream>
#include <stdexcept>
using std::runtime_error;
#include <string>

#include <pecunia/Codes.h>
using pecunia::currency::Iso4217Codes;
#include <pecunia/FloatingPoint.h>
using namespace pecunia::literals;
using pecunia::FloatingPoint;
#include <pecunia/Money.h>
using pecunia::currency::Money;
#include <pecunia/MoneyManip.h>
#include <pecunia/MoneySize.hpp>
using pecunia::defaultLocale;
using pecunia::FloatingPointBase;
#include <pecunia/MoneyTypes.h>
using pecunia::precision;
using pecunia::UnitStorage;
using pecunia::MinorUnit;
#include <pecunia/Rounders.h>
#include <internal/config.hpp>
// using FULL_VERSION
#include <pecunia/Information.h>
using pecunia::version;


namespace
{

const std::uint8_t smallExtraDigits{2};

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 currency
{
namespace tests
{

struct MoneyUT : QObject
{
	Q_OBJECT

	const UnitStorage majorMaxValue{92233720368547757};
	/// The lowest possible major value. N.B. Update these ranges based upon UnitStorage and
	/// number of extra digits.
	const UnitStorage majorMinValue{-92233720368547757};
	const MinorUnit maxDigitValue{static_cast<MinorUnit>(pow(10, smallExtraDigits))};
	const MinorUnit minorMaxValue{static_cast<MinorUnit>(maxDigitValue - 1)};

private slots:
	void init()
	{
		feclearexcept(FE_ALL_EXCEPT);
	}

	void currencyVersion_ShouldBeSameAsConstant()
	{
		QCOMPARE(version(), pecunia_FULL_VERSION);
	}

	//{ Constructor Tests

	void constructorDefault_ShouldBeZero()
	{
		Money money{};
		QCOMPARE(money.major(), static_cast<UnitStorage>(0));
		QCOMPARE(money.minor(), static_cast<MinorUnit>(0));
		QCOMPARE(money.code(), Iso4217Codes::XXX);
		QVERIFY2( ! money.isPositive(), "The value should not be positive.");
		QVERIFY2( ! money.hasAmount(), "Default constructed value is not set.");
	}
	void constructorExact_ShouldMatch()
	{
		Money money{
			static_cast<UnitStorage>(9876543210),
			static_cast<MinorUnit>(1234),
			Iso4217Codes::USD
		};
		QCOMPARE(money.major(), static_cast<UnitStorage>(9876543210));
		QCOMPARE(money.minor(), static_cast<MinorUnit>(1234));
		QVERIFY2(money.isPositive(), "The value should be positive.");
		QVERIFY2(money.hasAmount(), "Constructed with values should be set.");
	}
	void constructorPositiveMinor_ShouldMatch()
	{
		Money money{0, 3300, Iso4217Codes::USD}; // Two digits of precision.
		QCOMPARE(money.major(), static_cast<UnitStorage>(0));
		QCOMPARE(money.minor(), static_cast<MinorUnit>(3300));
		QVERIFY2(money.isPositive(), "The value should be positive.");
	}
	void constructorNegativeMinor_ShouldMatch()
	{
		Money money{pecunia::MinorSign::Negative, 3300, Iso4217Codes::USD};
		QCOMPARE(money.major(), static_cast<UnitStorage>(0));
		QCOMPARE(money.minor(), static_cast<MinorUnit>(3300));
		QVERIFY2( ! money.isPositive(), "The value should be negative.");
	}
	void constructorUnitStorageJustUnderTooLarge_ShouldMatch()
	{
		Money money{majorMaxValue, minorMaxValue};
		QCOMPARE(money.major(), majorMaxValue);
		QCOMPARE(money.minor(), minorMaxValue);
	}
	void constructorUnitStorageTooLarge_ShouldThrowOverflow()
	{
		QVERIFY_EXCEPTION_THROWN(Money(majorMaxValue + 1, 0), std::overflow_error);
	}
	void constructorUnitStorageTooSmall_ShouldThrowOverflow()
	{
		QVERIFY_EXCEPTION_THROWN(Money(majorMinValue - 1, 0), std::overflow_error);
	}
	void constructorUnitStorageJustAboveTooSmall_ShouldMatch()
	{
		Money money{majorMinValue, minorMaxValue};
		QCOMPARE(money.major(), majorMinValue);
		QCOMPARE(money.minor(), minorMaxValue);
	}
	void constructorMinorUnitTooLarge_ShouldThrowOverflow()
	{
		QVERIFY_EXCEPTION_THROWN(
			Money(0, static_cast<MinorUnit>(minorMaxValue + 1)),
			std::overflow_error
		);
	}
	void constructorFloatingPoint_ShouldMatch()
	{
		Money money{1.2345, Iso4217Codes::USD};
		QCOMPARE(money.major(), static_cast<UnitStorage>(1));
		QCOMPARE(money.minor(), static_cast<MinorUnit>(2345));
	}
	void constructorFloatingPointNegative_ShouldMatch()
	{
		Money money{-1.2345, Iso4217Codes::USD};
		QCOMPARE(money.major(), static_cast<UnitStorage>(-1));
		QCOMPARE(money.minor(), static_cast<MinorUnit>(2345));
	}
	void constructorFloatingPointMajorTooLarge_ShouldThrowOverflow()
	{
		QVERIFY_EXCEPTION_THROWN(
			Money(static_cast<FloatingPointBase>(majorMaxValue) + 1.0, Iso4217Codes::XXX),
			std::overflow_error
		);
	}
	void constructorFloatingPointMajorTooSmall_ShouldThrowOverflow()
	{
		const FloatingPointBase minorFp{
			minorMaxValue / static_cast<FloatingPointBase>(maxDigitValue)
		};
		const FloatingPointBase majorFp{static_cast<FloatingPointBase>(majorMinValue)};
		QVERIFY_EXCEPTION_THROWN(
			Money(majorFp - minorFp - 1.0, Iso4217Codes::XXX),
			std::overflow_error
		);
	}
	void constructorFloatingPointRoundMajor_ShouldMatch()
	{
		// Calculate all 9s in the minor spot plus one extra outside the range.
		const FloatingPointBase minorFp{
			static_cast<FloatingPointBase>(
				(pow(10, smallExtraDigits + 1) - 1) / pow(10, smallExtraDigits + 1)
			)
		};
		Money money{
			70368744177662 + minorFp, // Expect the major to round up by one.
			Iso4217Codes::XXX,
			[] (const FloatingPoint value, const std::uint8_t digits)
			{
				const double power{pow(10, digits)};
				const FloatingPointBase roundedValue{
					static_cast<FloatingPointBase>(std::round(value.underlying() * power) / power)
				};
				return roundedValue;
			}
		};
		QCOMPARE(money.major(), static_cast<UnitStorage>(70368744177663));
		QCOMPARE(money.minor(), static_cast<MinorUnit>(0));
	}
	void constructorFloatingPointRoundMinor_ShouldMatch()
	{
		Money money{
			static_cast<FloatingPointBase>(1234 / 6.0), // 1234.166666666667...
			Iso4217Codes::XXX,
			[] (const FloatingPoint value, const std::uint8_t digits)
			{
				const double power{pow(10, digits)};
				const FloatingPointBase roundedValue{
					static_cast<FloatingPointBase>(std::round(value.underlying() * power) / power)
				};
				return roundedValue;
			}
		};
		QCOMPARE(money.major(), static_cast<UnitStorage>(205));
		QCOMPARE(money.minor(), static_cast<MinorUnit>(66));
	}
	void constructorFloatingPointRoundEven_ShouldMatch()
	{
		Money money{
			static_cast<FloatingPointBase>(1.66555),
			Iso4217Codes::XXX,
			&pecunia::rounders::reals::even
		};
		QCOMPARE(money.major(), static_cast<UnitStorage>(1));
		QCOMPARE(money.minor(), static_cast<MinorUnit>(66));
	}
	void constructorFloatingPointRoundMinor_ShouldThrowUnderflow()
	{
		QVERIFY_EXCEPTION_THROWN(
			Money(
				static_cast<FloatingPointBase>(1 / (minorMaxValue + 2.0)),
				Iso4217Codes::XXX
			),
			std::underflow_error
		);
	}

	//}

	//{ Min/Max Values

	void maximumMajorValue_ShouldCalculate()
	{
		QCOMPARE(
			Money(0, 0u, Iso4217Codes::PLN).maximumMajorValue(),
			922337203685476l
		);
	}
	void minimumMajorValue_ShouldCalculate()
	{
		QCOMPARE(
			Money(0, 0u, Iso4217Codes::PLN).minimumMajorValue(),
			-922337203685476l
		);
	}
	void maximumMinorValue_ShouldCalculate()
	{
		QCOMPARE(
			Money(0, 0u, Iso4217Codes::PLN).maximumMinorValue(),
			static_cast<MinorUnit>(9999u)
		);
	}
	void maximumAmountValue_ShouldCalculate()
	{
		QCOMPARE(
			Money(0, 0u, Iso4217Codes::PLN).maximumAmountValue(),
			9223372036854769999l
		);
	}
	void minimumAmountValue_ShouldCalculate()
	{
		QCOMPARE(
			Money(0, 0u, Iso4217Codes::PLN).minimumAmountValue(),
			-9223372036854769999l
		);
	}
	void maximum_ShouldCalculate()
	{
		const auto m{Money::maximum(Iso4217Codes::PLN)};
		QCOMPARE(m.first, static_cast<UnitStorage>(922337203685476));
		QCOMPARE(static_cast<MinorSign>(m.second), static_cast<MinorSign>(9999));
	}
	void minimum_ShouldCalculate()
	{
		const auto m{Money::minimum(Iso4217Codes::PLN)};
		QCOMPARE(m.first, static_cast<UnitStorage>(-922337203685476));
		QCOMPARE(static_cast<MinorSign>(m.second), static_cast<MinorSign>(9999));
	}

	//}

	//}

	//{ Operator Native FloatingPointing-Point

	void operatorFloatingPointDefault_ShouldMatch()
	{
		const Money money{};
		const FloatingPointBase expected{0.0};
		const FloatingPointBase actual{money};
		QCOMPARE(actual, expected);
	}
	void operatorFloatingPoint_ShouldMatch()
	{
		Money money{static_cast<UnitStorage>(majorMaxValue / 2), minorMaxValue};
		FloatingPointBase expected{
			static_cast<FloatingPointBase>(majorMaxValue) / static_cast<FloatingPointBase>(2.0)
				+ (minorMaxValue / static_cast<FloatingPointBase>(maxDigitValue))
		};
		FloatingPointBase actual{money};
		QCOMPARE(actual, expected);
	}
	void operatorFloatingPointMajorMax_ShouldMatch()
	{
		Money money{majorMaxValue, minorMaxValue};
		FloatingPointBase expected{
			static_cast<FloatingPointBase>(majorMaxValue)
				+ (minorMaxValue / static_cast<FloatingPointBase>(maxDigitValue))
		};
		FloatingPointBase actual{money};
		QCOMPARE(actual, expected);
	}
	void operatorFloatingPointMajorMin_ShouldMatch()
	{
		Money money{majorMinValue, minorMaxValue};
		FloatingPointBase expected{
			static_cast<FloatingPointBase>(majorMinValue)
				- (minorMaxValue / static_cast<FloatingPointBase>(maxDigitValue))
		};
		FloatingPointBase actual{money};
		QCOMPARE(actual, expected);
	}
	void operatorFloatingPointPositiveMinor33_ShouldMatch()
	{
		Money money{pecunia::MinorSign::Positive, 33};
		FloatingPointBase expected{0.33};
		FloatingPointBase actual{money};
		QCOMPARE(actual, expected);
	}
	void operatorFloatingPointNegativeMinor33_ShouldMatch()
	{
		Money money{pecunia::MinorSign::Negative, 33};
		FloatingPointBase expected{-0.33};
		FloatingPointBase actual{money};
		QCOMPARE(actual, expected);
	}

	//}

	//{ Negation Operator

	void negationOperatorPositve_ShouldBeNegative()
	{
		Money money{8, 0};
		Money test{-money};
		QCOMPARE(test.major(), static_cast<UnitStorage>(-8));
		QCOMPARE(test.minor(), static_cast<MinorUnit>(0));
	}
	void negationOperatorNegative_ShouldBePositive()
	{
		Money money{-8, 0};
		Money test{-money};
		QCOMPARE(test.major(), static_cast<UnitStorage>(8));
		QCOMPARE(test.minor(), static_cast<MinorUnit>(0));
	}

	//}

	//{ Addition Operators

	void additiveOperator_ShouldBeCorrect()
	{
		Money test0{3, 5}, test1{2, 3}, value;
		value = test0 + test1;
		QCOMPARE(value.major(), static_cast<UnitStorage>(5));
		QCOMPARE(value.minor(), static_cast<MinorUnit>(8));
	}
	void additiveAssignmentOperator_ShouldBeCorrect()
	{
		Money test0{3, 5}, test1{2, 3};
		test0 += test1;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(5));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(8));
	}
	void additiveAssignmentOperatorFloatingPoint_ShouldBeCorrect()
	{
		Money test0{3, 5};
		test0 += Money{2.03, Iso4217Codes::XXX};
		QCOMPARE(test0.major(), static_cast<UnitStorage>(5));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(7));
	}
	void additiveOperatorMaximumPlusOne_ShouldThrowOverflow()
	{
		Money test0{majorMaxValue, minorMaxValue}, test1{0, 1};
		QVERIFY_EXCEPTION_THROWN(test0 + test1, std::overflow_error);
	}
	void additiveOperatorMinimumPlusOneNegative_ShouldThrowOverflow()
	{
		Money test0{majorMinValue, minorMaxValue}, test1{pecunia::MinorSign::Negative, 1};
		QVERIFY_EXCEPTION_THROWN(test0 + test1, std::overflow_error);
	}
	void additiveOperatorTwoDifferentCurrencies_ShouldConvert()
	{
		pecunia::converter = testConverter;
		Money test0{15, 3500, Iso4217Codes::USD}, test1{25, 6500, Iso4217Codes::PLN};
		Money sum{test0 + test1};
		QCOMPARE(sum.major(), static_cast<UnitStorage>(21));
		QCOMPARE(sum.minor(), static_cast<MinorUnit>(4785));
		QCOMPARE(sum.code(), Iso4217Codes::USD);
	}
	void additiveOperatorThreeDifferentCurrencies_ShouldConvert()
	{
		pecunia::converter = testConverter;
		Money test0{15, 3500, Iso4217Codes::USD}, test1{25, 6500, Iso4217Codes::PLN}, test2{Iso4217Codes::EUR};
		test2 = test0 + test1;
		QCOMPARE(test2.major(), static_cast<UnitStorage>(20));
		QCOMPARE(test2.minor(), static_cast<MinorUnit>(2147));
		QCOMPARE(test2.code(), Iso4217Codes::EUR);
	}

	//}

	//{ Subtractive Operators

	void subtractiveOperator_ShouldBeCorrect()
	{
		Money test0{3, 5}, test1{2, 3}, value;
		value = test0 - test1;
		QCOMPARE(value.major(), static_cast<UnitStorage>(1));
		QCOMPARE(value.minor(), static_cast<MinorUnit>(2));
	}
	void subtractiveAssignmentOperator_ShouldBeCorrect()
	{
		Money test0{3, 5}, test1{2, 3};
		test0 -= test1;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(1));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(2));
	}
	void subtractiveAssignmentOperatorFloatingPoint_ShouldBeCorrect()
	{
		Money test0{3, 5};
		test0 -= Money{2.03, Iso4217Codes::XXX};
		QCOMPARE(test0.major(), static_cast<UnitStorage>(1));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(3));
	}
	void subtractiveOperatorMaximumMinusMaximumMinorNegative_ShouldThrowOverflow()
	{
		Money test0{majorMaxValue, minorMaxValue}, test1{pecunia::MinorSign::Negative, 1};
		QVERIFY_EXCEPTION_THROWN(test0 - test1, std::overflow_error);
	}
	void subtractiveOperatorMinimumMinusOne_ShouldThrowOverflow()
	{
		Money test0{majorMinValue, minorMaxValue}, test1{pecunia::MinorSign::Positive, 1};
		QVERIFY_EXCEPTION_THROWN(test0 - test1, std::overflow_error);
	}
	void subtractiveOperatorTwoDifferentCurrencies_ShouldConvert()
	{
		pecunia::converter = testConverter;
		Money test0{15, 3500, Iso4217Codes::USD}, test1{25, 6500, Iso4217Codes::PLN};
		test0 = test0 - test1;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(9));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(2215));
	}
	void subtractiveOperatorThreeDifferentCurrencies_ShouldConvert()
	{
		pecunia::converter = testConverter;
		Money test0{15, 3500, Iso4217Codes::USD}, test1{25, 6500, Iso4217Codes::PLN}, test2{Iso4217Codes::EUR};
		test2 = test0 - test1;
		QCOMPARE(test2.major(), static_cast<UnitStorage>(8));
		QCOMPARE(test2.minor(), static_cast<MinorUnit>(6789));
	}

	//}

	//{ Multiplicative Operators

	void multiplicativeOperator_ShouldBeCorrect()
	{
		Money test0{3, 5}, value;
		value = test0 * 2.0f;
		QCOMPARE(value.major(), static_cast<UnitStorage>(6));
		QCOMPARE(value.minor(), static_cast<MinorUnit>(10));
	}
	void multiplicativeOperatorMoney_ShouldBeCorrect()
	{
		Money test0{3, 5}, value;
		value = 2.0f * test0;
		QCOMPARE(value.major(), static_cast<UnitStorage>(6));
		QCOMPARE(value.minor(), static_cast<MinorUnit>(10));
	}
	void multiplicativeAssignmentOperator_ShouldBeCorrect()
	{
		Money test0{3, 5};
		test0 *= 2;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(6));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(10));
	}
	void multiplicativeAssignmentOperatorTwoZeroFloatingPoint_ShouldBeCorrect()
	{
		Money test0{3, 5};
		test0 *= 2.0;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(6));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(10));
	}
	void multiplicativeAssignmentOperatorTwoFiveFloatingPoint_ShouldBeCorrect()
	{
		Money test0{3, 5};
		test0 *= 2.5;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(7));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(62));
	}
	void multiplicativeZeroMaximum_ShouldBeCorrect()
	{
		Money test0{};
		test0 *= majorMaxValue;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(0));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(0));
	}
	void multiplicativeMaximumZero_ShouldBeCorrect()
	{
		Money test0{majorMaxValue, minorMaxValue};
		test0 *= 0;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(0));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(0));
	}
	void multiplicativeFiveSixFiveOneNegative_ShouldBeCorrect()
	{
		Money test0{5, 65};
		test0 *= -1;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(-5));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(65));
	}
	void multiplicativeMaximumOneNegative_ShouldBeCorrect()
	{
		Money test0{majorMaxValue, minorMaxValue};
		test0 *= -1;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(majorMinValue));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(minorMaxValue));
	}
	void multiplicativeMinimumOneNegative_ShouldBeCorrect()
	{
		Money test0{majorMinValue, minorMaxValue};
		test0 *= -1;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(majorMaxValue));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(minorMaxValue));
	}
	void multiplicativeOperatorMaximumPositive_ShouldThrowOverflow()
	{
		Money test0{majorMaxValue, minorMaxValue};
		QVERIFY_EXCEPTION_THROWN(test0 * 1.001, std::overflow_error);
	}
	void multiplicativeOperatorMaximumNegative_ShouldThrowOverflow()
	{
		Money test0{majorMaxValue, minorMaxValue};
		QVERIFY_EXCEPTION_THROWN(test0 * -1.001, std::overflow_error);
	}
	void multiplicativeOperatorMinimumNegative_ShouldThrowOverflow()
	{
		Money test0{majorMinValue, minorMaxValue};
		QVERIFY_EXCEPTION_THROWN(test0 * 1.001, std::overflow_error);
	}
	void multiplicativeOperatorMinorTooSmall_ShouldThrowUnderflow()
	{
		Money test0{0, minorMaxValue};
		const auto mulVal = static_cast<FloatingPointBase>(minorMaxValue + 1);
		QVERIFY_EXCEPTION_THROWN(test0 * (1 / mulVal), std::underflow_error);
	}

	//}

	//{ Division Operators

	void divisionOperator_TwoZero_ShouldBeCorrect()
	{
		Money test0{3, 5}, value;
		value = test0 / 2;
		QCOMPARE(value.major(), static_cast<UnitStorage>(1));
		QCOMPARE(value.minor(), static_cast<MinorUnit>(52));
	}
	void divisionOperator_TwoZeroFloatingPoint_ShouldBeCorrect()
	{
		Money test0{3, 5}, value;
		value = test0 / 2.0;
		QCOMPARE(value.major(), static_cast<UnitStorage>(1));
		QCOMPARE(value.minor(), static_cast<MinorUnit>(52));
	}
	void divisionOperator_TwoFiveFloatingPoint_ShouldBeCorrect()
	{
		Money test0{3, 5}, value;
		value = test0 / 2.5;
		QCOMPARE(value.major(), static_cast<UnitStorage>(1));
		QCOMPARE(value.minor(), static_cast<MinorUnit>(22));
	}
	void divisionOperator_Ratio_ShouldBeCorrect()
	{
		const Money test0{1, 0};
		const Money test1{3, 0};
		const FloatingPointBase value{test0 / test1};
		QCOMPARE(value, 1 / 3.0);
	}
	void divisionOperator_Ratio_DifferentCurrencies_ShouldBeCorrect()
	{
		const Money test0{1, 0, Iso4217Codes::USD};
		const Money test1{2, 0, Iso4217Codes::EUR};
		const FloatingPointBase value{test0 / test1};
		QCOMPARE(value, 0.47058823529411764);
	}
	void divisionOperator_DivideByZero_ShouldThrow()
	{
		const Money test0{5, 0};
		const Money test1{0, 0};
		QVERIFY_EXCEPTION_THROWN(test0 / test1, std::domain_error);
	}
	void divisionAssignmentOperator_Two_ShouldBeCorrect()
	{
		Money test0{3, 5};
		test0 /= 2.0;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(1));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(52));
	}
	void divisionAssignmentOperator_TwoFive_ShouldBeCorrect()
	{
		Money test0{3, 5};
		test0 /= 2.5;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(1));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(22));
	}
	void divisionZeroMaximum_ShouldBeCorrect()
	{
		Money test0{};
		test0 /= majorMaxValue;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(0));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(0));
	}
	void division_ByZero_ShouldThrowDomain()
	{
		Money test0{15, minorMaxValue};
		QVERIFY_EXCEPTION_THROWN(test0 /= 0, std::domain_error);
	}
	void divisionFiveSixFiveOneNegative_ShouldBeCorrect()
	{
		Money test0{5, 65};
		test0 /= -1;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(-5));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(65));
	}
	void divisionMaximumOneNegative_ShouldBeCorrect()
	{
		Money test0{majorMaxValue, minorMaxValue};
		test0 /= -1;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(majorMinValue));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(minorMaxValue));
	}
	void divisionMinimumOneNegative_ShouldBeCorrect()
	{
		Money test0{majorMinValue, minorMaxValue};
		test0 /= -1;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(majorMaxValue));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(minorMaxValue));
	}
	void divisionOperatorMaximumPositive_ShouldThrowOverflow()
	{
		Money test0{majorMaxValue, minorMaxValue};
		QVERIFY_EXCEPTION_THROWN(test0 / 0.5f, std::overflow_error);
	}
	void divisionOperatorMaximumNegative_ShouldThrowOverflow()
	{
		Money test0{majorMaxValue, minorMaxValue};
		QVERIFY_EXCEPTION_THROWN(test0 / -0.5f, std::overflow_error);
	}
	void divisionOperatorMinimum_ShouldThrowOverflow()
	{
		Money test0{majorMinValue, minorMaxValue};
		QVERIFY_EXCEPTION_THROWN(test0 / 0.5f, std::overflow_error);
	}
	void divisionOperatorMinorTooSmall_ShouldThrowUnderflow()
	{
		Money test0{0, minorMaxValue};
		const FloatingPointBase divVal{minorMaxValue + 1.0};
		QVERIFY_EXCEPTION_THROWN(test0 / divVal, std::underflow_error);
	}

	//}

	//{ Modulus Operator

	void modulusOperatorFourFiveByTwo_ShouldBeZero()
	{
		Money test0{4, 5};
		UnitStorage value;
		value = test0 % 2;
		QCOMPARE(value, static_cast<UnitStorage>(0));
	}
	void modulusOperatorFiveFiveByTwo_ShouldBeZero()
	{
		Money test0{5, 5};
		UnitStorage value;
		value = test0 % 2;
		QCOMPARE(value, static_cast<UnitStorage>(1));
	}
	void modulusOperatorFiveFiveByZero_ShouldThrowDomain()
	{
		Money test0{5, 5};
		QVERIFY_EXCEPTION_THROWN(test0 % 0, std::domain_error);
	}

	//}

	//{ Assignment Operator

	void assignmentOperator_ShouldAssign()
	{
		Money test0{5, 5}, test1;
		test1 = test0;
		QCOMPARE(test1.major(), test0.major());
		QCOMPARE(test1.minor(), test0.minor());
	}
	void assignmentOperatorDifferentCurrency_ShouldAssign()
	{
		pecunia::converter = testConverter;
		Money usd{5, 5, Iso4217Codes::USD}, pln{Iso4217Codes::PLN};
		pln = usd;
		QCOMPARE(pln.major(), static_cast<UnitStorage>(20));
		QCOMPARE(pln.minor(), static_cast<MinorUnit>(9288));
		QCOMPARE(pln.code(), Iso4217Codes::PLN);
	}
	void assignmentOperator_DefaultToPolishCurrency_ShouldAssign()
	{
		pecunia::converter = testConverter;
		const Money polish{30, 5296u, Iso4217Codes::PLN};
		Money toSet{};
		toSet = polish;
		toSet.setCode(Iso4217Codes::PLN);
		QCOMPARE(toSet.major(), static_cast<UnitStorage>(30));
		QCOMPARE(toSet.minor(), static_cast<MinorUnit>(5296u));
		QCOMPARE(toSet.code(), Iso4217Codes::PLN);
	}
	void assignmentOperatorDifferentCurrencyTooLargePositive_ShouldThrowOverflow()
	{
		Money test0{majorMaxValue, minorMaxValue}, test1{Iso4217Codes::PLN};
		QVERIFY_EXCEPTION_THROWN(test1 = test0, std::overflow_error);
	}
	void assignmentOperatorDifferentCurrencyTooLargeNegative_ShouldThrowOverflow()
	{
		Money test0{majorMinValue, minorMaxValue}, test1{Iso4217Codes::PLN};
		QVERIFY_EXCEPTION_THROWN(test1 = test0, std::overflow_error);
	}

	//}

	//{ Relational Operators

	void lessThanZeroLessThanZero_ShouldBeFalse()
	{
		Money test0{0, 0}, test1{0, 0};
		QVERIFY( ! (test0 < test1));
		QVERIFY( ! (test0 < 0));
	}
	void lessThanFiveFiveToFiveFifty_ShouldBeTrue()
	{
		Money test0{5, 5}, test1{5, 50};
		QVERIFY(test0 < test1);
	}
	void lessThanFiveFiveToFiveFive_ShouldBeFalse()
	{
		Money test0{5, 5}, test1{5, 5};
		QVERIFY( ! (test1 < test0));
	}
	void lessThan_ZeroFiveToOneFifty_ShouldBeTrue()
	{
		Money test0{0, 5};
		UnitStorage test1{150};
		QVERIFY(test0 < test1);
	}
	void lessThanFourFiveUsdToFiveFivePln_ShouldBeFalse()
	{
		Money test0{4, 5, Iso4217Codes::USD}, test1{5, 5, Iso4217Codes::PLN};
		QVERIFY( ! (test0 < test1));
	}
	void lessThanEqualFiveFiveToFiveFifty_ShouldBeTrue()
	{
		Money test0{5, 5}, test1{5, 50};
		QVERIFY(test0 <= test1);
	}
	void lessThanEqualZeroToZero_ShouldBeTrue()
	{
		Money test0{0, 0}, test1{0, 0};
		QVERIFY(test0 <= test1);
		QVERIFY(test0 <= 0);
	}
	void lessThanEqual_ZeroFiveToZFifty_ShouldBeTrue()
	{
		Money test0{0, 5};
		UnitStorage test1{550};
		QVERIFY(test0 <= test1);
	}
	void lessThanEqualFiveFiveToFiveFive_ShouldBeTrue()
	{
		Money test0{5, 5}, test1{5, 5};
		QVERIFY(test0 <= test1);
	}
	void lessThanEqualFiveFiveUsdToFiveFivePln_ShouldBeFalse()
	{
		Money test0{5, 5, Iso4217Codes::USD}, test1{5, 5, Iso4217Codes::PLN};
		QVERIFY( ! (test0 <= test1));
	}
	void greaterThanFiveFiftyToFiveFive_ShouldBeTrue()
	{
		Money test0{5, 50}, test1{5, 5};
		QVERIFY(test0 > test1);
	}
	void greaterThan_FiveFiftyToZeroFive_ShouldBeTrue()
	{
		Money test0{5, 50};
		UnitStorage test1{50};
		QVERIFY(test0 > test1);
	}
	void greaterThanFiveFiveToFiveFive_ShouldBeFalse()
	{
		Money test0{5, 5}, test1{5, 5};
		QVERIFY( ! (test1 > test0));
	}
	void greaterThanSixFiveUsdToFifteenFivePln_ShouldBeTrue()
	{
		Money test0{6, 5, Iso4217Codes::USD}, test1{15, 5, Iso4217Codes::PLN};
		QVERIFY(test0 > test1);
	}
	void greaterThanEqualFiveFiftyTOFiveFive_ShouldBeTrue()
	{
		Money test0{5, 50}, test1{5, 5};
		QVERIFY(test0 >= test1);
	}
	void greaterThanEqual_FiveFiftyToZeroFive_ShouldBeTrue()
	{
		Money test0{5, 50};
		UnitStorage test1{50};
		QVERIFY(test0 >= test1);
	}
	void greaterThanEqualFiveFiveToFiveFive_ShouldBeTrue()
	{
		Money test0{5, 5}, test1{5, 5};
		QVERIFY(test0 >= test1);
	}
	void greaterThanEqualSixFiveUsdToFifteenFivePln_ShouldBeTrue()
	{
		Money test0{6, 5, Iso4217Codes::USD}, test1{15, 5, Iso4217Codes::PLN};
		QVERIFY(test0 >= test1);
	}

	void equalFiveFiveToFiveFive_ShouldBeTrue()
	{
		Money test0{5, 5}, test1{5, 5};
		QVERIFY(test0 == test1);
	}
	void equalFiveFiftyToFiveFive_ShouldBeFalse()
	{
		Money test0{5, 5}, test1{5, 50};
		QVERIFY( ! (test1 == test0));
	}
	void equalFiveFiveToFiveFiveInteger_ShouldBeTrue()
	{
		const Money test0{5, 5};
		const UnitStorage test1{505};
		QVERIFY(test0 == test1);
	}
	void equalFiveFiveUsdToTwentyNinetwoeightninePln_ShouldBeTrue()
	{
		Money test0{5, 5, Iso4217Codes::USD}, test1{20, 9289, Iso4217Codes::PLN};
		QVERIFY(test0 == test1);
	}

	void inequalFiveFiveToFiveFive_ShouldBeFalse()
	{
		Money test0{5, 5}, test1{5, 5};
		QVERIFY( ! (test0 != test1));
	}
	void inequalFiveFiftyToFiveFive_ShouldBeTrue()
	{
		Money test0{5, 5}, test1{5, 50};
		QVERIFY(test1 != test0);
	}
	void inequalFiveFiveUsdToFiveFivePln_ShouldBeTrue()
	{
		Money test0{5, 5, Iso4217Codes::USD}, test1{5, 5, Iso4217Codes::PLN};
		QVERIFY(test0 != test1);
	}
	void inequalFiveFiftyToFiveSixInteger_ShouldBeTrue()
	{
		const Money test0{5, 5};
		const UnitStorage test1{560};
		QVERIFY(test0 != test1);
	}

	//}

	//{ Stream Operators

	void streamInsertionPositive_ShouldInsert()
	{
		std::stringstream ss;
		ss.imbue(std::locale{defaultLocale});
		Money test0{11, 45};
		ss << test0;
		// N.B. The "XXX" currency doesn't have minor units.
		QCOMPARE(ss.str().c_str(), std::string{"11.45XXX"}.c_str());
	}
	void streamInsertionNegative_ShouldInsert()
	{
		std::stringstream ss;
		ss.imbue(std::locale{defaultLocale});
		Money test0{-11, 45};
		ss << test0;
		// N.B. The "XXX" currency doesn't have minor units.
		QCOMPARE(ss.str().c_str(), std::string{"-11.45XXX"}.c_str());
	}
	void streamInsertionNegativeUsdLead_ShouldInsert()
	{
		std::stringstream ss;
		ss.imbue(std::locale{defaultLocale});
		Money test0{-11, 4524, Iso4217Codes::USD};
		ss << pecunia::currency::lead << test0;
		QCOMPARE(ss.str().c_str(), std::string{"USD-11.4524"}.c_str());
	}
	void streamInsertionNegativeUsdTail_ShouldInsert()
	{
		std::stringstream ss;
		ss.imbue(std::locale{defaultLocale});
		Money test0{-11, 4524, Iso4217Codes::USD};
		ss << pecunia::currency::trail << test0;
		QCOMPARE(ss.str().c_str(), std::string{"-11.4524USD"}.c_str());
	}
	void streamInsertionNegativeUsdLeadSymbol_ShouldInsert()
	{
		std::stringstream ss;
		ss.imbue(std::locale{defaultLocale});
		Money test0{-11, 4524, Iso4217Codes::USD};
		ss << pecunia::currency::lead << pecunia::currency::symbol << test0;
		QCOMPARE(ss.str().c_str(), std::string{"$-11.4524"}.c_str());
	}
	void streamInsertionNegativeUsdTrailSymbol_ShouldInsert()
	{
		std::stringstream ss;
		ss.imbue(std::locale{defaultLocale});
		Money test0{-11, 4500, Iso4217Codes::USD};
		ss << pecunia::currency::trail << pecunia::currency::symbol << test0;
		QCOMPARE(ss.str().c_str(), std::string{"-11.4500$"}.c_str());
	}
	void streamInsertionNegativeUsdLeadSymbolSpace_ShouldInsert()
	{
		std::stringstream ss;
		ss.imbue(std::locale{defaultLocale});
		Money test0{-11, 4524, Iso4217Codes::USD};
		ss << pecunia::currency::lead << pecunia::currency::symbol << pecunia::currency::spaced << test0;
		QCOMPARE(ss.str().c_str(), std::string{"$ -11.4524"}.c_str());
	}
	void streamInsertionNegativeUsdTrailSymbolSpace_ShouldInsert()
	{
		std::stringstream ss;
		ss.imbue(std::locale{defaultLocale});
		Money test0{-11, 4524, Iso4217Codes::USD};
		ss << pecunia::currency::trail << pecunia::currency::symbol << pecunia::currency::spaced << test0;
		QCOMPARE(ss.str().c_str(), std::string{"-11.4524 $"}.c_str());
	}
	void streamExtractionUsdTrail_ShouldExtract()
	{
		std::stringstream ss;
		ss.imbue(std::locale{defaultLocale});
		Money test0;
		ss << "11.4500 USD";
		ss >> test0;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(11));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(4500));
		QCOMPARE(test0.code(),  Iso4217Codes::USD);
	}
	void streamExtractionUsdLead_ShouldExtract()
	{
		std::stringstream ss;
		ss.imbue(std::locale{defaultLocale});
		Money test0;
		ss << "USD 11.4500";
		ss >> pecunia::currency::lead >> test0;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(11));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(4500));
		QCOMPARE(test0.code(),  Iso4217Codes::USD);
	}
	void streamExtractionNegative_ShouldExtract()
	{
		std::stringstream ss;
		ss.imbue(std::locale{defaultLocale});
		Money test0;
		ss << "-11.4500 USD";
		ss >> test0;
		QCOMPARE(test0.major(), static_cast<UnitStorage>(-11));
		QCOMPARE(test0.minor(), static_cast<MinorUnit>(4500));
		QCOMPARE(test0.code(),  Iso4217Codes::USD);
	}
	void streamExtractionInvalidCurrencyFormat_ShouldThrow()
	{
		std::stringstream ss;
		ss.imbue(std::locale{defaultLocale});
		Money test0;
		ss << "-11~4500 USD";
		QVERIFY_EXCEPTION_THROWN(ss >> test0, std::runtime_error);
	}
	void streamExtractionInvalidAmountData_ShouldThrowRuntime()
	{
		std::stringstream ss;
		ss.imbue(std::locale{defaultLocale});
		Money test0;
		ss << "dsfa USD";
		QVERIFY_EXCEPTION_THROWN(ss >> test0, std::runtime_error);
	}
	void streamExtractionInvalidCodeData_ShouldThrowRuntime()
	{
		std::stringstream ss;
		ss.imbue(std::locale{defaultLocale});
		Money test0;
		ss << "11.45 dsaf";
		QVERIFY_EXCEPTION_THROWN(ss >> test0, std::runtime_error);
	}

	//}

	//{ to_string

	void toStringDefault_ShouldConvert()
	{
		Money test0{0, 0};
		std::string str{pecunia::currency::to_string(test0)};
		QCOMPARE(str.c_str(), "0.00 XXX");
	}

	void toStringDefaultUsd_ShouldConvert()
	{
		Money test0{0, 0, Iso4217Codes::USD};
		std::string str{pecunia::currency::to_string(test0)};
		QCOMPARE(str.c_str(), "0.0000 USD");
	}

	void toStringNegativeAmount_ShouldConvert()
	{
		Money test0{-11, 4500, Iso4217Codes::USD};
		std::string str{pecunia::currency::to_string(test0)};
		QCOMPARE(str.c_str(), "-11.4500 USD");
	}

	//}

	//{ toFloatingPoint

	void toFloatingPoint_defaultRound_ShouldBeSame()
	{
		const Money money{12, 66u, Iso4217Codes::XXX};
		const FloatingPointBase expected{12.66};
		QCOMPARE(money.toFloatingPoint(), expected);
	}
	void toFloatingPoint_defaultRound_ShouldMatch()
	{
		Money money{static_cast<UnitStorage>(majorMaxValue / 2), minorMaxValue};
		FloatingPointBase expected{
			static_cast<FloatingPointBase>(majorMaxValue) / static_cast<FloatingPointBase>(2.0)
				+ (minorMaxValue / static_cast<FloatingPointBase>(maxDigitValue))
		};
		QCOMPARE(money.toFloatingPoint(), expected);
	}
	void toFloatingPoint_defaultRound_MajorMax_ShouldMatch()
	{
		Money money{majorMaxValue, minorMaxValue};
		FloatingPointBase expected{
			static_cast<FloatingPointBase>(majorMaxValue)
				+ (minorMaxValue / static_cast<FloatingPointBase>(maxDigitValue))
		};
		QCOMPARE(money.toFloatingPoint(), expected);
	}
	void toFloatingPoint_defaultRound_MajorMin_ShouldMatch()
	{
		Money money{majorMinValue, minorMaxValue};
		FloatingPointBase expected{
			static_cast<FloatingPointBase>(majorMinValue)
				- (minorMaxValue / static_cast<FloatingPointBase>(maxDigitValue))
		};
		QCOMPARE(money.toFloatingPoint(), expected);
	}
	void toFloatingPoint_defaultRound_PositiveMinor33_ShouldMatch()
	{
		Money money{pecunia::MinorSign::Positive, 33};
		FloatingPointBase expected{0.33};
		QCOMPARE(money.toFloatingPoint(), expected);
	}
	void toFloatingPoint_defaultRound_NegativeMinor33_ShouldMatch()
	{
		Money money{pecunia::MinorSign::Negative, 33};
		FloatingPointBase expected{-0.33};
		QCOMPARE(money.toFloatingPoint(), expected);
	}
	void toFloatingPoint_evenRound_ShouldRoundUp()
	{
		Money money{1, 66u, Iso4217Codes::XXX};
		FloatingPointBase expected{1.7};
		QCOMPARE(money.toFloatingPoint(&pecunia::rounders::currency::even, 1), expected);
	}
	void toFloatingPoint_evenRound_ShouldDown()
	{
		Money money{1, 64u, Iso4217Codes::XXX};
		FloatingPointBase expected{1.6};
		QCOMPARE(money.toFloatingPoint(&pecunia::rounders::currency::even, 1), expected);
	}

	//}

	//{ round

	void round_Round_ShouldRoundUp()
	{
		Money money{3, 66u, Iso4217Codes::XXX};
		Money expected{3, 70u, Iso4217Codes::XXX};
		money.round(&pecunia::rounders::currency::even, 1);
		QCOMPARE(money, expected);
	}
	void round_Round_ShouldRoundDown()
	{
		Money money{8, 65u, Iso4217Codes::XXX};
		Money expected{8, 60u, Iso4217Codes::XXX};
		money.round(&pecunia::rounders::currency::even, 1);
		QCOMPARE(money, expected);
	}
	void round_RoundMajor_ShouldRoundUp()
	{
		Money money{7, 99u, Iso4217Codes::XXX};
		Money expected{8, 00u, Iso4217Codes::XXX};
		money.round(&pecunia::rounders::currency::even, 1);
		QCOMPARE(money, expected);
	}
	void round_RoundMaxMajor_ShouldRoundUp()
	{
		Money money{majorMaxValue - 1, 99u, Iso4217Codes::XXX};
		Money expected{majorMaxValue, 00u, Iso4217Codes::XXX};
		money.round(&pecunia::rounders::currency::up, 1);
		QCOMPARE(money, expected);
	}
	void round_RoundAtMaxMajor_ShouldThrow()
	{
		Money money{majorMaxValue, 99u, Iso4217Codes::XXX};
		QVERIFY_EXCEPTION_THROWN(
			money.round(&pecunia::rounders::currency::up, 1),
			std::overflow_error
		);
	}

	//}

	//{ setCode

	void setCode_ValidParam_ShouldSet()
	{
		Money test{-11, 4500, Iso4217Codes::USD};
		test.setCode(Iso4217Codes::PLN);
		QCOMPARE(test.code(), Iso4217Codes::PLN);
	}

	//}

	//{ minimum

	void minimum_ValidParam_ShouldHaveValue()
	{
		const auto min{Money::minimum(Iso4217Codes::USD)};
		QCOMPARE(min.first, static_cast<UnitStorage>(-922337203685476));
		QCOMPARE(min.second, static_cast<MinorUnit>(9999));
	}

	//}

	//{ maximum

	void maximum_ValidParam_ShouldHaveValue()
	{
		const auto max{Money::maximum(Iso4217Codes::USD)};
		QCOMPARE(max.first, static_cast<UnitStorage>(922337203685476));
		QCOMPARE(max.second, static_cast<MinorUnit>(9999));
	}

	//}

	//{ zero-out

	void zeroOut_ValidParam_ShouldBeZero()
	{
		Money m{12, 3456u, Iso4217Codes::USD};
		m.zeroOut();
		QCOMPARE(m, 0);
	}

	//}
};

}}}

QTEST_MAIN(pecunia::currency::tests::MoneyUT)
#include "money-unit-tests.moc"
