/*******************************************************************************
***  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 <qt-mocks/Matchers.h>
using qt_mocks::equals;
#include <QDir>
using ::QDir;
#include <QFile>
using ::QFile;
#include <QFileInfo>
using ::QFileInfo;
#include <QObject>
using ::QObject;
#include <QString>
using ::QString;
#include <QTemporaryDir>
using ::QTemporaryDir;
#include <QTest>
// using QCOMPARE.
// using QTEST_MAIN.
// using QVERIFY_EXCEPTION_THROWN.

#include <map>
using std::map;
#include <tuple>
using std::get;

#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/LedgerBooks.h>
using drn::accounting::GeneralLedger;
using drn::accounting::LedgerAccount;
#include <accounting/Transaction.h>
using drn::accounting::Transaction;
using drn::accounting::TransactionNumber;
#include "BudgetFileContents.h"
using drn::file_storage::tests::unitTestDirectoryPath;
using drn::file_storage::tests::validBudget;
using drn::file_storage::tests::validBudgetCodes;
using drn::file_storage::tests::validGeneralLedger;
#include <banking/Bank.h>
using drn::banking::Bank;
#include <banking/BankAccountType.h>
using drn::banking::SupportedAccountTypes;
#include <banking/BankMap.h>
using drn::banking::BankMap;
#include <banking/BankName.h>
using drn::banking::BankName;
#include <budgeting/Budget.h>
using drn::budgeting::Budget;
#include <budgeting/BudgetSource.h>
using drn::budgeting::BudgetSource;
#include <budgeting/Nontrack.h>
using drn::budgeting::Nontrack;
#include <budgeting/EventFrequency.h>
using drn::budgeting::EventFrequency;
#include <budgeting/Bill.h>
using drn::budgeting::Bill;
#include <budgeting/BillMap.hpp>
using drn::budgeting::BillMap;
#include <budgeting/Wage.h>
using drn::budgeting::Wage;
#include <budgeting/WageMap.hpp>
using drn::budgeting::WageMap;
#include <foundation/Error.h>
using drn::foundation::Error;
#include <file-storage/BudgetFile.h>
using drn::file_storage::BudgetFile;
#include "file-storage/FileStorageErrors.h"
using drn::file_storage::AttributeError;
#include <foundation/TypeIndexMap.hpp>
using drn::foundation::TypeIndexMap;


namespace drn
{
namespace file_storage
{
namespace tests
{

struct BudgetFileUT : QObject
{
	Q_OBJECT

private slots:
	//{ Constructor Tests

	void constructor_Empty_ShouldBeDefaults()
	{
		const BudgetFile bf{};
		QCOMPARE(bf.directoryPath(), QString{});
		QCOMPARE(bf.name(), QString{});
		QCOMPARE(bf.backUpExtension(), QString{});
	}
	void constructor_FullInit_ShouldBeValues()
	{
		const QString expectedDirectory{
		#	ifdef Q_OS_WIN
				"A:/home/johnms/.drn"
		#	else
				"/home/johnms/.drn"
		#	endif
		};
		const QString expectedExtension{"bak"};
		const QString expectedName{"family.budget"};
		const QString expectedLocation{expectedDirectory + '/' + expectedName};
		const BudgetFile bf{expectedLocation, expectedExtension};
		QCOMPARE(bf.directoryPath(), expectedDirectory);
		QCOMPARE(bf.name(), expectedName);
		QCOMPARE(bf.backUpExtension(), '.' + expectedExtension);
	}
	void constructor_FullInitBackUpExtensionNotStartingWtihFullStop_ShouldBeValues()
	{
		const QString expectedDirectory{
		#	ifdef Q_OS_WIN
				"A:/home/johnms/.drn"
		#	else
				"/home/johnms/.drn"
		#	endif
		};
		const QString expectedExtension{"b.a.k."};
		const QString expectedName{"family.budget"};
		const QString expectedLocation{expectedDirectory + '/' + expectedName};
		const BudgetFile bf{expectedLocation, expectedExtension};
		QCOMPARE(bf.directoryPath(), expectedDirectory);
		QCOMPARE(bf.name(), expectedName);
		QCOMPARE(bf.backUpExtension(), '.' + expectedExtension);
	}
	void constructor_InvalidEmptyLocation_ShouldThrow()
	{
		QVERIFY_EXCEPTION_THROWN(BudgetFile("   "), Error);
	}
	void constructor_InvalidRelativeLocation_ShouldThrow()
	{
		QVERIFY_EXCEPTION_THROWN(BudgetFile("../usr/share/family.budget"), Error);
	}
	void constructor_InvalidExtension_ShouldThrow()
	{
		QVERIFY_EXCEPTION_THROWN(BudgetFile("/home/johnms/.drn", ".bak"), Error);
	}

	//}

	// load Tests

	void load_FileValid_ShouldLoad()
	{
		const BudgetFile bf{unitTestDirectoryPath() + "/demo.budget"};
		const Budget expectedBudget{
			WageMap{
				{
					{
						BudgetSource{"McDonald's"},
						Wage{
							BudgetSource{"McDonald's"},
							Money{5, 1500u, Iso4217Codes::USD},
							EventFrequency::Hourly,
							{2019, 1, 1}
						}
					},
					{
						BudgetSource{"Burger King"},
						Wage{
							BudgetSource{"Burger King"},
							Money{2000, 0u, Iso4217Codes::PLN},
							EventFrequency::Monthly,
							{2019, 2, 1}
						}
					}
				}
			},
			BillMap{
				{
					{
						BudgetSource{"Film Rentals"},
						Bill{
							BudgetSource{"Film Rentals"},
							Money{20, 0u, Iso4217Codes::USD},
							EventFrequency::Monthly,
							{2019, 4, 1}
						}
					},
					{
						BudgetSource{"Rent"},
						Bill{
							BudgetSource{"Rent"},
							Money{1234, 5500u, Iso4217Codes::PLN},
							EventFrequency::Monthly,
							{2019, 3, 1}
						}
					}
				}
			},
			{},
			{},
			{
				{
					{
						BudgetSource{QStringLiteral("Discretionary Spending")},
						Nontrack{
							BudgetSource{QStringLiteral("Discretionary Spending")},
							Money{15, 7500u, Iso4217Codes::USD},
							EventFrequency::Daily,
							{2019, 5, 1}
						}
					}
				}

			}
		};
		const AccountCode cash{AccountNumber{1u}, "Cash"};
		const GeneralLedger gl{
			{
				{
					cash,
					LedgerAccount{
						Account{
							AccountTypes::Asset,
							cash,
							Money{1234, 5600u, Iso4217Codes::USD}
						},
						map<TransactionNumber, Transaction>{}
					}
				}
			}
		};
		TypeIndexMap<BudgetSource, AccountCode> budgetCodes{};

		for (const auto& wage : expectedBudget.wages_)
			budgetCodes.add<Wage>(wage.second.source(), cash);

		for (const auto& bill : expectedBudget.bills_)
			budgetCodes.add<Bill>(bill.second.source(), cash);

		for (const auto& nontrack : expectedBudget.nontracks_)
			budgetCodes.add<Nontrack>(nontrack.second.source(), cash);
		const auto actual{bf.load()};
		QCOMPARE(get<0>(actual), gl);
		QCOMPARE(get<1>(actual), expectedBudget);
		QCOMPARE(get<2>(actual), budgetCodes);
		QCOMPARE(get<3>(actual), BankMap{});
		QVERIFY_TRUE(get<4>(actual).empty());
	}
	void load_FileMissing_ShouldThrow()
	{
		const BudgetFile bf{"/fake/path/to/file/budget"};
		QVERIFY_EXCEPTION_THROWN(bf.load(), Error);
	}

	//}

	// store Tests

	void store_FileValid_ShouldSave()
	{
		QTemporaryDir td{};
		const auto fileName{td.filePath("My Budget.bf")};
		const BudgetFile bf{fileName};
		const Budget expectedBudget{
			WageMap{
				{
					{
						BudgetSource{"McDonald's"},
						Wage{
							BudgetSource{"McDonald's"},
							Money{5, 1500u, Iso4217Codes::USD},
							EventFrequency::Hourly,
							{2019, 7 , 4}
						}
					},
					{
						BudgetSource{"Burger King"},
						Wage{
							BudgetSource{"Burger King"},
							Money{2000, 0u, Iso4217Codes::PLN},
							EventFrequency::Monthly,
							{2019, 7 , 4}
						}
					}
				}
			}
		};
		const AccountCode cash{AccountNumber{1u}, "Cash"};
		const GeneralLedger gl{
			{
				{
					cash,
					LedgerAccount{
						Account{
							AccountTypes::Asset,
							cash,
							Money{1234, 5600u, Iso4217Codes::USD}
						},
						map<TransactionNumber, Transaction>{}
					}
				}
			}
		};
		TypeIndexMap<BudgetSource, AccountCode> budgetCodes{};

		for (const auto& wage : expectedBudget.wages_)
			budgetCodes.add<Wage>(wage.second.source(), cash);

		for (const auto& bill : expectedBudget.bills_)
			budgetCodes.add<Bill>(bill.second.source(), cash);

		for (const auto& nontrack : expectedBudget.nontracks_)
			budgetCodes.add<Nontrack>(nontrack.second.source(), cash);
		bf.store(gl, expectedBudget, budgetCodes, {}, {});
		const auto actual{bf.load()};
		QCOMPARE(get<0>(actual), gl);
		QCOMPARE(get<1>(actual), expectedBudget);
		QCOMPARE(get<2>(actual), budgetCodes);
		QCOMPARE(get<3>(actual), BankMap{});
		QVERIFY_TRUE(get<4>(actual).empty());
	}
	void store_FileEmptyFirstSave_ShouldSave()
	{
		QTemporaryDir td{};
		const auto fileName{td.filePath("My Budget.bf")};
		const BudgetFile bf{fileName};
		const Budget expectedBudget{};
		const GeneralLedger expectedLedger{};
		bf.store(expectedLedger, expectedBudget, {}, {}, {});
		QVERIFY_TRUE(QFile::exists(fileName));
		const auto actual{bf.load()};
		QCOMPARE(get<0>(actual), expectedLedger);
		QCOMPARE(get<1>(actual), expectedBudget);
		QVERIFY_TRUE(get<2>(actual).empty());
		QCOMPARE(get<3>(actual), BankMap{});
		QVERIFY_TRUE(get<4>(actual).empty());
	}
	void store_SaveFollowedBySave_ShouldSavePlusBackUp()
	{
		QTemporaryDir td{};
		const auto fileName{td.filePath("My Budget.bf")};
		const BudgetFile bf{fileName};
		bf.store({}, {}, {}, {}, {});
		QVERIFY_TRUE(QFile::exists(fileName));
		const auto backUpFileName{fileName + bf.backUpExtension()};
		QVERIFY_FALSE(QFile::exists(backUpFileName));
		bf.store(validGeneralLedger(), validBudget(), validBudgetCodes(), {}, {});
		QVERIFY_THAT(QDir{td.path()}.entryList(QDir::Files).count(), equals(2));
		QVERIFY_TRUE(QFile::exists(fileName));
		QVERIFY_TRUE(QFile::exists(backUpFileName));
		const auto newest{bf.load()};
		QCOMPARE(get<0>(newest), validGeneralLedger());
		QCOMPARE(get<1>(newest), validBudget());
		QCOMPARE(get<2>(newest), validBudgetCodes());
		const auto oldest{BudgetFile{backUpFileName}.load()};
		QCOMPARE(get<0>(oldest), GeneralLedger());
		QCOMPARE(get<1>(oldest), Budget());
		decltype(validBudgetCodes()) expectedBudgetCodes{};
		QCOMPARE(get<2>(oldest), expectedBudgetCodes);
	}
	void store_SaveFollowedBySaveFollowedBySave_ShouldSavePlusOneBackUp()
	{
		QTemporaryDir td{};
		const auto fileName{td.filePath("My Budget.bf")};
		const BudgetFile bf{fileName};
		bf.store({}, {}, {}, {}, {});
		QVERIFY_TRUE(QFile::exists(fileName));
		const auto backUpFileName{fileName + bf.backUpExtension()};
		QVERIFY_FALSE(QFile::exists(backUpFileName));
		bf.store(validGeneralLedger(), validBudget(), validBudgetCodes(), {}, {});
		QVERIFY_THAT(QDir{td.path()}.entryList(QDir::Files).count(), equals(2));
		QVERIFY_TRUE(QFile::exists(fileName));
		QVERIFY_TRUE(QFile::exists(backUpFileName));
		QVERIFY_FALSE(QFile::exists(backUpFileName + BudgetFile::oldBackUpExtension_));
		QVERIFY_NO_THROW(bf.store({}, {}, {}, {}, {}));
		// Confirm the old back-up file is removed.
		QVERIFY_THAT(QDir{td.path()}.entryList(QDir::Files).count(), equals(2));
		QVERIFY_TRUE(QFile::exists(fileName));
		QVERIFY_TRUE(QFile::exists(backUpFileName));
		QVERIFY_FALSE(QFile::exists(backUpFileName + BudgetFile::oldBackUpExtension_));
	}
	void store_DirectoryLocationDoesNotExist_ShouldThrow()
	{
		const BudgetFile bf{"/fake/path/to/file/budget"};
		QVERIFY_EXCEPTION_THROWN(bf.store(GeneralLedger{}, Budget{}, {}, {}, {}), Error);
	}
	void store_invalidSaveDuringWrite_ShouldRemoveBrokenFileAndThrow()
	{
		const Budget brokenBudget{
			WageMap{
				{
					{
						BudgetSource{"McDonald's"},
						Wage{BudgetSource{"McDonald's"}}
					}
				}
			}
		};
		const AccountCode cash{AccountNumber{1u}, "Cash"};
		const GeneralLedger gl{
			{
				{
					cash,
					LedgerAccount{
						Account{
							AccountTypes::Asset,
							cash,
							Money{1234, 5600u, Iso4217Codes::USD}
						},
						map<TransactionNumber, Transaction>{}
					}
				}
			}
		};
		TypeIndexMap<BudgetSource, AccountCode> budgetCodes{};
		budgetCodes.add<Wage>(BudgetSource{"McDonald's"}, cash);
		QTemporaryDir td{};
		const auto fileName{td.filePath("My Budget.bf")};
		const BudgetFile bf{fileName};
		QVERIFY_EXCEPTION_THROWN(
			bf.store(gl, brokenBudget, budgetCodes, {}, {}),
			AttributeError
		);
		QVERIFY_FALSE(QFile::exists(fileName + bf.backUpExtension()));
		QVERIFY_FALSE(QFile::exists(fileName));
	}
	void store_emptySavedThenInvalidSaveDuringWrite_ShouldRestoreBackUpAndThrow()
	{
		QTemporaryDir td{};
		const auto fileName{td.filePath("My Budget.bf")};
		const BudgetFile bf{fileName};
		QVERIFY_NO_THROW(bf.store({}, {}, {}, {}, {}));
		const Budget brokenBudget{
			WageMap{
				{
					{
						BudgetSource{"McDonald's"},
						Wage{BudgetSource{"McDonald's"}}
					}
				}
			}
		};
		const AccountCode cash{AccountNumber{1u}, "Cash"};
		const GeneralLedger gl{
			{
				{
					cash,
					LedgerAccount{
						Account{
							AccountTypes::Asset,
							cash,
							Money{1234, 5600u, Iso4217Codes::USD}
						},
						map<TransactionNumber, Transaction>{}
					}
				}
			}
		};
		TypeIndexMap<BudgetSource, AccountCode> budgetCodes{};
		budgetCodes.add<Wage>(BudgetSource{"McDonald's"}, cash);
		QVERIFY_EXCEPTION_THROWN(
			bf.store(gl, brokenBudget, budgetCodes, {}, {}),
			AttributeError
		);
		QVERIFY_FALSE(QFile::exists(fileName + bf.backUpExtension()));
		QVERIFY_TRUE(QFile::exists(fileName));
		const auto actual{bf.load()};
		QCOMPARE(get<0>(actual), GeneralLedger{});
		QCOMPARE(get<1>(actual), Budget{});
		QVERIFY_TRUE(get<2>(actual).empty());
		QCOMPARE(get<3>(actual), BankMap{});
		QVERIFY_TRUE(get<4>(actual).empty());
	}

	//}
};

}}}

QTEST_MAIN(drn::file_storage::tests::BudgetFileUT)
#include "BudgetFile-unit-tests.moc"
