/*  Copyright (C) CZ.NIC, z.s.p.o. and contributors
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  For more information, see <https://www.knot-dns.cz/>
 */

#include <pthread.h>
#include <tap/basic.h>
#include <unistd.h>

#include "knot/common/fdset.h"
#include "libknot/attribute.h"
#include "contrib/time.h"

#define PATTERN1		"0x45"
#define PATTERN2		"0xED"

#define TIMEOUT0		2000
#define TIMEOUT1		10
#define TIMEOUT2		400
#define JITTER			(TIMEOUT2 - TIMEOUT1 - 10)

void *thr_action1(void *arg)
{
	usleep(1000 * TIMEOUT1);
	_unused_ int ret = write(*((int *)arg), &PATTERN1, 1);
	return NULL;
}

void *thr_action2(void *arg)
{
	usleep(1000 * TIMEOUT2);
	_unused_ int ret = write(*((int *)arg), &PATTERN2, 1);
	return NULL;
}

int main(int argc, char *argv[])
{
	plan_lazy();

	fdset_t fdset;
	int ret = fdset_init(&fdset, 32, 2);
	ok(ret == KNOT_EOK, "fdset_init");

	int fds0[2], fds1[2], fds2[2];

	ret = pipe(fds0);
	ok(ret >= 0, "create pipe 0");
	ret = fdset_add(&fdset, fds0[0], FDSET_POLLIN, NULL);
	ok(ret >= 0, "add pipe 0 to fdset");

	ret = pipe(fds1);
	ok(ret >= 0, "create pipe 1");
	ret = fdset_add(&fdset, fds1[0], FDSET_POLLIN, NULL);
	ok(ret >= 0, "add pipe 1 to fdset");

	ret = pipe(fds2);
	ok(ret >= 0, "create pipe 2");
	ret = fdset_add(&fdset, fds2[0], FDSET_POLLIN, NULL);
	ok(ret >= 0, "add pipe 2 to fdset");

	ok(fdset_get_length(&fdset) == 3, "fdset size full");

	struct timespec time0 = time_now();

	pthread_t t1, t2;
	ret = pthread_create(&t1, 0, thr_action1, &fds1[1]);
	ok(ret == 0, "create thread 1");
	ret = pthread_create(&t2, 0, thr_action2, &fds2[1]);
	ok(ret == 0, "create thread 2");

	fdset_it_t it;
	ret = fdset_poll(&fdset, &it, 0, TIMEOUT0);
	struct timespec time1 = time_now();
	double diff1 = time_diff_ms(&time0, &time1);
	ok(ret == 1, "fdset_poll return 1");
	ok(diff1 >= TIMEOUT1 && diff1 < TIMEOUT1 + JITTER, "fdset_poll timeout 1 (%f)", diff1);
	for(; !fdset_it_is_done(&it); fdset_it_next(&it)) {
		ok(!fdset_it_is_error(&it), "fdset no error");
		ok(fdset_it_is_pollin(&it), "fdset can read");

		int fd = fdset_it_get_fd(&it);
		ok(fd == fds1[0], "fdset_it fd check");

		char buf = 0x00;
		ret = read(fd, &buf, sizeof(buf));
		ok(ret == 1 && buf == PATTERN1[0], "fdset_it value check");

		fdset_it_remove(&it);
	}
	fdset_it_commit(&it);
	ok(fdset_get_length(&fdset) == 2, "fdset size 2");
	close(fds1[1]);

	int fd2_dup = dup(fds2[0]);
	ok(fd2_dup >= 0, "duplicate fd");

	ret = fdset_poll(&fdset, &it, 0, TIMEOUT0);
	struct timespec time2 = time_now();
	double diff2 = time_diff_ms(&time0, &time2);
	ok(ret == 1, "fdset_poll return 2");
	ok(diff2 >= TIMEOUT2 && diff2 < TIMEOUT2 + JITTER, "fdset_poll timeout 2 (%f)", diff2);
	for(; !fdset_it_is_done(&it); fdset_it_next(&it)) {
		ok(!fdset_it_is_error(&it), "fdset no error");
		ok(fdset_it_is_pollin(&it), "fdset can read");

		int fd = fdset_it_get_fd(&it);
		ok(fd == fds2[0], "fdset_it fd check");

		char buf = 0x00;
		ret = read(fd, &buf, sizeof(buf));
		ok(ret == 1 && buf == PATTERN2[0], "fdset_it value check");

		fdset_it_remove(&it);
	}
	fdset_it_commit(&it);
	ok(fdset_get_length(&fdset) == 1, "fdset size 1");

	pthread_join(t1, 0);
	pthread_join(t2, 0);

	ret = fdset_remove(&fdset, 0);
	ok(ret == KNOT_EOK, "fdset remove");
	close(fds0[1]);
	ok(fdset_get_length(&fdset) == 0, "fdset size 0");

	ret = write(fds2[1], &PATTERN2, 1);
	ok(ret == 1, "write to removed fd");
	ret = fdset_poll(&fdset, &it, 0, 100);
	ok(ret == 0, "fdset_poll return 3");


	close(fds2[1]);
	if (fd2_dup >= 0) {
		close(fd2_dup);
	}
	fdset_clear(&fdset);

	return 0;
}
