/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only */
/* Copyright (c) 2022-2025 Brett Sheffield <bacs@librecast.net> */

#include "testnet.h"
#ifdef HAVE_LIBLIBRECAST
#include <librecast.h>
#endif
#include <mld.h>
#include <mld_pvt.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

#ifdef HAVE_LIBLIBRECAST
static mld_t *mld;
static struct in6_addr *addr;
static unsigned int ifx;
static sem_t sem;

void *thread_mld_wait(void *arg)
{
	int rc;
	int *waitcount = (int *)arg;
	rc = mld_wait(mld, ifx, addr, 0);
	if (rc == -1 && errno == EINTR) return NULL; /* mld_wait was interupted */
	test_assert(rc == 0, "mld_wait() returns %i", rc);
	if (rc) test_log("%s (errno = %i)\n", strerror(errno), errno);
	(*waitcount)++;
	sem_post(&sem);
	return arg;
}
#endif

int main(void)
{
	char name[] = "mld_watch()";
#ifdef HAVE_LIBLIBRECAST
	pthread_t tid;
	lc_ctx_t *lctx;
	lc_channel_t *chan;
	struct timespec ts = {0};
	int waitcount = 0;
	int rc;

	test_cap_require(CAP_NET_RAW);
	test_name(name);
	test_require_net(TEST_NET_BASIC);

	ifx = get_multicast_if();
	test_assert(ifx, "get_multicast_if() - find multicast capable interface");
	if (!ifx) return TEST_WARN;
	mld = mld_init(0);
	test_assert(mld != NULL, "mld_t allocated");
	if (!mld) return test_status;

	if (!mld_start(mld)) {
		test_assert(0, "mld_start() failed: %s", strerror(errno));
		goto err_mld_free;
	}
	/* ensure all threads created */
	for (int i = 0; i < MLD_THREADS; i++) assert(mld->q[i]);

	/* generate a random multicast address */
	lctx = lc_ctx_new();
	test_assert(lctx != NULL, "lc_ctx_new()");
	if (!lctx) goto err_mld_stop;
	chan = lc_channel_random(lctx);
	test_assert(chan != NULL, "lc_channel_random()");
	if (!chan) goto err_ctx_free;
	addr = lc_channel_in6addr(chan);
	test_assert(addr != NULL, "lc_channel_in6addr()");
	if (!addr) goto err_ctx_free;

	rc = sem_init(&sem, 0, 0);
	test_assert(rc == 0, "sem_init returned %i", rc);
	if (rc) goto err_ctx_free;

	/* ensure group s NOT in filter, or no point running test */
	rc = mld_filter_grp_cmp(mld, ifx, addr);
	test_assert(rc == 0, "mld_filter_grp_cmp returned %i", rc);
	if (rc != 0) goto err_sem_destroy;

	/* mld_wait() will block unless MLD_DONTWAIT */
	rc = pthread_create(&tid, NULL, thread_mld_wait, &waitcount);
	test_assert(rc == 0, "pthread_create returned %i", rc);
	if (rc) goto err_sem_destroy;
	rc = clock_gettime(CLOCK_REALTIME, &ts);
	test_assert(rc == 0, "clock_gettime returned %i", rc);
	if (rc) goto err_sem_destroy;
	ts.tv_sec++; /* timeout = 1s */
	errno = 0, rc = sem_timedwait(&sem, &ts);
	test_assert(errno == ETIMEDOUT, "sem_wait() blocks without MLD_DONTWAIT");
	test_assert(rc == -1, "sem_timedwait returned %i", rc);
	pthread_cancel(tid);
	pthread_join(tid, NULL);
	if (!rc) goto err_sem_destroy;

	/* MLD_DONTWAIT causes mld_wait() to be non-blocking */
	errno = 0;
	rc = mld_wait(mld, ifx, addr, MLD_DONTWAIT);
	test_assert(errno == EWOULDBLOCK, "mld_wait() - EWOULDBLOCK");
	test_assert(rc == -1, "mld_wait with MLD_DONTWAIT returns -1 - group not added");

	/* now add a group to the filter */
	rc = mld_filter_grp_add(mld, ifx, addr);
	test_assert(rc == 0, "add group to filter");

	/* ensure group was added to filter, or no point running test */
	rc = mld_filter_grp_cmp(mld, ifx, addr);
	test_assert(rc == 1, "mld_filter_grp_cmp returned %i", rc);
	if (rc != 1) goto err_sem_destroy;

	rc = mld_wait(mld, ifx, addr, 0);
	test_assert(rc == 0, "mld_wait() returns 0 when grp in filter");

	waitcount = 0;
	pthread_create(&tid, NULL, thread_mld_wait, &waitcount);
	test_assert(rc == 0, "pthread_create returned %i", rc);
	if (rc) goto err_sem_destroy;
	sem_wait(&sem);
	test_assert(rc == 0, "sem_wait() blocks, then returns when grp added");
	pthread_cancel(tid);
	pthread_join(tid, NULL);
	test_assert(waitcount == 1, "waitcount = %i", waitcount);
err_sem_destroy:
	sem_destroy(&sem);
err_ctx_free:
	lc_ctx_free(lctx);
err_mld_stop:
	mld_stop(mld);
err_mld_free:
	mld_free(mld);
	return test_status;
#else
	return test_skip("%s - requires liblibrecast", name);
#endif
}
