/*******************************************************************************
***  This file is part of Dux Rei Nummariae.                                 ***
***                                                                          ***
***  Copyright (C) 2017, 2018, 2019, 2020, 2021                              ***
***  John Schneiderman <Licensing _AT_ Schneiderman _DOT_ me>                ***
***                                                                          ***
***  This program is free software: you can redistribute it and/or modify it ***
***  under the terms of the GNU Affero 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 Affero General Public License for more details.             ***
***                                                                          ***
***  You should have received a copy of the GNU Affero General Public License***
***  along with this program. If not, see <http://www.gnu.org/licenses/>.    ***
*******************************************************************************/
#include <pecunia/Codes.h>
using pecunia::currency::Iso4217Codes;
#include <pecunia/Money.h>
using pecunia::currency::Money;
#include <pecunia/MoneyTypes.h>
using pecunia::converter;
using pecunia::FloatingPoint;
#include <QDate>
using ::QDate;
#include <QString>
using ::QString;
#include <QTest>
using ::QObject;
// using QTEST_MAIN
#include <qt-mocks/Matchers.h>
using qt_mocks::equals;
using qt_mocks::lessThan;
using qt_mocks::memberFunction;
using qt_mocks::not_;
// using QVERIFY_EXCEPTION_THROWN
// using QVERIFY_THAT;

#include <stdexcept>
using std::logic_error;
using std::runtime_error;
#include <utility>
using std::move;

#include <accounting/Account.h>
using drn::accounting::Account;
using drn::accounting::AccountTypes;
#include <accounting/Transaction.h>
using drn::accounting::Transaction;
using drn::accounting::TransactionStatuses;
#include <accounting/AccountingErrors.h>
using drn::accounting::TransactionError;
#include <internal/AccountTransactionKey.hpp>
using drn::accounting::internal::AccountTransactionKey;


namespace drn
{
namespace accounting
{
namespace tests
{

struct TransactionUT : QObject
{
	Q_OBJECT

private slots:
	void initTestCase()
	{
		converter =
				[] (const Iso4217Codes& from, const Iso4217Codes& to) -> FloatingPoint
				{
					if (from == to)
						return static_cast<FloatingPoint>(1.0);
					switch (from)
					{
					case Iso4217Codes::XXX:
						return static_cast<FloatingPoint>(1.0);
					case Iso4217Codes::PLN:
						switch (to)
						{
						case Iso4217Codes::USD:
							return static_cast<FloatingPoint>(0.27);
						case Iso4217Codes::XXX:
							return static_cast<FloatingPoint>(1.0);
						default:
							throw logic_error{"Unhandled to code."};
						};
					case Iso4217Codes::USD:
						switch (to)
						{
						case Iso4217Codes::PLN:
							return static_cast<FloatingPoint>(3.76);
						case Iso4217Codes::XXX:
							return static_cast<FloatingPoint>(1.0);
						default:
							throw logic_error{"Unhandled to code."};
						};
					default:
						throw logic_error{"Unhandled from code."};
					}
				};
	}

	//{ Constructor Tests

	void constructor_fullInit_ShouldBeValues()
	{
		const QDate occurredOn{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		const QString description{"I am Jack's transaction."};
		const Transaction t{occurredOn, debit.code(), credit.code(), amount, description};
		QVERIFY_THAT(t.occurredOn(), equals(occurredOn));
		QVERIFY_THAT(t.debiting(), equals(debit.code()));
		QVERIFY_THAT(t.crediting(), equals(credit.code()));
		QVERIFY_THAT(t.amount(), equals(amount));
		QVERIFY_THAT(*t.description(), equals(description));
		QVERIFY_THAT(t.status(), equals(TransactionStatuses::Unreconciled));
	}
	void constructor_emptyOccurredOn_ShouldThrow()
	{
		const QDate occurredOn{};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		QVERIFY_EXCEPTION_THROWN(
			Transaction(occurredOn, debit.code(), credit.code(), amount),
			TransactionError
		);
	}
	void constructor_debitCreditAccountsSame_ShouldThrow()
	{
		const QDate occurredOn{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{debit};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		QVERIFY_EXCEPTION_THROWN(
			Transaction(occurredOn, debit.code(), credit.code(), amount),
			TransactionError
		);
	}
	void constructor_amountNegative_ShouldThrow()
	{
		const QDate occurredOn{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{-123, 4500u, Iso4217Codes::USD};
		QVERIFY_EXCEPTION_THROWN(
			Transaction(occurredOn, debit.code(), credit.code(), amount),
			TransactionError
		);
	}

	//}

	//{ operator< Tests

	void operatorLt_DateLessThan_ShoulBeTrue()
	{
		const QDate occurredOn0{2018, 6, 1};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		const Transaction t0{occurredOn0, debit.code(), credit.code(), amount};
		const QDate occurredOn1{2018, 6, 2};
		const Transaction t1{occurredOn1, debit.code(), credit.code(), amount};
		QVERIFY_THAT(t0, lessThan(t1));
	}

	void operatorLt_DateNotLessThan_ShoulBeFalse()
	{
		const QDate occurredOn0{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		const Transaction t0{occurredOn0, debit.code(), credit.code(), amount};
		const QDate occurredOn1{2018, 6, 1};
		const Transaction t1{occurredOn1, debit.code(), credit.code(), amount};
		QVERIFY_THAT(t0, not_(lessThan(t1)));
	}

	//}

	//{ operator= Tests

	void operatorAssignment_validValuesCopy_ShouldAssign()
	{
		const QDate occurredOn{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		const QString description{"I am Jack's transaction."};
		const Transaction t0{occurredOn, debit.code(), credit.code(), amount, description};
		Transaction t1;
		QVERIFY_NO_THROW(t1 = t0);
		QVERIFY_THAT(t1.occurredOn(), equals(t0.occurredOn()));
		QVERIFY_THAT(t1.debiting(), equals(t0.debiting()));
		QVERIFY_THAT(t1.crediting(), equals(t0.crediting()));
		QVERIFY_THAT(t1.amount(), equals(t0.amount()));
		QVERIFY_THAT(t1.description(), equals(t0.description()));
		QVERIFY_THAT(t1.status(), equals(t0.status()));
	}

	void operatorAssignment_validValuesMove_ShouldAssign()
	{
		const QDate occurredOn{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		const QString description{"I am Jack's transaction."};
		Transaction t0{occurredOn, debit.code(), credit.code(), amount, description};
		Transaction t1;
		QVERIFY_NO_THROW(t1 = move(t0));
		QVERIFY_THAT(t1.occurredOn(), equals(occurredOn));
		QVERIFY_THAT(t1.debiting(), equals(debit.code()));
		QVERIFY_THAT(t1.crediting(), equals(credit.code()));
		QVERIFY_THAT(t1.amount(), equals(amount));
		QVERIFY_THAT(*t1.description(), equals(description));
		QVERIFY_THAT(t1.status(), equals(TransactionStatuses::Unreconciled));
	}

	//}

	//{ markUnreconciled Tests

	void markUnreconciled_transactionNotUnreconciled_ShouldBeUnreconciled()
	{
		const QDate occurredOn{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		const QString description{"I am Jack's transaction."};
		Transaction t{occurredOn, debit.code(), credit.code(), amount, description};
		t.markCleared();
		t.markUnreconciled();
		QVERIFY_THAT(
			t,
			memberFunction(&Transaction::status, equals(TransactionStatuses::Unreconciled))
		);
	}

	void markUnreconciled_transactionUnreconciled_ShouldBeUnreconciled()
	{
		const QDate occurredOn{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		const QString description{"I am Jack's transaction."};
		Transaction t{occurredOn, debit.code(), credit.code(), amount, description};
		t.markUnreconciled();
		t.markUnreconciled();
		QVERIFY_THAT(
			t,
			memberFunction(&Transaction::status, equals(TransactionStatuses::Unreconciled))
		);
	}

	void markUnreconciled_transactionRecociled_ShouldThrow()
	{
		const QDate occurredOn{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		const QString description{"I am Jack's transaction."};
		Transaction t{occurredOn, debit.code(), credit.code(), amount, description};
		t.markReconciled();
		QVERIFY_EXCEPTION_THROWN(t.markUnreconciled(), TransactionError);
	}

	//}

	//{ markCleared Tests

	void markCleared_transactionNotCleared_ShouldBeCleared()
	{
		const QDate occurredOn{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		const QString description{"I am Jack's transaction."};
		Transaction t{occurredOn, debit.code(), credit.code(), amount, description};
		t.markCleared();
		QVERIFY_THAT(
			t,
			memberFunction(&Transaction::status, equals(TransactionStatuses::Cleared))
		);
	}

	void markCleared_transactionCleared_ShouldBeCleared()
	{
		const QDate occurredOn{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		const QString description{"I am Jack's transaction."};
		Transaction t{occurredOn, debit.code(), credit.code(), amount, description};
		t.markCleared();
		t.markCleared();
		QVERIFY_THAT(
			t,
			memberFunction(&Transaction::status, equals(TransactionStatuses::Cleared))
		);
	}

	void markCleared_transactionRecociled_ShouldThrow()
	{
		const QDate occurredOn{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		const QString description{"I am Jack's transaction."};
		Transaction t{occurredOn, debit.code(), credit.code(), amount, description};
		t.markReconciled();
		QVERIFY_EXCEPTION_THROWN(t.markCleared(), TransactionError);
	}

	//}

	//{ markReconciled Tests

	void markReconciled_transactionNotReconciled_ShouldBeReconciled()
	{
		const QDate occurredOn{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		const QString description{"I am Jack's transaction."};
		Transaction t{occurredOn, debit.code(), credit.code(), amount, description};
		t.markReconciled();
		QVERIFY_THAT(
			t,
			memberFunction(&Transaction::status, equals(TransactionStatuses::Reconciled))
		);
	}

	void markReconciled_transactionReconciled_ShouldBeReconciled()
	{
		const QDate occurredOn{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		const QString description{"I am Jack's transaction."};
		Transaction t{occurredOn, debit.code(), credit.code(), amount, description};
		t.markReconciled();
		t.markReconciled();
		QVERIFY_THAT(
			t,
			memberFunction(&Transaction::status, equals(TransactionStatuses::Reconciled))
		);
	}

	//}

	//{ convertAmount Tests

	void convertAmount_validParams_ShouldBeConverted()
	{
		const QDate occurredOn{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::PLN};
		const QString description{"I am Jack's transaction."};
		Transaction t{occurredOn, debit.code(), credit.code(), amount, description};
		QVERIFY_NO_THROW(t.convertAmount(Iso4217Codes::USD, AccountTransactionKey{}));
		QVERIFY_THAT(t.amount(), equals(Money(33, 3300u, Iso4217Codes::USD)));
		QVERIFY_THAT(t.amount().code(), equals(Iso4217Codes::USD));
		QVERIFY_THAT(
			*t.description(),
			equals(description + " Converted from PLN at the exchange rate of USD 0.27.")
		);
	}

	void convertAmount_sameBalanceCode_ShouldThrow()
	{
		const QDate occurredOn{2018, 6, 2};
		const Account debit{
			AccountTypes::Asset,
			AccountCode{AccountNumber{1u}, "Current"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Account credit{
			AccountTypes::Liability,
			AccountCode{AccountNumber{10u}, "Mortgage"},
			Money{0, 0u, Iso4217Codes::USD}
		};
		const Money amount{123, 4500u, Iso4217Codes::USD};
		const QString description{"I am Jack's transaction."};
		Transaction t{occurredOn, debit.code(), credit.code(), amount, description};
		QVERIFY_EXCEPTION_THROWN(
			t.convertAmount(Iso4217Codes::USD, AccountTransactionKey{}),
			TransactionError
		);
	}

	//}

};

}}}

QTEST_MAIN(drn::accounting::tests::TransactionUT)
#include "Transaction-unit-tests.moc"
