/**
 * Copyright (c) 2017-2026 Governikus GmbH & Co. KG, Germany
 */

#include "RemoteIfdClient.h"

#include "AppSettings.h"
#include "DatagramHandler.h"
#include "Env.h"
#include "IfdListImpl.h"
#include "LogHandler.h"
#include "TestFileHelper.h"
#include "VolatileSettings.h"
#include "messages/Discovery.h"
#include "messages/IfdEstablishContext.h"

#include <QPointer>
#include <QtTest>


using namespace Qt::Literals::StringLiterals;
using namespace governikus;


namespace governikus
{

class DatagramHandlerMock
	: public DatagramHandler
{
	Q_OBJECT

	public:
		~DatagramHandlerMock() override;


		[[nodiscard]] bool isBound() const override
		{
			return true;
		}


		void send(const QByteArray&, const QList<QNetworkAddressEntry>&) override
		{
		}


};

DatagramHandlerMock::~DatagramHandlerMock()
{
}


class RemoteConnectorMock
	: public IfdConnector
{
	Q_OBJECT

	public Q_SLOTS:
		void onConnectRequest(const Discovery& pDiscovery, const QByteArray& pPsk) override;

	Q_SIGNALS:
		void fireConnectionRequestReceived();
};


void RemoteConnectorMock::onConnectRequest(const Discovery&, const QByteArray&)
{
	Q_EMIT fireConnectionRequestReceived();
}


} // namespace governikus

class test_RemoteIfdClient
	: public QObject
{
	Q_OBJECT

	private:
		QPointer<DatagramHandlerMock> mDatagramHandlerMock;
		QPointer<IfdList> mIfdList;
		QPointer<RemoteConnectorMock> mRemoteConnectorMock;

	private Q_SLOTS:
		void initTestCase()
		{
			Env::getSingleton<LogHandler>()->init();
			Env::getSingleton<VolatileSettings>()->setUsedAsSDK(false);
		}


		void init()
		{
			Env::setCreator<DatagramHandler*>(std::function<DatagramHandler* ()>([this] {
						mDatagramHandlerMock = new DatagramHandlerMock();
						return mDatagramHandlerMock;
					}));

			Env::setCreator<IfdList*>(std::function<IfdList* ()>([this] {
						mIfdList = new IfdListImpl(5000, 5000);
						return mIfdList;
					}));

			Env::setCreator<IfdConnector*>(std::function<IfdConnector* ()>([this] {
						mRemoteConnectorMock = new RemoteConnectorMock();
						return mRemoteConnectorMock;
					}));
		}


		void cleanup()
		{
			Env::getSingleton<AppSettings>()->getGeneralSettings().clearIfdServiceToken();
			QVERIFY(Env::getCounter<DatagramHandler*>() <= 2);
			Env::clear();
			QVERIFY(mDatagramHandlerMock.isNull());
			QVERIFY(mIfdList.isNull());
			QVERIFY(mRemoteConnectorMock.isNull());
			Env::getSingleton<LogHandler>()->resetBacklog();
			QCoreApplication::instance()->processEvents();
		}


		void testStartStopDetection()
		{
			RemoteIfdClient client;

			QVERIFY(mDatagramHandlerMock.isNull());

			client.startDetection();
			QCOMPARE(Env::getCounter<DatagramHandler*>(), 1);
			QVERIFY(!mDatagramHandlerMock.isNull());

			client.stopDetection();
			QVERIFY(mDatagramHandlerMock.isNull());

			Env::getSingleton<AppSettings>()->getGeneralSettings().generateIfdServiceToken();
			client.startDetection();
			QCOMPARE(Env::getCounter<DatagramHandler*>(), 1);
			QVERIFY(mDatagramHandlerMock.isNull());

			Env::getSingleton<AppSettings>()->getGeneralSettings().clearIfdServiceToken();
			client.startDetection();
			QCOMPARE(Env::getCounter<DatagramHandler*>(), 2);
			QVERIFY(!mDatagramHandlerMock.isNull());

			client.stopDetection();
			QVERIFY(mDatagramHandlerMock.isNull());
		}


		void testEnableLocalIfdDetection()
		{
			QVERIFY(Env::getSingleton<AppSettings>()->getGeneralSettings().getIfdServiceToken().isNull());
			QSignalSpy logSpy(Env::getSingleton<LogHandler>()->getEventHandler(), &LogEventHandler::fireLog);

			RemoteIfdClient client;
			QVERIFY(!client.enableLocalIfdDetection(true));
			QTRY_COMPARE(logSpy.size(), 1);
			QVERIFY(logSpy.at(0).at(0).toString().contains("Local IFD workflow is not running"_L1));

			Env::getSingleton<AppSettings>()->getGeneralSettings().generateIfdServiceToken();
			QVERIFY(client.enableLocalIfdDetection(true));
			QTRY_COMPARE(logSpy.size(), 2);
			QVERIFY(logSpy.at(1).at(0).toString().contains("Generate IFD Service token"_L1));
		}


		void testOnNewMessage_data()
		{
			const QByteArray missingIFDID(R"({
								"IF__DID": "fingerprint",
								"IFDName": "Sony Xperia Z5 compact",
								"SupportedAPI": [ "IFDInterface_WebSocket_v2" ],
								"msg": "REMOTE_IFD",
								"pairing": true,
								"port": 24728
							   })");

			const QByteArray emptyAddresses(R"({
								"IFDID": "fingerprint",
								"IFDName": "Sony Xperia Z5 compact",
								"SupportedAPI": [ "IFDInterface_WebSocket_v2" ],
								"msg": "REMOTE_IFD",
								"pairing": true,
								"port": 24728,
								"addresses": []
							   })");

			const QByteArray invalidAddresses(R"({
								"IFDID": "fingerprint",
								"IFDName": "Sony Xperia Z5 compact",
								"SupportedAPI": [ "IFDInterface_WebSocket_v2" ],
								"msg": "REMOTE_IFD",
								"pairing": true,
								"port": 24728,
								"addresses": [ "FooBar" ]
							   })");

			const QByteArray validAddresses("{"
											"	\"IFDID\": \"fingerprint\","
											"	\"IFDName\": \"Sony Xperia Z5 compact\","
											"	\"SupportedAPI\": [ \"IFDInterface_WebSocket_v2\" ],"
											"	\"msg\": \"REMOTE_IFD\","
											"	\"pairing\": true,"
											"	\"port\": 24728,"
											"	\"addresses\": [ \"wss://192.168.1.87:24728\" ]"
											"}");

			QTest::addColumn<QByteArray>("discovery");
			QTest::addColumn<QHostAddress>("sender");
			QTest::addColumn<QList<QLatin1String>>("logging");

			QTest::newRow("Missing IFDID - No sender")
				<< missingIFDID << QHostAddress()
				<< QList<QLatin1String>({"Missing value \"IFDID\""_L1, "Discarding unparsable message"_L1});

			QTest::newRow("Empty addresses - No sender")
				<< emptyAddresses << QHostAddress()
				<< QList<QLatin1String>({"At least one entry is required for \"addresses\""_L1, "Discarding unparsable message"_L1});

			QTest::newRow("Invalid address - No sender")
				<< invalidAddresses << QHostAddress()
				<< QList<QLatin1String>({"Found \"addresses\" entry with wrong scheme: "_L1, "Discarding unparsable message"_L1});

			QTest::newRow("Valid address - No sender")
				<< validAddresses << QHostAddress()
				<< QList<QLatin1String>();

			QTest::newRow("Empty addresses - Valid sender")
				<< emptyAddresses
				<< QHostAddress("192.168.1.88"_L1)
				<< QList<QLatin1String>({"At least one entry is required for \"addresses\""_L1, "Discarding unparsable message"_L1});

			QTest::newRow("Invalid address - Valid sender")
				<< invalidAddresses << QHostAddress("192.168.1.88"_L1)
				<< QList<QLatin1String>({"Found \"addresses\" entry with wrong scheme: "_L1, "Discarding unparsable message"_L1});

			QTest::newRow("Valid address - Valid sender")
				<< validAddresses << QHostAddress("192.168.1.88"_L1)
				<< QList<QLatin1String>();

		}


		void testOnNewMessage()
		{
			QFETCH(QByteArray, discovery);
			QFETCH(QHostAddress, sender);
			QFETCH(QList<QLatin1String>, logging);
			logging << "Local IFD workflow is not running"_L1;

			QSignalSpy logSpy(Env::getSingleton<LogHandler>()->getEventHandler(), &LogEventHandler::fireLog);

			RemoteIfdClient client;
			QSignalSpy deviceAppeared(&client, &RemoteIfdClient::fireDeviceAppeared);
			client.startDetection();
			QVERIFY(!mDatagramHandlerMock.isNull());

			Q_EMIT mDatagramHandlerMock->fireNewMessage(discovery, sender);
			QTRY_COMPARE(logSpy.count(), logging.size());
			for (const auto& log : std::as_const(logging))
			{
				QVERIFY(TestFileHelper::containsLog(logSpy, log));
			}

			if (logging.isEmpty())
			{
				QCOMPARE(deviceAppeared.size(), 1);
				QCOMPARE(client.getAnnouncingRemoteDevices().size(), 1);
				QCOMPARE(client.getAnnouncingRemoteDevices().at(0)->getDiscovery().getAddresses().size(), 1);
				QCOMPARE(*client.getAnnouncingRemoteDevices().at(0)->getDiscovery().getAddresses().constBegin(), QUrl("wss://192.168.1.87:24728"_L1));
			}
		}


		void testReceiveUnexpected()
		{
			QSignalSpy logSpy(Env::getSingleton<LogHandler>()->getEventHandler(), &LogEventHandler::fireLog);

			RemoteIfdClient client;
			client.startDetection();
			QVERIFY(!mDatagramHandlerMock.isNull());

			const auto& json = IfdEstablishContext(IfdVersion::Version::latest, DeviceInfo::getName()).toByteArray(IfdVersion::Version::latest, QStringLiteral("TestContext"));
			Q_EMIT mDatagramHandlerMock->fireNewMessage(json, QHostAddress("192.168.1.88"_L1));
			QTRY_COMPARE(logSpy.count(), 7);
			QVERIFY(logSpy.at(1).at(0).toString().contains("The value of msg should be REMOTE_IFD"_L1));
			QVERIFY(logSpy.at(6).at(0).toString().contains("Discarding unparsable message"_L1));
		}


		void testReceive_data()
		{
			QTest::addColumn<QString>("hostAddress");

			QTest::newRow("IPv4") << "192.168.1.88";
			QTest::newRow("IPv6 link-global") << "2001:0db8:85a3:08d3:1319:8a2e:0370:7344";
			QTest::newRow("IPv6 link-local") << "fe80::5a38:a519:8ff4:1f1f%enp0s31f6";
		}


		void testReceive()
		{
			QFETCH(QString, hostAddress);

			QByteArray ifdId("0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"_ba);
			IfdVersion::Version version = IfdVersion::Version::latest;

			if (IfdVersion::supported().contains(IfdVersion::Version::v2))
			{
				ifdId = QByteArrayLiteral(R"({
							-----BEGIN CERTIFICATE-----
							MIIC4zCCAcsCBEQvMpowDQYJKoZIhvcNAQELBQAwNDEUMBIGA1UEAwwLQXVzd2Vp
							c0FwcDIxHDAaBgNVBAUTEzY0MTgwMjY3MTE5MTA5MjY2MzQwIhgPMTk3MDAxMDEw
							MDAwMDBaGA85OTk5MTIzMTIzNTk1OVowNDEUMBIGA1UEAwwLQXVzd2Vpc0FwcDIx
							HDAaBgNVBAUTEzY0MTgwMjY3MTE5MTA5MjY2MzQwggEiMA0GCSqGSIb3DQEBAQUA
							A4IBDwAwggEKAoIBAQDGJ9C76Cb8iHuaZJxcFY0NpNllcAK5JKcrigKBki7EvF9z
							5Q/MNek2pxwTMp5SilUDJOkwgcTdm7liC/Zx+lPX8MZjhWxV73DGt9DDyJh91ypl
							6B8vZbpJlL83Vo4C4BLBG6ZaElPpyTitWWKQ4BFUoH0h2utsNFV7FHz+1oZcvhv0
							gQuzd7gQaVV6mzCePRn+4qgxYSXSJ8Ix21udgT3LoHDOBrOWSIt0g/Q1mkzJcnaC
							EnS2s6Ib0xPY5PsL1YN/dZn88/gs9Za4rZSBGIIDrpCt5WCkYbkg45LwXLmaPUrg
							uuFIFIR0HH4pxgajLHpMgYaszxkg4SkdxwJ8vIytAgMBAAEwDQYJKoZIhvcNAQEL
							BQADggEBAB4grwHZ8NMrs3vRInJQc3ftYDCAjDPjjTg/G4BVz07DmlQZpFyPfInb
							UaKfpMlaEd1EoRuNIxC796+rZhy+j97DoLkT1qnPP30GLwlZaVZeOCKIIQ+tGjUU
							cWhhIC6kRCPIQAKxKDNGIUwBAkwludieGa7Ytl7qmnnJDMNe+Ox7Sf+UOa12bJKH
							d27MYoWMfecJdTmF8xXQ7EEYjMwWHd5t5tJG9AVgzhO8zC+iJTqc9I34sIa8+9WE
							oQu+/VZgDkJaSdDJ4LqVFIvUy3CFGh6ahDVsHGC5kTDm5EQWh3puWR0AkIjUWMPi
							xU/nr0Jsab99VgX4/nnCW92v/DIRc1c=
							-----END CERTIFICATE-----
						})");
				version = IfdVersion::Version::v2;
			}
			Discovery discovery("Sony Xperia Z5 compact"_L1, ifdId, 24728, {version});
			discovery.setAddresses({QHostAddress(hostAddress)});
			const QByteArray offerJson = discovery.toByteArray(version);

			RemoteIfdClient client;
			client.startDetection();
			QVERIFY(!mDatagramHandlerMock.isNull());

			QVERIFY(!mIfdList.isNull());
			QSignalSpy spyAppearedList(mIfdList.data(), &IfdList::fireDeviceAppeared);
			QSignalSpy spyAppeared(&client, &IfdClient::fireDeviceAppeared);

			Q_EMIT mDatagramHandlerMock->fireNewMessage(offerJson, QHostAddress(hostAddress));
			QCOMPARE(spyAppearedList.count(), 1);
			QCOMPARE(spyAppeared.count(), 1);

			QSignalSpy spyVanished(&client, &IfdClient::fireDeviceVanished);
			QCOMPARE(spyVanished.count(), 0);
			Q_EMIT mIfdList->fireDeviceVanished(QSharedPointer<IfdListEntry>());
			QCOMPARE(spyVanished.count(), 1);
		}


		void testRemoteConnectorThread()
		{
			QScopedPointer<IfdClient> client(new RemoteIfdClient());
			QCOMPARE(Env::getCounter<IfdConnector*>(), 1);
			QVERIFY(!mRemoteConnectorMock.isNull());
			QVERIFY(client->thread() != mRemoteConnectorMock->thread());

			client.reset();
			QVERIFY(mRemoteConnectorMock.isNull());
		}


		void testRemoteConnectorEstablish()
		{
			const QByteArray offerJson("{\n"
									   "    \"deviceName\": \"Sony Xperia Z5 compact\",\n"
									   "    \"encrypted\": true,\n"
									   "    \"port\": 24728,\n"
									   "    \"availableApiLevels\": [\"IFDInterface_WebSocket_v0\", \"IFDInterface_WebSocket_v2\"]\n"
									   "}");

			RemoteIfdClient client;
			client.startDetection();
			QVERIFY(!mDatagramHandlerMock.isNull());
			Q_EMIT mDatagramHandlerMock->fireNewMessage(offerJson, QHostAddress("192.168.1.88"_L1));

			QVERIFY(!mRemoteConnectorMock.isNull());
			QSignalSpy spyConnectionRequest(mRemoteConnectorMock.data(), &RemoteConnectorMock::fireConnectionRequestReceived);
			Discovery discovery(QString(), QByteArrayLiteral("0123456789ABCDEF"), 12345, {IfdVersion::Version::latest, IfdVersion::Version::v2});
			discovery.setAddresses({QHostAddress("192.168.1.88"_L1)});
			QSharedPointer<IfdListEntry> emptyEntry(new IfdListEntry(discovery));
			client.establishConnection(emptyEntry, "password1");

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

			QSignalSpy spyConnectionDone(&client, &IfdClient::fireEstablishConnectionDone);
			Q_EMIT mRemoteConnectorMock->fireDispatcherError(emptyEntry->getDiscovery().getIfdId(),
					IfdErrorCode::CONNECTION_ERROR);
			QCOMPARE(spyConnectionDone.count(), 1);
		}


};

QTEST_GUILESS_MAIN(test_RemoteIfdClient)
#include "test_RemoteIfdClient.moc"
