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

#include "DatagramHandlerImpl.h"

#include "Env.h"
#include "HttpServer.h"
#include "LogHandler.h"

#include "TestFileHelper.h"

#include <QNetworkInterface>
#include <QNetworkProxy>
#include <QSharedPointer>
#include <QtTest>
#ifdef Q_OS_MACOS
	#include <QOperatingSystemVersion>
#endif


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


class test_DatagramHandlerImpl
	: public QObject
{
	Q_OBJECT

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


		void init()
		{
			HttpServer::cPort = 0;
		}


		void cleanup()
		{
			Env::getSingleton<LogHandler>()->resetBacklog();
			QCoreApplication::instance()->processEvents();
		}


		void startUpShutDown()
		{
			QSignalSpy logSpy(Env::getSingleton<LogHandler>()->getEventHandler(), &LogEventHandler::fireLog);
			QSharedPointer<DatagramHandler> socket(Env::create<DatagramHandler*>());

			QVERIFY(socket->isBound());
			QTRY_VERIFY(!logSpy.isEmpty());
			QTRY_VERIFY(logSpy.last().at(0).toString().contains("Bound on port:"_L1));
			logSpy.removeLast();

			socket.reset();
			QTRY_VERIFY(!logSpy.isEmpty());
			QTRY_VERIFY(logSpy.last().at(0).toString().contains("Shutdown socket"_L1));
		}


		void cannotStart()
		{
			HttpServer::cPort = 80;

#ifdef Q_OS_WIN
			QSKIP("Windows does not block privileged ports");
#elif defined(Q_OS_MACOS)
			QSKIP("macOS >= 10.14 does not block privileged ports - https://news.ycombinator.com/item?id=18302380");
#elif defined(Q_OS_LINUX)
			if (TestFileHelper::systemAllowsPort(HttpServer::cPort))
			{
				QSKIP("Cannot check, privileged port allowed.");
			}
#endif

			QSignalSpy logSpy(Env::getSingleton<LogHandler>()->getEventHandler(), &LogEventHandler::fireLog);
			QSharedPointer<DatagramHandler> socket(Env::create<DatagramHandler*>());

			QVERIFY(!socket->isBound());
			QTRY_COMPARE(logSpy.count(), 1);
			QVERIFY(logSpy.first().at(0).toString().contains("Cannot bind socket: \"The address is protected\""_L1));
		}


		void getNonJsonDatagram()
		{
			QSharedPointer<DatagramHandler> socket(Env::create<DatagramHandler*>());
			QVERIFY(socket->isBound());
			QSignalSpy spySocket(socket.data(), &DatagramHandler::fireNewMessage);

			QSignalSpy logSpy(Env::getSingleton<LogHandler>()->getEventHandler(), &LogEventHandler::fireLog);
			QUdpSocket clientSocket;
			clientSocket.setProxy(QNetworkProxy::NoProxy);

			auto written = clientSocket.writeDatagram("dummy", QHostAddress::LocalHost, socket.staticCast<DatagramHandlerImpl>()->mSocket->localPort());
			QTRY_COMPARE(spySocket.count(), 1); // clazy:exclude=qstring-allocations
			QCOMPARE(written, 5);
			QVERIFY(TestFileHelper::containsLog(logSpy, "Bound on port:"_L1));
			QCOMPARE(spySocket.first().at(0).toByteArray(), QByteArray("dummy"));
		}


		void getJsonDatagram_data()
		{
			QTest::addColumn<QHostAddress>("address");

			QTest::newRow("LocalHost") << QHostAddress(QHostAddress::LocalHost);

			QSet<QString> dataRows;
			const auto entries = DatagramHandler::getAllBroadcastEntries();
			for (const auto& entry : entries)
			{
				const auto& addr = DatagramHandler::getBroadcastAddress(entry);
				if (addr.isNull() || dataRows.contains(addr.toString()))
				{
					continue;
				}

				const auto& dataTag = addr.toString();
				dataRows << dataTag;
				QTest::newRow(qPrintable(dataTag)) << addr;
			}
		}


		void getJsonDatagram()
		{
			QFETCH(QHostAddress, address);

#ifdef Q_OS_MACOS
			if (address.protocol() == QAbstractSocket::IPv6Protocol)
			{
				QSKIP("macOS does not allow to send multicasts without a signed app bundle containing networking.multicast entitlement");
			}
#endif

			DatagramHandlerImpl socket;
			QVERIFY(socket.isBound());
			QSignalSpy spySocket(&socket, &DatagramHandler::fireNewMessage);

			QUdpSocket clientSocket;
			clientSocket.setProxy(QNetworkProxy::NoProxy);

			QByteArray data(R"({"key":"value"})");
			const auto written = clientSocket.writeDatagram(data, address, socket.mSocket->localPort());
			if (written == -1)
			{
				qCritical() << address << clientSocket.error() << clientSocket.errorString();
			}
			QTRY_COMPARE(spySocket.count(), 1); // clazy:exclude=qstring-allocations
			QCOMPARE(written, data.size());
			const auto& msg = spySocket.takeFirst();
			QCOMPARE(msg.size(), 2);
			QCOMPARE(msg.at(0).toByteArray(), data);
		}


		void sendDatagram_data()
		{
			QTest::addColumn<bool>("broadcast");

			QTest::newRow("WithBroadcast - Unique") << true;
			QTest::newRow("WithoutBroadcast") << false;
		}


		void sendDatagram()
		{
			QFETCH(bool, broadcast);

			QUdpSocket receiver;
			receiver.setProxy(QNetworkProxy::NoProxy);

			QVERIFY(receiver.bind());
			QSignalSpy spyReceiver(&receiver, &QUdpSocket::readyRead);

			QSharedPointer<DatagramHandlerImpl> datagramHandlerImpl = QSharedPointer<DatagramHandlerImpl>::create(false);
			QSignalSpy logSpy(Env::getSingleton<LogHandler>()->getEventHandler(), &LogEventHandler::fireLog);

			QJsonObject obj;
			obj["test"_L1] = "dummy"_L1;
			QJsonDocument doc;
			doc.setObject(obj);
			if (broadcast)
			{
#ifdef Q_OS_FREEBSD
				QSKIP("FreeBSD does not like that");
#endif
				auto entries = datagramHandlerImpl->getAllBroadcastEntries();
				datagramHandlerImpl->sendToAddressEntries(doc.toJson(QJsonDocument::Compact), entries, receiver.localPort());
			}
			else
			{
				QVERIFY(datagramHandlerImpl->sendToAddress(doc.toJson(QJsonDocument::Compact), QHostAddress::LocalHost, receiver.localPort()));
			}

			QTRY_COMPARE(spyReceiver.count(), 1); // clazy:exclude=qstring-allocations
			if (broadcast)
			{
				QTRY_VERIFY(logSpy.count() >= 2);
				QVERIFY(TestFileHelper::containsLog(logSpy, QLatin1String("Broadcast Addresses changed...")));
			}
			else
			{
				QTRY_VERIFY(logSpy.count() == 2);
			}

			QVERIFY(receiver.hasPendingDatagrams());
			QByteArray msg;
			msg.resize(static_cast<int>(receiver.pendingDatagramSize()));
			receiver.readDatagram(msg.data(), msg.size());
			QCOMPARE(msg, QByteArray("{\"test\":\"dummy\"}"));
		}


};

QTEST_GUILESS_MAIN(test_DatagramHandlerImpl)
#include "test_DatagramHandlerImpl.moc"
