/*
 * Copyright (C) 2014-2026 CZ.NIC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations including
 * the two.
 */

#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QSet>
#include <QSignalSpy>
#include <QString>
#include <QtTest/QtTest>

#include "src/datovka_shared/graphics/colour.h"
#include "src/datovka_shared/isds/types.h"
#include "src/datovka_shared/json/basic.h"
#include "src/datovka_shared/log/log.h"
#include "src/global.h"
#include "src/io/tag_db.h"
#include "src/json/tag_assignment.h"
#include "src/json/tag_assignment_hash.h"
#include "src/json/tag_entry.h"
#include "tests/test_tag_db.h"

static TagDb *tagDbPtr = Q_NULLPTR;

class TestTagDb : public QObject {
	Q_OBJECT

public:
	TestTagDb(void);

private slots:
	void initTestCase(void);

	void cleanupTestCase(void);

	void tagEntry(void);

	void tagEntryCompare(void);

	void tagEntryConstructor(void);

	void tagEntryCopy(void);

	void tagEntrySetValues(void);

	void tagEntryMisc(void);

	void tagAssignment(void);

	void tagAssignmentCompare(void);

	void tagAssignmentConstructor(void);

	void tagAssignmentCopy(void);

	void tagAssignmentSetvalues(void);

	void testTagDbInsert(void);

	void testTagDbUpdate(void);

	void testTagDbDelete(void);

	void testTagDbAssignmentChange(void);

private:
	void _init(void);

	void _cleanup(void);

	/*
	 * An application object must exist before any QObject.
	 * See https://doc.qt.io/qt-6/threads-qobject.html ,
	 * also see QTEST_GUILESS_MAIN().
	 */
	int m_argc;
	char **m_argv;
	QCoreApplication m_app;

	const QString m_connectionPrefix; /*!< SQL connection prefix. */
	const QString m_dbLocationPath; /*!< Directory path. */
	QDir m_dbDir;  /*!< Directory containing testing databases. */
	const QString m_dbName; /*!< Database file name. */
	const QString m_dbPath;
};

TestTagDb::TestTagDb(void)
    : m_argc(0), m_argv(NULL), m_app(m_argc, m_argv),
    m_connectionPrefix(QLatin1String("TAGDB")),
    m_dbLocationPath(QDir::currentPath() + QDir::separator() + QLatin1String("_db")),
    m_dbDir(m_dbLocationPath),
    m_dbName(QLatin1String("tag.db")),
    m_dbPath(m_dbLocationPath + QDir::separator() + m_dbName)
{
}

void TestTagDb::initTestCase(void)
{
	/*
	 * https://stackoverflow.com/questions/22044737/when-where-and-why-use-namespace-when-registering-custom-types-for-qt
	 */
	qRegisterMetaType<Json::Int64StringList>("Json::Int64StringList");
	qRegisterMetaType<Json::TagAssignmentList>("Json::TagAssignmentList");
	qRegisterMetaType<Json::TagEntryList>("Json::TagEntryList");

	QVERIFY(GlobInstcs::logPtr == Q_NULLPTR);
	GlobInstcs::logPtr = new (::std::nothrow) LogDevice;
	QVERIFY(GlobInstcs::logPtr != Q_NULLPTR);

	/* Pointer must be null before initialisation. */
	QVERIFY(tagDbPtr == Q_NULLPTR);

	QVERIFY(m_dbDir.removeRecursively());
	QVERIFY(!m_dbDir.exists());

	QVERIFY(m_dbDir.mkpath("."));
}

void TestTagDb::cleanupTestCase(void)
{
	QVERIFY(tagDbPtr == Q_NULLPTR);

	delete GlobInstcs::logPtr; GlobInstcs::logPtr = Q_NULLPTR;

	QVERIFY(m_dbDir.removeRecursively());
	QVERIFY(!m_dbDir.exists());
}

void TestTagDb::tagEntry(void)
{
	Json::TagEntry e1, e2;
	int id = 1;
	QString name("name");
	QString colour("000000");
	QString description("description");

	/* Must be null. */
	QVERIFY(e1.id() == -1);
	QVERIFY(e1.name().isNull());
	QVERIFY(e1.colour().isNull());
	QVERIFY(e1.description().isNull());
	QVERIFY(e2.id() == -1);
	QVERIFY(e2.name().isNull());
	QVERIFY(e2.colour().isNull());
	QVERIFY(e2.description().isNull());

	QVERIFY(e1.isNull());
	QVERIFY(e2.isNull());
	QVERIFY(e1 == e2);

	/* Check whether values are not cross-linked. */
	e1.setId(id);
	e1.setName(name);
	e1.setColour(colour);
	e1.setDescription(description);

	QVERIFY(e1.id() == id);
	QVERIFY(e1.name() == name);
	QVERIFY(e1.colour() == colour);
	QVERIFY(e1.description() == description);

	QVERIFY(!e1.isNull());
	QVERIFY(e2.isNull());
	QVERIFY(e1 != e2);

	/* Check value copying. */
	e2 = e1;

	QVERIFY(e2.id() == id);
	QVERIFY(e2.name() == name);
	QVERIFY(e2.colour() == colour);
	QVERIFY(e2.description() == description);

	QVERIFY(!e1.isNull());
	QVERIFY(!e2.isNull());
	QVERIFY(e1 == e2);

	/* Clear value. */
	e1 = Json::TagEntry();

	QVERIFY(e1.id() == -1);
	QVERIFY(e1.name().isNull());
	QVERIFY(e1.colour().isNull());
	QVERIFY(e1.description().isNull());
	QVERIFY(e2.id() == id);
	QVERIFY(e2.name() == name);
	QVERIFY(e2.colour() == colour);
	QVERIFY(e2.description() == description);

	QVERIFY(e1.isNull());
	QVERIFY(!e2.isNull());
	QVERIFY(e1 != e2);
}

void TestTagDb::tagEntryCompare(void)
{
	Json::TagEntry e1, e2;
	int id = 1;
	QString name("name");
	QString colour("000000");
	QString description("description");

	e1.setId(id);
	e1.setName(name);
	e1.setColour(colour);
	e1.setDescription(description);
	e2.setId(id);
	e2.setName(name);
	e2.setColour(colour);
	e2.setDescription(description);

	QVERIFY(e1 == e2);

	e2.setId(0);
	QVERIFY(e1 != e2);
	e2.setId(id);
	QVERIFY(e1 == e2);

	e2.setName("");
	QVERIFY(e1 != e2);
	e2.setName(name);
	QVERIFY(e1 == e2);

	e2.setColour("");
	QVERIFY(e1 != e2);
	e2.setColour(colour);
	QVERIFY(e1 == e2);

	e2.setDescription("");
	QVERIFY(e1 != e2);
	e2.setDescription(description);
	QVERIFY(e1 == e2);
}

void TestTagDb::tagEntryConstructor(void)
{
	const QString cName("name");
	const QString cColour("000000");
	const QString cDescription("description");

	{
		Json::TagEntry e;

		QVERIFY(e.isNull());
		QVERIFY(!e.isValid());

		QVERIFY(e.id() == -1);
		QVERIFY(e.name().isNull());
		QVERIFY(e.colour().isNull());
		QVERIFY(e.description().isNull());
	}

	{
		int id = 1;
		QString name(cName);
		QString colour(cColour);
		QString description(cDescription);
		Json::TagEntry e(id, name, colour, description);

		QVERIFY(!e.isNull());
		QVERIFY(e.isValid());

		QVERIFY(e.id() == id);
		QVERIFY(e.name() == name);
		QVERIFY(e.colour() == colour);
		QVERIFY(e.description() == description);

		QVERIFY(name == cName);
		QVERIFY(colour == cColour);
		QVERIFY(description == cDescription);
	}

#ifdef Q_COMPILER_RVALUE_REFS
	{
		int id = 1;
		QString name(cName);
		QString colour(cColour);
		QString description(cDescription);
		Json::TagEntry e(id, ::std::move(name), ::std::move(colour),
		    ::std::move(description));

		QVERIFY(!e.isNull());
		QVERIFY(e.isValid());

		QVERIFY(e.id() == id);
		QVERIFY(e.name() == cName);
		QVERIFY(e.colour() == cColour);
		QVERIFY(e.description() == cDescription);

		QVERIFY(name.isNull());
		QVERIFY(colour == "ffffff"); /* Constructor creates "ffffff" by default. */
		QVERIFY(description.isNull());
	}
#endif /* Q_COMPILER_RVALUE_REFS */
}

void TestTagDb::tagEntryCopy(void)
{
	Json::TagEntry e1;
	int id = 1;
	QString name("name");
	QString colour("000000");
	QString description("description");

	e1.setId(id);
	e1.setName(name);
	e1.setColour(colour);
	e1.setDescription(description);

	/* Constructor. */
	{
		Json::TagEntry e2(e1);

		QVERIFY(!e1.isNull());
		QVERIFY(!e2.isNull());
		QVERIFY(e1 == e2);

#ifdef Q_COMPILER_RVALUE_REFS
		/* Move. */
		Json::TagEntry e3(::std::move(e2));

		QVERIFY(e2.isNull());
		QVERIFY(e1 != e2);

		QVERIFY(!e1.isNull());
		QVERIFY(!e3.isNull());
		QVERIFY(e1 == e3);
#endif /* Q_COMPILER_RVALUE_REFS */
	}

	/* Assignment operator. */
	{
		Json::TagEntry e2;

		QVERIFY(e2.isNull());

		e2 = e1;

		QVERIFY(!e1.isNull());
		QVERIFY(!e2.isNull());
		QVERIFY(e1 == e2);

#ifdef Q_COMPILER_RVALUE_REFS
		/* Move. */
		Json::TagEntry e3;

		QVERIFY(e3.isNull());

		e3 = ::std::move(e2);

		QVERIFY(e2.isNull());
		QVERIFY(e1 != e2);

		QVERIFY(!e1.isNull());
		QVERIFY(!e3.isNull());
		QVERIFY(e1 == e3);
#endif /* Q_COMPILER_RVALUE_REFS */
	}
}

void TestTagDb::tagEntrySetValues(void)
{
	const QString cName("name");
	const QString cColour("000000");
	const QString cDescription("description");

	Json::TagEntry e;
	//int id = 1;
	QString name(cName);
	QString colour(cColour);
	QString description(cDescription);

	QVERIFY(e.name().isNull());
	e.setName(name);
	QVERIFY(e.name() == cName);
	QVERIFY(name == cName);
#ifdef Q_COMPILER_RVALUE_REFS
	e.setName("other_name");
	QVERIFY(e.name() != cName);
	e.setName(::std::move(name));
	QVERIFY(e.name() == cName);
	QVERIFY(name != cName);
#endif /* Q_COMPILER_RVALUE_REFS */

	QVERIFY(e.colour() == "ffffff");
	e.setColour(colour);
	QVERIFY(e.colour() == cColour);
	QVERIFY(colour == cColour);
#ifdef Q_COMPILER_RVALUE_REFS
	e.setColour("000001");
	QVERIFY(e.colour() != cColour);
	e.setColour(::std::move(colour));
	QVERIFY(e.colour() == cColour);
	QVERIFY(colour != cColour);
#endif /* Q_COMPILER_RVALUE_REFS */

	QVERIFY(e.description().isNull());
	e.setDescription(description);
	QVERIFY(e.description() == cDescription);
	QVERIFY(description == cDescription);
#ifdef Q_COMPILER_RVALUE_REFS
	e.setDescription("other_description");
	QVERIFY(e.description() != cDescription);
	e.setDescription(::std::move(description));
	QVERIFY(e.description() == cDescription);
	QVERIFY(description != cDescription);
#endif /* Q_COMPILER_RVALUE_REFS */
}

void TestTagDb::tagEntryMisc(void)
{
	QVERIFY(!Colour::isValidColourStr(QString()));
	QVERIFY(!Colour::isValidColourStr(""));
	QVERIFY(!Colour::isValidColourStr("0"));
	QVERIFY(!Colour::isValidColourStr("00"));
	QVERIFY(!Colour::isValidColourStr("000"));
	QVERIFY(!Colour::isValidColourStr("0000"));
	QVERIFY(!Colour::isValidColourStr("00000"));
	QVERIFY(Colour::isValidColourStr("000000"));
	QVERIFY(!Colour::isValidColourStr("0000000"));
	QVERIFY(!Colour::isValidColourStr("#000000"));

	QVERIFY(Colour::isValidColourStr("012345"));
	QVERIFY(Colour::isValidColourStr("6789ab"));
	QVERIFY(Colour::isValidColourStr("cdef00"));

	QVERIFY(Colour::isValidColourStr("000000"));
}

void TestTagDb::tagAssignment(void)
{
	Json::TagAssignment a1, a2;
	enum Isds::Type::NilBool testEnv = Isds::Type::BOOL_TRUE;
	qint64 dmId = 1;
	Json::TagEntryList entries({
	    Json::TagEntry(1, "name", "000000", "description")});

	/* Must be null. */
	QVERIFY(a1.testEnv() == Isds::Type::BOOL_NULL);
	QVERIFY(a1.dmId() == -1);
	QVERIFY(a1.entries().isEmpty());
	QVERIFY(a2.testEnv() == Isds::Type::BOOL_NULL);
	QVERIFY(a2.dmId() == -1);
	QVERIFY(a2.entries().isEmpty());

	QVERIFY(a1.isNull());
	QVERIFY(a2.isNull());
	QVERIFY(a1 == a2);

	/* Check whether values are not cross-linked. */
	a1.setTestEnv(testEnv);
	a1.setDmId(dmId);
	a1.setEntries(entries);

	QVERIFY(a1.testEnv() == testEnv);
	QVERIFY(a1.dmId() == dmId);
	QVERIFY(a1.entries() == entries);

	QVERIFY(!a1.isNull());
	QVERIFY(a2.isNull());
	QVERIFY(a1 != a2);

	/* Check value copying. */
	a2 = a1;

	QVERIFY(a2.testEnv() == testEnv);
	QVERIFY(a2.dmId() == dmId);
	QVERIFY(a2.entries() == entries);

	QVERIFY(!a1.isNull());
	QVERIFY(!a2.isNull());
	QVERIFY(a1 == a2);

	/* Clear value. */
	a1 = Json::TagAssignment();

	QVERIFY(a1.testEnv() == Isds::Type::BOOL_NULL);
	QVERIFY(a1.dmId() == -1);
	QVERIFY(a1.entries().isEmpty());
	QVERIFY(a2.testEnv() == testEnv);
	QVERIFY(a2.dmId() == dmId);
	QVERIFY(a2.entries() == entries);

	QVERIFY(a1.isNull());
	QVERIFY(!a2.isNull());
	QVERIFY(a1 != a2);
}

void TestTagDb::tagAssignmentCompare(void)
{
	Json::TagAssignment a1, a2;
	enum Isds::Type::NilBool testEnv = Isds::Type::BOOL_TRUE;
	qint64 dmId = 1;
	Json::TagEntryList entries({
	    Json::TagEntry(1, "name", "000000", "description")});

	a1.setTestEnv(testEnv);
	a1.setDmId(dmId);
	a1.setEntries(entries);
	a2.setTestEnv(testEnv);
	a2.setDmId(dmId);
	a2.setEntries(entries);

	QVERIFY(a1 == a2);

	a1.setTestEnv(Isds::Type::BOOL_FALSE);
	QVERIFY(a1 != a2);
	a1.setTestEnv(testEnv);
	QVERIFY(a1 == a2);

	a1.setDmId(0);
	QVERIFY(a1 != a2);
	a1.setDmId(dmId);
	QVERIFY(a1 == a2);

	a1.setEntries(Json::TagEntryList());
	QVERIFY(a1 != a2);
	a1.setEntries(entries);
	QVERIFY(a1 == a2);
}

void TestTagDb::tagAssignmentConstructor(void)
{
	const Json::TagEntryList cEntries({
	    Json::TagEntry(1, "name", "000000", "description")});

	{
		Json::TagAssignment a;

		QVERIFY(a.testEnv() == Isds::Type::BOOL_NULL);
		QVERIFY(a.dmId() == -1);
		QVERIFY(a.entries().isEmpty());
	}

	{
		enum Isds::Type::NilBool testEnv = Isds::Type::BOOL_TRUE;
		qint64 dmId = 1;
		Json::TagEntryList entries(cEntries);
		Json::TagAssignment a(testEnv, dmId, entries);

		QVERIFY(a.testEnv() == testEnv);
		QVERIFY(a.dmId() == dmId);
		QVERIFY(a.entries() == entries);

		QVERIFY(entries == cEntries);
	}

#ifdef Q_COMPILER_RVALUE_REFS
	{
		enum Isds::Type::NilBool testEnv = Isds::Type::BOOL_TRUE;
		qint64 dmId = 1;
		Json::TagEntryList entries(cEntries);
		Json::TagAssignment a(testEnv, dmId, ::std::move(entries));

		QVERIFY(a.testEnv() == testEnv);
		QVERIFY(a.dmId() == dmId);
		QVERIFY(a.entries() == cEntries);

		QVERIFY(entries.isEmpty());
	}
#endif /* Q_COMPILER_RVALUE_REFS */
}

void TestTagDb::tagAssignmentCopy(void)
{
	Json::TagAssignment a1;
	enum Isds::Type::NilBool testEnv = Isds::Type::BOOL_TRUE;
	qint64 dmId = 1;
	Json::TagEntryList entries({
	    Json::TagEntry(1, "name", "000000", "description")});

	a1.setTestEnv(testEnv);
	a1.setDmId(dmId);
	a1.setEntries(entries);

	/* Constructor. */
	{
		Json::TagAssignment a2(a1);

		QVERIFY(!a1.isNull());
		QVERIFY(!a2.isNull());
		QVERIFY(a1 == a2);

#ifdef Q_COMPILER_RVALUE_REFS
		/* Move. */
		Json::TagAssignment a3(::std::move(a2));

		QVERIFY(a2.isNull());
		QVERIFY(a1 != a2);

		QVERIFY(!a1.isNull());
		QVERIFY(!a3.isNull());
		QVERIFY(a1 == a3);
#endif /* Q_COMPILER_RVALUE_REFS */
	}

	/* Assignment operator. */
	{
		Json::TagAssignment a2;

		QVERIFY(a2.isNull());

		a2 = a1;

		QVERIFY(!a1.isNull());
		QVERIFY(!a2.isNull());
		QVERIFY(a1 == a2);

#ifdef Q_COMPILER_RVALUE_REFS
		/* Move. */
		Json::TagAssignment a3;

		QVERIFY(a3.isNull());

		a3 = ::std::move(a2);

		QVERIFY(a2.isNull());
		QVERIFY(a1 != a2);

		QVERIFY(!a1.isNull());
		QVERIFY(!a3.isNull());
		QVERIFY(a1 == a3);
#endif /* Q_COMPILER_RVALUE_REFS */
	}
}

void TestTagDb::tagAssignmentSetvalues(void)
{
	const Json::TagEntryList cEntries({
	    Json::TagEntry(1, "name", "000000", "description")});

	Json::TagAssignment a;
	//enum Isds::Type::NilBool testEnv = Isds::Type::BOOL_TRUE;
	//qint64 dmId = 1;
	Json::TagEntryList entries(cEntries);

	QVERIFY(a.entries().isEmpty());
	a.setEntries(entries);
	QVERIFY(a.entries() == cEntries);
	QVERIFY(entries == cEntries);
#ifdef Q_COMPILER_RVALUE_REFS
	a.setEntries(Json::TagEntryList({
	    Json::TagEntry(2, "name", "000000", "description")}));
	QVERIFY(a.entries() != cEntries);
	a.setEntries(::std::move(entries));
	QVERIFY(a.entries() == cEntries);
	QVERIFY(entries != cEntries);
#endif /* Q_COMPILER_RVALUE_REFS */
}

void TestTagDb::testTagDbInsert(void)
{
	bool ret = false;
	Json::TagEntry entry;
	Json::TagEntryList entries;
	qint64 tagId = -1;

	_init();

	tagDbPtr = new (::std::nothrow) TagDb(m_connectionPrefix, false);
	QVERIFY(tagDbPtr != Q_NULLPTR);
	QVERIFY(tagDbPtr->openDb(m_dbPath, SQLiteDb::CREATE_MISSING));
	QVERIFY(QFileInfo::exists(m_dbPath));

	QSignalSpy spyInserted(tagDbPtr, SIGNAL(tagsInserted(Json::TagEntryList)));
	QSignalSpy spyUpdated(tagDbPtr, SIGNAL(tagsUpdated(Json::TagEntryList)));
	QSignalSpy spyDeleted(tagDbPtr, SIGNAL(tagsDeleted(Json::Int64StringList)));
	QSignalSpy spyAssignmentChanged(tagDbPtr, SIGNAL(tagAssignmentChanged(Json::TagAssignmentList)));

	ret = tagDbPtr->getTagData(Json::Int64StringList({-1}), entries);
	QVERIFY(!ret);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	QVERIFY(entries.isEmpty());

	ret = tagDbPtr->getTagData(Json::Int64StringList({0}), entries);
	QVERIFY(!ret);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	QVERIFY(entries.isEmpty());

	ret = tagDbPtr->getTagData(Json::Int64StringList({1}), entries);
	QVERIFY(!ret);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	QVERIFY(entries.isEmpty());

	/* Null name should fail. */
	QVERIFY(!tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, QString(), "000000", "description")})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Empty name should fail. */
	QVERIFY(!tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "", "000000", "description")})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Null colour should fail. */
	QVERIFY(!tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag001", QString(), "description")})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Empty colour should fail. */
	QVERIFY(!tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag001", "", "description")})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	ret = tagDbPtr->getAllTags(entries);
	QVERIFY(ret);
	QCOMPARE(entries.size(), 0);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Inserting valid tag. */
	{
		QVERIFY(tagDbPtr->insertTags(Json::TagEntryList({
		    Json::TagEntry(-1, "tag001", "000000", QString())})));
		QCOMPARE(spyInserted.count(), 1);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QList<QVariant> arguments = spyInserted.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::TagEntryList>());
		const Json::TagEntryList entries =
		    qvariant_cast<Json::TagEntryList>(arguments.at(0));
		QCOMPARE(entries.size(), 1);
		const Json::TagEntry &entry = entries.at(0);
		QVERIFY(entry.id() >= 0);
		QCOMPARE(entry.name(), QString("tag001"));
		QCOMPARE(entry.colour(), QString("000000"));

		tagId = entry.id();
	}

	/* Re-inserting should fail. */
	QVERIFY(!tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag001", "000000", QString())})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Read tag data. */
	ret = tagDbPtr->getTagData(Json::Int64StringList({tagId}),
	    entries);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	QCOMPARE(entries.size(), 1);
	entry = entries.at(0);
	QCOMPARE(entry.id(), tagId);
	QCOMPARE(entry.name(), QString("tag001"));
	QCOMPARE(entry.colour(), QString("000000"));
	QCOMPARE(entry.description(), QString());

	entries.clear();
	ret = tagDbPtr->getAllTags(entries);
	QVERIFY(ret);
	QCOMPARE(entries.size(), 1);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	entry = entries.at(0);
	QVERIFY(entry.isValid());
	QCOMPARE(entry.id(), tagId);
	QCOMPARE(entry.name(), QString("tag001"));
	QCOMPARE(entry.colour(), QString("000000"));

	Json::TagIdToAssignmentCountHash counts;
	ret = tagDbPtr->getTagAssignmentCounts(
	    Json::Int64StringList({-1, 0}), counts);
	QVERIFY(ret);
	QVERIFY(!counts.contains(-1));
	QCOMPARE(counts[-1], 0);
	QVERIFY(!counts.contains(0));
	QCOMPARE(counts[0], 0);

	_cleanup();
}

void TestTagDb::testTagDbUpdate(void)
{
	bool ret = false;
	Json::TagEntry entry;
	Json::TagEntryList entries;
	qint64 tagId1 = -1;
	qint64 tagId2 = -1;

	_init();

	tagDbPtr = new (::std::nothrow) TagDb(m_connectionPrefix, false);
	QVERIFY(tagDbPtr != Q_NULLPTR);
	QVERIFY(tagDbPtr->openDb(m_dbPath, SQLiteDb::CREATE_MISSING));
	QVERIFY(QFileInfo::exists(m_dbPath));

	QSignalSpy spyInserted(tagDbPtr, SIGNAL(tagsInserted(Json::TagEntryList)));
	QSignalSpy spyUpdated(tagDbPtr, SIGNAL(tagsUpdated(Json::TagEntryList)));
	QSignalSpy spyDeleted(tagDbPtr, SIGNAL(tagsDeleted(Json::Int64StringList)));
	QSignalSpy spyAssignmentChanged(tagDbPtr, SIGNAL(tagAssignmentChanged(Json::TagAssignmentList)));

	QVERIFY(tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag001", "000000", "description001")})));
	QCOMPARE(spyInserted.count(), 1);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	tagId1 = qvariant_cast<Json::TagEntryList>(spyInserted.takeFirst().at(0)).at(0).id();

	QVERIFY(tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag002", "000000", "description002")})));
	QCOMPARE(spyInserted.count(), 1);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	tagId2 = qvariant_cast<Json::TagEntryList>(spyInserted.takeFirst().at(0)).at(0).id();

	/* Updating non-existent tag. */
	QVERIFY(!tagDbPtr->updateTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag001", "000000", "description001")})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Updating to null name. */
	QVERIFY(!tagDbPtr->updateTags(Json::TagEntryList({
	    Json::TagEntry(tagId1, QString(), "000000", "description001")})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Updating to empty name. */
	QVERIFY(!tagDbPtr->updateTags(Json::TagEntryList({
	    Json::TagEntry(tagId1, "", "000000", "description001")})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Updating to null colour. */
	QVERIFY(!tagDbPtr->updateTags(Json::TagEntryList({
	    Json::TagEntry(tagId1, "tag001", QString(), "description001")})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Updating to empty colour. */
	QVERIFY(!tagDbPtr->updateTags(Json::TagEntryList({
	    Json::TagEntry(tagId1, "tag001", "", "description001")})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Updating to same values. */
	QVERIFY(tagDbPtr->updateTags(Json::TagEntryList({
	    Json::TagEntry(tagId1, "tag001", "000000", "description001")})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Updating to other existent name. */
	QVERIFY(!tagDbPtr->updateTags(Json::TagEntryList({
	    Json::TagEntry(tagId1, "tag002", "000000", "description001")})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Updating to non-existent name. */
	{
		QVERIFY(tagDbPtr->updateTags(Json::TagEntryList({
		    Json::TagEntry(tagId1, "tag003", "000000", "description001")})));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 1);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QList<QVariant> arguments = spyUpdated.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::TagEntryList>());
		const Json::TagEntryList entries =
		    qvariant_cast<Json::TagEntryList>(arguments.at(0));
		QCOMPARE(entries.size(), 1);
		const Json::TagEntry &entry = entries.at(0);
		QVERIFY(entry.id() == tagId1);
		QCOMPARE(entry.name(), QString("tag003"));
		QCOMPARE(entry.colour(), QString("000000"));
		QCOMPARE(entry.description(), QString("description001"));
	}

	/* Updating to non-existent name and changing colour. */
	{
		QVERIFY(tagDbPtr->updateTags(Json::TagEntryList({
		    Json::TagEntry(tagId1, "tag001", "000001", "description001")})));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 1);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QList<QVariant> arguments = spyUpdated.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::TagEntryList>());
		const Json::TagEntryList entries =
		    qvariant_cast<Json::TagEntryList>(arguments.at(0));
		QCOMPARE(entries.size(), 1);
		const Json::TagEntry &entry = entries.at(0);
		QVERIFY(entry.id() == tagId1);
		QCOMPARE(entry.name(), QString("tag001"));
		QCOMPARE(entry.colour(), QString("000001"));
		QCOMPARE(entry.description(), QString("description001"));
	}

	/* Change description to null string. */
	{
		QVERIFY(tagDbPtr->updateTags(Json::TagEntryList({
		    Json::TagEntry(tagId1, "tag001", "000001", QString())})));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 1);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QList<QVariant> arguments = spyUpdated.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::TagEntryList>());
		const Json::TagEntryList entries =
		    qvariant_cast<Json::TagEntryList>(arguments.at(0));
		QCOMPARE(entries.size(), 1);
		const Json::TagEntry &entry = entries.at(0);
		QVERIFY(entry.id() == tagId1);
		QCOMPARE(entry.name(), QString("tag001"));
		QCOMPARE(entry.colour(), QString("000001"));
		QCOMPARE(entry.description(), QString());
	}

	/* Change description to non-empty string. */
	{
		QVERIFY(tagDbPtr->updateTags(Json::TagEntryList({
		    Json::TagEntry(tagId1, "tag001", "000001", "description001")})));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 1);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QList<QVariant> arguments = spyUpdated.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::TagEntryList>());
		const Json::TagEntryList entries =
		    qvariant_cast<Json::TagEntryList>(arguments.at(0));
		QCOMPARE(entries.size(), 1);
		const Json::TagEntry &entry = entries.at(0);
		QVERIFY(entry.id() == tagId1);
		QCOMPARE(entry.name(), QString("tag001"));
		QCOMPARE(entry.colour(), QString("000001"));
		QCOMPARE(entry.description(), QString("description001"));
	}

	/* Change description to empty string. */
	{
		QVERIFY(tagDbPtr->updateTags(Json::TagEntryList({
		    Json::TagEntry(tagId1, "tag001", "000001", "")})));
		QCOMPARE(QString(), QString(""));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 1);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QList<QVariant> arguments = spyUpdated.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::TagEntryList>());
		const Json::TagEntryList entries =
		    qvariant_cast<Json::TagEntryList>(arguments.at(0));
		QCOMPARE(entries.size(), 1);
		const Json::TagEntry &entry = entries.at(0);
		QVERIFY(entry.id() == tagId1);
		QCOMPARE(entry.name(), QString("tag001"));
		QCOMPARE(entry.colour(), QString("000001"));
		QCOMPARE(entry.description(), QString(""));
	}

	/* Changing colour. */
	{
		QVERIFY(tagDbPtr->updateTags(Json::TagEntryList({
		    Json::TagEntry(tagId1, "tag001", "000000", "description001")})));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 1);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QList<QVariant> arguments = spyUpdated.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::TagEntryList>());
		const Json::TagEntryList entries =
		    qvariant_cast<Json::TagEntryList>(arguments.at(0));
		QCOMPARE(entries.size(), 1);
		const Json::TagEntry &entry = entries.at(0);
		QVERIFY(entry.id() == tagId1);
		QCOMPARE(entry.name(), QString("tag001"));
		QCOMPARE(entry.colour(), QString("000000"));
		QCOMPARE(entry.description(), QString("description001"));
	}

	/* Read tag data. */
	entries.clear();
	ret = tagDbPtr->getTagData(Json::Int64StringList({tagId1}), entries);
	QVERIFY(ret);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	QCOMPARE(entries.size(), 1);
	entry = entries.at(0);
	QVERIFY(entry.isValid());
	QCOMPARE(entry.id(), tagId1);
	QCOMPARE(entry.name(), QString("tag001"));
	QCOMPARE(entry.colour(), QString("000000"));
	QCOMPARE(entry.description(), QString("description001"));

	entries.clear();
	ret = tagDbPtr->getTagData(Json::Int64StringList({tagId2}), entries);
	QVERIFY(ret);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	QCOMPARE(entries.size(), 1);
	entry = entries.at(0);
	QVERIFY(entry.isValid());
	QCOMPARE(entry.id(), tagId2);
	QCOMPARE(entry.name(), QString("tag002"));
	QCOMPARE(entry.colour(), QString("000000"));
	QCOMPARE(entry.description(), QString("description002"));

	entries.clear();
	ret = tagDbPtr->getAllTags(entries);
	QVERIFY(ret);
	QCOMPARE(entries.size(), 2);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	_cleanup();
}

void TestTagDb::testTagDbDelete(void)
{
	bool ret = false;
	Json::TagEntry entry;
	Json::TagEntryList entries;
	qint64 tagId1 = -1;
	qint64 tagId2 = -1;
	qint64 tagId3 = -1;

	_init();

	tagDbPtr = new (::std::nothrow) TagDb(m_connectionPrefix, false);
	QVERIFY(tagDbPtr != Q_NULLPTR);
	QVERIFY(tagDbPtr->openDb(m_dbPath, SQLiteDb::CREATE_MISSING));
	QVERIFY(QFileInfo::exists(m_dbPath));

	QSignalSpy spyInserted(tagDbPtr, SIGNAL(tagsInserted(Json::TagEntryList)));
	QSignalSpy spyUpdated(tagDbPtr, SIGNAL(tagsUpdated(Json::TagEntryList)));
	QSignalSpy spyDeleted(tagDbPtr, SIGNAL(tagsDeleted(Json::Int64StringList)));
	QSignalSpy spyAssignmentChanged(tagDbPtr, SIGNAL(tagAssignmentChanged(Json::TagAssignmentList)));

	/* Deleting empty database. */
	QVERIFY(!tagDbPtr->deleteTags(Json::Int64StringList({1})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Populate. */
	QVERIFY(tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag001", "000000", "description001")})));
	QCOMPARE(spyInserted.count(), 1);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	tagId1 = qvariant_cast<Json::TagEntryList>(spyInserted.takeFirst().at(0)).at(0).id();

	QVERIFY(tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag002", "000000", "description002")})));
	QCOMPARE(spyInserted.count(), 1);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	tagId2 = qvariant_cast<Json::TagEntryList>(spyInserted.takeFirst().at(0)).at(0).id();

	QVERIFY(tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag003", "000000", "description003")})));
	QCOMPARE(spyInserted.count(), 1);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	tagId3 = qvariant_cast<Json::TagEntryList>(spyInserted.takeFirst().at(0)).at(0).id();

	ret = tagDbPtr->getAllTags(entries);
	QVERIFY(ret);
	QCOMPARE(entries.size(), 3);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Deleting non-existent tags. */
	QVERIFY(!tagDbPtr->deleteTags(Json::Int64StringList()));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	QVERIFY(!tagDbPtr->deleteTags(Json::Int64StringList({100, 101})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Deleting non-existent and existing tags. */
	{
		QVERIFY(tagDbPtr->deleteTags(Json::Int64StringList({tagId3, 101})));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 1);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QList<QVariant> arguments = spyDeleted.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::Int64StringList>());
		const Json::Int64StringList ids =
		    qvariant_cast<Json::Int64StringList>(arguments.at(0));
		QCOMPARE(ids.size(), 1);
		QCOMPARE(ids.at(0), tagId3);
	}

	entries.clear();
	ret = tagDbPtr->getAllTags(entries);
	QVERIFY(ret);
	QCOMPARE(entries.size(), 2);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Repopulate. */
	QVERIFY(tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag003", "000000", "description003")})));
	QCOMPARE(spyInserted.count(), 1);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	tagId3 = qvariant_cast<Json::TagEntryList>(spyInserted.takeFirst().at(0)).at(0).id();

	entries.clear();
	ret = tagDbPtr->getAllTags(entries);
	QVERIFY(ret);
	QCOMPARE(entries.size(), 3);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Deleting existing tags. */
	{
		QVERIFY(tagDbPtr->deleteTags(Json::Int64StringList({tagId2, tagId3})));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 1);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QList<QVariant> arguments = spyDeleted.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::Int64StringList>());
		const Json::Int64StringList ids =
		    qvariant_cast<Json::Int64StringList>(arguments.at(0));
		QCOMPARE(ids.size(), 2);
		QSet<qint64> deletedSet({ids.at(0), ids.at(1)});
		QCOMPARE(deletedSet.size(), 2);
		QVERIFY(deletedSet.contains(tagId2));
		QVERIFY(deletedSet.contains(tagId3));
	}

	entries.clear();
	ret = tagDbPtr->getAllTags(entries);
	QVERIFY(ret);
	QCOMPARE(entries.size(), 1);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Repopulate. */
	QVERIFY(tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag002", "000000", "description002")})));
	QCOMPARE(spyInserted.count(), 1);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	tagId2 = qvariant_cast<Json::TagEntryList>(spyInserted.takeFirst().at(0)).at(0).id();

	QVERIFY(tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag003", "000000", "description003")})));
	QCOMPARE(spyInserted.count(), 1);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	tagId3 = qvariant_cast<Json::TagEntryList>(spyInserted.takeFirst().at(0)).at(0).id();

	entries.clear();
	ret = tagDbPtr->getAllTags(entries);
	QVERIFY(ret);
	QCOMPARE(entries.size(), 3);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Deleting all existing tags by listing. */
	{
		/* Intentionally repeating one identifier. */
		QVERIFY(tagDbPtr->deleteTags(Json::Int64StringList({tagId1, tagId1, tagId2, tagId3})));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 1);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QList<QVariant> arguments = spyDeleted.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::Int64StringList>());
		const Json::Int64StringList ids =
		    qvariant_cast<Json::Int64StringList>(arguments.at(0));
		QCOMPARE(ids.size(), 3);
		QSet<qint64> deletedSet({ids.at(0), ids.at(1), ids.at(2)});
		QCOMPARE(deletedSet.size(), 3);
		QVERIFY(deletedSet.contains(tagId1));
		QVERIFY(deletedSet.contains(tagId2));
		QVERIFY(deletedSet.contains(tagId3));
	}

	entries.clear();
	ret = tagDbPtr->getAllTags(entries);
	QVERIFY(ret);
	QCOMPARE(entries.size(), 0);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Repopulate. */
	QVERIFY(tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag001", "000000", "description001")})));
	QCOMPARE(spyInserted.count(), 1);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	tagId1 = qvariant_cast<Json::TagEntryList>(spyInserted.takeFirst().at(0)).at(0).id();

	QVERIFY(tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag002", "000000", "description002")})));
	QCOMPARE(spyInserted.count(), 1);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	tagId2 = qvariant_cast<Json::TagEntryList>(spyInserted.takeFirst().at(0)).at(0).id();

	QVERIFY(tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag003", "000000", "description003")})));
	QCOMPARE(spyInserted.count(), 1);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	tagId3 = qvariant_cast<Json::TagEntryList>(spyInserted.takeFirst().at(0)).at(0).id();

	entries.clear();
	ret = tagDbPtr->getAllTags(entries);
	QVERIFY(ret);
	QCOMPARE(entries.size(), 3);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Deleting all existing tags by listing. */
	{
		entries.clear();
		ret = tagDbPtr->getAllTags(entries);
		QVERIFY(ret);
		Json::Int64StringList toBeDeletedIds;
		for (const Json::TagEntry &entry : entries) {
			toBeDeletedIds.append(entry.id());
		}
		QCOMPARE(entries.size(), toBeDeletedIds.size());

		QVERIFY(tagDbPtr->deleteTags(toBeDeletedIds));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 1);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QList<QVariant> arguments = spyDeleted.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::Int64StringList>());
		const Json::Int64StringList ids =
		    qvariant_cast<Json::Int64StringList>(arguments.at(0));
		QCOMPARE(ids.size(), toBeDeletedIds.size());
		QCOMPARE(ids.size(), 3);
		QSet<qint64> deletedSet({ids.at(0), ids.at(1), ids.at(2)});
		QCOMPARE(deletedSet.size(), 3);
		QVERIFY(deletedSet.contains(tagId1));
		QVERIFY(deletedSet.contains(tagId2));
		QVERIFY(deletedSet.contains(tagId3));
	}

	entries.clear();
	ret = tagDbPtr->getAllTags(entries);
	QVERIFY(ret);
	QCOMPARE(entries.size(), 0);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	_cleanup();
}

void TestTagDb::testTagDbAssignmentChange(void)
{
	bool ret = false;
	Json::TagEntry entry;
	Json::TagIdToAssignmentCountHash counts;
	Json::TagAssignmentHash assignments;
	Json::TagAssignmentCommand command;
	qint64 tagId1 = -1;
	qint64 tagId2 = -1;
	qint64 tagId3 = -1;

	_init();

	tagDbPtr = new (::std::nothrow) TagDb(m_connectionPrefix, false);
	QVERIFY(tagDbPtr != Q_NULLPTR);
	QVERIFY(tagDbPtr->openDb(m_dbPath, SQLiteDb::CREATE_MISSING));
	QVERIFY(QFileInfo::exists(m_dbPath));

	QSignalSpy spyInserted(tagDbPtr, SIGNAL(tagsInserted(Json::TagEntryList)));
	QSignalSpy spyUpdated(tagDbPtr, SIGNAL(tagsUpdated(Json::TagEntryList)));
	QSignalSpy spyDeleted(tagDbPtr, SIGNAL(tagsDeleted(Json::Int64StringList)));
	QSignalSpy spyAssignmentChanged(tagDbPtr, SIGNAL(tagAssignmentChanged(Json::TagAssignmentList)));

	/* Deleting empty database. */
	QVERIFY(!tagDbPtr->deleteTags(Json::Int64StringList({1})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Populate. */
	QVERIFY(tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag001", "000001", "description001")})));
	QCOMPARE(spyInserted.count(), 1);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	tagId1 = qvariant_cast<Json::TagEntryList>(spyInserted.takeFirst().at(0)).at(0).id();

	QVERIFY(tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag002", "000002", "description002")})));
	QCOMPARE(spyInserted.count(), 1);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	tagId2 = qvariant_cast<Json::TagEntryList>(spyInserted.takeFirst().at(0)).at(0).id();

	QVERIFY(tagDbPtr->insertTags(Json::TagEntryList({
	    Json::TagEntry(-1, "tag003", "000003", "description003")})));
	QCOMPARE(spyInserted.count(), 1);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);
	tagId3 = qvariant_cast<Json::TagEntryList>(spyInserted.takeFirst().at(0)).at(0).id();

	{
		Json::TagEntryList entries;
		ret = tagDbPtr->getAllTags(entries);
		QVERIFY(ret);
		QCOMPARE(entries.size(), 3);
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);
	}

	/* Check assignment. */
	counts.clear();
	QVERIFY(tagDbPtr->getTagAssignmentCounts(Json::Int64StringList({-1}), counts));
	QCOMPARE(counts.size(), 0);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	counts.clear();
	QVERIFY(tagDbPtr->getTagAssignmentCounts(Json::Int64StringList({tagId1}), counts));
	QCOMPARE(counts.size(), 0);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	counts.clear();
	QVERIFY(tagDbPtr->getTagAssignmentCounts(Json::Int64StringList({tagId2}), counts));
	QCOMPARE(counts.size(), 0);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	counts.clear();
	QVERIFY(tagDbPtr->getTagAssignmentCounts(Json::Int64StringList({tagId3}), counts));
	QCOMPARE(counts.size(), 0);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	assignments.clear();
	QVERIFY(!tagDbPtr->getMessageTags(Json::MsgId1List(), assignments));
	QCOMPARE(assignments.size(), 0);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	assignments.clear();
	QVERIFY(!tagDbPtr->getMessageTags(
	    Json::MsgId1List({Json::MsgId1()}), assignments));
	QCOMPARE(assignments.size(), 0);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Returns empty list. */
	{
		const Json::MsgId1 msgId(Isds::Type::BOOL_TRUE, 1001);
		assignments.clear();
		QVERIFY(tagDbPtr->getMessageTags(
		    Json::MsgId1List({msgId}), assignments));
		QCOMPARE(assignments.size(), 1);
		QVERIFY(assignments.contains(msgId));
		QVERIFY(assignments[msgId].isEmpty());
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);
	}

	/* Nothing assigned. */
	QVERIFY(tagDbPtr->removeAllTagsFromMsgs(
	    Json::MsgId1List({Json::MsgId1(Isds::Type::BOOL_TRUE, 1001)})));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	{
		const Json::MsgId1 msgId(Isds::Type::BOOL_TRUE, 1001);
		assignments.clear();
		QVERIFY(tagDbPtr->getMessageTags(
		    Json::MsgId1List({msgId}), assignments));
		QCOMPARE(assignments.size(), 1);
		QVERIFY(assignments.contains(msgId));
		QVERIFY(assignments[msgId].isEmpty());
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);
	}

	/* Assign non-existent tags. */
	command.clear();
	command[Json::MsgId1(Isds::Type::BOOL_TRUE, 1001)] = Json::Int64StringList({100, 101});
	QVERIFY(!tagDbPtr->assignTagsToMsgs(command));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	{
		const Json::MsgId1 msgId(Isds::Type::BOOL_TRUE, 1001);
		assignments.clear();
		QVERIFY(tagDbPtr->getMessageTags(
		    Json::MsgId1List({msgId}), assignments));
		QCOMPARE(assignments.size(), 1);
		QVERIFY(assignments.contains(msgId));
		QVERIFY(assignments[msgId].isEmpty());
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);
	}

	/* Assign existent and non-existent tags. */
	command.clear();
	command[Json::MsgId1(Isds::Type::BOOL_TRUE, 1001)] = Json::Int64StringList({tagId1, 101});
	QVERIFY(!tagDbPtr->assignTagsToMsgs(command));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	{
		const Json::MsgId1 msgId(Isds::Type::BOOL_TRUE, 1001);
		assignments.clear();
		QVERIFY(tagDbPtr->getMessageTags(
		    Json::MsgId1List({msgId}), assignments));
		QCOMPARE(assignments.size(), 1);
		QVERIFY(assignments.contains(msgId));
		QVERIFY(assignments[msgId].isEmpty());
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);
	}

	/* Assign existent tags. */
	{
		command.clear();
		command[Json::MsgId1(Isds::Type::BOOL_TRUE, 1001)] = Json::Int64StringList({tagId1, tagId2});
		QVERIFY(tagDbPtr->assignTagsToMsgs(command));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 1);

		QList<QVariant> arguments = spyAssignmentChanged.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::TagAssignmentList>());
		const Json::TagAssignmentList assignments =
		    qvariant_cast<Json::TagAssignmentList>(arguments.at(0));
		QCOMPARE(assignments.size(), 1);

		const Json::TagAssignment &assignment = assignments.at(0);
		QCOMPARE(assignment.testEnv(), Isds::Type::BOOL_TRUE);
		QCOMPARE(assignment.dmId(), 1001);
		const Json::TagEntryList &entries = assignment.entries();
		QCOMPARE(entries.size(), 2);
		if (entries.at(0).id() == tagId1) {
			QCOMPARE(entries.at(0).id(), tagId1);
			QCOMPARE(entries.at(0).name(), QString("tag001"));
			QCOMPARE(entries.at(0).colour(), QString("000001"));
			QCOMPARE(entries.at(0).description(), QString("description001"));
			QCOMPARE(entries.at(1).id(), tagId2);
			QCOMPARE(entries.at(1).name(), QString("tag002"));
			QCOMPARE(entries.at(1).colour(), QString("000002"));
			QCOMPARE(entries.at(1).description(), QString("description002"));
		} else if (entries.at(0).id() == tagId2) {
			QCOMPARE(entries.at(1).id(), tagId1);
			QCOMPARE(entries.at(1).name(), QString("tag001"));
			QCOMPARE(entries.at(1).colour(), QString("000001"));
			QCOMPARE(entries.at(1).description(), QString("description001"));
			QCOMPARE(entries.at(0).id(), tagId2);
			QCOMPARE(entries.at(0).name(), QString("tag002"));
			QCOMPARE(entries.at(0).colour(), QString("000002"));
			QCOMPARE(entries.at(0).description(), QString("description002"));
		} else {
			QVERIFY(false);
		}
	}

	{
		const Json::MsgId1 msgId(Isds::Type::BOOL_TRUE, 1001);
		assignments.clear();
		QVERIFY(tagDbPtr->getMessageTags(
		    Json::MsgId1List({msgId}), assignments));
		QCOMPARE(assignments.size(), 1);
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QVERIFY(assignments.contains(msgId));
		const Json::TagEntryList &entries = assignments[msgId];
		QCOMPARE(entries.size(), 2);
		if (entries.at(0).id() == tagId1) {
			QCOMPARE(entries.at(0).id(), tagId1);
			QCOMPARE(entries.at(0).name(), QString("tag001"));
			QCOMPARE(entries.at(0).colour(), QString("000001"));
			QCOMPARE(entries.at(1).id(), tagId2);
			QCOMPARE(entries.at(1).name(), QString("tag002"));
			QCOMPARE(entries.at(1).colour(), QString("000002"));
		} else if (entries.at(0).id() == tagId2) {
			QCOMPARE(entries.at(1).id(), tagId1);
			QCOMPARE(entries.at(1).name(), QString("tag001"));
			QCOMPARE(entries.at(1).colour(), QString("000001"));
			QCOMPARE(entries.at(0).id(), tagId2);
			QCOMPARE(entries.at(0).name(), QString("tag002"));
			QCOMPARE(entries.at(0).colour(), QString("000002"));
		} else {
			QVERIFY(false);
		}
	}

	/* Assign already assigned tags. */
	command.clear();
	command[Json::MsgId1(Isds::Type::BOOL_TRUE, 1001)] = Json::Int64StringList({tagId1, tagId2});
	QVERIFY(tagDbPtr->assignTagsToMsgs(command));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	{
		const Json::MsgId1 msgId(Isds::Type::BOOL_TRUE, 1001);
		assignments.clear();
		QVERIFY(tagDbPtr->getMessageTags(
		    Json::MsgId1List({msgId}), assignments));
		QCOMPARE(assignments.size(), 1);
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QVERIFY(assignments.contains(msgId));
		const Json::TagEntryList &entries = assignments[msgId];
		QCOMPARE(entries.size(), 2);
	}

	/* Assign new existent and a non-existent tag. */
	command.clear();
	command[Json::MsgId1(Isds::Type::BOOL_TRUE, 1001)] = Json::Int64StringList({tagId3, 101});
	QVERIFY(!tagDbPtr->assignTagsToMsgs(command));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	{
		const Json::MsgId1 msgId(Isds::Type::BOOL_TRUE, 1001);
		assignments.clear();
		QVERIFY(tagDbPtr->getMessageTags(
		    Json::MsgId1List({msgId}), assignments));
		QCOMPARE(assignments.size(), 1);
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QVERIFY(assignments.contains(msgId));
		const Json::TagEntryList &entries = assignments[msgId];
		QCOMPARE(entries.size(), 2);
	}

	/* Remove unassigned tags. */
	command.clear();
	command[Json::MsgId1(Isds::Type::BOOL_TRUE, 1001)] = Json::Int64StringList({100, 101});
	QVERIFY(tagDbPtr->removeTagsFromMsgs(command));
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	{
		const Json::MsgId1 msgId(Isds::Type::BOOL_TRUE, 1001);
		assignments.clear();
		QVERIFY(tagDbPtr->getMessageTags(
		    Json::MsgId1List({msgId}), assignments));
		QCOMPARE(assignments.size(), 1);
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QVERIFY(assignments.contains(msgId));
		const Json::TagEntryList &entries = assignments[msgId];
		QCOMPARE(entries.size(), 2);
	}

	/* Remove unassigned and assigned tags. */
	{
		command.clear();
		command[Json::MsgId1(Isds::Type::BOOL_TRUE, 1001)] = Json::Int64StringList({tagId2, 101});
		QVERIFY(tagDbPtr->removeTagsFromMsgs(command));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 1);

		QList<QVariant> arguments = spyAssignmentChanged.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::TagAssignmentList>());
		const Json::TagAssignmentList assignments =
		    qvariant_cast<Json::TagAssignmentList>(arguments.at(0));
		QCOMPARE(assignments.size(), 1);

		const Json::TagAssignment &assignment = assignments.at(0);
		QCOMPARE(assignment.testEnv(), Isds::Type::BOOL_TRUE);
		QCOMPARE(assignment.dmId(), 1001);
		const Json::TagEntryList &entries = assignment.entries();
		QCOMPARE(entries.size(), 1);
		QCOMPARE(entries.at(0).id(), tagId1);
		QCOMPARE(entries.at(0).name(), QString("tag001"));
		QCOMPARE(entries.at(0).colour(), QString("000001"));
		QCOMPARE(entries.at(0).description(), QString("description001"));
	}

	{
		const Json::MsgId1 msgId(Isds::Type::BOOL_TRUE, 1001);
		assignments.clear();
		QVERIFY(tagDbPtr->getMessageTags(
		    Json::MsgId1List({msgId}), assignments));
		QCOMPARE(assignments.size(), 1);
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QVERIFY(assignments.contains(msgId));
		const Json::TagEntryList &entries = assignments[msgId];
		QCOMPARE(entries.size(), 1);
	}

	/* Assign existent assigned and unassigned tags. */
	{
		command.clear();
		command[Json::MsgId1(Isds::Type::BOOL_TRUE, 1001)] = Json::Int64StringList({tagId1, tagId2});
		QVERIFY(tagDbPtr->assignTagsToMsgs(command));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 1);

		QList<QVariant> arguments = spyAssignmentChanged.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::TagAssignmentList>());
		const Json::TagAssignmentList assignments =
		    qvariant_cast<Json::TagAssignmentList>(arguments.at(0));
		QCOMPARE(assignments.size(), 1);

		const Json::TagAssignment &assignment = assignments.at(0);
		QCOMPARE(assignment.testEnv(), Isds::Type::BOOL_TRUE);
		QCOMPARE(assignment.dmId(), 1001);
		const Json::TagEntryList &entries = assignment.entries();
		QCOMPARE(entries.size(), 2);
		if (entries.at(0).id() == tagId1) {
			QCOMPARE(entries.at(0).id(), tagId1);
			QCOMPARE(entries.at(0).name(), QString("tag001"));
			QCOMPARE(entries.at(0).colour(), QString("000001"));
			QCOMPARE(entries.at(0).description(), QString("description001"));
			QCOMPARE(entries.at(1).id(), tagId2);
			QCOMPARE(entries.at(1).name(), QString("tag002"));
			QCOMPARE(entries.at(1).colour(), QString("000002"));
			QCOMPARE(entries.at(1).description(), QString("description002"));
		} else if (entries.at(0).id() == tagId2) {
			QCOMPARE(entries.at(1).id(), tagId1);
			QCOMPARE(entries.at(1).name(), QString("tag001"));
			QCOMPARE(entries.at(1).colour(), QString("000001"));
			QCOMPARE(entries.at(1).description(), QString("description001"));
			QCOMPARE(entries.at(0).id(), tagId2);
			QCOMPARE(entries.at(0).name(), QString("tag002"));
			QCOMPARE(entries.at(0).colour(), QString("000002"));
			QCOMPARE(entries.at(0).description(), QString("description002"));
		} else {
			QVERIFY(false);
		}
	}

	{
		command.clear();
		command[Json::MsgId1(Isds::Type::BOOL_TRUE, 1002)] = Json::Int64StringList({tagId2});
		QVERIFY(tagDbPtr->assignTagsToMsgs(command));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 1);

		QList<QVariant> arguments = spyAssignmentChanged.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::TagAssignmentList>());
		const Json::TagAssignmentList assignments =
		    qvariant_cast<Json::TagAssignmentList>(arguments.at(0));
		QCOMPARE(assignments.size(), 1);

		const Json::TagAssignment &assignment = assignments.at(0);
		QCOMPARE(assignment.testEnv(), Isds::Type::BOOL_TRUE);
		QCOMPARE(assignment.dmId(), 1002);
		const Json::TagEntryList &entries = assignment.entries();
		QCOMPARE(entries.size(), 1);
		QCOMPARE(entries.at(0).id(), tagId2);
		QCOMPARE(entries.at(0).name(), QString("tag002"));
		QCOMPARE(entries.at(0).colour(), QString("000002"));
		QCOMPARE(entries.at(0).description(), QString("description002"));
	}

	{
		command.clear();
		command[Json::MsgId1(Isds::Type::BOOL_TRUE, 1003)] = Json::Int64StringList({tagId3});
		QVERIFY(tagDbPtr->assignTagsToMsgs(command));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 1);

		QList<QVariant> arguments = spyAssignmentChanged.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::TagAssignmentList>());
		const Json::TagAssignmentList assignments =
		    qvariant_cast<Json::TagAssignmentList>(arguments.at(0));
		QCOMPARE(assignments.size(), 1);

		const Json::TagAssignment &assignment = assignments.at(0);
		QCOMPARE(assignment.testEnv(), Isds::Type::BOOL_TRUE);
		QCOMPARE(assignment.dmId(), 1003);
		const Json::TagEntryList &entries = assignment.entries();
		QCOMPARE(entries.size(), 1);
		QCOMPARE(entries.at(0).id(), tagId3);
		QCOMPARE(entries.at(0).name(), QString("tag003"));
		QCOMPARE(entries.at(0).colour(), QString("000003"));
		QCOMPARE(entries.at(0).description(), QString("description003"));
	}

	{
		const Json::MsgId1 msgId(Isds::Type::BOOL_TRUE, 1001);
		assignments.clear();
		QVERIFY(tagDbPtr->getMessageTags(
		    Json::MsgId1List({msgId}), assignments));
		QCOMPARE(assignments.size(), 1);
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QVERIFY(assignments.contains(msgId));
		const Json::TagEntryList &entries = assignments[msgId];
		QCOMPARE(entries.size(), 2);
		if (entries.at(0).id() == tagId1) {
			QCOMPARE(entries.at(0).id(), tagId1);
			QCOMPARE(entries.at(0).name(), QString("tag001"));
			QCOMPARE(entries.at(0).colour(), QString("000001"));
			QCOMPARE(entries.at(0).description(), QString("description001"));
			QCOMPARE(entries.at(1).id(), tagId2);
			QCOMPARE(entries.at(1).name(), QString("tag002"));
			QCOMPARE(entries.at(1).colour(), QString("000002"));
			QCOMPARE(entries.at(1).description(), QString("description002"));
		} else if (entries.at(0).id() == tagId2) {
			QCOMPARE(entries.at(1).id(), tagId1);
			QCOMPARE(entries.at(1).name(), QString("tag001"));
			QCOMPARE(entries.at(1).colour(), QString("000001"));
			QCOMPARE(entries.at(1).description(), QString("description001"));
			QCOMPARE(entries.at(0).id(), tagId2);
			QCOMPARE(entries.at(0).name(), QString("tag002"));
			QCOMPARE(entries.at(0).colour(), QString("000002"));
			QCOMPARE(entries.at(0).description(), QString("description002"));
		} else {
			QVERIFY(false);
		}
	}

	{
		const Json::MsgId1 msgId(Isds::Type::BOOL_TRUE, 1002);
		assignments.clear();
		QVERIFY(tagDbPtr->getMessageTags(
		    Json::MsgId1List({msgId}), assignments));
		QCOMPARE(assignments.size(), 1);
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		const Json::TagEntryList &entries = assignments[msgId];
		QCOMPARE(entries.at(0).id(), tagId2);
		QCOMPARE(entries.at(0).name(), QString("tag002"));
		QCOMPARE(entries.at(0).colour(), QString("000002"));
		QCOMPARE(entries.at(0).description(), QString("description002"));
	}

	{
		const Json::MsgId1 msgId(Isds::Type::BOOL_TRUE, 1003);
		assignments.clear();
		QVERIFY(tagDbPtr->getMessageTags(
		    Json::MsgId1List({msgId}), assignments));
		QCOMPARE(assignments.size(), 1);
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		const Json::TagEntryList &entries = assignments[msgId];
		QCOMPARE(entries.at(0).id(), tagId3);
		QCOMPARE(entries.at(0).name(), QString("tag003"));
		QCOMPARE(entries.at(0).colour(), QString("000003"));
		QCOMPARE(entries.at(0).description(), QString("description003"));
	}

	counts.clear();
	ret = tagDbPtr->getTagAssignmentCounts(
	    Json::Int64StringList({tagId1}), counts);
	QVERIFY(ret);
	QCOMPARE(counts.size(), 1);
	counts.clear();
	ret = tagDbPtr->getTagAssignmentCounts(
	    Json::Int64StringList({tagId2}), counts);
	QVERIFY(ret);
	QCOMPARE(counts.size(), 1);
	counts.clear();
	ret = tagDbPtr->getTagAssignmentCounts(
	    Json::Int64StringList({tagId3}), counts);
	QVERIFY(ret);
	QCOMPARE(counts.size(), 1);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Remove all tags from a single message. */
	{
		QVERIFY(tagDbPtr->removeAllTagsFromMsgs(
		    Json::MsgId1List({Json::MsgId1(Isds::Type::BOOL_TRUE, 1002)})));
		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 0);
		QCOMPARE(spyAssignmentChanged.count(), 1);

		QList<QVariant> arguments = spyAssignmentChanged.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::TagAssignmentList>());
		const Json::TagAssignmentList assignments =
		    qvariant_cast<Json::TagAssignmentList>(arguments.at(0));
		QCOMPARE(assignments.size(), 1);

		const Json::TagAssignment &assignment = assignments.at(0);
		QCOMPARE(assignment.testEnv(), Isds::Type::BOOL_TRUE);
		QCOMPARE(assignment.dmId(), 1002);
		const Json::TagEntryList &entries = assignment.entries();
		QCOMPARE(entries.size(), 0);
	}

	counts.clear();
	ret = tagDbPtr->getTagAssignmentCounts(
	    Json::Int64StringList({tagId1}), counts);
	QVERIFY(ret);
	QCOMPARE(counts.size(), 1);
	counts.clear();
	ret = tagDbPtr->getTagAssignmentCounts(
	    Json::Int64StringList({tagId2}), counts);
	QVERIFY(ret);
	QCOMPARE(counts.size(), 1);
	counts.clear();
	ret = tagDbPtr->getTagAssignmentCounts(
	    Json::Int64StringList({tagId3}), counts);
	QVERIFY(ret);
	QCOMPARE(counts.size(), 1);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	/* Remove all remaining tags from the messages. */
	{
		Json::TagEntryList entries;
		ret = tagDbPtr->getAllTags(entries);
		QVERIFY(ret);
		Json::Int64StringList toBeDeletedIds;
		for (const Json::TagEntry &entry : entries) {
			toBeDeletedIds.append(entry.id());
		}
		QCOMPARE(entries.size(), toBeDeletedIds.size());

		QVERIFY(tagDbPtr->deleteTags(toBeDeletedIds));

		QCOMPARE(spyInserted.count(), 0);
		QCOMPARE(spyUpdated.count(), 0);
		QCOMPARE(spyDeleted.count(), 1);
		QCOMPARE(spyAssignmentChanged.count(), 0);

		QList<QVariant> arguments = spyDeleted.takeFirst();
		QCOMPARE(arguments.size(), 1);
		QVERIFY(arguments.at(0).canConvert<Json::Int64StringList>());
		const Json::Int64StringList ids =
		    qvariant_cast<Json::Int64StringList>(arguments.at(0));
		QCOMPARE(ids.size(), 3);

		QSet<qint64> deletedSet({ids.at(0), ids.at(1), ids.at(2)});
		QCOMPARE(deletedSet.size(), 3);
		QVERIFY(deletedSet.contains(tagId1));
		QVERIFY(deletedSet.contains(tagId2));
		QVERIFY(deletedSet.contains(tagId3));
	}

	counts.clear();
	ret = tagDbPtr->getTagAssignmentCounts(
	    Json::Int64StringList({tagId1}), counts);
	QVERIFY(ret);
	QCOMPARE(counts.size(), 0);
	counts.clear();
	ret = tagDbPtr->getTagAssignmentCounts(
	    Json::Int64StringList({tagId2}), counts);
	QVERIFY(ret);
	QCOMPARE(counts.size(), 0);
	counts.clear();
	ret = tagDbPtr->getTagAssignmentCounts(
	    Json::Int64StringList({tagId3}), counts);
	QVERIFY(ret);
	QCOMPARE(counts.size(), 0);
	QCOMPARE(spyInserted.count(), 0);
	QCOMPARE(spyUpdated.count(), 0);
	QCOMPARE(spyDeleted.count(), 0);
	QCOMPARE(spyAssignmentChanged.count(), 0);

	_cleanup();
}

void TestTagDb::_init(void)
{
	QVERIFY(tagDbPtr == Q_NULLPTR);

	QFile(m_dbPath).remove();

	QVERIFY(!QFileInfo::exists(m_dbPath));
}

void TestTagDb::_cleanup(void)
{
	delete tagDbPtr; tagDbPtr = Q_NULLPTR;
}

QObject *newTestTagDb(void)
{
	return new (::std::nothrow) TestTagDb();
}

//QTEST_MAIN(TestTagDb)
#include "test_tag_db.moc"
