/*******************************************************************************
***  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/SetUp.h>
using pecunia::currency::setUpCurrency;
#include <QTest>
using ::QObject;
// using QTEST_MAIN
#include <qt-mocks/Matchers.h>
using qt_mocks::equals;
using qt_mocks::isTrue;
using qt_mocks::lessThan;
using qt_mocks::memberFunction;
using qt_mocks::notEquals;
using qt_mocks::not_;

#include <stdexcept>
using std::logic_error;

#include <accounting/Account.h>
using drn::accounting::Account;
using drn::accounting::AccountCode;
using drn::accounting::AccountTypes;
#include <accounting/AccountingErrors.h>
using drn::accounting::AccountError;
#include <internal/AccountTransactionKey.hpp>
using drn::accounting::internal::AccountTransactionKey;


namespace drn
{
namespace accounting
{
namespace tests
{

struct AccountUT : QObject
{
	Q_OBJECT

private slots:
	//{ Constructor Tests

	void constructor_typeInit_ShouldBeTypeAndUnidentified()
	{
		const AccountTypes expectedType{AccountTypes::Asset};
		const AccountCode expectedCode{AccountNumber{1u}, "Cash"};
		const Money expectedBalance{0, 0u, Iso4217Codes::USD};
		const Account a{expectedType, expectedCode, Iso4217Codes::USD};
		QVERIFY_THAT(a.code(), equals(expectedCode));
		QVERIFY_THAT(a.type(), equals(expectedType));
		QVERIFY_THAT(a.balance(), equals(expectedBalance));
	}

	void constructor_fullInit_ShouldBeValues()
	{
		const AccountTypes expectedType{AccountTypes::Asset};
		const AccountCode expectedCode{AccountNumber{1u}, "Cash"};
		const Money expectedBalance{123, 4500u, Iso4217Codes::USD};
		const Account a{expectedType, expectedCode, expectedBalance, true};
		QVERIFY_THAT(a.code(), equals(expectedCode));
		QVERIFY_THAT(a.type(), equals(expectedType));
		QVERIFY_THAT(a.balance(), equals(expectedBalance));
		QVERIFY_THAT(a.isClosed(), isTrue());
	}

	void constructor_typeInitInvalidCurrency_ShouldThrow()
	{
		QVERIFY_EXCEPTION_THROWN(
			Account(AccountTypes::Asset, AccountCode(AccountNumber{1u}), Iso4217Codes::XXX),
			AccountError
		);
	}

	void constructor_typeInitInvalidAccountType_ShouldThrow()
	{
		QVERIFY_EXCEPTION_THROWN(
			Account(AccountTypes::Unknown, AccountCode(AccountNumber{1u}), Iso4217Codes::USD),
			AccountError
		);
	}

	void constructor_fullInitInvalidCurrency_ShouldThrow()
	{
		const AccountTypes expectedType{AccountTypes::Asset};
		const Money expectedBalance{123, 45u, Iso4217Codes::XXX};
		const AccountCode expectedCode{AccountNumber{1u}, "Cash"};
		QVERIFY_EXCEPTION_THROWN(
			Account(expectedType, expectedCode, expectedBalance),
			AccountError
		);
	}

	void constructor_fullInitInvalidAccountType_ShouldThrow()
	{
		const AccountTypes expectedType{AccountTypes::Unknown};
		const Money expectedBalance{123, 45u, Iso4217Codes::USD};
		const AccountCode expectedCode{AccountNumber{1u}, "Cash"};
		QVERIFY_EXCEPTION_THROWN(
			Account(expectedType, expectedCode, expectedBalance),
			AccountError
		);
	}

	void constructor_fullInitInvalidAccountCode_ShouldThrow()
	{
		const AccountTypes expectedType{AccountTypes::Asset};
		const Money expectedBalance{123, 45u, Iso4217Codes::USD};
		const AccountCode expectedCode{};
		QVERIFY_EXCEPTION_THROWN(
			Account(expectedType, expectedCode, expectedBalance),
			AccountError
		);
	}

	void constructor_fullInitInvalidAccountCodeNumber_ShouldThrow()
	{
		const AccountTypes expectedType{AccountTypes::Asset};
		const Money expectedBalance{123, 45u, Iso4217Codes::USD};
		const AccountCode expectedCode{AccountNumber{}, "Cash"};
		QVERIFY_EXCEPTION_THROWN(
			Account(expectedType, expectedCode, expectedBalance),
			AccountError
		);
	}

	//}

	//{ operator== Tests

	void operatorEq_SameCode_ShouldBeTrue()
	{
		const AccountTypes expectedType{AccountTypes::Asset};
		const AccountCode expectedCode{AccountNumber{1u}, "Cash"};
		const Money expectedBalance{123, 4500u, Iso4217Codes::USD};
		const Account a0{expectedType, expectedCode, expectedBalance};
		const Account a1{expectedType, expectedCode, expectedBalance};
		QVERIFY_THAT(a0, equals(a1));
	}
	void operatorEq_DifferentCode_ShouldBeFalse()
	{
		const AccountTypes expectedType{AccountTypes::Asset};
		const Money expectedBalance{123, 4500u, Iso4217Codes::USD};
		const Account a0{expectedType, AccountCode{AccountNumber{1u}, "Cash"}, expectedBalance};
		const Account a1{expectedType, AccountCode{AccountNumber{2u}, "Current"}, expectedBalance};
		QVERIFY_THAT(a0, notEquals(a1));
	}

	//}

	//{ operator< Tests

	void operatorThan_CodeLessThan_ShouldBeTrue()
	{
		const AccountTypes expectedType{AccountTypes::Asset};
		const Money expectedBalance{123, 4500u, Iso4217Codes::USD};
		const Account a0{expectedType, AccountCode{AccountNumber{1u}, "Cash"}, expectedBalance};
		const Account a1{expectedType, AccountCode{AccountNumber{2u}, "Current"}, expectedBalance};
		QVERIFY_THAT(a0, lessThan(a1));
	}
	void operatorLessThan_CodeEqual_ShouldBeFalse()
	{
		const AccountTypes expectedType{AccountTypes::Asset};
		const Money expectedBalance{123, 4500u, Iso4217Codes::USD};
		const AccountCode expectedCode{AccountNumber{1u}, "Cash"};
		const Account a0{expectedType, AccountCode{AccountNumber{1u}, "Cash"}, expectedBalance};
		const Account a1{expectedType, AccountCode{AccountNumber{1u}, "Current"}, expectedBalance};
		QVERIFY_THAT(a0, not_(lessThan(a1)));
	}
	void operatorThan_CodeGreaterThan_ShouldBeFalse()
	{
		const AccountTypes expectedType{AccountTypes::Asset};
		const Money expectedBalance{123, 4500u, Iso4217Codes::USD};
		const Account a0{expectedType, AccountCode{AccountNumber{2u}, "Cash"}, expectedBalance};
		const Account a1{expectedType, AccountCode{AccountNumber{1u}, "Current"}, expectedBalance};
		QVERIFY_THAT(a0, not_(lessThan(a1)));
	}

	//}

	//{ setCode Tests

	void setCode_NewCode_ShouldBeNewCode()
	{
		const AccountTypes type{AccountTypes::Asset};
		const AccountCode code{AccountNumber{1u}, "Cash"};
		const Money balance{123, 4500u, Iso4217Codes::USD};
		Account account{type, code, balance};
		const AccountCode newCode{AccountNumber{1u}, "Cash"};
		account.setCode(newCode);
		QVERIFY_THAT(account, memberFunction(&Account::code, equals(newCode)));
	}
	void setCode_AccountClosed_ShouldThrow()
	{
		const AccountTypes type{AccountTypes::Asset};
		const AccountCode code{AccountNumber{1u}, "Cash"};
		const Money balance{123, 4500u, Iso4217Codes::USD};
		Account account{type, code, balance};
		const AccountCode newCode{AccountNumber{1u}, "Cash"};
		account.close();
		QVERIFY_EXCEPTION_THROWN(account.setCode(newCode), AccountError);
	}
	void setCode_AccountEmpty_ShouldThrow()
	{
		const AccountTypes type{AccountTypes::Asset};
		const AccountCode code{AccountNumber{1u}, "Cash"};
		const Money balance{123, 4500u, Iso4217Codes::USD};
		Account account{type, code, balance};
		const AccountCode newCode{};
		QVERIFY_EXCEPTION_THROWN(account.setCode(newCode), AccountError);
	}

	//}

	//{ convertTo Tests

	void convertTo_SameCurrency_ShouldBeSame()
	{
		const AccountTypes type{AccountTypes::Asset};
		const AccountCode code{AccountNumber{1u}, "Cash"};
		const Money balance{123, 4500u, Iso4217Codes::USD};
		Account account{type, code, balance};
		account.convertTo(Iso4217Codes::USD);
		QVERIFY_THAT(account, memberFunction(&Account::balance, equals(balance)));
	}
	void convertTo_NewCurrency_ShouldBeConverted()
	{
		const AccountTypes type{AccountTypes::Asset};
		const AccountCode code{AccountNumber{1u}, "Cash"};
		const Money balance{123, 4500u, Iso4217Codes::USD};
		Account account{type, code, balance};
		account.convertTo(Iso4217Codes::EUR);
		const Money newBalance{106, 1670u, Iso4217Codes::EUR};
		QVERIFY_THAT(account, memberFunction(&Account::balance, equals(newBalance)));
	}
	void convertTo_UnknownCurrency_ShouldThrow()
	{
		const AccountTypes type{AccountTypes::Asset};
		const AccountCode code{AccountNumber{1u}, "Cash"};
		const Money balance{123, 4500u, Iso4217Codes::USD};
		Account account{type, code, balance};
		QVERIFY_EXCEPTION_THROWN(account.convertTo(Iso4217Codes::XXX), AccountError);
	}
	void convertTo_AccountClosed_ShouldThrow()
	{
		const AccountTypes type{AccountTypes::Asset};
		const AccountCode code{AccountNumber{1u}, "Cash"};
		const Money balance{123, 4500u, Iso4217Codes::USD};
		Account account{type, code, balance};
		account.close();
		QVERIFY_EXCEPTION_THROWN(account.convertTo(Iso4217Codes::USD), AccountError);
	}

	//}

	//{ increase Tests

	void increase_ValidParam_ShouldIncrease()
	{
		Account account{AccountTypes::Asset, AccountCode(AccountNumber{1u}), Iso4217Codes::USD};
		const Money expectedBalance{123, 4500u, Iso4217Codes::USD};
		account.increase(expectedBalance, AccountTransactionKey{});
		QVERIFY_THAT(account, memberFunction(&Account::balance, equals(expectedBalance)));
	}
	void increase_AmountLessThanZero_ShouldThrow()
	{
		Account account{AccountTypes::Asset, AccountCode(AccountNumber{1u}), Iso4217Codes::USD};
		const Money amount{-123, 4500u, Iso4217Codes::USD};
		QVERIFY_EXCEPTION_THROWN(account.increase(amount, AccountTransactionKey{}), AccountError);
	}
	void increase_AccountClosed_ShouldThrow()
	{
		Account account{AccountTypes::Asset, AccountCode(AccountNumber{1u}), Iso4217Codes::USD};
		account.close();
		const Money amount{123, 4500u, Iso4217Codes::USD};
		QVERIFY_EXCEPTION_THROWN(account.increase(amount, AccountTransactionKey{}), AccountError);
	}

	//}

	//{ decrease Tests

	void decrease_ValidParam_ShouldIncrease()
	{
		Account account{AccountTypes::Asset, AccountCode(AccountNumber{1u}), Iso4217Codes::USD};
		const Money expectedBalance{123, 4500u, Iso4217Codes::USD};
		account.decrease(expectedBalance, AccountTransactionKey{});
		QVERIFY_THAT(account, memberFunction(&Account::balance, equals(-expectedBalance)));
	}
	void decrease_AmountLessThanZero_ShouldThrow()
	{
		Account account{AccountTypes::Asset, AccountCode(AccountNumber{1u}), Iso4217Codes::USD};
		const Money amount{-123, 4500u, Iso4217Codes::USD};
		QVERIFY_EXCEPTION_THROWN(account.decrease(amount, AccountTransactionKey{}), AccountError);
	}
	void decrease_AccountClosed_ShouldThrow()
	{
		Account account{AccountTypes::Asset, AccountCode(AccountNumber{1u}), Iso4217Codes::USD};
		account.close();
		const Money amount{123, 4500u, Iso4217Codes::USD};
		QVERIFY_EXCEPTION_THROWN(account.decrease(amount, AccountTransactionKey{}), AccountError);
	}

	//}

	//{ close

	void close_isNotClosed_ShouldClose()
	{
		Account account{AccountTypes::Asset, AccountCode(AccountNumber{1u}), Iso4217Codes::USD};
		account.close();
		QVERIFY_TRUE(account.isClosed());
	}

	//}

	//{ setParent Tests

	void setParent_NewParent_ShouldBeNewParent()
	{
		const AccountTypes type{AccountTypes::Asset};
		const AccountCode code{AccountNumber{1u}, "Cash"};
		const Money balance{123, 4500u, Iso4217Codes::USD};
		Account account{type, code, balance};
		const AccountNumber parent{1010u};
		QVERIFY_NO_THROW(account.setParent(parent));
		QVERIFY_TRUE(account.code().parent().hasValue());
		QVERIFY_THAT(*account.code().parent(), equals(parent));
	}
	void setParent_Unparent_ShouldHaveNoParent()
	{
		const AccountTypes type{AccountTypes::Asset};
		const AccountCode code{AccountNumber{1u}, "Cash", AccountNumber{1010u}};
		const Money balance{123, 4500u, Iso4217Codes::USD};
		Account account{type, code, balance};
		QVERIFY_TRUE(account.code().parent().hasValue());
		QVERIFY_NO_THROW(account.setParent({}));
		QVERIFY_FALSE(account.code().parent().hasValue());
	}
	void setParent_AccountClosed_ShouldThrow()
	{
		const AccountTypes type{AccountTypes::Asset};
		const AccountCode code{AccountNumber{1u}, "Cash"};
		const Money balance{123, 4500u, Iso4217Codes::USD};
		Account account{type, code, balance};
		const AccountNumber parent{1010u};
		account.close();
		QVERIFY_EXCEPTION_THROWN(account.setParent(parent), AccountError);
	}

	//}

public:
	AccountUT()
	{
		setUpCurrency(
			[] (const Iso4217Codes& from, const Iso4217Codes& to)
			{
				if (from == to)
					return 1.0;

				if (from == Iso4217Codes::USD && to == Iso4217Codes::EUR)
					return 0.86;

				if (from == Iso4217Codes::EUR && to == Iso4217Codes::USD)
					return 1.17;
				throw logic_error{"Unexpected test code."};
			}
		);
	}

};

}}}

QTEST_MAIN(drn::accounting::tests::AccountUT)
#include "Account-unit-tests.moc"
