/**********************************************************************
 * LeechCraft - modular cross-platform feature rich internet client.
 * Copyright (C) 2006-2014  Georg Rudoy
 *
 * Boost Software License - Version 1.0 - August 17th, 2003
 *
 * Permission is hereby granted, free of charge, to any person or organization
 * obtaining a copy of the software and accompanying documentation covered by
 * this license (the "Software") to use, reproduce, display, distribute,
 * execute, and transmit the Software, and to prepare derivative works of the
 * Software, and to permit third-parties to whom the Software is furnished to
 * do so, all subject to the following:
 *
 * The copyright notices in the Software and this entire statement, including
 * the above license grant, this restriction and the following disclaimer,
 * must be included in all copies of the Software, in whole or in part, and
 * all derivative works of the Software, unless such copies or derivative
 * works are solely in the form of machine-executable object code generated by
 * a source language processor.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 **********************************************************************/

#include "oraltest.h"
#include <tuple>
#include <QtTest>
#include <oral.h>

QTEST_MAIN (LeechCraft::Util::OralTest)

namespace lco = LeechCraft::Util::oral;

template<typename T, typename = decltype (T {}.AsTuple ())>
auto operator== (const T& left, const T& right)
{
	return left.AsTuple () == right.AsTuple ();
}

namespace LeechCraft::Util::oral
{
	template<typename T, typename... Args>
	char* toString (const PKey<T, Args...>& pkey)
	{
		return QTest::toString (pkey.Val_);
	}
}

#define TOSTRING(n) char* toString (const n& rec) { return toString (#n, rec); }

template<typename T, typename TupleType = decltype (T {}.AsTuple ())>
char* toString (const char *name, const T& t)
{
	using QTest::toString;

	QByteArray ba { name };
	ba.append (" { ");

	std::apply ([&ba] (const auto&... args) { (ba.append (toString (args)).append (", "), ...); }, t.AsTuple ());

	if (std::tuple_size<TupleType>::value >= 1)
		ba.chop (2);
	ba.append (" }");

	return qstrdup (ba.data ());
}

struct SimpleRecord
{
	lco::PKey<int, lco::NoAutogen> ID_;
	QString Value_;

	static QString ClassName ()
	{
		return "SimpleRecord";
	}

	static QString FieldNameMorpher (const QString& str)
	{
		return str.left (str.size () - 1);
	}

	auto AsTuple () const
	{
		return std::tie (ID_, Value_);
	}
};

BOOST_FUSION_ADAPT_STRUCT (SimpleRecord,
		ID_,
		Value_)

TOSTRING (SimpleRecord)

struct AutogenPKeyRecord
{
	lco::PKey<int> ID_;
	QString Value_;

	static QString ClassName ()
	{
		return "AutogenPKeyRecord";
	}

	static QString FieldNameMorpher (const QString& str)
	{
		return str.left (str.size () - 1);
	}

	auto AsTuple () const
	{
		return std::tie (ID_, Value_);
	}
};

BOOST_FUSION_ADAPT_STRUCT (AutogenPKeyRecord,
		ID_,
		Value_)

TOSTRING (AutogenPKeyRecord)

struct NoPKeyRecord
{
	int ID_;
	QString Value_;

	static QString ClassName ()
	{
		return "NoPKeyRecord";
	}

	static QString FieldNameMorpher (const QString& str)
	{
		return str.left (str.size () - 1);
	}

	auto AsTuple () const
	{
		return std::tie (ID_, Value_);
	}
};

BOOST_FUSION_ADAPT_STRUCT (NoPKeyRecord,
		ID_,
		Value_)

TOSTRING (NoPKeyRecord)

namespace LeechCraft
{
namespace Util
{
	namespace
	{
		QSqlDatabase MakeDatabase ()
		{
			auto db = QSqlDatabase::addDatabase ("QSQLITE", Util::GenConnectionName ("TestConnection"));
			db.setDatabaseName (":memory:");
			if (!db.open ())
				throw std::runtime_error { "cannot create test database" };
			return db;
		}

		template<typename T>
		auto PrepareRecords (QSqlDatabase db)
		{
			auto adapted = Util::oral::AdaptPtr<T> (db);
			for (int i = 0; i < 3; ++i)
				adapted->DoInsert_ ({ i, QString::number (i) });
			return adapted;
		}
	}

	namespace sph = oral::sph;

	void OralTest::testSimpleRecordInsertSelect ()
	{
		auto adapted = PrepareRecords<SimpleRecord> (MakeDatabase ());
		const auto& list = adapted->DoSelectAll_ ();
		QCOMPARE (list, (QList<SimpleRecord> { { 0, "0" }, { 1, "1" }, { 2, "2" } }));
	}

	void OralTest::testSimpleRecordInsertReplaceSelect ()
	{
		auto db = MakeDatabase ();

		auto adapted = Util::oral::AdaptPtr<SimpleRecord> (db);
		for (int i = 0; i < 3; ++i)
			adapted->DoInsert_ ({ 0, QString::number (i) }, lco::InsertAction::Replace);

		const auto& list = adapted->DoSelectAll_ ();
		QCOMPARE (list, (QList<SimpleRecord> { { 0, "2" } }));
	}

	void OralTest::testSimpleRecordInsertIgnoreSelect ()
	{
		auto db = MakeDatabase ();

		auto adapted = Util::oral::AdaptPtr<SimpleRecord> (db);
		for (int i = 0; i < 3; ++i)
			adapted->DoInsert_ ({ 0, QString::number (i) }, lco::InsertAction::Ignore);

		const auto& list = adapted->DoSelectAll_ ();
		QCOMPARE (list, (QList<SimpleRecord> { { 0, "0" } }));
	}

	void OralTest::testSimpleRecordInsertSelectByFields ()
	{
		auto adapted = PrepareRecords<SimpleRecord> (MakeDatabase ());
		const auto& list = adapted->DoSelectByFields_ (sph::_0 == 1);
		QCOMPARE (list, (QList<SimpleRecord> { { 1, "1" } }));
	}

	void OralTest::testSimpleRecordInsertSelectByFields2 ()
	{
		auto adapted = PrepareRecords<SimpleRecord> (MakeDatabase ());
		const auto& list = adapted->DoSelectByFields_ (sph::_0 < 2);
		QCOMPARE (list, (QList<SimpleRecord> { { 0, "0" }, { 1, "1" } }));
	}

	void OralTest::testSimpleRecordInsertSelectByFields3 ()
	{
		auto adapted = PrepareRecords<SimpleRecord> (MakeDatabase ());
		const auto& list = adapted->DoSelectByFields_ (sph::_0 < 2 && sph::_1 == QString { "1" });
		QCOMPARE (list, (QList<SimpleRecord> { { 1, "1" } }));
	}

	void OralTest::testSimpleRecordInsertSelectOneByFields ()
	{
		auto adapted = PrepareRecords<SimpleRecord> (MakeDatabase ());
		const auto& single = adapted->DoSelectOneByFields_ (sph::_0 == 1);
		QCOMPARE (single, (boost::optional<SimpleRecord> { { 1, "1" } }));
	}

	void OralTest::testAutoPKeyRecordInsertSelect ()
	{
		auto adapted = PrepareRecords<AutogenPKeyRecord> (MakeDatabase ());
		const auto& list = adapted->DoSelectAll_ ();
		QCOMPARE (list, (QList<AutogenPKeyRecord> { { 1, "0" }, { 2, "1" }, { 3, "2" } }));
	}

	void OralTest::testAutoPKeyRecordInsertRvalueReturnsPKey ()
	{
		auto adapted = Util::oral::AdaptPtr<AutogenPKeyRecord> (MakeDatabase ());

		QList<int> ids;
		for (int i = 0; i < 3; ++i)
			ids << adapted->DoInsert_ ({ 0, QString::number (i) });

		QCOMPARE (ids, (QList<int> { 1, 2, 3 }));
	}

	void OralTest::testAutoPKeyRecordInsertConstLvalueReturnsPKey ()
	{
		auto adapted = Util::oral::AdaptPtr<AutogenPKeyRecord> (MakeDatabase ());

		QList<AutogenPKeyRecord> records;
		for (int i = 0; i < 3; ++i)
			records.push_back ({ 0, QString::number (i) });

		QList<int> ids;
		for (const auto& record : records)
			ids << adapted->DoInsert_ (record);

		QCOMPARE (ids, (QList<int> { 1, 2, 3 }));
	}

	void OralTest::testAutoPKeyRecordInsertSetsPKey ()
	{
		auto adapted = Util::oral::AdaptPtr<AutogenPKeyRecord> (MakeDatabase ());

		QList<AutogenPKeyRecord> records;
		for (int i = 0; i < 3; ++i)
			records.push_back ({ 0, QString::number (i) });

		for (auto& record : records)
			adapted->DoInsert_ (record);

		QCOMPARE (records, (QList<AutogenPKeyRecord> { { 1, "0" }, { 2, "1" }, { 3, "2" } }));
	}

	void OralTest::testNoPKeyRecordInsertSelect ()
	{
		auto adapted = PrepareRecords<NoPKeyRecord> (MakeDatabase ());
		const auto& list = adapted->DoSelectAll_ ();
		QCOMPARE (list, (QList<NoPKeyRecord> { { 0, "0" }, { 1, "1" }, { 2, "2" } }));
	}

	void OralTest::benchSimpleRecordAdapt ()
	{
		auto db = MakeDatabase ();
		Util::oral::Adapt<SimpleRecord> (db);

		QBENCHMARK { Util::oral::Adapt<SimpleRecord> (db); }
	}

	void OralTest::benchBaselineInsert ()
	{
		auto db = MakeDatabase ();
		Util::oral::Adapt<SimpleRecord> (db);

		QSqlQuery query { db };
		query.prepare ("INSERT OR IGNORE INTO SimpleRecord (ID, Value) VALUES (:id, :val);");

		QBENCHMARK
		{
			query.bindValue (":id", 0);
			query.bindValue (":val", "0");
			query.exec ();
		}
	}

	void OralTest::benchSimpleRecordInsert ()
	{
		auto db = MakeDatabase ();
		const auto& adapted = Util::oral::Adapt<SimpleRecord> (db);

		QBENCHMARK { adapted.DoInsert_ ({ 0, "0" }, lco::InsertAction::Ignore); }
	}

	void OralTest::benchBaselineUpdate ()
	{
		auto db = MakeDatabase ();
		const auto& adapted = Util::oral::Adapt<SimpleRecord> (db);
		adapted.DoInsert_ ({ 0, "0" });

		QSqlQuery query { db };
		query.prepare ("UPDATE SimpleRecord SET Value = :val WHERE Id = :id;");

		QBENCHMARK
		{
			query.bindValue (":id", 0);
			query.bindValue (":val", "1");
			query.exec ();
		}
	}

	void OralTest::benchSimpleRecordUpdate ()
	{
		auto db = MakeDatabase ();
		auto adapted = Util::oral::Adapt<SimpleRecord> (db);
		adapted.DoInsert_ ({ 0, "0" });

		QBENCHMARK { adapted.DoUpdate_ ({ 0, "1" }); }
	}
}
}
