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

#include "RemoteConnectorImpl.h"

#include "AppSettings.h"
#include "Env.h"
#include "KeyPair.h"
#include "messages/Discovery.h"
#include "RemoteWebSocketServer.h"
#include "SecureStorage.h"

#include <QtTest>
#include <QWebSocket>
#include <QWebSocketServer>

#include <functional>

using namespace governikus;

Q_DECLARE_METATYPE(RemoteDeviceDescriptor)
Q_DECLARE_METATYPE(QSharedPointer<RemoteDispatcher>)
Q_DECLARE_METATYPE(RemoteErrorCode)


class test_RemoteConnector
	: public QObject
{
	Q_OBJECT

	private:
		void sendRequest(const QSharedPointer<RemoteConnectorImpl>& pConnector,
			const QHostAddress& pHostAddress,
			const Discovery& pDiscovery,
			const QString& pPassword)
		{
			const RemoteDeviceDescriptor descr(pDiscovery, pHostAddress);
			QMetaObject::invokeMethod(pConnector.data(), [ = ] {
						pConnector->onConnectRequest(descr, pPassword);
					}, Qt::QueuedConnection);
		}


		void verifySuccessSignal(const QSignalSpy& pSignalSpy,
			const quint16 pPort)
		{
			const static QHostAddress HOST_ADDRESS(QHostAddress::LocalHost);
			const static QString IFD_NAME("Smartphone1");

			bool signalFound = false;
			for (const QList<QVariant>& arguments : pSignalSpy)
			{
				const QVariant remoteDeviceDescriptorVariant = arguments.at(0);
				QVERIFY(remoteDeviceDescriptorVariant.canConvert<RemoteDeviceDescriptor>());
				const RemoteDeviceDescriptor descr = remoteDeviceDescriptorVariant.value<RemoteDeviceDescriptor>();
				QVERIFY(!descr.isNull());

				const QVariant dispatcherVariant = arguments.at(1);
				QVERIFY(dispatcherVariant.canConvert<QSharedPointer<RemoteDispatcherClient> >());
				const QSharedPointer<RemoteDispatcherClient> dispatcher = dispatcherVariant.value<QSharedPointer<RemoteDispatcherClient> >();
				QVERIFY(dispatcher);

				const QUrl remoteUrl = descr.getUrl();
				const QString remoteAddress = remoteUrl.host();
				const int remotePort = remoteUrl.port();
				const bool signalMatches = remoteAddress == HOST_ADDRESS.toString() && remotePort == pPort &&
						descr.getIfdName() == IFD_NAME;
				if (signalMatches)
				{
					qDebug() << "Success verified";
					signalFound = true;
					break;
				}
			}
			QVERIFY(signalFound);
		}


		void verifyErrorSignal(const QSignalSpy& pSignalSpy,
			const QList<RemoteErrorCode>& pRemoteErrorCodeList,
			const quint16 pPort,
			const QString& pIfdName,
			const bool pExpectingNullDeviceDescriptor = false)
		{
			const static QHostAddress HOST_ADDRESS(QHostAddress::LocalHost);

			bool signalFound = false;
			for (const QList<QVariant>& arguments : pSignalSpy)
			{
				const QVariant remoteDeviceDescriptorVariant = arguments.at(0);
				QVERIFY(remoteDeviceDescriptorVariant.canConvert<RemoteDeviceDescriptor>());
				const RemoteDeviceDescriptor descr = remoteDeviceDescriptorVariant.value<RemoteDeviceDescriptor>();

				if (pExpectingNullDeviceDescriptor && descr.isNull())
				{
					qDebug() << "Error verified";
					signalFound = true;
					break;
				}
				QVERIFY(!descr.isNull());

				const QVariant errorCodeVariant = arguments.at(1);
				QVERIFY(errorCodeVariant.canConvert<RemoteErrorCode>());
				const auto errorCode = errorCodeVariant.value<RemoteErrorCode>();

				const QUrl remoteUrl = descr.getUrl();
				const QString remoteAddress = remoteUrl.host();
				const int remotePort = remoteUrl.port();
				const bool signalMatches = remoteAddress == HOST_ADDRESS.toString() &&
						remotePort == pPort &&
						descr.getIfdName() == pIfdName &&
						pRemoteErrorCodeList.contains(errorCode);
				if (signalMatches)
				{
					qDebug() << "Error verified";
					signalFound = true;
					break;
				}
			}
			QVERIFY(signalFound);
		}

	private Q_SLOTS:
		void initTestCase()
		{
			qRegisterMetaType<QWebSocketProtocol::CloseCode>("QWebSocketProtocol::CloseCode");
		}


		void requestWithNoDeviceNameIsRejected()
		{
			QThread clientThread;

			const QSharedPointer<RemoteConnectorImpl> connector(new RemoteConnectorImpl());
			QSignalSpy spyError(connector.data(), &RemoteConnector::fireRemoteDispatcherError);
			QSignalSpy spySuccess(connector.data(), &RemoteConnector::fireRemoteDispatcherCreated);

			connector->moveToThread(&clientThread);
			clientThread.start();

			// No device name.
			const QHostAddress hostAddress(QHostAddress::LocalHost);
			const Discovery discoveryMsg(QString(), QStringLiteral("0123456789ABCDEF"), 2020, {IfdVersion::Version::v0});
			sendRequest(connector, hostAddress, discoveryMsg, QString());
			QTRY_COMPARE(spyError.count(), 1); // clazy:exclude=qstring-allocations

			clientThread.exit();
			QVERIFY(clientThread.wait());

			verifyErrorSignal(spyError, {RemoteErrorCode::INVALID_REQUEST}, 2020, QString());
			QCOMPARE(spySuccess.count(), 0);
		}


		void requestWithNoDeviceInfoIsRejected()
		{
			QThread clientThread;

			const QSharedPointer<RemoteConnectorImpl> connector(new RemoteConnectorImpl());
			QSignalSpy spyError(connector.data(), &RemoteConnector::fireRemoteDispatcherError);
			QSignalSpy spySuccess(connector.data(), &RemoteConnector::fireRemoteDispatcherCreated);

			connector->moveToThread(&clientThread);
			clientThread.start();

			// Device information is null.
			const QHostAddress hostAddress(QHostAddress::LocalHost);
			sendRequest(connector, hostAddress, Discovery(QJsonObject()), QStringLiteral("secret"));
			QTRY_COMPARE(spyError.count(), 1); // clazy:exclude=qstring-allocations

			clientThread.exit();
			QVERIFY(clientThread.wait());

			verifyErrorSignal(spyError, {RemoteErrorCode::INVALID_REQUEST}, 0, QString(), true);
			QCOMPARE(spySuccess.count(), 0);
		}


		void requestWithEncryptedServerAndReconnectFails()
		{
			QScopedPointer<RemoteWebSocketServer> server(Env::create<RemoteWebSocketServer*>());
			server->listen("dummy");
			QThread clientThread;

			const QSharedPointer<RemoteConnectorImpl> connector(new RemoteConnectorImpl());
			QSignalSpy spyError(connector.data(), &RemoteConnector::fireRemoteDispatcherError);
			QSignalSpy spySuccess(connector.data(), &RemoteConnector::fireRemoteDispatcherCreated);

			connector->moveToThread(&clientThread);
			clientThread.start();

			// Password is empty.
			const QHostAddress hostAddress(QHostAddress::LocalHost);
			const Discovery discoveryMsg(QStringLiteral("Smartphone1"), QStringLiteral("0123456789ABCDEF"), server->getServerPort(), {IfdVersion::Version::v0});
			sendRequest(connector, hostAddress, discoveryMsg, QString());
			QTRY_COMPARE(spyError.count(), 1); // clazy:exclude=qstring-allocations

			clientThread.exit();
			QVERIFY(clientThread.wait());

			verifyErrorSignal(spyError, {RemoteErrorCode::REMOTE_HOST_REFUSED_CONNECTION}, server->getServerPort(), QStringLiteral("Smartphone1"));
			QCOMPARE(spySuccess.count(), 0);
		}


		void requestWithUnsupportedApiLevelIsRejected()
		{
			QThread clientThread;

			const QSharedPointer<RemoteConnectorImpl> connector(new RemoteConnectorImpl());
			QSignalSpy spyError(connector.data(), &RemoteConnector::fireRemoteDispatcherError);
			QSignalSpy spySuccess(connector.data(), &RemoteConnector::fireRemoteDispatcherCreated);

			connector->moveToThread(&clientThread);
			clientThread.start();

			// Currently, only API level 1 is supported.
			const QHostAddress hostAddress(QHostAddress::LocalHost);
			const Discovery discoveryMsg(QStringLiteral("Smartphone1"), QStringLiteral("0123456789ABCDEF"), 2020, {IfdVersion::Version::v_test});
			sendRequest(connector, hostAddress, discoveryMsg, QStringLiteral("secret"));
			QTRY_COMPARE(spyError.count(), 1); // clazy:exclude=qstring-allocations

			clientThread.exit();
			QVERIFY(clientThread.wait());

			verifyErrorSignal(spyError, {RemoteErrorCode::NO_SUPPORTED_API_LEVEL}, 2020, QStringLiteral("Smartphone1"));
			QCOMPARE(spySuccess.count(), 0);
		}


		void noServerResponseCausesConnectionErrorOrTimeout()
		{
			QThread clientThread;

			const QSharedPointer<RemoteConnectorImpl> connector(new RemoteConnectorImpl(2500));
			QSignalSpy spyError(connector.data(), &RemoteConnector::fireRemoteDispatcherError);
			QSignalSpy spySuccess(connector.data(), &RemoteConnector::fireRemoteDispatcherCreated);

			connector->moveToThread(&clientThread);
			clientThread.start();

			// Correct request but no server is running.
			const QHostAddress hostAddress(QHostAddress::LocalHost);
			const Discovery discoveryMsg(QStringLiteral("Smartphone1"), QStringLiteral("0123456789ABCDEF"), 2020, {IfdVersion::Version::v0});
			sendRequest(connector, hostAddress, discoveryMsg, QString("dummy"));
			QTRY_COMPARE(spyError.count(), 1); // clazy:exclude=qstring-allocations

			clientThread.exit();
			QVERIFY(clientThread.wait());

#if !defined(Q_OS_FREEBSD) && !defined(Q_OS_WIN)
			verifyErrorSignal(spyError, {RemoteErrorCode::CONNECTION_ERROR}, 2020, QStringLiteral("Smartphone1"));
#else
			verifyErrorSignal(spyError, {RemoteErrorCode::CONNECTION_TIMEOUT, RemoteErrorCode::CONNECTION_ERROR}, 2020, QStringLiteral("Smartphone1"));
#endif

			QCOMPARE(spySuccess.count(), 0);
		}


		void encryptedConnectionSucceeds_data()
		{
			QTest::addColumn<QString>("psk");
			QTest::addColumn<QSslConfiguration>("sslConfigServer");
			QTest::addColumn<QList<QSslCertificate> >("trustedCertificates");

			auto& settings = Env::getSingleton<AppSettings>()->getRemoteServiceSettings();
			QVERIFY(settings.checkAndGenerateKey());

			const KeyPair pair = KeyPair::generate();
			QVERIFY(pair.isValid());

			QSslConfiguration config = Env::getSingleton<SecureStorage>()->getTlsConfigRemote().getConfiguration();
			config.setPrivateKey(pair.getKey());
			config.setLocalCertificate(pair.getCertificate());
			config.setCaCertificates({KeyPair::generate().getCertificate(), settings.getCertificate(), KeyPair::generate().getCertificate()});
			QTest::newRow("paired") << QString() << config << QList<QSslCertificate>({KeyPair::generate().getCertificate(), pair.getCertificate(), KeyPair::generate().getCertificate()});

			config = Env::getSingleton<SecureStorage>()->getTlsConfigRemote(SecureStorage::TlsSuite::PSK).getConfiguration();
			config.setPrivateKey(pair.getKey());
			config.setLocalCertificate(pair.getCertificate());
			QTest::newRow("unpaired") << QString("123456") << config << QList<QSslCertificate>();
		}


		void encryptedConnectionSucceeds()
		{
			QFETCH(QString, psk);
			QFETCH(QSslConfiguration, sslConfigServer);
			QFETCH(QList<QSslCertificate>, trustedCertificates);

			QWebSocketServer webSocketServer(QStringLiteral("Smartphone1"), QWebSocketServer::SecureMode);
			connect(&webSocketServer, &QWebSocketServer::preSharedKeyAuthenticationRequired, this, [&](QSslPreSharedKeyAuthenticator* pAuthenticator){
						pAuthenticator->setPreSharedKey(psk.toLatin1());
					});
			QSignalSpy spySocketError(&webSocketServer, &QWebSocketServer::serverError);
			QSignalSpy spySocketSuccess(&webSocketServer, &QWebSocketServer::newConnection);

			auto& settings = Env::getSingleton<AppSettings>()->getRemoteServiceSettings();
			settings.setTrustedCertificates(trustedCertificates);
			webSocketServer.setSslConfiguration(sslConfigServer);

			// Listening with proxy leads to socket error QNativeSocketEnginePrivate::InvalidProxyTypeString
			webSocketServer.setProxy(QNetworkProxy(QNetworkProxy::NoProxy));
			webSocketServer.listen(QHostAddress(QHostAddress::LocalHost));
			const quint16 serverPort = webSocketServer.serverPort();

			// Set up client thread.
			QThread clientThread;

			// Execute test in internal block so that all relevant smart pointers are released before stopping the client thread.
			QSharedPointer<QSignalSpy> remoteDispatcherDestructionSpy;
			{
				const QSharedPointer<RemoteConnectorImpl> connector(new RemoteConnectorImpl());
				QSignalSpy spyConnectorError(connector.data(), &RemoteConnector::fireRemoteDispatcherError);
				QSignalSpy spyConnectorSuccess(connector.data(), &RemoteConnector::fireRemoteDispatcherCreated);

				connector->moveToThread(&clientThread);
				clientThread.start();

				// Send valid encrypted connect request.
				const QHostAddress hostAddress(QHostAddress::LocalHost);
				const Discovery discoveryMsg(QStringLiteral("Smartphone1"), QStringLiteral("0123456789ABCDEF"), serverPort, {IfdVersion::Version::v0});
				sendRequest(connector, hostAddress, discoveryMsg, psk);

				QTRY_COMPARE(spyConnectorSuccess.count(), 1); // clazy:exclude=qstring-allocations
				QCOMPARE(spyConnectorError.count(), 0);
				verifySuccessSignal(spyConnectorSuccess, serverPort);

				const QVariant dispatcherVariant = spyConnectorSuccess.first().at(1);
				QVERIFY(dispatcherVariant.canConvert<QSharedPointer<RemoteDispatcherClient> >());
				const QSharedPointer<RemoteDispatcherClient> dispatcher = dispatcherVariant.value<QSharedPointer<RemoteDispatcherClient> >();
				remoteDispatcherDestructionSpy.reset(new QSignalSpy(dispatcher.data(), &QObject::destroyed));
			}

			QCOMPARE(spySocketError.count(), 0);
			QCOMPARE(spySocketSuccess.count(), 1);

			QTRY_COMPARE(remoteDispatcherDestructionSpy->count(), 1); // clazy:exclude=qstring-allocations
			QCOMPARE(remoteDispatcherDestructionSpy->count(), 1);

			clientThread.exit();
			QVERIFY(clientThread.wait());

			// Make sure that no pending web socket events are in the event queue when the test completes.
			// Pending events were occasionally delivered after the web socket had been destroyed, leading to a crash.
			QCoreApplication::processEvents();
		}


		void encryptedConnectionWithWrongPasswordFails()
		{
			QWebSocketServer webSocketServer(QStringLiteral("Smartphone1"), QWebSocketServer::SecureMode);
			QObject::connect(&webSocketServer, &QWebSocketServer::preSharedKeyAuthenticationRequired, [](QSslPreSharedKeyAuthenticator* pAuthenticator){
						pAuthenticator->setPreSharedKey(QByteArray("secret"));
					});
			QSignalSpy spySocketError(&webSocketServer, &QWebSocketServer::serverError);
			QSignalSpy spySocketSuccess(&webSocketServer, &QWebSocketServer::newConnection);

			QSslConfiguration config = Env::getSingleton<SecureStorage>()->getTlsConfigRemote().getConfiguration();
			const KeyPair pair = KeyPair::generate();
			config.setPrivateKey(pair.getKey());
			config.setLocalCertificate(pair.getCertificate());
			webSocketServer.setSslConfiguration(config);

			// Listening with proxy leads to socket error QNativeSocketEnginePrivate::InvalidProxyTypeString
			webSocketServer.setProxy(QNetworkProxy(QNetworkProxy::NoProxy));
			webSocketServer.listen(QHostAddress(QHostAddress::LocalHost));
			const quint16 serverPort = webSocketServer.serverPort();

			// Set up client thread.
			QThread clientThread;

			const QSharedPointer<RemoteConnectorImpl> connector(new RemoteConnectorImpl());
			QSignalSpy spyConnectorError(connector.data(), &RemoteConnector::fireRemoteDispatcherError);
			QSignalSpy spyConnectorSuccess(connector.data(), &RemoteConnector::fireRemoteDispatcherCreated);

			connector->moveToThread(&clientThread);
			clientThread.start();

			// Send encrypted connect request with wrong psk.
			const QHostAddress hostAddress(QHostAddress::LocalHost);
			const Discovery discoveryMsg(QStringLiteral("Smartphone1"), QStringLiteral("0123456789ABCDEF"), serverPort, {IfdVersion::Version::v0});
			sendRequest(connector, hostAddress, discoveryMsg, QStringLiteral("sekret"));

			QTRY_COMPARE(spyConnectorError.count(), 1); // clazy:exclude=qstring-allocations
			QCOMPARE(spyConnectorSuccess.count(), 0);
			verifyErrorSignal(spyConnectorError, {RemoteErrorCode::REMOTE_HOST_REFUSED_CONNECTION}, serverPort, QStringLiteral("Smartphone1"));

			QCOMPARE(spySocketError.count(), 0);
			QCOMPARE(spySocketSuccess.count(), 0);

			clientThread.exit();
			QVERIFY(clientThread.wait());
		}


};

QTEST_GUILESS_MAIN(test_RemoteConnector)
#include "test_RemoteConnector.moc"
