/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only */
/* Copyright (c) 2022 Claudio Calvelli <clc@librecast.net> */

#include "test.h"
#include "testnet.h"
#include <librecast_pvt.h>
#include <librecast/net.h>
#include <pthread.h>
#include <assert.h>
#include <stdlib.h>
#include <unistd.h>
#ifdef HAVE_ENDIAN_H
#include <endian.h>
#elif HAVE_SYS_ENDIAN_H
#include <sys/endian.h>
#endif
#include <semaphore.h>

#define NUM_MESSAGES 10

static sem_t sem_nack, sem_main;
static char channel_name[] = "0000-0042";
static int msg_seen = -1;

static void *nack_thread(void * _logging)
{
	lc_channel_logging_t * logging = _logging;
	lc_seq_t msg_expected = 1;
	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
	pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
	/* tell main() we have started */
	sem_post(&sem_nack);
	while (1) {
		int ok = 0;
		pthread_testcancel();
		/* wait for main() to send a message (the thread could be cancelled
		 * while waiting and that's OK */
		sem_wait(&sem_main);
		/* check that the message is in the queue; it will have the be the newest
		 * because we know it's just been sent */
		pthread_mutex_lock(&logging->mutex);
		if (logging->newest && logging->newest->seq == msg_expected) {
			/* check that the message is in fact what we expect it to be */
			if (logging->newest->len >= sizeof(lc_message_head_t) + sizeof(int)) {
				lc_message_head_t head;
				memcpy(&head, logging->newest->data, sizeof(lc_message_head_t));
				/* provisionally OK unless some of the checks below fail */
				ok = 1;
				if (be64toh(head.seq) != msg_expected) ok = 0;
				if (be64toh(head.timestamp) != (uint64_t)msg_expected << 10) ok = 0;
				if (be64toh(head.len) != sizeof(int)) ok = 0;
				if (ok) {
					int data;
					char *ptr = logging->newest->data;
					memcpy(&data, &ptr[sizeof(lc_message_head_t)], sizeof(int));
					if ((lc_seq_t)data != msg_expected) ok = 0;
				}
			}
		}
		pthread_mutex_unlock(&logging->mutex);
		if (ok) msg_seen = msg_expected;
		else msg_seen = -1;
		msg_expected++;
		/* let main() know we've done the checking */
		sem_post(&sem_nack);
	}
	return NULL;
}

int main(void)
{
	lc_ctx_t * lctx;
	lc_socket_t * sock;
	lc_channel_t * chan;
	lc_message_t message = { 0, };
	int rc;

	test_name("Message logging for NACKs");
	test_require_net(TEST_NET_BASIC);

	/* create a channel and enable the logging for the NACK thread, however instead
	 * of using the normal NACK thread we provide one so that we can check what has
	 * been logged */
	lctx = lc_ctx_new();
	if (!test_assert(lctx != NULL, "lc_ctx_new()")) return test_status;
	sock = lc_socket_new(lctx);
	if (!test_assert(sock != NULL, "lc_socket_new()")) goto err_ctx_free;
	chan = lc_channel_new(lctx, channel_name);
	if (!test_assert(chan != NULL, "lc_channel_new()")) goto err_ctx_free;
	rc = lc_channel_bind(sock, chan);
	if (!test_assert(rc == 0, "lc_channel_bind()")) goto err_ctx_free;
	rc = sem_init(&sem_main, 0, 0);
	if (!test_assert(rc == 0, "sem_init(sem_main) returned %i", rc)) goto err_ctx_free;
	rc = sem_init(&sem_nack, 0, 0);
	if (!test_assert(rc == 0, "sem_init(sem_nack) returned %i", rc)) goto err_sem_destroy_main;
	if (!test_assert(lc_channel_nack_handler_thr(chan, 10, nack_thread) >= 0, "Could not set up logging for NACKs"))
		goto err_sem_destroy_main;

	/* make sure the nack thread has started */
	sem_wait(&sem_nack);
	/* now go and send a number of messages */
	for (int msg_sent = 1; msg_sent <= NUM_MESSAGES; msg_sent++) {
		char description[64];
		message.timestamp = (uint64_t)msg_sent << 10;
		message.seq = msg_sent;
		message.len = sizeof(msg_sent);
		message.data = &msg_sent;
		message.op = LC_OP_DATA;
		lc_msg_send(chan, &message);
		/* let the nack thread we have something for them */
		sem_post(&sem_main);
		/* wait for them to look at it */
		sem_wait(&sem_nack);
		/* and see if what we see is what we expect */
		snprintf(description, sizeof(description), "Invalid message #%d saved for the NACK thread", msg_sent);
		test_assert(msg_seen == msg_sent, description);
	}

	sem_destroy(&sem_nack);
err_sem_destroy_main:
	sem_destroy(&sem_main);
err_ctx_free:
	lc_ctx_free(lctx);
	return test_status;
}
