/*******************************************************************************
***  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::FloatingPoint;
#include <pecunia/SetUp.h>
using pecunia::converter;
#include <QTest>
using ::QObject;
// using QTEST_MAIN
#include <qt-mocks/Matchers.h>
using qt_mocks::equals;
using qt_mocks::notEquals;
// using QVERIFY_EXCEPTION_THROWN;
// using QVERIFY_THAT;

#include <map>
using std::map;
#include <memory>
using std::make_unique;
#include <set>
using std::set;
#include <stdexcept>
using std::logic_error;
#include <string>
using namespace std::string_literals;
using std::string;

#include <accounting/Account.h>
using drn::accounting::Account;
using drn::accounting::AccountTypes;
#include <accounting/AccountCode.h>
using drn::accounting::AccountCode;
using drn::accounting::AccountNumber;
#include <accounting/AccountNumberGenerator.h>
using drn::accounting::AccountNumberGenerator;
using drn::accounting::SequentialAccountNumberGenerator;
#include <accounting/AccountingErrors.h>
using drn::accounting::AccountError;
#include <accounting/LedgerBooks.h>
using drn::accounting::GeneralLedger;
#include <accounting/Transaction.h>
using drn::accounting::Transaction;
using drn::accounting::TransactionNumber;
using drn::accounting::TransactionStatuses;
#include <banking/Bank.h>
using drn::banking::Bank;
#include <banking/BankAccount.h>
using drn::banking::BankAccount;
#include <banking/BankAccountType.h>
using drn::banking::SupportedAccountTypes;
#include <banking/BankingErrors.h>
using drn::banking::BankError;
#include <banking/BankMap.h>
using drn::banking::BankMap;
#include <banking/BankName.h>
using drn::banking::BankName;
#include <budgeting/Bill.h>
using drn::budgeting::Bill;
#include <budgeting/Budget.h>
using drn::budgeting::Budget;
#include <budgeting/BudgetingErrors.h>
using drn::budgeting::BudgetItemError;
#include <budgeting/BudgetItemIdentifier.h>
using drn::budgeting::BudgetItemIdentifier;
#include <budgeting/BudgetItemTypes.h>
using drn::budgeting::BudgetItemTypes;
#include <budgeting/BudgetSource.h>
using drn::budgeting::BudgetSource;
#include <budgeting/Debt.h>
using drn::budgeting::Debt;
using drn::budgeting::Percentage;
#include <budgeting/EventFrequency.h>
using drn::budgeting::EventFrequency;
#include <budgeting/Goal.h>
using drn::budgeting::Goal;
#include <budgeting/Nontrack.h>
using drn::budgeting::Nontrack;
#include <budgeting/Wage.h>
using drn::budgeting::Wage;
#include <foundation/CalendarTime.hpp>
using drn::foundation::Days;
#include <foundation/Optional.hpp>
using drn::foundation::Optional;
#include <foundation/TypeIndexMap.hpp>
using drn::foundation::TypeIndexMap;
#include "internal/BankLedgers.h"
using drn::navigation::internal::BankLedgers;
#include "internal/BudgetBankLedgers.h"
using drn::navigation::internal::BudgetBankLedgers;
#include "internal/BudgetLedgers.h"
using drn::navigation::internal::BudgetLedgers;
#include <navigation/BudgetBankAccount.h>
using drn::navigation::BudgetBankAccount;


namespace drn
{
namespace navigation
{
namespace internal
{
namespace tests
{

struct BudgetBankLedgersUT : QObject
{
	Q_OBJECT

	const AccountCode wendysAccountCode_;
	const Account wendysAccount_;
	const AccountCode openingAccountCode_;
	const Account openingAccount_;
	const AccountCode burgerKingAccountCode_;
	const Account burgerKingAccount_;
	const BudgetSource wendysSource_;
	const Wage wendys_;
	const Wage updateWendys_;
	const AccountCode newWendysAccountCode_;
	const BudgetSource newWendysSource_;
	const Transaction wendysTxn_;
	const TransactionNumber wendysTxnNum_;
	const Transaction burgerKingTxn_;
	const BankName c1stName_;
	const Bank c1st_;
	const BankName hfName_;
	const Bank hf_;
	const AccountCode vacationAccountCode_;
	const Account vacationAccount_;
	const AccountCode computerAccountCode_;
	const Account computerAccount_;
	const Transaction vacationTxn_;
	SequentialAccountNumberGenerator sang_;

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_empty_ShouldBeDefaults()
	{
		const BudgetBankLedgers bbl{};
		QVERIFY_TRUE(bbl.ledgers().isEmpty());
		QVERIFY_TRUE(bbl.budget().isEmpty());
		QVERIFY_TRUE(bbl.banks().empty());
		QVERIFY_TRUE(bbl.bankLedgers().isEmpty());
		QVERIFY_TRUE(bbl.budgetLedgers().isEmpty());
	}

	void constructor_sourceNameSameAsBankAccountCodeName_ShouldThrow()
	{
		const Account a{
			AccountTypes::Expense,
			AccountCode{AccountNumber{1234u}, "Visa"},
			Iso4217Codes::USD
		};
		GeneralLedger gl{};
		gl.add(a, this->sang_);
		const Debt d{
			BudgetSource{a.code().name()},
			Money{15, 0u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2020, 11, 29},
			Money{1500, 0u, Iso4217Codes::USD},
			Percentage{40}
		};
		Budget b{};
		b.debts_.add(d);
		TypeIndexMap<BudgetSource, AccountCode> budgetCodes{};
		budgetCodes.add<Debt>(d.source(), gl.ledger(a.code()).account_.code());
		QVERIFY_EXCEPTION_THROWN(
			BudgetBankLedgers(
				gl,
				b,
				budgetCodes,
				{},
				{},
				make_unique<SequentialAccountNumberGenerator>()
			),
			AccountError
		);
	}

	//}

	//{ add Budget item

	void addWage_validParam_ShouldBeAdded()
	{
		BudgetBankLedgers bbl{};
		AccountCode created{};
		QVERIFY_NO_THROW(created = bbl.add(this->wendys_));
		QVERIFY_THAT(
			bbl.budgetLedgers().budget().wages_.at(this->wendysSource_),
			equals(this->wendys_)
		);
		QVERIFY_THAT(created, equals(this->wendysAccount_.code()));
		QVERIFY_THAT(
			bbl.budgetLedgers().ledgers().ledger(this->wendysAccountCode_).account_.type(),
			equals(this->wendysAccount_.type())
		);
		QVERIFY_THAT(
			bbl.budgetLedgers().ledgers().ledger(this->wendysAccountCode_).account_.code(),
			equals(this->wendysAccount_.code())
		);
		QVERIFY_THAT(
			bbl.budgetLedgers().ledgers().ledger(this->wendysAccountCode_).account_.balance(),
			equals(Money(0, 0u, this->wendys_.amount().code()))
		);
	}

	void addBudgetItem_sourceNameSameAsBankAccountCodeName_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		const Account a{
			AccountTypes::Expense,
			AccountCode{AccountNumber{1234u}, "Electricity"},
			Iso4217Codes::USD
		};
		QVERIFY_NO_THROW(bbl.add(a));
		const Bill b{
			BudgetSource{a.code().name()},
			Money{1500, 0u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2020, 11, 29}
		};
		QVERIFY_EXCEPTION_THROWN(bbl.add(b), BudgetItemError);
	}

	void add_debtBankAccountUsage_shouldAddBankAssociation()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		const auto sat{SupportedAccountTypes::Loan};
		bbl.add(Bank{bn});
		const Debt debt{
			BudgetSource{"Visa"},
			Money{5, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{2500, 2500u, Iso4217Codes::USD},
			Percentage{20.0}
		};
		QVERIFY_NO_THROW(bbl.add(debt, BankAccount{bn, {{}, sat}}));
		const auto& an{bbl.budgetLedgers().budgetAccountCode<Debt>(debt.source()).number()};
		const auto ledger{bbl.ledgers().ledger(an)};
		QVERIFY_TRUE(ledger.account_.code().name().startsWith(debt.source().name()));
		QVERIFY_FALSE(ledger.account_.code().parent().hasValue());
		const auto bankAccounts{bbl.banks().bank(bn).accounts()};
		QVERIFY_THAT(bankAccounts.find(an), notEquals(bankAccounts.cend()));
	}

	void add_debtBankMissing_shouldThrow()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		const auto sat{SupportedAccountTypes::Loan};
		const Debt debt{
			BudgetSource{"Visa"},
			Money{5, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{2500, 2500u, Iso4217Codes::USD},
			Percentage{20.0}
		};
		QVERIFY_EXCEPTION_THROWN(bbl.add(debt, BankAccount{bn, {{}, sat}}), BudgetItemError);
		QVERIFY_FALSE(bbl.budget().debts_.has(debt.source()));
		QVERIFY_THAT(*bbl.accountCodeNumberGenerator(), equals(AccountNumber{1u}));
	}

	void add_goalBankAccountUsage_shouldAddBankAssociation()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		bbl.add(Bank{bn});
		const auto sat{SupportedAccountTypes::Savings};
		const auto savings{
			bbl.addAccount("Savings", sat, Money{Iso4217Codes::USD})
		};
		const Goal goal{
			BudgetSource{"Vacation"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{1500, 2500u, Iso4217Codes::USD}
		};
		AccountCode created{};
		QVERIFY_NO_THROW(created = bbl.add(goal, BankAccount{bn, {savings, sat}}));
		const auto& an{bbl.budgetLedgers().budgetAccountCode<Goal>(goal.source()).number()};
		const auto ledger{bbl.ledgers().ledger(an)};
		QVERIFY_THAT(created, equals(ledger.account_.code()));
		QVERIFY_TRUE(ledger.account_.code().name().startsWith(goal.source().name()));
		QVERIFY_TRUE(ledger.account_.code().parent().hasValue());
		QVERIFY_THAT(*ledger.account_.code().parent(), equals(savings.number()));
		const auto bankAccounts{bbl.banks().bank(bn).accounts()};
		QVERIFY_THAT(bankAccounts.find(an), notEquals(bankAccounts.cend()));
	}

	void add_goalBankMissing_shouldThrow()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		const auto sat{SupportedAccountTypes::Savings};
		const auto savings{
			bbl.addAccount("Savings", sat, Money{Iso4217Codes::USD})
		};
		const Goal goal{
			BudgetSource{"Vacation"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{1500, 2500u, Iso4217Codes::USD}
		};
		QVERIFY_EXCEPTION_THROWN(
			bbl.add(goal, BankAccount{bn, {savings, sat}}),
			BudgetItemError
		);
		QVERIFY_FALSE(bbl.budget().goals_.has(goal.source()));
		QVERIFY_THAT(*bbl.accountCodeNumberGenerator(), equals(AccountNumber{1u}));
	}

	void add_billBankAccountUsage_shouldThrow()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		bbl.add(Bank{bn});
		const auto sat{SupportedAccountTypes::Chequeing};
		const auto chequeing{
			bbl.addAccount("Chequeing", sat, Money{Iso4217Codes::USD})
		};
		const Bill bill{
			BudgetSource{"Xcel"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2}
		};
		QVERIFY_EXCEPTION_THROWN(
			bbl.add(bill, BankAccount{bn, {{}, sat}}),
			BudgetItemError
		);
		QVERIFY_FALSE(bbl.budget().bills_.has(bill.source()));
		QVERIFY_THAT(*bbl.accountCodeNumberGenerator(), equals(AccountNumber{1u}));
	}

	void add_nontrackBankAccountUsage_shouldThrow()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		bbl.add(Bank{bn});
		const auto sat{SupportedAccountTypes::Chequeing};
		const auto chequeing{
			bbl.addAccount("Chequeing", sat, Money{Iso4217Codes::USD})
		};
		const Nontrack nontrack{
			BudgetSource{"Discretionary Spending"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2}
		};
		QVERIFY_EXCEPTION_THROWN(
			bbl.add(nontrack, BankAccount{bn, {{}, sat}}),
			BudgetItemError
		);
		QVERIFY_FALSE(bbl.budget().nontracks_.has(nontrack.source()));
		QVERIFY_THAT(*bbl.accountCodeNumberGenerator(), equals(AccountNumber{1u}));
	}

	void add_wageBankAccountUsage_shouldThrow()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		bbl.add(Bank{bn});
		const auto sat{SupportedAccountTypes::Chequeing};
		const auto chequeing{
			bbl.addAccount("Chequeing", sat, Money{Iso4217Codes::USD})
		};
		const Wage wage{
			BudgetSource{"Wendy's"},
			Money{5, 1500u, Iso4217Codes::USD},
			EventFrequency::Hourly,
			{2031, 8, 2}
		};
		QVERIFY_EXCEPTION_THROWN(
			bbl.add(wage, BankAccount{bn, {{}, sat}}),
			BudgetItemError
		);
		QVERIFY_FALSE(bbl.budget().wages_.has(wage.source()));
		QVERIFY_THAT(*bbl.accountCodeNumberGenerator(), equals(AccountNumber{1u}));
	}

	//}

	//{ update Budget item

	void updateWage_validParam_ShouldBeUpdated()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->wendys_);
		QVERIFY_NO_THROW(bbl.update<Wage>(this->updateWendys_));
		QVERIFY_THAT(
			bbl.budgetLedgers().budget().wages_.at(this->wendysSource_),
			equals(this->updateWendys_)
		);
		QVERIFY_THAT(
			bbl.budgetLedgers().ledgers().ledger(this->wendysAccountCode_)
				.account_.balance().code(),
			equals(Iso4217Codes::PLN)
		);
	}

	//}

	//{ remove Budget Item

	void removeWage_validParam_ShouldBeRemoved()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->wendys_);
		QVERIFY_NO_THROW(bbl.removeWage(this->wendysSource_));
		QVERIFY_FALSE(bbl.budgetLedgers().budget().wages_.has(this->wendysSource_));
		QVERIFY_FALSE(bbl.budgetLedgers().ledgers().hasLedger(this->wendysAccountCode_));
		QVERIFY_FALSE(bbl.budgetLedgers().budgetAccountCodes().has<Wage>(this->wendysSource_));
	}

	//}

	//{ rename Budget item

	void renameBill_validParam_ShouldBeRenamed()
	{
		BudgetBankLedgers bbl{};
		const BudgetSource source{"Electricity"};
		const Bill b{
			source,
			Money{1500, 0u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2020, 11, 29}
		};
		bbl.add(b);
		QVERIFY_NO_THROW(bbl.renameBill(source, this->newWendysSource_));
		QVERIFY_FALSE(bbl.budgetLedgers().budget().bills_.has(this->wendysSource_));
		QVERIFY_TRUE(bbl.budgetLedgers().budget().bills_.has(this->newWendysSource_));
		QVERIFY_FALSE(bbl.budgetLedgers().budgetAccountCodes().has<Bill>(this->wendysSource_));
		QVERIFY_TRUE(bbl.budgetLedgers().budgetAccountCodes().has<Bill>(this->newWendysSource_));
		QVERIFY_THAT(
			bbl.budgetLedgers().budgetAccountCodes().value<Bill>(this->newWendysSource_).name(),
			equals(this->newWendysSource_.name() + ' ' + BudgetLedgers::budgetItemAccountSuffix_)
		);
		QVERIFY_THAT(
			bbl.budgetLedgers().ledgers().ledger(this->wendysAccountCode_).account_.code().name(),
			equals(this->newWendysSource_.name() + ' ' + BudgetLedgers::budgetItemAccountSuffix_)
		);
	}

	void renameBill_sourceNameSameAsBankAccountCodeName_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		const Bill b{
			BudgetSource{"Electricity"},
			Money{1500, 0u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2020, 11, 29}
		};
		QVERIFY_NO_THROW(bbl.add(b));
		const Account badNameAccount{
			AccountTypes::Expense,
			AccountCode{AccountNumber{1234u}, "Elec"},
			Iso4217Codes::USD
		};
		QVERIFY_NO_THROW(bbl.add(badNameAccount));
		QVERIFY_EXCEPTION_THROWN(
			bbl.renameBill(b.source(), BudgetSource{badNameAccount.code().name()}),
			BudgetItemError
		);
	}

	void renameDebt_validParam_ShouldBeRenamed()
	{
		BudgetBankLedgers bbl{};
		const BudgetSource source{"Visa Card"};
		const Debt d{
			source,
			Money{15, 0u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2020, 11, 29},
			Money{1500, 0u, Iso4217Codes::USD},
			Percentage{40}
		};
		bbl.add(d);
		QVERIFY_NO_THROW(bbl.renameDebt(source, this->newWendysSource_));
		QVERIFY_FALSE(bbl.budgetLedgers().budget().debts_.has(this->wendysSource_));
		QVERIFY_TRUE(bbl.budgetLedgers().budget().debts_.has(this->newWendysSource_));
		QVERIFY_FALSE(bbl.budgetLedgers().budgetAccountCodes().has<Debt>(this->wendysSource_));
		QVERIFY_TRUE(bbl.budgetLedgers().budgetAccountCodes().has<Debt>(this->newWendysSource_));
		QVERIFY_THAT(
			bbl.budgetLedgers().budgetAccountCodes().value<Debt>(this->newWendysSource_).name(),
			equals(this->newWendysSource_.name() + ' ' + BudgetLedgers::budgetItemAccountSuffix_)
		);
		QVERIFY_THAT(
			bbl.budgetLedgers().ledgers().ledger(this->wendysAccountCode_).account_.code().name(),
			equals(this->newWendysSource_.name() + ' ' + BudgetLedgers::budgetItemAccountSuffix_)
		);
	}

	void renameDebt_sourceNameSameAsBankAccountCodeName_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		const Debt d{
			BudgetSource{"Visa Card"},
			Money{15, 0u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2020, 11, 29},
			Money{1500, 0u, Iso4217Codes::USD},
			Percentage{40}
		};
		QVERIFY_NO_THROW(bbl.add(d));
		const Account badNameAccount{
			AccountTypes::Expense,
			AccountCode{AccountNumber{1234u}, "Visa"},
			Iso4217Codes::USD
		};
		QVERIFY_NO_THROW(bbl.add(badNameAccount));
		QVERIFY_EXCEPTION_THROWN(
			bbl.renameDebt(d.source(), BudgetSource{badNameAccount.code().name()}),
			BudgetItemError
		);
	}

	void renameGoal_validParam_ShouldBeRenamed()
	{
		BudgetBankLedgers bbl{};
		const BudgetSource source{"Holiday"};
		const Goal g{
			source,
			Money{15, 0u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2020, 11, 29},
			Money{1500, 0u, Iso4217Codes::USD}
		};
		bbl.add(g);
		QVERIFY_NO_THROW(bbl.renameGoal(source, this->newWendysSource_));
		QVERIFY_FALSE(bbl.budgetLedgers().budget().goals_.has(this->wendysSource_));
		QVERIFY_TRUE(bbl.budgetLedgers().budget().goals_.has(this->newWendysSource_));
		QVERIFY_FALSE(bbl.budgetLedgers().budgetAccountCodes().has<Goal>(this->wendysSource_));
		QVERIFY_TRUE(bbl.budgetLedgers().budgetAccountCodes().has<Goal>(this->newWendysSource_));
		QVERIFY_THAT(
			bbl.budgetLedgers().budgetAccountCodes().value<Goal>(this->newWendysSource_).name(),
			equals(this->newWendysSource_.name() + ' ' + BudgetLedgers::budgetItemAccountSuffix_)
		);
		QVERIFY_THAT(
			bbl.budgetLedgers().ledgers().ledger(this->wendysAccountCode_).account_.code().name(),
			equals(this->newWendysSource_.name() + ' ' + BudgetLedgers::budgetItemAccountSuffix_)
		);
	}

	void renameGoal_sourceNameSameAsBankAccountCodeName_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		const Goal g{
			BudgetSource{"Holiday"},
			Money{15, 0u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2020, 11, 29},
			Money{1500, 0u, Iso4217Codes::USD}
		};
		QVERIFY_NO_THROW(bbl.add(g));
		const Account badNameAccount{
			AccountTypes::Expense,
			AccountCode{AccountNumber{1234u}, "Vacation"},
			Iso4217Codes::USD
		};
		QVERIFY_NO_THROW(bbl.add(badNameAccount));
		QVERIFY_EXCEPTION_THROWN(
			bbl.renameGoal(g.source(), BudgetSource{badNameAccount.code().name()}),
			BudgetItemError
		);
	}

	void renameNontrack_validParam_ShouldBeRenamed()
	{
		BudgetBankLedgers bbl{};
		const BudgetSource source{"Pop & Candy"};
		const Nontrack n{
			source,
			Money{1500, 0u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2020, 11, 29}
		};
		bbl.add(n);
		QVERIFY_NO_THROW(bbl.renameNontrack(source, this->newWendysSource_));
		QVERIFY_FALSE(bbl.budgetLedgers().budget().nontracks_.has(this->wendysSource_));
		QVERIFY_TRUE(bbl.budgetLedgers().budget().nontracks_.has(this->newWendysSource_));
		QVERIFY_FALSE(bbl.budgetLedgers().budgetAccountCodes().has<Nontrack>(this->wendysSource_));
		QVERIFY_TRUE(bbl.budgetLedgers().budgetAccountCodes().has<Nontrack>(this->newWendysSource_));
		QVERIFY_THAT(
			bbl.budgetLedgers().budgetAccountCodes().value<Nontrack>(this->newWendysSource_).name(),
			equals(this->newWendysSource_.name() + ' ' + BudgetLedgers::budgetItemAccountSuffix_)
		);
		QVERIFY_THAT(
			bbl.budgetLedgers().ledgers().ledger(this->wendysAccountCode_).account_.code().name(),
			equals(this->newWendysSource_.name() + ' ' + BudgetLedgers::budgetItemAccountSuffix_)
		);
	}

	void renameNontrack_sourceNameSameAsBankAccountCodeName_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		const Nontrack n{
			BudgetSource{"Pop & Candy"},
			Money{1500, 0u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2020, 11, 29}
		};
		QVERIFY_NO_THROW(bbl.add(n));
		const Account badNameAccount{
			AccountTypes::Expense,
			AccountCode{AccountNumber{1234u}, "Pop"},
			Iso4217Codes::USD
		};
		QVERIFY_NO_THROW(bbl.add(badNameAccount));
		QVERIFY_EXCEPTION_THROWN(
			bbl.renameNontrack(n.source(), BudgetSource{badNameAccount.code().name()}),
			BudgetItemError
		);
	}

	void renameWage_validParam_ShouldBeRenamed()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->wendys_);
		QVERIFY_NO_THROW(bbl.renameWage(this->wendysSource_, this->newWendysSource_));
		QVERIFY_FALSE(bbl.budgetLedgers().budget().wages_.has(this->wendysSource_));
		QVERIFY_TRUE(bbl.budgetLedgers().budget().wages_.has(this->newWendysSource_));
		QVERIFY_FALSE(bbl.budgetLedgers().budgetAccountCodes().has<Wage>(this->wendysSource_));
		QVERIFY_TRUE(bbl.budgetLedgers().budgetAccountCodes().has<Wage>(this->newWendysSource_));
		QVERIFY_THAT(
			bbl.budgetLedgers().budgetAccountCodes().value<Wage>(this->newWendysSource_).name(),
			equals(this->newWendysSource_.name() + ' ' + BudgetLedgers::budgetItemAccountSuffix_)
		);
		QVERIFY_THAT(
			bbl.budgetLedgers().ledgers().ledger(this->wendysAccountCode_).account_.code().name(),
			equals(this->newWendysSource_.name() + ' ' + BudgetLedgers::budgetItemAccountSuffix_)
		);
	}

	void renameWage_sourceNameSameAsBankAccountCodeName_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		const Wage w{
			BudgetSource{"Wendy's"},
			Money{1500, 0u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2020, 11, 29}
		};
		QVERIFY_NO_THROW(bbl.add(w));
		const Account badNameAccount{
			AccountTypes::Expense,
			AccountCode{AccountNumber{1234u}, "Job"},
			Iso4217Codes::USD
		};
		QVERIFY_NO_THROW(bbl.add(badNameAccount));
		QVERIFY_EXCEPTION_THROWN(
			bbl.renameWage(w.source(), BudgetSource{badNameAccount.code().name()}),
			BudgetItemError
		);
	}

	//}

	//{ add Account Tests

	void add_acountValidParams_ShouldBeAdded()
	{
		BudgetBankLedgers bbl{};
		QVERIFY_NO_THROW(bbl.add(this->wendysAccount_));
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->wendysAccountCode_));
		QVERIFY_THAT(
			bbl.ledgers().ledger(this->wendysAccountCode_).account_,
			equals(this->wendysAccount_)
		);
	}

	void add_acountCodeNameSameAsBudgetSourceName_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		const Debt d{
			BudgetSource{"Visa"},
			Money{15, 0u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2020, 11, 29},
			Money{1500, 0u, Iso4217Codes::USD},
			Percentage{40}
		};
		QVERIFY_NO_THROW(bbl.add(d));
		const Account a{
			AccountTypes::Expense,
			AccountCode{AccountNumber{1234u}, d.source().name()},
			Iso4217Codes::USD
		};
		QVERIFY_EXCEPTION_THROWN(bbl.add(a), AccountError);
	}

	//}

	//{ close Tests

	void close_validParams_ShouldBeClosed()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->wendysAccount_);
		bbl.close(this->wendysAccountCode_);
		QVERIFY_TRUE(bbl.ledgers().ledger(this->wendysAccountCode_).account_.isClosed());
	}

	//}

	//{ open Tests

	void open_validParams_ShouldBeOpened()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->wendysAccount_);
		bbl.close(this->wendysAccountCode_);
		QVERIFY_TRUE(bbl.ledgers().ledger(this->wendysAccountCode_).account_.isClosed());
		bbl.open(this->wendysAccountCode_);
		QVERIFY_FALSE(bbl.ledgers().ledger(this->wendysAccountCode_).account_.isClosed());
	}

	//}

	//{ remove Account Tests

	void remove_validParamsHasNoBankAccount_ShouldBeRemoved()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->wendys_);
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->wendysAccountCode_));
		QVERIFY_NO_THROW(bbl.remove(this->wendysAccountCode_));
		QVERIFY_FALSE(bbl.ledgers().hasLedger(this->wendysAccountCode_));
		QVERIFY_FALSE(bbl.budgetLedgers().budgetAccountCodes().has<Wage>(this->wendys_.source()));
		QVERIFY_FALSE(bbl.budget().wages_.has(this->wendys_.source()));
	}

	void remove_validParamsHasBankAccount_ShouldBeRemoved()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->wendys_);
		bbl.add(this->c1st_);
		bbl.addAssociation(this->c1stName_, this->wendysAccountCode_.number(), SupportedAccountTypes::Savings);
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->wendysAccountCode_));
		QVERIFY_THAT(
			bbl.banks().bank(this->c1stName_).accounts().count(this->wendysAccountCode_.number()),
			equals(size_t(1))
		);
		QVERIFY_NO_THROW(bbl.remove(this->wendysAccountCode_));
		QVERIFY_FALSE(bbl.ledgers().hasLedger(this->wendysAccountCode_));
		QVERIFY_FALSE(bbl.budgetLedgers().budgetAccountCodes().has<Wage>(this->wendys_.source()));
		QVERIFY_FALSE(bbl.budget().wages_.has(this->wendys_.source()));
		QVERIFY_TRUE(bbl.banks().bank(this->c1stName_).accounts().empty());
	}

	//}

	//{ recode Tests

	void recode_validParamsHasNoBankAssociatedAccount_ShouldBeRecoded()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->wendys_);
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->wendysAccountCode_));
		QVERIFY_NO_THROW(bbl.recode(this->wendysAccountCode_, this->newWendysAccountCode_));
		QVERIFY_FALSE(bbl.ledgers().hasLedger(this->wendysAccountCode_));
		QVERIFY_FALSE(bbl.budgetLedgers().budgetAccountCodes().has<Wage>(this->wendys_.source()));
		QVERIFY_FALSE(bbl.budget().wages_.has(this->wendys_.source()));
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->newWendysAccountCode_));
		QVERIFY_TRUE(bbl.budgetLedgers().budgetAccountCodes().has<Wage>(this->newWendysSource_));
		QVERIFY_THAT(
			bbl.budgetLedgers().budgetAccountCodes().value<Wage>(this->newWendysSource_),
			equals(this->newWendysAccountCode_)
		);
		QVERIFY_TRUE(bbl.budget().wages_.has(this->newWendysSource_));
	}

	void recode_validParamsHasBankAssociatedAccount_ShouldBeRecoded()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->wendys_);
		bbl.add(this->c1st_);
		bbl.addAssociation(this->c1stName_, this->wendysAccountCode_.number(), SupportedAccountTypes::Savings);
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->wendysAccountCode_));
		QVERIFY_NO_THROW(bbl.recode(this->wendysAccountCode_, this->newWendysAccountCode_));
		QVERIFY_FALSE(bbl.ledgers().hasLedger(this->wendysAccountCode_));
		QVERIFY_FALSE(bbl.budgetLedgers().budgetAccountCodes().has<Wage>(this->wendys_.source()));
		QVERIFY_FALSE(bbl.budget().wages_.has(this->wendys_.source()));
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->newWendysAccountCode_));
		QVERIFY_TRUE(bbl.budgetLedgers().budgetAccountCodes().has<Wage>(this->newWendysSource_));
		QVERIFY_THAT(
			bbl.budgetLedgers().budgetAccountCodes().value<Wage>(this->newWendysSource_),
			equals(this->newWendysAccountCode_)
		);
		QVERIFY_TRUE(bbl.budget().wages_.has(this->newWendysSource_));
		QVERIFY_THAT(
			bbl.banks().bank(this->c1stName_).accounts().count(this->wendysAccountCode_.number()),
			equals(size_t(0))
		);
		QVERIFY_THAT(
			bbl.banks().bank(this->c1stName_).accounts().count(this->newWendysAccountCode_.number()),
			equals(size_t(1))
		);
		QVERIFY_THAT(
			bbl.bankLedgers().bankAccountTypes().count(this->c1stName_),
			equals(size_t(1))
		);
		QVERIFY_THAT(
			bbl.bankLedgers().bankAccountTypes().at(this->c1stName_)
				.count(this->newWendysAccountCode_.number()),
			equals(size_t(1))
		);
		QVERIFY_THAT(
			bbl.bankLedgers().bankAccountTypes().at(this->c1stName_)
				.at(this->newWendysAccountCode_.number()),
			equals(SupportedAccountTypes::Savings)
		);
	}

	void recode_missingAccount_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		QVERIFY_EXCEPTION_THROWN(
			bbl.recode(this->wendysAccountCode_, this->newWendysAccountCode_),
			AccountError
		);
		QVERIFY_FALSE(bbl.ledgers().hasLedger(this->wendysAccountCode_));
		QVERIFY_FALSE(bbl.ledgers().hasLedger(this->newWendysAccountCode_));
	}

	void recode_accountCodeNameSameAsBudgetSourceName_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		const Debt d{
			BudgetSource{"Visa"},
			Money{15, 0u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2020, 11, 29},
			Money{1500, 0u, Iso4217Codes::USD},
			Percentage{40}
		};
		QVERIFY_NO_THROW(bbl.add(d));
		const Account a{
			AccountTypes::Expense,
			AccountCode{AccountNumber{1234u}, "Vsia"},
			Iso4217Codes::USD
		};
		QVERIFY_NO_THROW(bbl.add(a));
		QVERIFY_EXCEPTION_THROWN(
			bbl.recode(a.code(), AccountCode{AccountNumber{1234u}, d.source().name()}),
			AccountError
		);
	}

	//}

	//{ post Tests

	void post_validParamsRemoveBudgetAccount_ShouldBePosted()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->wendysAccount_);
		bbl.add(this->burgerKingAccount_);
		const auto txnNum{bbl.post(this->wendysTxn_)};
		QVERIFY_THAT(txnNum, equals(this->wendysTxnNum_));
		QVERIFY_THAT(
			bbl.ledgers().ledger(this->wendysAccountCode_).transactions_
				.at(this->wendysTxnNum_).crediting(),
			equals(this->wendysTxn_.crediting())
		);
		QVERIFY_THAT(
			bbl.ledgers().ledger(this->wendysAccountCode_).transactions_
				.at(this->wendysTxnNum_).debiting(),
			equals(this->wendysTxn_.debiting())
		);
	}

	//}

	//{ repost Tests

	void repost_validParamsRemoveBudgetAccount_ShouldBeReposted()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->wendysAccount_);
		bbl.add(this->burgerKingAccount_);
		bbl.post(this->wendysTxn_);
		QVERIFY_NO_THROW(bbl.repost(this->wendysTxnNum_, this->burgerKingTxn_));
		QVERIFY_THAT(
			bbl.ledgers().ledger(this->wendysAccountCode_).transactions_
				.at(this->wendysTxnNum_).crediting(),
			equals(this->burgerKingTxn_.crediting())
		);
		QVERIFY_THAT(
			bbl.ledgers().ledger(this->wendysAccountCode_).transactions_
				.at(this->wendysTxnNum_).debiting(),
			equals(this->burgerKingTxn_.debiting())
		);
	}

	//}

	//{ clear Tests

	void clear_validParamsRemoveBudgetAccount_ShouldBeCleared()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->wendysAccount_);
		bbl.add(this->burgerKingAccount_);
		bbl.post(this->wendysTxn_);
		QVERIFY_NO_THROW(bbl.clear(this->wendysTxnNum_));
		QVERIFY_THAT(
			bbl.ledgers().ledger(this->wendysAccountCode_).transactions_
				.at(this->wendysTxnNum_).status(),
			equals(TransactionStatuses::Cleared)
		);
	}

	//}

	//{ reconcile Tests

	void reconcile_validParamsRemoveBudgetAccount_ShouldBeReconcileed()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->wendysAccount_);
		bbl.add(this->burgerKingAccount_);
		bbl.post(this->wendysTxn_);
		QVERIFY_NO_THROW(bbl.reconcile(this->wendysTxnNum_));
		QVERIFY_THAT(
			bbl.ledgers().ledger(this->wendysAccountCode_).transactions_
				.at(this->wendysTxnNum_).status(),
			equals(TransactionStatuses::Reconciled)
		);
	}

	//}

	//{ remove Transaction Tests

	void remove_validParams_ShouldBeRemoved()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->wendysAccount_);
		bbl.add(this->burgerKingAccount_);
		bbl.post(this->wendysTxn_);
		QVERIFY_NO_THROW(bbl.remove(this->wendysTxnNum_));
		QVERIFY_THAT(
			bbl.ledgers().ledger(this->wendysAccountCode_).transactions_
				.count(this->wendysTxnNum_),
			equals(size_t(0))
		);
	}

	//}

	//{ add Bank Tests

	void add_bankValidParamsNoAccounts_ShouldBeAdded()
	{
		BudgetBankLedgers bbl{};
		QVERIFY_NO_THROW(bbl.add(this->c1st_));
		QVERIFY_TRUE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_THAT(bbl.banks().bank(this->c1stName_), equals(this->c1st_));
	}

	void add_bankValidParamsHasAccounts_ShouldBeAdded()
	{
		BudgetBankLedgers bbl{};
		bbl.addAccount(
			this->vacationAccountCode_.name(),
			SupportedAccountTypes::Savings,
			Money{Iso4217Codes::USD}
		);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		QVERIFY_NO_THROW(
			bbl.add(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}})
		);
		QVERIFY_TRUE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_THAT(bbl.banks().bank(this->c1stName_), equals(c1st));
		const auto& bankAccountTypes{bbl.bankLedgers().bankAccountTypes()};
		QVERIFY_THAT(bankAccountTypes.count(this->c1stName_), equals(static_cast<size_t>(1)));
		const auto& accountCodeBankType{bankAccountTypes.at(this->c1stName_)};
		QVERIFY_THAT(
			accountCodeBankType.count(this->vacationAccountCode_.number()),
			equals(static_cast<size_t>(1))
		);
		QVERIFY_THAT(
			accountCodeBankType.at(this->vacationAccountCode_.number()),
			equals(SupportedAccountTypes::Savings)
		);
	}

	void add_bankBankAccountTypeUnknown_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.addAccount(
			this->vacationAccountCode_.name(),
			SupportedAccountTypes::Savings,
			Money{Iso4217Codes::USD}
		);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		QVERIFY_EXCEPTION_THROWN(
			bbl.add(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Unknown}}),
			BankError
		);
		QVERIFY_FALSE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_TRUE(bbl.bankLedgers().bankAccountTypes().empty());
	}

	void add_bankAccountTypeInvalidForBankAccountType_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->vacationAccount_);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		QVERIFY_EXCEPTION_THROWN(
			bbl.add(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::CreditCard}}),
			BankError
		);
		QVERIFY_FALSE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_TRUE(bbl.bankLedgers().bankAccountTypes().empty());
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->vacationAccountCode_));
	}

	void add_isClosed_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		auto c1st{this->c1st_};
		c1st.close();
		QVERIFY_EXCEPTION_THROWN(bbl.add(c1st), BankError);
	}

	void add_objectIsClosed_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		const Bank c1st{this->c1stName_, {}, true};
		QVERIFY_EXCEPTION_THROWN(bbl.add(c1st), BankError);
	}

	//}

	//{ add Bank Account Tests

	void addAssociation_validParams_ShouldBeAdded()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		bbl.add(Account{AccountTypes::Asset, this->wendysAccountCode_, Money{0, 0u, Iso4217Codes::USD}});
		QVERIFY_NO_THROW(
			bbl.addAssociation(this->c1stName_, this->wendysAccountCode_.number(), SupportedAccountTypes::Savings)
		);
		QVERIFY_THAT(
			bbl.banks().bank(this->c1stName_).accounts().count(this->wendysAccountCode_.number()),
			equals(size_t(1))
		);
		QVERIFY_THAT(
			bbl.bankLedgers().bankAccountTypes().count(this->c1stName_),
			equals(size_t(1))
		);
		const auto accountCodesBankTypes{bbl.bankLedgers().bankAccountTypes().at(this->c1stName_)};
		QVERIFY_THAT(
			accountCodesBankTypes.count(this->wendysAccountCode_.number()),
			equals(size_t(1))
		);
		QVERIFY_THAT(
			accountCodesBankTypes.at(this->wendysAccountCode_.number()),
			equals(SupportedAccountTypes::Savings)
		);
	}

	void addAssociation_missingBank_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		QVERIFY_EXCEPTION_THROWN(
			bbl.addAssociation(
				this->c1stName_,
				this->wendysAccountCode_.number(),
				SupportedAccountTypes::Savings
			),
			BankError
		);
	}

	void addAssociation_accountDoesNotExist_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		QVERIFY_EXCEPTION_THROWN(
			bbl.addAssociation(
				this->c1stName_,
				this->wendysAccountCode_.number(),
				SupportedAccountTypes::Savings
			),
			BankError
		);
	}

	void addAssociation_isClosed_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		bbl.close(this->c1stName_);
		QVERIFY_EXCEPTION_THROWN(
			bbl.addAssociation(
				this->c1stName_,
				this->wendysAccountCode_.number(),
				SupportedAccountTypes::Savings
			),
			BankError
		);
	}

	void addAssociation_associatedWithAnotherBank_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		bbl.add(this->hf_);
		bbl.add(Account{AccountTypes::Asset, this->wendysAccountCode_, Money{0, 0u, Iso4217Codes::USD}});
		bbl.addAssociation(this->c1stName_, this->wendysAccountCode_.number(), SupportedAccountTypes::Savings);
		QVERIFY_EXCEPTION_THROWN(
			bbl.addAssociation(this->hfName_, this->wendysAccountCode_.number(), SupportedAccountTypes::Savings),
			BankError
		);
	}

	void addAssociation_bankAccountTypeUnknown_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		bbl.add(Account{AccountTypes::Asset, this->wendysAccountCode_, Money{0, 0u, Iso4217Codes::USD}});
		QVERIFY_EXCEPTION_THROWN(
			bbl.addAssociation(
				this->c1stName_,
				this->wendysAccountCode_.number(),
				SupportedAccountTypes::Unknown
			),
			BankError
		);
	}

	//}

	//{ Add Account Tests

	void addAccount_validParamsWithOpeningBalance_ShouldBeAdded()
	{
		BudgetBankLedgers bbl{};
		const QString name{"Vacation Savings"};
		const Money balance{500, 2500u, Iso4217Codes::USD};
		const auto ac{bbl.addAccount(name, SupportedAccountTypes::Savings, balance)};
		QVERIFY_TRUE(bbl.ledgers().hasLedger(ac));
		QVERIFY_THAT(*bbl.accountCodeNumberGenerator(), equals(AccountNumber{2}));
		QVERIFY_THAT(bbl.ledgers().ledger(ac).account_.code().name(), equals(name));
		QVERIFY_THAT(bbl.ledgers().ledger(ac).account_.type(), equals(AccountTypes::Asset));
		QVERIFY_THAT(bbl.ledgers().ledger(ac).account_.balance(), equals(balance));
		QVERIFY_TRUE(bbl.ledgers().ledger(ac).account_.balance().hasAmount());
	}

	void addAccount_validParamsWithOutOpeningBalance_ShouldBeAdded()
	{
		BudgetBankLedgers bbl{};
		const QString name{"Vacation Savings"};
		const Money balance{Iso4217Codes::USD};
		const auto ac{bbl.addAccount(name, SupportedAccountTypes::Savings, balance)};
		QVERIFY_TRUE(bbl.ledgers().hasLedger(ac));
		QVERIFY_THAT(*bbl.accountCodeNumberGenerator(), equals(AccountNumber{1}));
		QVERIFY_THAT(bbl.ledgers().ledger(ac).account_.code().name(), equals(name));
		QVERIFY_THAT(bbl.ledgers().ledger(ac).account_.type(), equals(AccountTypes::Asset));
		QVERIFY_THAT(bbl.ledgers().ledger(ac).account_.balance(), equals(balance));
		QVERIFY_FALSE(bbl.ledgers().ledger(ac).account_.balance().hasAmount());
	}

	void addAccount_emptyName_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		QVERIFY_EXCEPTION_THROWN(
			bbl.addAccount("", SupportedAccountTypes::Savings, Money{Iso4217Codes::USD}),
			AccountError
		);
		QVERIFY_THAT(*bbl.accountCodeNumberGenerator(), equals(AccountNumber{}));
	}

	void addAccount_unknownBankAccountType_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		QVERIFY_EXCEPTION_THROWN(
			bbl.addAccount("Vacation Savings", SupportedAccountTypes::Unknown, Money{Iso4217Codes::USD}),
			AccountError
		);
		QVERIFY_THAT(*bbl.accountCodeNumberGenerator(), equals(AccountNumber{}));
	}

	void addAccount_unknownCurrency_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		QVERIFY_EXCEPTION_THROWN(
			bbl.addAccount("Vacation Savings", SupportedAccountTypes::Savings, Money{Iso4217Codes::XXX}),
			AccountError
		);
		QVERIFY_THAT(*bbl.accountCodeNumberGenerator(), equals(AccountNumber{}));
	}

	//}

	//{ Update Bank Account

	void update_bankAddANewAccount_ShouldBeAdded()
	{
		BudgetBankLedgers bbl{};
		bbl.addAccount(
			this->vacationAccountCode_.name(),
			SupportedAccountTypes::Savings,
			Money{Iso4217Codes::USD}
		);
		bbl.add(this->c1st_);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		QVERIFY_NO_THROW(
			bbl.update(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}})
		);
		QVERIFY_TRUE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_THAT(bbl.banks().bank(this->c1stName_), equals(c1st));
		const auto& bankAccountTypes{bbl.bankLedgers().bankAccountTypes()};
		QVERIFY_THAT(bankAccountTypes.count(this->c1stName_), equals(static_cast<size_t>(1)));
		const auto& accountCodeBankType{bankAccountTypes.at(this->c1stName_)};
		QVERIFY_THAT(
			accountCodeBankType.count(this->vacationAccountCode_.number()),
			equals(static_cast<size_t>(1))
		);
		QVERIFY_THAT(
			accountCodeBankType.at(this->vacationAccountCode_.number()),
			equals(SupportedAccountTypes::Savings)
		);
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->vacationAccountCode_));
	}

	void update_bankHasRenamedAccount_ShouldBeNewName()
	{
		BudgetBankLedgers bbl{};
		bbl.addAccount(
			this->vacationAccountCode_.name(),
			SupportedAccountTypes::Savings,
			Money{Iso4217Codes::USD}
		);
		const Bank originalC1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		bbl.add(originalC1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}});
		const AccountCode renameVacationAccountCode{
			this->vacationAccountCode_.number(),
			"Spring Vacation"
		};
		bbl.recode(this->vacationAccountCode_, renameVacationAccountCode);
		const Bank c1st{this->c1stName_, {renameVacationAccountCode.number()}};
		bbl.recode(this->vacationAccountCode_, renameVacationAccountCode);
		QVERIFY_NO_THROW(
			bbl.update(c1st, {{renameVacationAccountCode.number(), SupportedAccountTypes::Savings}})
		);
		QVERIFY_TRUE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_THAT(bbl.banks().bank(this->c1stName_), equals(c1st));
		const auto& bankAccountTypes{bbl.bankLedgers().bankAccountTypes()};
		QVERIFY_THAT(bankAccountTypes.count(this->c1stName_), equals(static_cast<size_t>(1)));
		const auto& accountCodeBankType{bankAccountTypes.at(this->c1stName_)};
		QVERIFY_THAT(
			accountCodeBankType.count(renameVacationAccountCode.number()),
			equals(static_cast<size_t>(1))
		);
		QVERIFY_THAT(
			accountCodeBankType.at(renameVacationAccountCode.number()),
			equals(SupportedAccountTypes::Savings)
		);
		QVERIFY_TRUE(bbl.ledgers().hasLedger(renameVacationAccountCode));
		QVERIFY_THAT(
			bbl.ledgers().ledger(renameVacationAccountCode).account_.code().name(),
			equals(renameVacationAccountCode.name())
		);
	}

	void update_bankAddAnExistingAccount_ShouldBeUpdated()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->vacationAccount_);
		bbl.add(this->c1st_);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		QVERIFY_NO_THROW(
			bbl.update(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}})
		);
		QVERIFY_TRUE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_THAT(bbl.banks().bank(this->c1stName_), equals(c1st));
		const auto& bankAccountTypes{bbl.bankLedgers().bankAccountTypes()};
		QVERIFY_THAT(bankAccountTypes.count(this->c1stName_), equals(static_cast<size_t>(1)));
		const auto& accountCodeBankType{bankAccountTypes.at(this->c1stName_)};
		QVERIFY_THAT(
			accountCodeBankType.count(this->vacationAccountCode_.number()),
			equals(static_cast<size_t>(1))
		);
		QVERIFY_THAT(
			accountCodeBankType.at(this->vacationAccountCode_.number()),
			equals(SupportedAccountTypes::Savings)
		);
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->vacationAccountCode_));
	}

	void update_bankRemoveAnAccount_ShouldOnlyRemoveAssociation()
	{
		BudgetBankLedgers bbl{};
		bbl.addAccount(
			this->vacationAccountCode_.name(),
			SupportedAccountTypes::Savings,
			Money{Iso4217Codes::USD}
		);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		bbl.add(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}});
		QVERIFY_NO_THROW(bbl.update(this->c1st_));
		QVERIFY_TRUE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_THAT(bbl.banks().bank(this->c1stName_), equals(this->c1st_));
		QVERIFY_TRUE(bbl.bankLedgers().bankAccountTypes().empty());
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->vacationAccountCode_));
	}

	void update_bankRemoveAnAccountMultipleAccounts_ShouldOnlyRemoveMissing()
	{
		BudgetBankLedgers bbl{};
		bbl.addAccount(
			this->vacationAccountCode_.name(),
			SupportedAccountTypes::Savings,
			Money{Iso4217Codes::USD}
		);
		bbl.add(this->computerAccount_);
		const Bank original{
			this->c1stName_,
			{this->computerAccountCode_.number(), this->vacationAccountCode_.number()}
		};
		bbl.add(
			original,
			{
				{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings},
				{this->computerAccountCode_.number(), SupportedAccountTypes::Chequeing}
			}
		);
		const Bank c1st{this->c1stName_, {this->computerAccountCode_.number()}};
		QVERIFY_NO_THROW(
			bbl.update(c1st, {{this->computerAccountCode_.number(), SupportedAccountTypes::Chequeing}})
		);
		QVERIFY_TRUE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_THAT(bbl.banks().bank(this->c1stName_), equals(c1st));
		QVERIFY_THAT(
			bbl.bankLedgers().bankAccountTypes().size(),
			equals(static_cast<size_t>(1))
		);
		QVERIFY_THAT(
			bbl.bankLedgers().bankAccountTypes().at(this->c1stName_)
				.count(this->computerAccountCode_.number()),
			equals(static_cast<size_t>(1))
		);
		QVERIFY_THAT(
			bbl.bankLedgers().bankAccountTypes().at(this->c1stName_)
				.at(this->computerAccountCode_.number()),
			equals(SupportedAccountTypes::Chequeing)
		);
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->vacationAccountCode_));
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->computerAccountCode_));
	}

	void update_bankBankIsClosed_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.addAccount(
			this->vacationAccountCode_.name(),
			SupportedAccountTypes::Savings,
			Money{Iso4217Codes::USD}
		);
		bbl.add(this->c1st_);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		bbl.close(this->c1stName_);
		QVERIFY_EXCEPTION_THROWN(
			bbl.update(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}}),
			BankError
		);
		QVERIFY_TRUE(bbl.banks().hasBank(this->c1stName_));
		const Bank c1stAfterClose{this->c1stName_, {}, true};
		QVERIFY_THAT(bbl.banks().bank(this->c1stName_), equals(c1stAfterClose));
		QVERIFY_TRUE(bbl.bankLedgers().bankAccountTypes().empty());
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->vacationAccountCode_));
	}

	void update_bankUpdateObjectIsClosed_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.addAccount(
			this->vacationAccountCode_.name(),
			SupportedAccountTypes::Savings,
			Money{Iso4217Codes::USD}
		);
		bbl.add(this->c1st_);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}, true};
		QVERIFY_EXCEPTION_THROWN(
			bbl.update(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}}),
			BankError
		);
		QVERIFY_TRUE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_THAT(bbl.banks().bank(this->c1stName_), equals(this->c1st_));
		QVERIFY_TRUE(bbl.bankLedgers().bankAccountTypes().empty());
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->vacationAccountCode_));
	}

	void update_accountDoesNotExist_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		QVERIFY_EXCEPTION_THROWN(
			bbl.update(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}}),
			BankError
		);
		QVERIFY_TRUE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_THAT(bbl.banks().bank(this->c1stName_), equals(this->c1st_));
		QVERIFY_TRUE(bbl.bankLedgers().bankAccountTypes().empty());
		QVERIFY_FALSE(bbl.ledgers().hasLedger(this->vacationAccountCode_));
	}

	void update_bankDoesNotExist_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->vacationAccount_);
		QVERIFY_EXCEPTION_THROWN(
			bbl.update(this->c1st_, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}}),
			BankError
		);
		QVERIFY_FALSE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_TRUE(bbl.bankLedgers().bankAccountTypes().empty());
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->vacationAccountCode_));
	}

	void update_accountAssociatedWithAnotherBank_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.addAccount(
			this->vacationAccountCode_.name(),
			SupportedAccountTypes::Savings,
			Money{Iso4217Codes::USD}
		);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		bbl.add(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}});
		bbl.add(this->hf_);
		const Bank hf{this->hfName_, {this->vacationAccountCode_.number()}};
		QVERIFY_EXCEPTION_THROWN(
			bbl.update(hf, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}}),
			BankError
		);
		QVERIFY_TRUE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_THAT(bbl.banks().bank(this->c1stName_), equals(c1st));
		const auto& bankAccountTypes{bbl.bankLedgers().bankAccountTypes()};
		QVERIFY_THAT(bankAccountTypes.count(this->c1stName_), equals(static_cast<size_t>(1)));
		const auto& accountCodeBankType{bankAccountTypes.at(this->c1stName_)};
		QVERIFY_THAT(
			accountCodeBankType.count(this->vacationAccountCode_.number()),
			equals(static_cast<size_t>(1))
		);
		QVERIFY_THAT(
			accountCodeBankType.at(this->vacationAccountCode_.number()),
			equals(SupportedAccountTypes::Savings)
		);
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->vacationAccountCode_));
		QVERIFY_TRUE(bbl.banks().hasBank(this->hfName_));
		QVERIFY_THAT(bbl.banks().bank(this->hfName_), equals(this->hf_));
	}

	void update_bankChangeAnAccountType_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.addAccount(
			this->vacationAccountCode_.name(),
			SupportedAccountTypes::Savings,
			Money{Iso4217Codes::USD}
		);
		bbl.add(this->c1st_);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		QVERIFY_NO_THROW(
			bbl.update(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}})
		);
		QVERIFY_EXCEPTION_THROWN(
			bbl.update(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::CreditCard}}),
			BankError
		);
	}

	void update_bankAccountTypeInvalidForBankAccountType_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->vacationAccount_);
		bbl.add(this->c1st_);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		QVERIFY_EXCEPTION_THROWN(
			bbl.update(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::CreditCard}}),
			BankError
		);
		QVERIFY_TRUE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_THAT(bbl.banks().bank(this->c1stName_), equals(this->c1st_));
		QVERIFY_TRUE(bbl.bankLedgers().bankAccountTypes().empty());
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->vacationAccountCode_));
	}

	void update_debtAddingBankAccountUsage_shouldAddBankAssociation()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		const auto sat{SupportedAccountTypes::Loan};
		bbl.add(Bank{bn});
		const Debt debt{
			BudgetSource{"Visa"},
			Money{5, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{2500, 2500u, Iso4217Codes::USD},
			Percentage{20.0}
		};
		const auto created{bbl.add(debt, {})};
		const Debt updated{
			debt.source(),
			Money{5, 2500u, Iso4217Codes::USD},
			EventFrequency::BiWeekly,
			{2031, 8, 2},
			Money{2500, 2500u, Iso4217Codes::USD},
			Percentage{20.0}
		};
		QVERIFY_NO_THROW(bbl.update(updated, BankAccount{bn, {{}, sat}}));
		QVERIFY_TRUE(bbl.budget().debts_.has(updated.source()));
		QVERIFY_THAT(bbl.budget().debts_.at(updated.source()), equals(updated));
		const auto& an{bbl.budgetLedgers().budgetAccountCode<Debt>(updated.source()).number()};
		const auto ledger{bbl.ledgers().ledger(an)};
		QVERIFY_TRUE(ledger.account_.code().name().startsWith(updated.source().name()));
		QVERIFY_FALSE(ledger.account_.code().parent().hasValue());
		const auto bankAccounts{bbl.banks().bank(bn).accounts()};
		QVERIFY_THAT(bankAccounts.find(an), notEquals(bankAccounts.cend()));
	}

	void update_debtRemovingBankAccountUsage_shouldRemoveBankAssociation()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		const auto sat{SupportedAccountTypes::Loan};
		bbl.add(Bank{bn});
		const Debt debt{
			BudgetSource{"Visa"},
			Money{5, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{2500, 2500u, Iso4217Codes::USD},
			Percentage{20.0}
		};
		bbl.add(debt, BankAccount{bn, {{}, sat}});
		const Debt updated{
			debt.source(),
			Money{5, 2500u, Iso4217Codes::USD},
			EventFrequency::BiWeekly,
			{2031, 8, 2},
			Money{2500, 2500u, Iso4217Codes::USD},
			Percentage{20.0}
		};
		QVERIFY_NO_THROW(bbl.update(updated, {}));
		QVERIFY_TRUE(bbl.budget().debts_.has(updated.source()));
		QVERIFY_THAT(bbl.budget().debts_.at(updated.source()), equals(updated));
		const auto& an{bbl.budgetLedgers().budgetAccountCode<Debt>(updated.source()).number()};
		const auto ledger{bbl.ledgers().ledger(an)};
		QVERIFY_TRUE(ledger.account_.code().name().startsWith(updated.source().name()));
		QVERIFY_FALSE(ledger.account_.code().parent().hasValue());
		const auto bankAccounts{bbl.banks().bank(bn).accounts()};
		QVERIFY_THAT(bankAccounts.find(an), equals(bankAccounts.cend()));
	}

	void update_debtMisingBank_shouldThrow()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		const auto sat{SupportedAccountTypes::Loan};
		const Debt debt{
			BudgetSource{"Visa"},
			Money{5, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{2500, 2500u, Iso4217Codes::USD},
			Percentage{20.0}
		};
		const auto createdAccountCode{bbl.add(debt, {})};
		const Debt updated{
			debt.source(),
			Money{5, 2500u, Iso4217Codes::USD},
			EventFrequency::BiWeekly,
			{2031, 8, 2},
			Money{2500, 2500u, Iso4217Codes::USD},
			Percentage{20.0}
		};
		QVERIFY_EXCEPTION_THROWN(
			bbl.update(updated, BankAccount{bn, {{}, sat}}),
			BudgetItemError
		);
		QVERIFY_TRUE(bbl.budget().debts_.has(debt.source()));
		QVERIFY_THAT(bbl.budget().debts_.at(debt.source()), equals(debt));
		const auto& an{bbl.budgetLedgers().budgetAccountCode<Debt>(debt.source()).number()};
		const auto ledger{bbl.ledgers().ledger(an)};
		QVERIFY_TRUE(ledger.account_.code().name().startsWith(debt.source().name()));
		QVERIFY_FALSE(ledger.account_.code().parent().hasValue());
	}

	void update_goalAddingBankAccountUsage_shouldAddBankAssociation()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		bbl.add(Bank{bn});
		const auto sat{SupportedAccountTypes::Savings};
		const auto savings{
			bbl.addAccount("Savings", sat, Money{Iso4217Codes::USD})
		};
		const Goal goal{
			BudgetSource{"Vacation"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{1500, 2500u, Iso4217Codes::USD}
		};
		QVERIFY_NO_THROW(bbl.add(goal, {}));
		const Goal updated{
			BudgetSource{"Vacation"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::BiWeekly,
			{2031, 8, 2},
			Money{1500, 2500u, Iso4217Codes::USD}
		};
		QVERIFY_NO_THROW(bbl.update(updated, BankAccount{bn, {savings, sat}}));
		QVERIFY_TRUE(bbl.budget().goals_.has(updated.source()));
		QVERIFY_THAT(bbl.budget().goals_.at(updated.source()), equals(updated));
		const auto& an{bbl.budgetLedgers().budgetAccountCode<Goal>(updated.source()).number()};
		const auto ledger{bbl.ledgers().ledger(an)};
		QVERIFY_TRUE(ledger.account_.code().name().startsWith(updated.source().name()));
		QVERIFY_TRUE(ledger.account_.code().parent().hasValue());
		QVERIFY_THAT(*ledger.account_.code().parent(), equals(savings.number()));
		const auto bankAccounts{bbl.banks().bank(bn).accounts()};
		QVERIFY_THAT(bankAccounts.find(an), notEquals(bankAccounts.cend()));
	}

	void update_goalRemovingBankAccountUsage_shouldRemoveBankAssociation()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		bbl.add(Bank{bn});
		const auto sat{SupportedAccountTypes::Savings};
		const auto savings{
			bbl.addAccount("Savings", sat, Money{Iso4217Codes::USD})
		};
		const Goal goal{
			BudgetSource{"Vacation"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{1500, 2500u, Iso4217Codes::USD}
		};
		bbl.add(goal, BankAccount{bn, {savings, sat}});
		const Goal updated{
			BudgetSource{"Vacation"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::BiWeekly,
			{2031, 8, 2},
			Money{1500, 2500u, Iso4217Codes::USD}
		};
		QVERIFY_NO_THROW(bbl.update(updated, {}));
		QVERIFY_TRUE(bbl.budget().goals_.has(updated.source()));
		QVERIFY_THAT(bbl.budget().goals_.at(updated.source()), equals(updated));
		const auto& an{bbl.budgetLedgers().budgetAccountCode<Goal>(updated.source()).number()};
		const auto ledger{bbl.ledgers().ledger(an)};
		QVERIFY_TRUE(ledger.account_.code().name().startsWith(updated.source().name()));
		QVERIFY_FALSE(ledger.account_.code().parent().hasValue());
		const auto bankAccounts{bbl.banks().bank(bn).accounts()};
		QVERIFY_THAT(bankAccounts.find(an), equals(bankAccounts.cend()));
	}

	void update_goalMisingBank_shouldThrow()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		const auto sat{SupportedAccountTypes::Savings};
		const auto savings{
			bbl.addAccount("Savings", sat, Money{Iso4217Codes::USD})
		};
		const Goal goal{
			BudgetSource{"Vacation"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{1500, 2500u, Iso4217Codes::USD}
		};
		bbl.add(goal, {});
		const Goal updated{
			BudgetSource{"Vacation"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::BiWeekly,
			{2031, 8, 2},
			Money{1500, 2500u, Iso4217Codes::USD}
		};
		QVERIFY_EXCEPTION_THROWN(
			bbl.update(updated, BankAccount{bn, {savings, sat}}),
			BudgetItemError
		);
		QVERIFY_TRUE(bbl.budget().goals_.has(goal.source()));
		QVERIFY_THAT(bbl.budget().goals_.at(goal.source()), equals(goal));
		const auto& an{bbl.budgetLedgers().budgetAccountCode<Goal>(goal.source()).number()};
		const auto ledger{bbl.ledgers().ledger(an)};
		QVERIFY_TRUE(ledger.account_.code().name().startsWith(goal.source().name()));
		QVERIFY_FALSE(ledger.account_.code().parent().hasValue());
	}

	void update_billBankAccountUsage_shouldThrow()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		bbl.add(Bank{bn});
		const auto sat{SupportedAccountTypes::Chequeing};
		const auto chequeing{
			bbl.addAccount("Chequeing", sat, Money{Iso4217Codes::USD})
		};
		const Bill bill{
			BudgetSource{"Xcel"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2}
		};
		bbl.add(bill, {});
		const Bill updated{
			BudgetSource{"Xcel"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::BiWeekly,
			{2031, 8, 2}
		};
		QVERIFY_EXCEPTION_THROWN(
			bbl.update(updated, BankAccount{bn, {{}, sat}}),
			BudgetItemError
		);
		QVERIFY_TRUE(bbl.budget().bills_.has(bill.source()));
		QVERIFY_THAT(bbl.budget().bills_.at(bill.source()), equals(bill));
		const auto& an{bbl.budgetLedgers().budgetAccountCode<Bill>(bill.source()).number()};
		const auto bankAccounts{bbl.banks().bank(bn).accounts()};
		QVERIFY_THAT(bankAccounts.find(an), equals(bankAccounts.cend()));
	}

	void update_nontrackBankAccountUsage_shouldThrow()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		bbl.add(Bank{bn});
		const auto sat{SupportedAccountTypes::Chequeing};
		const auto chequeing{
			bbl.addAccount("Chequeing", sat, Money{Iso4217Codes::USD})
		};
		const Nontrack nontrack{
			BudgetSource{"Discretionary Spending"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2}
		};
		bbl.add(nontrack, {});
		const Nontrack updated{
			BudgetSource{"Discretionary Spending"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::BiWeekly,
			{2031, 8, 2}
		};
		QVERIFY_EXCEPTION_THROWN(
			bbl.update(updated, BankAccount{bn, {{}, sat}}),
			BudgetItemError
		);
		QVERIFY_TRUE(bbl.budget().nontracks_.has(nontrack.source()));
		QVERIFY_THAT(bbl.budget().nontracks_.at(nontrack.source()), equals(nontrack));
		const auto& an{bbl.budgetLedgers().budgetAccountCode<Nontrack>(nontrack.source()).number()};
		const auto bankAccounts{bbl.banks().bank(bn).accounts()};
		QVERIFY_THAT(bankAccounts.find(an), equals(bankAccounts.cend()));
	}

	void update_wageBankAccountUsage_shouldThrow()
	{
		BudgetBankLedgers bbl{};
		const BankName bn{"Community 1st"};
		bbl.add(Bank{bn});
		const auto sat{SupportedAccountTypes::Chequeing};
		const auto chequeing{
			bbl.addAccount("Chequeing", sat, Money{Iso4217Codes::USD})
		};
		const Wage wage{
			BudgetSource{"Wendy's"},
			Money{5, 1500u, Iso4217Codes::USD},
			EventFrequency::Hourly,
			{2031, 8, 2}
		};
		bbl.add(wage, {});
		const Wage updated{
			BudgetSource{"Wendy's"},
			Money{5, 1500u, Iso4217Codes::USD},
			EventFrequency::BiWeekly,
			{2031, 8, 2}
		};
		QVERIFY_EXCEPTION_THROWN(
			bbl.update(updated, BankAccount{bn, {{}, sat}}),
			BudgetItemError
		);
		QVERIFY_TRUE(bbl.budget().wages_.has(wage.source()));
		QVERIFY_THAT(bbl.budget().wages_.at(wage.source()), equals(wage));
		const auto& an{bbl.budgetLedgers().budgetAccountCode<Wage>(wage.source()).number()};
		const auto bankAccounts{bbl.banks().bank(bn).accounts()};
		QVERIFY_THAT(bankAccounts.find(an), equals(bankAccounts.cend()));
	}

	//}

	//{ close Bank

	void close_bankIsOpen_ShouldClose()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		QVERIFY_NO_THROW(bbl.close(this->c1stName_));
		QVERIFY_TRUE(bbl.banks().bank(this->c1stName_).isClosed());
	}

	void close_bankHasAccounts_ShouldCloseAllAccounts()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->vacationAccount_);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		bbl.add(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}});
		QVERIFY_NO_THROW(bbl.close(this->c1stName_));
		QVERIFY_TRUE(bbl.banks().bank(this->c1stName_).isClosed());
		QVERIFY_TRUE(bbl.ledgers().ledger(this->vacationAccountCode_).account_.isClosed());
	}

	void close_bankIsClosed_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		QVERIFY_NO_THROW(bbl.close(this->c1stName_));
		QVERIFY_EXCEPTION_THROWN(bbl.close(this->c1stName_), BankError);
	}

	void close_bankDoesNotExist_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		QVERIFY_EXCEPTION_THROWN(bbl.close(this->c1stName_), BankError);
	}

	//}

	//{ open Bank

	void open_bankIsClosed_ShouldOpen()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		bbl.close(this->c1stName_);
		QVERIFY_NO_THROW(bbl.open(this->c1stName_));
		QVERIFY_FALSE(bbl.banks().bank(this->c1stName_).isClosed());
	}

	void open_bankHasAccounts_ShouldOpenAllAccounts()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->vacationAccount_);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		bbl.add(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}});
		bbl.close(this->c1stName_);
		QVERIFY_NO_THROW(bbl.open(this->c1stName_));
		QVERIFY_FALSE(bbl.banks().bank(this->c1stName_).isClosed());
		QVERIFY_FALSE(bbl.ledgers().ledger(this->vacationAccountCode_).account_.isClosed());
	}

	void open_bankIsOpend_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		QVERIFY_EXCEPTION_THROWN(bbl.open(this->c1stName_), BankError);
	}

	void open_bankDoesNotExist_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		QVERIFY_EXCEPTION_THROWN(bbl.open(this->c1stName_), BankError);
	}

	//}

	//{ rename Bank account

	void rename_bankNoBankAccounts_ShouldRename()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		const BankName newName{"Com. 1st CU"};
		QVERIFY_NO_THROW(bbl.rename(this->c1stName_, newName));
		QVERIFY_FALSE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_TRUE(bbl.banks().hasBank(newName));
	}

	void rename_bankWithBankAccounts_ShouldRename()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->vacationAccount_);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		bbl.add(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}});
		const BankName newName{"Com. 1st CU"};
		QVERIFY_NO_THROW(bbl.rename(this->c1stName_, newName));
		QVERIFY_FALSE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_TRUE(bbl.banks().hasBank(newName));
		QVERIFY_THAT(
			bbl.bankLedgers().bankAccountTypes().count(this->c1stName_),
			equals(static_cast<size_t>(0))
		);
		QVERIFY_THAT(
			bbl.bankLedgers().bankAccountTypes().count(newName),
			equals(static_cast<size_t>(1))
		);
	}

	void rename_bankBankNameDoesNotExist_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		const BankName newName{"Com. 1st CU"};
		QVERIFY_EXCEPTION_THROWN(bbl.rename(this->c1stName_, newName), BankError);
		QVERIFY_FALSE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_FALSE(bbl.banks().hasBank(newName));
	}

	void rename_bankBankNameAlreadyInUse_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		bbl.add(this->hf_);
		QVERIFY_EXCEPTION_THROWN(bbl.rename(this->c1stName_, this->hfName_), BankError);
	}

	void rename_bankIsClosed_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		bbl.close(this->c1stName_);
		const BankName newName{"Com. 1st CU"};
		QVERIFY_EXCEPTION_THROWN(bbl.rename(this->c1stName_, newName), BankError);
		QVERIFY_TRUE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_FALSE(bbl.banks().hasBank(newName));
	}

	//}

	//{ Remove Bank

	void remove_bankHasNoAccounts_ShouldRemove()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		QVERIFY_NO_THROW(bbl.remove(this->c1stName_));
		QVERIFY_FALSE(bbl.banks().hasBank(this->c1stName_));
	}

	void remove_bankHasAccountsWithoutTransactions_ShouldRemove()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->vacationAccount_);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		bbl.add(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}});
		QVERIFY_NO_THROW(bbl.remove(this->c1stName_));
		QVERIFY_FALSE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_FALSE(bbl.ledgers().hasLedger(this->vacationAccountCode_));
	}

	void remove_bankHasAccountsWithTransactions_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->vacationAccount_);
		bbl.add(this->computerAccount_);
		const Bank c1st{this->c1stName_, {this->vacationAccountCode_.number()}};
		bbl.add(c1st, {{this->vacationAccountCode_.number(), SupportedAccountTypes::Savings}});
		bbl.post(this->vacationTxn_);
		QVERIFY_EXCEPTION_THROWN(bbl.remove(this->c1stName_), BankError);
		QVERIFY_TRUE(bbl.banks().hasBank(this->c1stName_));
		QVERIFY_TRUE(bbl.ledgers().hasLedger(this->vacationAccountCode_));
	}

	void remove_bankIsClosed_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		bbl.close(this->c1stName_);
		QVERIFY_EXCEPTION_THROWN(bbl.remove(this->c1stName_), BankError);
		QVERIFY_TRUE(bbl.banks().hasBank(this->c1stName_));
	}

	void remove_bankDoesNotExist_ShouldThrow()
	{
		BudgetBankLedgers bbl{};
		QVERIFY_EXCEPTION_THROWN(bbl.remove(this->c1stName_), BankError);
	}

	//}

	//{ setManaged

	void setManaged_valid_shouldSet()
	{
		Budget budget{};
		const BudgetSource bs{"Wendy's Job"};
		budget.wages_.add(
			Wage{
				bs,
				Money{400, 2500u, Iso4217Codes::USD},
				EventFrequency::Weekly,
				{2020, 11, 19}
			}
		);
		GeneralLedger ledgers{};
		ledgers.add(this->wendysAccount_, this->sang_);
		BankMap banks{};
		banks.add(this->c1st_);
		map<BankName, map<AccountNumber, SupportedAccountTypes>> bankAccountTypes{};
		bankAccountTypes[this->c1stName_][this->wendysAccountCode_.number()]
				= SupportedAccountTypes::Chequeing;
		TypeIndexMap<BudgetSource, AccountCode> budgetAccountCodes{};
		budgetAccountCodes.add<Wage>(bs, this->wendysAccountCode_);
		BudgetBankLedgers bbl{};
		bbl.setManaged(
			ledgers,
			budget,
			budgetAccountCodes,
			banks,
			bankAccountTypes
		);
		QVERIFY_THAT(bbl.budget(), equals(budget));
		QVERIFY_THAT(bbl.ledgers(), equals(ledgers));
		QVERIFY_THAT(bbl.banks(), equals(banks));
		QVERIFY_THAT(bbl.bankLedgers().bankAccountTypes(), equals(bankAccountTypes));
		QVERIFY_THAT(bbl.budgetLedgers().budgetAccountCodes(), equals(budgetAccountCodes));
	}

	void setManaged_invalid_shouldThrow()
	{
		Budget budget{};
		const BudgetSource bs{"Wendy's Job"};
		budget.wages_.add(
			Wage{
				bs,
				Money{400, 2500u, Iso4217Codes::USD},
				EventFrequency::Weekly,
				{2020, 11, 19}
			}
		);
		GeneralLedger ledgers{};
		ledgers.add(this->wendysAccount_, this->sang_);
		BankMap banks{};
		banks.add(this->c1st_);
		map<BankName, map<AccountNumber, SupportedAccountTypes>> bankAccountTypes{};
		bankAccountTypes[this->hfName_][AccountNumber{1234u}] = SupportedAccountTypes::Unknown;
		TypeIndexMap<BudgetSource, AccountCode> budgetAccountCodes{};
		budgetAccountCodes.add<Wage>(bs, this->wendysAccountCode_);
		BudgetBankLedgers bbl{};
		QVERIFY_EXCEPTION_THROWN(
			bbl.setManaged(
				ledgers,
				budget,
				budgetAccountCodes,
				banks,
				bankAccountTypes
			),
			BankError
		);
		QVERIFY_THAT(bbl, equals(BudgetBankLedgers()));
	}

	//}

	//{ bankNames Tests

	void bankNames_hasBanks_ShouldGive()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		const set<BankName> expected{this->c1stName_};
		QVERIFY_THAT(bbl.bankNames().size(), equals(expected.size()));
		QVERIFY_THAT(*bbl.bankNames().cbegin(), equals(*expected.cbegin()));
	}

	//}

	//{ budgetBankAccounts Tests

	void budgetBankAccounts_hasAccounts_ShouldGive()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		const auto savings{
			bbl.addAccount("Savings", SupportedAccountTypes::Savings, Money{Iso4217Codes::USD})
		};
		const Goal vacation{
			BudgetSource{"Vacation"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{1500, 2500u, Iso4217Codes::USD}
		};
		const BankAccount bankSavings{
			this->c1st_.name_,
			{savings, SupportedAccountTypes::Savings}
		};
		const auto vacationAccount{bbl.add(vacation, bankSavings)};
		const Debt visa{
			BudgetSource{"Visa"},
			Money{5, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{2500, 2500u, Iso4217Codes::USD},
			Percentage{20.0}
		};
		const auto visaAccount{
			bbl.add(
				visa,
				BankAccount{this->c1st_.name_, {{}, SupportedAccountTypes::CreditCard}}
			)
		};
		const Debt masterCard{
			BudgetSource{"Master Card"},
			Money{5, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{2500, 2500u, Iso4217Codes::USD},
			Percentage{20.0}
		};
		const auto masterCardAccount{
			bbl.add(
				masterCard,
				BankAccount{this->c1st_.name_, {{}, SupportedAccountTypes::CreditCard}}
			)
		};
		const Debt amex{
			BudgetSource{"American Express"},
			Money{5, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{2500, 2500u, Iso4217Codes::USD},
			Percentage{20.0}
		};
		const auto amexAccount{
			bbl.add(
				amex,
				BankAccount{this->c1st_.name_, {{}, SupportedAccountTypes::CreditCard}}
			)
		};
		const auto goalAccounts{bbl.budgetBankAccounts(BudgetItemTypes::Goal)};
		QVERIFY_THAT(goalAccounts.size(), equals(static_cast<size_t>(1)));
		const auto firstGoalAccount{*goalAccounts.cbegin()};
		QVERIFY_THAT(
			firstGoalAccount,
			equals(
				BudgetBankAccount{
					BudgetItemIdentifier{BudgetItemTypes::Goal, vacation.source()},
					BankAccount{
						this->c1st_.name_,
						{vacationAccount, SupportedAccountTypes::Savings}
					}
				}
			)
		);
		const auto debtAccounts{bbl.budgetBankAccounts(BudgetItemTypes::Debt)};
		QVERIFY_THAT(debtAccounts.size(), equals(static_cast<size_t>(3)));
		auto debitAccount{debtAccounts.cbegin()};
		QVERIFY_THAT(
			*debitAccount,
			equals(
				BudgetBankAccount{
					BudgetItemIdentifier{BudgetItemTypes::Debt, amex.source()},
					BankAccount{
						this->c1st_.name_,
						{amexAccount, SupportedAccountTypes::CreditCard}
					}
				}
			)
		);
		++debitAccount;
		QVERIFY_THAT(
			*debitAccount,
			equals(
				BudgetBankAccount{
					BudgetItemIdentifier{BudgetItemTypes::Debt, masterCard.source()},
					BankAccount{
						this->c1st_.name_,
						{masterCardAccount, SupportedAccountTypes::CreditCard}
					}
				}
			)
		);
		++debitAccount;
		QVERIFY_THAT(
			*debitAccount,
			equals(
				BudgetBankAccount{
					BudgetItemIdentifier{BudgetItemTypes::Debt, visa.source()},
					BankAccount{
						this->c1st_.name_,
						{visaAccount, SupportedAccountTypes::CreditCard}
					}
				}
			)
		);
	}

	void budgetBankAccounts_hasNoAccounts_ShouldBeEmpty()
	{
		BudgetBankLedgers bbl{};
		bbl.add(this->c1st_);
		const auto savings{
			bbl.addAccount("Savings", SupportedAccountTypes::Savings, Money{Iso4217Codes::USD})
		};
		const Goal goal{
			BudgetSource{"Vacation"},
			Money{150, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{1500, 2500u, Iso4217Codes::USD}
		};
		const BankAccount bankSavings{
			this->c1st_.name_,
			{savings, SupportedAccountTypes::Savings}
		};
		const auto vacationAccount{bbl.add(goal, bankSavings)};
		const Debt creditCard{
			BudgetSource{"Visa"},
			Money{5, 2500u, Iso4217Codes::USD},
			EventFrequency::Monthly,
			{2031, 8, 2},
			Money{2500, 2500u, Iso4217Codes::USD},
			Percentage{20.0}
		};
		const auto creditCardAccount{
			bbl.add(
				creditCard,
				BankAccount{this->c1st_.name_, {{}, SupportedAccountTypes::CreditCard}}
			)
		};
		const auto accounts{bbl.budgetBankAccounts(BudgetItemTypes::Bill)};
		QVERIFY_TRUE(accounts.empty());
	}

	//}

public:
	BudgetBankLedgersUT() :
		wendysAccountCode_{AccountNumber{1}, "Wendy's"},
		wendysAccount_{
			AccountTypes::Revenue,
			this->wendysAccountCode_,
			Money{150, 1500, Iso4217Codes::USD}
		},
		openingAccountCode_{AccountNumber{2}, "Opening Balances"},
		openingAccount_{
			AccountTypes::Capital,
			this->openingAccountCode_,
			Money{150, 1500, Iso4217Codes::USD}
		},
		burgerKingAccountCode_{AccountNumber{3}, "Burger King"},
		burgerKingAccount_{
			AccountTypes::Revenue,
			this->burgerKingAccountCode_,
			Money{150, 1500u, Iso4217Codes::USD}
		},
		wendysSource_{"Wendy's"},
		wendys_{
			this->wendysSource_,
			Money{5, 1500, Iso4217Codes::USD},
			EventFrequency::Hourly,
			{2019, 7, 8}
		},
		updateWendys_{
			this->wendysSource_,
			Money{7, 5000, Iso4217Codes::PLN},
			EventFrequency::Weekly,
			{2019, 7, 8}
		},
		newWendysAccountCode_{AccountNumber{1010u}, "Wendy's Wages"},
		newWendysSource_{"Wendy's Wages"},
		wendysTxn_{
			QDate{2019, 7, 12},
			this->wendysAccountCode_,
			this->burgerKingAccountCode_,
			Money{200, 7500u, Iso4217Codes::USD},
			QStringLiteral("Fry Transplant")
		},
		wendysTxnNum_{3u},
		burgerKingTxn_{
			QDate{2019, 7, 12},
			this->burgerKingAccountCode_,
			this->wendysAccountCode_,
			Money{200, 7500u, Iso4217Codes::USD},
			QStringLiteral("Fry Transplant")
		},
		c1stName_{"Community 1st Credit Union"},
		c1st_{this->c1stName_},
		hfName_{"Home Federal"},
		hf_{this->hfName_},
		vacationAccountCode_{AccountNumber{1}, "Vacation Savings"},
		vacationAccount_{
			AccountTypes::Asset,
			this->vacationAccountCode_,
			Money{150, 1500, Iso4217Codes::USD}
		},
		computerAccountCode_{AccountNumber{3}, "Computer Savings"},
		computerAccount_{
			AccountTypes::Asset,
			this->computerAccountCode_,
			Money{150, 1500u, Iso4217Codes::USD}
		},
		vacationTxn_{
			QDate{2019, 8, 12},
			this->vacationAccountCode_,
			this->computerAccountCode_,
			Money{500, 2500u, Iso4217Codes::USD},
			QStringLiteral("Get there sooner")
		},
		sang_{}
	{}

};

}}}}

QTEST_MAIN(drn::navigation::internal::tests::BudgetBankLedgersUT)
#include "BudgetBankLedgers-unit-tests.moc"

