/* SPDX-License-Identifier: MIT */
/*
 * Description: stress test for buffer ring drain and refill. Creates a
 *		small buffer ring, submits many reads that consume all
 *		buffers, verifies correct behavior when buffers are
 *		exhausted, then refills and continues. Tests both read
 *		and recv paths with buffer rings under pressure.
 */
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>

#include "liburing.h"
#include "helpers.h"

#define BGID		1
#define BUF_SIZE	256
#define NR_BUFS		8
#define NR_ROUNDS	10000
#define FILE_SIZE	(BUF_SIZE * NR_BUFS * NR_ROUNDS)

static void provide_buffers(struct io_uring_buf_ring *br, void *base,
			    int nr_bufs, int buf_size)
{
	int i;

	for (i = 0; i < nr_bufs; i++) {
		void *addr = base + i * buf_size;
		io_uring_buf_ring_add(br, addr, buf_size, i,
				      io_uring_buf_ring_mask(nr_bufs), i);
	}
	io_uring_buf_ring_advance(br, nr_bufs);
}

/*
 * Test: submit reads consuming all buffers, refill, repeat.
 * Verifies data integrity through the whole process.
 */
static int test_read_drain_refill(void)
{
	struct io_uring ring;
	struct io_uring_buf_ring *br;
	struct io_uring_sqe *sqe;
	struct io_uring_cqe *cqe;
	void *buf_base;
	char *file_data;
	const char *fname = ".buf-ring-stress-tmp";
	int fd, ret, round;
	int total_read = 0;

	ret = io_uring_queue_init(32, &ring, 0);
	if (ret) {
		fprintf(stderr, "ring setup: %d\n", ret);
		return T_EXIT_FAIL;
	}

	/* Setup buffer ring */
	br = io_uring_setup_buf_ring(&ring, NR_BUFS, BGID, 0, &ret);
	if (!br) {
		if (ret == -EINVAL || ret == -ENOENT) {
			io_uring_queue_exit(&ring);
			return T_EXIT_SKIP;
		}
		fprintf(stderr, "buf ring setup: %d\n", ret);
		io_uring_queue_exit(&ring);
		return T_EXIT_FAIL;
	}

	buf_base = malloc(NR_BUFS * BUF_SIZE);
	if (!buf_base)
		goto err;

	/* Create test file with known pattern */
	file_data = malloc(FILE_SIZE);
	if (!file_data)
		goto err;

	for (int i = 0; i < FILE_SIZE; i++)
		file_data[i] = (char)(i & 0xff);

	fd = open(fname, O_CREAT | O_WRONLY | O_TRUNC, 0644);
	if (fd < 0) {
		perror("open write");
		free(file_data);
		goto err;
	}
	if (write(fd, file_data, FILE_SIZE) != FILE_SIZE) {
		perror("write");
		close(fd);
		free(file_data);
		goto err;
	}
	close(fd);

	fd = open(fname, O_RDONLY);
	if (fd < 0) {
		perror("open read");
		free(file_data);
		goto err;
	}

	for (round = 0; round < NR_ROUNDS; round++) {
		int reads_submitted = 0;
		int reads_completed = 0;

		/* Provide fresh buffers for this round */
		provide_buffers(br, buf_base, NR_BUFS, BUF_SIZE);

		/* Submit NR_BUFS reads using buffer selection */
		for (int i = 0; i < NR_BUFS; i++) {
			sqe = io_uring_get_sqe(&ring);
			if (!sqe) {
				fprintf(stderr, "get sqe round %d/%d\n", round, i);
				goto err_file;
			}
			io_uring_prep_read(sqe, fd, NULL, BUF_SIZE, total_read + i * BUF_SIZE);
			sqe->flags |= IOSQE_BUFFER_SELECT;
			sqe->buf_group = BGID;
			sqe->user_data = (round << 16) | i;
			reads_submitted++;
		}

		ret = io_uring_submit(&ring);
		if (ret != reads_submitted) {
			fprintf(stderr, "submit round %d: %d\n", round, ret);
			goto err_file;
		}

		/* Reap all completions */
		while (reads_completed < reads_submitted) {
			ret = io_uring_wait_cqe(&ring, &cqe);
			if (ret) {
				fprintf(stderr, "wait round %d: %d\n", round, ret);
				goto err_file;
			}

			if (cqe->res < 0) {
				fprintf(stderr, "read error round %d: %d\n",
					round, cqe->res);
				io_uring_cqe_seen(&ring, cqe);
				goto err_file;
			}

			if (!(cqe->flags & IORING_CQE_F_BUFFER)) {
				fprintf(stderr, "no buffer flag round %d\n", round);
				io_uring_cqe_seen(&ring, cqe);
				goto err_file;
			}

			int bid = cqe->flags >> IORING_CQE_BUFFER_SHIFT;
			if (bid >= NR_BUFS) {
				fprintf(stderr, "bad bid %d round %d\n", bid, round);
				io_uring_cqe_seen(&ring, cqe);
				goto err_file;
			}

			/* Verify data integrity */
			void *got = buf_base + bid * BUF_SIZE;
			int idx = cqe->user_data & 0xffff;
			int off = total_read + idx * BUF_SIZE;
			if (cqe->res > 0 && memcmp(got, file_data + off, cqe->res) != 0) {
				fprintf(stderr, "data mismatch round %d bid %d\n",
					round, bid);
				io_uring_cqe_seen(&ring, cqe);
				goto err_file;
			}

			io_uring_cqe_seen(&ring, cqe);
			reads_completed++;
		}

		total_read += NR_BUFS * BUF_SIZE;
	}

	if (total_read != FILE_SIZE) {
		fprintf(stderr, "total read %d, expected %d\n", total_read, FILE_SIZE);
		goto err_file;
	}

	close(fd);
	unlink(fname);
	free(file_data);
	free(buf_base);
	io_uring_free_buf_ring(&ring, br, NR_BUFS, BGID);
	io_uring_queue_exit(&ring);
	return T_EXIT_PASS;

err_file:
	close(fd);
	unlink(fname);
	free(file_data);
err:
	free(buf_base);
	io_uring_queue_exit(&ring);
	return T_EXIT_FAIL;
}

/*
 * Test: submit more reads than available buffers. Excess reads should
 * fail with -ENOBUFS. Then refill and retry.
 */
static int test_read_overflow_refill(void)
{
	struct io_uring ring;
	struct io_uring_buf_ring *br;
	struct io_uring_sqe *sqe;
	struct io_uring_cqe *cqe;
	void *buf_base;
	const char *fname = ".buf-ring-overflow-tmp";
	int fd, ret;
	int nr_success = 0, nr_enobufs = 0;

	ret = io_uring_queue_init(32, &ring, 0);
	if (ret) {
		fprintf(stderr, "ring setup: %d\n", ret);
		return T_EXIT_FAIL;
	}

	br = io_uring_setup_buf_ring(&ring, NR_BUFS, BGID, 0, &ret);
	if (!br) {
		if (ret == -EINVAL || ret == -ENOENT) {
			io_uring_queue_exit(&ring);
			return T_EXIT_SKIP;
		}
		fprintf(stderr, "buf ring setup: %d\n", ret);
		io_uring_queue_exit(&ring);
		return T_EXIT_FAIL;
	}

	buf_base = malloc(NR_BUFS * BUF_SIZE);
	if (!buf_base)
		goto err;

	/* Create test file */
	t_create_file(fname, NR_BUFS * BUF_SIZE * 4);
	fd = open(fname, O_RDONLY);
	if (fd < 0) {
		perror("open");
		goto err;
	}

	/* Provide only NR_BUFS buffers */
	provide_buffers(br, buf_base, NR_BUFS, BUF_SIZE);

	/* Submit more reads than we have buffers */
	for (int i = 0; i < NR_BUFS * 2; i++) {
		sqe = io_uring_get_sqe(&ring);
		io_uring_prep_read(sqe, fd, NULL, BUF_SIZE, i * BUF_SIZE);
		sqe->flags |= IOSQE_BUFFER_SELECT;
		sqe->buf_group = BGID;
		sqe->user_data = i;
	}

	ret = io_uring_submit(&ring);
	if (ret != NR_BUFS * 2) {
		fprintf(stderr, "submit: %d\n", ret);
		goto err_file;
	}

	/* Collect all completions */
	for (int i = 0; i < NR_BUFS * 2; i++) {
		ret = io_uring_wait_cqe(&ring, &cqe);
		if (ret) {
			fprintf(stderr, "wait: %d\n", ret);
			goto err_file;
		}

		if (cqe->res == -ENOBUFS) {
			nr_enobufs++;
		} else if (cqe->res > 0) {
			nr_success++;
		} else {
			fprintf(stderr, "unexpected res %d for ud %llu\n",
				cqe->res, (unsigned long long)cqe->user_data);
			io_uring_cqe_seen(&ring, cqe);
			goto err_file;
		}
		io_uring_cqe_seen(&ring, cqe);
	}

	/* We should have gotten exactly NR_BUFS successes */
	if (nr_success != NR_BUFS) {
		fprintf(stderr, "expected %d success, got %d\n", NR_BUFS, nr_success);
		goto err_file;
	}

	if (nr_enobufs != NR_BUFS) {
		fprintf(stderr, "expected %d ENOBUFS, got %d\n", NR_BUFS, nr_enobufs);
		goto err_file;
	}

	/* Now refill and do another round - should work fine */
	provide_buffers(br, buf_base, NR_BUFS, BUF_SIZE);

	nr_success = 0;
	for (int i = 0; i < NR_BUFS; i++) {
		sqe = io_uring_get_sqe(&ring);
		io_uring_prep_read(sqe, fd, NULL, BUF_SIZE, i * BUF_SIZE);
		sqe->flags |= IOSQE_BUFFER_SELECT;
		sqe->buf_group = BGID;
		sqe->user_data = 100 + i;
	}

	ret = io_uring_submit(&ring);
	if (ret != NR_BUFS) {
		fprintf(stderr, "refill submit: %d\n", ret);
		goto err_file;
	}

	for (int i = 0; i < NR_BUFS; i++) {
		ret = io_uring_wait_cqe(&ring, &cqe);
		if (ret) {
			fprintf(stderr, "refill wait: %d\n", ret);
			goto err_file;
		}

		if (cqe->res <= 0) {
			fprintf(stderr, "refill read res: %d\n", cqe->res);
			io_uring_cqe_seen(&ring, cqe);
			goto err_file;
		}
		nr_success++;
		io_uring_cqe_seen(&ring, cqe);
	}

	if (nr_success != NR_BUFS) {
		fprintf(stderr, "refill success: %d\n", nr_success);
		goto err_file;
	}

	close(fd);
	unlink(fname);
	free(buf_base);
	io_uring_free_buf_ring(&ring, br, NR_BUFS, BGID);
	io_uring_queue_exit(&ring);
	return T_EXIT_PASS;

err_file:
	close(fd);
	unlink(fname);
err:
	free(buf_base);
	io_uring_queue_exit(&ring);
	return T_EXIT_FAIL;
}

/*
 * Test: rapid drain/refill cycles with concurrent reads
 */
static int test_rapid_drain_refill(void)
{
	struct io_uring ring;
	struct io_uring_buf_ring *br;
	struct io_uring_sqe *sqe;
	struct io_uring_cqe *cqe;
	void *buf_base;
	const char *fname = ".buf-ring-rapid-tmp";
	int fd, ret;
	int total_success = 0;

	ret = io_uring_queue_init(64, &ring, 0);
	if (ret) {
		fprintf(stderr, "ring setup: %d\n", ret);
		return T_EXIT_FAIL;
	}

	br = io_uring_setup_buf_ring(&ring, NR_BUFS, BGID, 0, &ret);
	if (!br) {
		if (ret == -EINVAL || ret == -ENOENT) {
			io_uring_queue_exit(&ring);
			return T_EXIT_SKIP;
		}
		fprintf(stderr, "buf ring setup: %d\n", ret);
		io_uring_queue_exit(&ring);
		return T_EXIT_FAIL;
	}

	buf_base = malloc(NR_BUFS * BUF_SIZE);
	if (!buf_base)
		goto err;

	t_create_file(fname, FILE_SIZE);
	fd = open(fname, O_RDONLY);
	if (fd < 0) {
		perror("open");
		goto err;
	}

	/*
	 * Do many rapid cycles: provide 1 buffer, submit 1 read,
	 * reap, repeat. This tests the fast path of single-buffer
	 * provide/consume cycles.
	 */
	for (int i = 0; i < NR_ROUNDS * NR_BUFS; i++) {
		/* Provide exactly 1 buffer */
		void *addr = buf_base + (i % NR_BUFS) * BUF_SIZE;
		io_uring_buf_ring_add(br, addr, BUF_SIZE, i % NR_BUFS,
				      io_uring_buf_ring_mask(NR_BUFS), 0);
		io_uring_buf_ring_advance(br, 1);

		sqe = io_uring_get_sqe(&ring);
		io_uring_prep_read(sqe, fd, NULL, BUF_SIZE,
				   (i * BUF_SIZE) % FILE_SIZE);
		sqe->flags |= IOSQE_BUFFER_SELECT;
		sqe->buf_group = BGID;
		sqe->user_data = i;

		ret = io_uring_submit(&ring);
		if (ret != 1) {
			fprintf(stderr, "rapid submit %d: %d\n", i, ret);
			goto err_file;
		}

		ret = io_uring_wait_cqe(&ring, &cqe);
		if (ret) {
			fprintf(stderr, "rapid wait %d: %d\n", i, ret);
			goto err_file;
		}

		if (cqe->res <= 0) {
			fprintf(stderr, "rapid read %d res: %d\n", i, cqe->res);
			io_uring_cqe_seen(&ring, cqe);
			goto err_file;
		}

		if (!(cqe->flags & IORING_CQE_F_BUFFER)) {
			fprintf(stderr, "rapid read %d: no buffer flag\n", i);
			io_uring_cqe_seen(&ring, cqe);
			goto err_file;
		}

		total_success++;
		io_uring_cqe_seen(&ring, cqe);
	}

	if (total_success != NR_ROUNDS * NR_BUFS) {
		fprintf(stderr, "rapid: %d success, expected %d\n",
			total_success, NR_ROUNDS * NR_BUFS);
		goto err_file;
	}

	close(fd);
	unlink(fname);
	free(buf_base);
	io_uring_free_buf_ring(&ring, br, NR_BUFS, BGID);
	io_uring_queue_exit(&ring);
	return T_EXIT_PASS;

err_file:
	close(fd);
	unlink(fname);
err:
	free(buf_base);
	io_uring_queue_exit(&ring);
	return T_EXIT_FAIL;
}

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

	if (argc > 1)
		return T_EXIT_SKIP;

	ret = test_read_drain_refill();
	if (ret == T_EXIT_SKIP) {
		printf("Buffer rings not supported, skipping\n");
		return T_EXIT_SKIP;
	}
	if (ret == T_EXIT_FAIL) {
		fprintf(stderr, "test_read_drain_refill failed\n");
		return T_EXIT_FAIL;
	}

	ret = test_read_overflow_refill();
	if (ret == T_EXIT_FAIL) {
		fprintf(stderr, "test_read_overflow_refill failed\n");
		return T_EXIT_FAIL;
	}

	ret = test_rapid_drain_refill();
	if (ret == T_EXIT_FAIL) {
		fprintf(stderr, "test_rapid_drain_refill failed\n");
		return T_EXIT_FAIL;
	}

	return T_EXIT_PASS;
}
