/*******************************************************************************
***  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 <QTest>
using ::QObject;

#include <cmath>
using std::pow;
#include <exception>
using std::exception;
#include <iostream>
using std::cout;
using std::endl;
#include <limits>
using std::numeric_limits;
#include <tuple>
using std::get;
using std::make_tuple;
using std::tuple;

#include <pecunia/Codes.h>
using pecunia::currency::Iso4217Codes;
#include <pecunia/Money.h>
using pecunia::currency::Money;
#include <pecunia/MoneySize.hpp>
using pecunia::FloatingPointBase;
#include <pecunia/MoneyTypes.h>
using pecunia::precision;
using pecunia::MinorUnit;
using pecunia::UnitStorage;


namespace pecunia
{
namespace currency
{
namespace tests
{

struct RangeUT : QObject
{
	Q_OBJECT

	const MinorUnit maxDigitValue{static_cast<MinorUnit>(pow(10, precision))};
	const MinorUnit minorMaxValue{static_cast<MinorUnit>(maxDigitValue - 1)};
	const FloatingPointBase minorFp{minorMaxValue / static_cast<FloatingPointBase>(maxDigitValue)};

	tuple<UnitStorage, UnitStorage> findMaximum()
	{
		UnitStorage maximum{0};
		UnitStorage lowest{0};
		UnitStorage highest{numeric_limits<UnitStorage>::max()};
		UnitStorage test{static_cast<UnitStorage>(highest / 2)};

		do
		{
//			cout << "Trying: " << test << '.' << minorMaxValue;

			try
			{
				const Money _{test, minorMaxValue, Iso4217Codes::XXX}; // XXX does not have any minor units.
//				cout << " -- within range" << endl;

				if (test > maximum)
					maximum = test;
				lowest = test;
			}
			catch (const exception& /*error*/)
			{
//				cout << " -- outside range -- " << error.what() << endl;
				highest = test;
			}
			test = (highest - lowest) / 2 + lowest;
		}
		while (lowest != test);
		return make_tuple(maximum, highest);
	}
	tuple<UnitStorage, UnitStorage> findMinimum()
	{
		UnitStorage minimum{0};
		UnitStorage lowest{numeric_limits<UnitStorage>::min()};
		UnitStorage highest{0};
		UnitStorage test{static_cast<UnitStorage>(lowest / 2)};

		do
		{
//			cout << "Trying: " << test << '.' << minorMaxValue;

			try
			{
				const Money _{test, minorMaxValue, Iso4217Codes::XXX}; // XXX does not have any minor units.
//				cout << " -- within range" << endl;

				if (test < minimum)
					minimum = test;
				highest = test;
			}
			catch (const exception& /*error*/)
			{
//				cout << " -- outside range -- " << error.what() << endl;
				lowest = test;
			}
			test = (lowest - highest) / 2 + highest;
		}
		while (highest != test);
		return make_tuple(lowest, minimum);
	}

private slots:
	void testMaximumRange()
	{
		const auto maxRange{findMaximum()};
		const UnitStorage max{get<0>(maxRange)};
		const UnitStorage upper{get<1>(maxRange)};
		QVERIFY(upper - max == 1);
		cout << "Maximum Value: " << max << '.' << minorMaxValue << endl;
	}
	void testMinimumRange()
	{
		const auto maxRange{findMinimum()};
		const UnitStorage lower{get<0>(maxRange)};
		const UnitStorage min{get<1>(maxRange)};
		QVERIFY(lower - min == -1);
		cout << "Minimum Value: " << min << '.' << minorMaxValue << endl;
	}
	void testMaximumFloatingPointRange()
	{
		UnitStorage max{0};
		UnitStorage lower{0};
		UnitStorage upper{numeric_limits<UnitStorage>::max()};
		UnitStorage test{upper / 2};
		cout.precision(numeric_limits<FloatingPointBase>::max_digits10);

		do
		{
//			cout << "Trying: " << test << '.' << minorMaxValue;

			try
			{
				// XXX does not have any minor units.
				Money money{static_cast<FloatingPointBase>(test) + minorFp, Iso4217Codes::XXX};
//				cout << " -- within range" << endl;

				if (money.major() != test)
					throw exception{};

				if (money.minor() != minorMaxValue)
					throw exception{};

				if (test > max)
					max = test;
				lower = test;
			}
			catch (const exception& /*error*/)
			{
//				cout << " -- outside range -- " << error.what() << endl;
				upper = test;
			}
			test = (upper - lower) / 2 + lower;
		}
		while (lower != test);
		QVERIFY(upper - max == 1);
		const FloatingPointBase foundMax{static_cast<FloatingPointBase>(max) + minorFp};
		QCOMPARE(foundMax - minorFp, static_cast<FloatingPointBase>(max));
		QCOMPARE(
			static_cast<MinorUnit>((foundMax - static_cast<FloatingPointBase>(max)) * maxDigitValue)
				/ static_cast<FloatingPointBase>(maxDigitValue),
			minorFp
		);
		cout << "Maximum Floating-point Value: " << foundMax << endl;
	}
	void testMinimumFloatingPointRange()
	{
		UnitStorage min{0};
		UnitStorage lower{numeric_limits<UnitStorage>::min()};
		UnitStorage upper{0};
		UnitStorage test{lower / 2};
		cout.precision(numeric_limits<FloatingPointBase>::max_digits10);

		do
		{
//			cout << "Trying: " << test << '.' << minorMaxValue;

			try
			{
				// XXX does not have any minor units.
				Money money{static_cast<FloatingPointBase>(test) - minorFp, Iso4217Codes::XXX};
//				cout << " -- within range" << endl;

				if (money.major() != test)
					throw exception{};

				if (money.minor() != minorMaxValue)
					throw exception{};

				if (test < min)
					min = test;
				upper = test;
			}
			catch (const exception& /*error*/)
			{
//				cout << " -- outside range -- " << error.what() << endl;
				lower = test;
			}
			test = (upper - lower) / 2 + lower;
		}
		while (upper - lower > 1);
		QVERIFY(lower - min == -1);
		const FloatingPointBase foundMinimum{static_cast<FloatingPointBase>(min) - minorFp};
		QCOMPARE(foundMinimum + minorFp, static_cast<FloatingPointBase>(min));
		QCOMPARE(
			static_cast<MinorUnit>((foundMinimum - static_cast<FloatingPointBase>(min)) * -maxDigitValue)
			/ static_cast<FloatingPointBase>(-maxDigitValue),
			-minorFp
		);
		cout << "Minimum Floating-point Value: " << foundMinimum << endl;
	}
};

}}}

QTEST_MAIN(pecunia::currency::tests::RangeUT)
#include "range-unit-tests.moc"
