/*!
 * \brief Unit tests for \ref RemoteTlsServer
 *
 * \copyright Copyright (c) 2017-2020 Governikus GmbH & Co. KG, Germany
 */

#include "RemoteTlsServer.h"

#include "AppSettings.h"
#include "KeyPair.h"
#include "SecureStorage.h"

#include <QSslCipher>
#include <QSslSocket>
#include <QtTest>

using namespace governikus;

Q_DECLARE_METATYPE(SecureStorage::TlsSuite)

class test_RemoteTlsServer
	: public QObject
{
	Q_OBJECT

	private:
		QByteArray psk;
		const KeyPair pair = KeyPair::generate();

	private Q_SLOTS:
		void checkFailingConnectionOnDifferentMode_data()
		{
			QTest::addColumn<bool>("serverPairing");
			QTest::addColumn<SecureStorage::TlsSuite>("clientConfig");

			QTest::newRow("server in pairing mode") << true << SecureStorage::TlsSuite::DEFAULT;
			QTest::newRow("client in pairing mode") << false << SecureStorage::TlsSuite::PSK;
		}


		void checkFailingConnectionOnDifferentMode()
		{
			QFETCH(bool, serverPairing);
			QFETCH(SecureStorage::TlsSuite, clientConfig);

			auto& settings = Env::getSingleton<AppSettings>()->getRemoteServiceSettings();
			settings.setTrustedCertificates({pair.getCertificate()});
			RemoteTlsServer server;
			server.setPairing(serverPairing);
			server.listen();

			auto config = Env::getSingleton<SecureStorage>()->getTlsConfigRemote(clientConfig).getConfiguration();
			config.setPrivateKey(pair.getKey());
			config.setLocalCertificate(pair.getCertificate());

			QSslSocket client;
			client.setSslConfiguration(config);

			qRegisterMetaType<QSslPreSharedKeyAuthenticator>("QSslPreSharedKeyAuthenticator*");
			QSignalSpy clientPsk(&client, &QSslSocket::preSharedKeyAuthenticationRequired);
			QSignalSpy clientEncrypted(&client, &QSslSocket::encrypted);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
			QSignalSpy clientErrors(&client, &QAbstractSocket::errorOccurred);
#else
			QSignalSpy clientErrors(&client, QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::error));
#endif
			client.connectToHostEncrypted(QHostAddress(QHostAddress::LocalHost).toString(), server.serverPort());

			QTRY_COMPARE(clientErrors.count(), 1); // clazy:exclude=qstring-allocations
			QCOMPARE(clientErrors.takeFirst().at(0).value<QAbstractSocket::SocketError>(), QAbstractSocket::SocketError::RemoteHostClosedError);
			QCOMPARE(clientEncrypted.count(), 0);
			QCOMPARE(clientPsk.count(), 0);
		}


		void requiredPskForPairing()
		{
			RemoteTlsServer server;
			QCOMPARE(server.getCurrentCertificate(), QSslCertificate());
			connect(&server, &RemoteTlsServer::firePskChanged, this, [this](const QByteArray& pPsk){
						psk = pPsk;
					});
			server.setPairing();
			server.listen();

			auto config = Env::getSingleton<SecureStorage>()->getTlsConfigRemote(SecureStorage::TlsSuite::PSK).getConfiguration();
			config.setPrivateKey(pair.getKey());
			config.setLocalCertificate(pair.getCertificate());

			bool pskSignalFired = false;
			QSslSocket client;
			client.setSslConfiguration(config);
			connect(&client, QOverload<const QList<QSslError>&>::of(&QSslSocket::sslErrors), this, [&](const QList<QSslError>& pErrors){
						QList<QSslError> ignoreList;
						for (const auto& error : pErrors)
						{
							if (error.error() == QSslError::SelfSignedCertificate ||
							error.error() == QSslError::HostNameMismatch)
							{
								ignoreList << error;
							}
						}
						client.ignoreSslErrors(ignoreList);
					});

			connect(&client, &QSslSocket::preSharedKeyAuthenticationRequired, this, [&](QSslPreSharedKeyAuthenticator* pAuthenticator){
						pAuthenticator->setPreSharedKey(psk);
						pskSignalFired = true;
					});

			QTcpSocket* remoteSocket = nullptr;
			connect(&server, &RemoteTlsServer::newConnection, this, [&](QTcpSocket* pSocket){
						remoteSocket = pSocket;
					});

			QSignalSpy newConnection(&server, &RemoteTlsServer::newConnection);
			QSignalSpy clientEncrypted(&client, &QSslSocket::encrypted);

			client.connectToHostEncrypted(QHostAddress(QHostAddress::LocalHost).toString(), server.serverPort());

			QTRY_COMPARE(newConnection.count(), 1); // clazy:exclude=qstring-allocations
			QVERIFY(pskSignalFired);
			QTRY_COMPARE(clientEncrypted.count(), 1); // clazy:exclude=qstring-allocations

			QVERIFY(remoteSocket);
			const QByteArray sendData("hello world");
			QSignalSpy spyRead(remoteSocket, &QIODevice::readyRead);
			client.write(sendData);
			QTRY_COMPARE(spyRead.count(), 1); // clazy:exclude=qstring-allocations
			QCOMPARE(remoteSocket->readAll(), sendData);
			QCOMPARE(server.getCurrentCertificate(), pair.getCertificate());
		}


		void tryReconnectWithoutPairedDeviceAndReconnectedPaired()
		{
			auto& settings = Env::getSingleton<AppSettings>()->getRemoteServiceSettings();
			settings.setTrustedCertificates({});
			RemoteTlsServer server;
			server.listen();
			QSignalSpy newConnection(&server, &RemoteTlsServer::newConnection);

			auto config = Env::getSingleton<SecureStorage>()->getTlsConfigRemote().getConfiguration();
			config.setPrivateKey(pair.getKey());
			config.setLocalCertificate(pair.getCertificate());

			{
				QSslSocket client;
				client.setSslConfiguration(config);
				const QList<QSslError> errors({QSslError(QSslError::SelfSignedCertificate), QSslError(QSslError::HostNameMismatch)});
				client.ignoreSslErrors(errors);

				QSignalSpy clientFailed(&client, &QAbstractSocket::disconnected);
				client.connectToHostEncrypted(QHostAddress(QHostAddress::LocalHost).toString(), server.serverPort());
				QTRY_COMPARE(clientFailed.count(), 1); // clazy:exclude=qstring-allocations
				QCOMPARE(newConnection.count(), 0);
			}

			QSslSocket clientPaired;
			config.setCaCertificates({settings.getCertificate()});
			clientPaired.setSslConfiguration(config);
			connect(&clientPaired, QOverload<const QList<QSslError>&>::of(&QSslSocket::sslErrors), this, [&](const QList<QSslError>& pErrors){
						QList<QSslError> ignoreList;
						for (const auto& error : pErrors)
						{
							if (error.error() == QSslError::HostNameMismatch)
							{
								ignoreList << error;
							}
						}
						clientPaired.ignoreSslErrors(ignoreList);
					});
			settings.addTrustedCertificate(config.localCertificate());

			QSignalSpy clientPairedEncrypted(&clientPaired, &QSslSocket::encrypted);
			clientPaired.connectToHostEncrypted(QHostAddress(QHostAddress::LocalHost).toString(), server.serverPort());
			QTRY_COMPARE(newConnection.count(), 1); // clazy:exclude=qstring-allocations
			QTRY_COMPARE(clientPairedEncrypted.count(), 1); // clazy:exclude=qstring-allocations
		}


		void pairDeviceAndReconnect()
		{
			auto& settings = Env::getSingleton<AppSettings>()->getRemoteServiceSettings();
			settings.setTrustedCertificates({});
			requiredPskForPairing();
			QVERIFY(settings.checkAndGenerateKey());

			RemoteTlsServer server;
			server.listen();

			auto config = Env::getSingleton<SecureStorage>()->getTlsConfigRemote().getConfiguration();
			config.setPrivateKey(pair.getKey());
			config.setLocalCertificate(pair.getCertificate());
			config.setCaCertificates({settings.getCertificate()});

			QSslSocket client;
			client.setSslConfiguration(config);
			connect(&client, QOverload<const QList<QSslError>&>::of(&QSslSocket::sslErrors), this, [&](const QList<QSslError>& pErrors){
						QList<QSslError> ignoreList;
						for (const auto& error : pErrors)
						{
							if (error.error() == QSslError::HostNameMismatch)
							{
								ignoreList << error;
							}
						}
						client.ignoreSslErrors(ignoreList);
					});

			QSignalSpy clientEncrypted(&client, &QSslSocket::encrypted);
			QSignalSpy newConnection(&server, &RemoteTlsServer::newConnection);

			client.connectToHostEncrypted(QHostAddress(QHostAddress::LocalHost).toString(), server.serverPort());
			QTRY_COMPARE(newConnection.count(), 1); // clazy:exclude=qstring-allocations
			QTRY_COMPARE(clientEncrypted.count(), 1); // clazy:exclude=qstring-allocations
		}


		void setPairing()
		{
			RemoteTlsServer server;
			QSignalSpy pskChanged(&server, &RemoteTlsServer::firePskChanged);

			server.setPairing(false);
			QTRY_COMPARE(pskChanged.count(), 0); // clazy:exclude=qstring-allocations

			server.setPairing();
			QTRY_COMPARE(pskChanged.count(), 1); // clazy:exclude=qstring-allocations

			server.setPairing();
			QTRY_COMPARE(pskChanged.count(), 2); // clazy:exclude=qstring-allocations

			server.setPairing(false);
			QTRY_COMPARE(pskChanged.count(), 3); // clazy:exclude=qstring-allocations

			server.setPairing(false);
			QTRY_COMPARE(pskChanged.count(), 3); // clazy:exclude=qstring-allocations
		}


		void checkPskSize()
		{
			RemoteTlsServer server;
			QSignalSpy pskChanged(&server, &RemoteTlsServer::firePskChanged);
			for (int i = 0; i < 100; ++i)
			{
				server.setPairing();
				QTRY_COMPARE(pskChanged.count(), 1); // clazy:exclude=qstring-allocations
				const auto& pin = pskChanged.takeFirst().at(0).toByteArray();
				QCOMPARE(pin.size(), 4);
			}
		}


};


QTEST_GUILESS_MAIN(test_RemoteTlsServer)
#include "test_RemoteTlsServer.moc"
