/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "array.h"
#include "ioloop.h"
#include "istream.h"
#include "ostream.h"
#include "guid.h"
#include "llist.h"
#include "settings.h"
#include "master-service.h"
#include "master-service-settings.h"
#include "dict.h"
#include "fs-api.h"

#define DEFAULT_MAX_PARALLEL_OPS 30
#define DEFAULT_FILES_COUNT 100
/* Allow +-10% difference in number of files */
#define FILES_COUNT_APPROX 0.9

struct test_read {
	struct test_read *prev, *next;

	struct test_ctx *ctx;
	struct fs_file *file;
	struct istream *input;
	struct io *io;
};

struct test_write {
	struct test_write *prev, *next;

	struct test_ctx *ctx;
	struct fs_file *file;
};

struct test_delete {
	struct test_delete *prev, *next;

	struct test_ctx *ctx;
	struct fs_file *file;
};

struct test_iter {
	struct test_iter *prev, *next;

	struct test_ctx *ctx;
	pool_t pool;
	ARRAY_TYPE(const_string) files;
	struct fs_iter *iter;
	bool object_ids;
};

struct test_ctx {
	struct fs *fs;
	const char *prefix;
	bool sync_only, async_only;
	unsigned int files_count;
	unsigned int max_parallel_ops;

	pool_t files_pool;
	ARRAY_TYPE(const_string) files;

	struct timeout *to;
	struct test_read *reads;
	struct test_write *writes;
	struct test_delete *deletes;
	struct test_iter *iters;
	unsigned int running_op_count;

	unsigned int total_reads, total_writes, total_deletes, total_iters;
};

static struct ioloop *root_ioloop;

static void test_more(struct test_ctx *ctx);
static void test_read_callback(struct test_read *r);

static bool test_want_async(struct test_ctx *ctx)
{
	if (ctx->async_only)
		return TRUE;
	if (ctx->sync_only)
		return FALSE;
	return (i_rand_limit(2) == 0);
}

static void test_op_finish(struct test_ctx *ctx)
{
	i_assert(ctx->running_op_count > 0);
	ctx->running_op_count--;

	if (ctx->to == NULL)
		ctx->to = timeout_add_short_to(root_ioloop, 0, test_more, ctx);
}

static void test_write_finish(struct test_write *w)
{
	DLLIST_REMOVE(&w->ctx->writes, w);
	fs_file_deinit(&w->file);

	w->ctx->total_writes++;
	test_op_finish(w->ctx);
	i_free(w);
}

static void test_write_callback(struct test_write *w)
{
	int ret;

	ret = fs_write_stream_finish_async(w->file);
	if (ret < 0) {
		i_error("fs_write_stream() failed: %s",
			fs_file_last_error(w->file));
	}
	if (ret != 0)
		test_write_finish(w);
}

static void test_next_op_write(struct test_ctx *ctx)
{
	struct test_write *w;
	struct istream *input, *input2;
	struct ostream *output;
	const char *path;

	w = i_new(struct test_write, 1);
	w->ctx = ctx;
	DLLIST_PREPEND(&ctx->writes, w);

	path = t_strconcat(ctx->prefix, guid_generate(), NULL);
	w->file = fs_file_init(ctx->fs, path, FS_OPEN_MODE_REPLACE |
			       (test_want_async(ctx) ? FS_OPEN_FLAG_ASYNC : 0));
	input = i_stream_create_file("/dev/urandom", IO_BLOCK_SIZE);
	input2 = i_stream_create_limit(input, i_rand_limit(1024*1024));
	output = fs_write_stream(w->file);
	switch (o_stream_send_istream(output, input2)) {
	case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
		break;
	case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
	case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
		i_unreached();
	case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
		i_fatal("read(/dev/urandom) failed: %s",
			i_stream_get_error(input));
		case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
			i_fatal("write() failed: %s",
				o_stream_get_error(output));
	}
	i_stream_unref(&input);
	i_stream_unref(&input2);

	int ret = fs_write_stream_finish(w->file, &output);
	if (ret < 0) {
		i_error("fs_write_stream(%s) failed: %s",
			path, fs_file_last_error(w->file));
	}
	if (ret == 0)
		fs_file_set_async_callback(w->file, test_write_callback, w);
	else
		test_write_finish(w);
}

static void test_iter_finish(struct test_iter *i)
{
	const char *error;

	DLLIST_REMOVE(&i->ctx->iters, i);

	if (fs_iter_deinit(&i->iter, &error) < 0) {
		i_error("fs_iter_deinit() failed: %s", error);
		pool_unref(&i->pool);
	} else {
		pool_unref(&i->ctx->files_pool);
		i->ctx->files_pool = i->pool;
		i->ctx->files = i->files;
	}

	i->ctx->total_iters++;
	test_op_finish(i->ctx);
	i_free(i);
}

static void test_iter_callback(struct test_iter *i)
{
	const char *fname;

	while ((fname = fs_iter_next(i->iter)) != NULL) {
		if (i->object_ids) {
			/* skip object ID */
			fname = strchr(fname, '/');
			i_assert(fname != NULL);
			fname++;
		}
		fname = p_strdup(i->pool, fname);
		array_push_back(&i->files, &fname);
	}

	if (!fs_iter_have_more(i->iter))
		test_iter_finish(i);
}

static void test_next_op_iter(struct test_ctx *ctx)
{
	struct test_iter *i;

	i = i_new(struct test_iter, 1);
	i->ctx = ctx;
	i->pool = pool_alloconly_create(MEMPOOL_GROWING"test iter", 256);
	p_array_init(&i->files, i->pool, 16);
	DLLIST_PREPEND(&ctx->iters, i);

	i->object_ids = (fs_get_properties(ctx->fs) &
			 FS_PROPERTY_OBJECTIDS) == 0 ?
		FALSE : (i_rand_limit(2) == 0);
	i->iter = fs_iter_init(ctx->fs, ctx->prefix,
			       (test_want_async(ctx) ? FS_ITER_FLAG_ASYNC : 0) |
			       (i->object_ids ? FS_ITER_FLAG_OBJECTIDS : 0));
	fs_iter_set_async_callback(i->iter, test_iter_callback, i);
	test_iter_callback(i);
}

static void test_read_finish(struct test_read *r)
{
	DLLIST_REMOVE(&r->ctx->reads, r);
	io_remove(&r->io);
	i_stream_unref(&r->input);
	fs_file_deinit(&r->file);

	r->ctx->total_reads++;
	test_op_finish(r->ctx);
	i_free(r);
}

static void test_read_callback(struct test_read *r)
{
	const unsigned char *data;
	size_t size;
	int ret;

	while ((ret = i_stream_read_more(r->input, &data, &size)) > 0)
		i_stream_skip(r->input, size);
	if (ret == 0) {
		if (r->io == NULL) {
			r->io = io_add_istream(r->input, test_read_callback, r);
			fs_file_set_async_callback(r->file, test_read_callback, r);
		}
		return;
	}
	i_assert(ret == -1);
	if (r->input->stream_errno != 0 &&
	    r->input->stream_errno != ENOENT)
		i_error("read() failed: %s", i_stream_get_error(r->input));
	test_read_finish(r);
}

static void test_next_read(struct test_ctx *ctx)
{
	struct test_read *r;

	r = i_new(struct test_read, 1);
	r->ctx = ctx;
	DLLIST_PREPEND(&ctx->reads, r);

	const char *const *fnamep =
		array_idx(&ctx->files, i_rand_limit(array_count(&ctx->files)));
	const char *path = t_strconcat(ctx->prefix, *fnamep, NULL);
	r->file = fs_file_init(ctx->fs, path, FS_OPEN_MODE_READONLY |
			       (test_want_async(ctx) ? FS_OPEN_FLAG_ASYNC : 0));
	r->input = fs_read_stream(r->file, IO_BLOCK_SIZE);
	test_read_callback(r);
}

static void test_delete_finish(struct test_delete *d)
{
	DLLIST_REMOVE(&d->ctx->deletes, d);
	fs_file_deinit(&d->file);

	d->ctx->total_deletes++;
	test_op_finish(d->ctx);
	i_free(d);
}

static void test_delete_callback(struct test_delete *d)
{
	int ret = fs_delete(d->file);
	if (ret < 0 && errno != ENOENT) {
		if (errno == EAGAIN)
			return;
		i_error("fs_delete() failed: %s", fs_file_last_error(d->file));
	}
	test_delete_finish(d);
}

static void test_next_delete(struct test_ctx *ctx)
{
	struct test_delete *d;

	d = i_new(struct test_delete, 1);
	d->ctx = ctx;
	DLLIST_PREPEND(&ctx->deletes, d);

	const char *const *fnamep =
		array_idx(&ctx->files, i_rand_limit(array_count(&ctx->files)));
	const char *path = t_strconcat(ctx->prefix, *fnamep, NULL);
	d->file = fs_file_init(ctx->fs, path, FS_OPEN_MODE_READONLY |
			       (test_want_async(ctx) ? FS_OPEN_FLAG_ASYNC : 0));
	int ret = fs_delete(d->file);
	if (ret < 0 && errno == EAGAIN) {
		fs_file_set_async_callback(d->file, test_delete_callback, d);
		return;
	}
	if (ret < 0 && errno != ENOENT)
		i_error("fs_delete() failed: %s", fs_file_last_error(d->file));
	test_delete_finish(d);
}

static void test_next_op(struct test_ctx *ctx)
{
	switch (i_rand_limit(4)) {
	case 0:
		if (array_is_created(&ctx->files) &&
		    array_count(&ctx->files) * FILES_COUNT_APPROX > ctx->files_count)
			break;
		ctx->running_op_count++;
		test_next_op_write(ctx);
		break;
	case 1:
		ctx->running_op_count++;
		test_next_op_iter(ctx);
		break;
	case 2:
		if (!array_is_created(&ctx->files) ||
		    array_count(&ctx->files) == 0)
			break;
		ctx->running_op_count++;
		test_next_read(ctx);
		break;
	case 3:
		if (!array_is_created(&ctx->files) ||
		    array_count(&ctx->files) / FILES_COUNT_APPROX < ctx->files_count)
			break;
		ctx->running_op_count++;
		test_next_delete(ctx);
		break;
	}
}

static void test_more(struct test_ctx *ctx)
{
	timeout_remove(&ctx->to);
	/* note that all operations may be synchronous */
	for (unsigned int i = ctx->running_op_count; i < ctx->max_parallel_ops; i++)
		test_next_op(ctx);
	if (ctx->to == NULL) {
		ctx->to = timeout_add(ctx->running_op_count == 0 ? 0 :
				      i_rand_limit(100), test_more, ctx);
	}
}

static void stats_output(struct test_ctx *ctx)
{
	printf("%u iters, %u reads, %u writes, %u deletes\n",
	       ctx->total_iters, ctx->total_reads, ctx->total_writes,
	       ctx->total_deletes);
}

int main(int argc, char *argv[])
{
	struct fs_parameters params;
	struct test_ctx ctx;
	const char *error;
	unsigned int timeout_secs = 0;
	struct timeout *to_stop = NULL;
	int c;

	i_zero(&ctx);
	ctx.max_parallel_ops = DEFAULT_MAX_PARALLEL_OPS;
	ctx.files_count = DEFAULT_FILES_COUNT;

	i_zero(&params);
	params.base_dir = PKG_RUNDIR;
	master_service = master_service_init("test-fs",
					     MASTER_SERVICE_FLAG_STANDALONE |
					     MASTER_SERVICE_FLAG_CONFIG_DEFAULTS,
					     &argc, &argv, "Daf:p:st:u:");
	while ((c = master_getopt(master_service)) > 0) {
		switch (c) {
		case 'D':
			event_set_forced_debug(
				master_service_get_event(master_service), TRUE);
			break;
		case 'a':
			ctx.async_only = TRUE;
			break;
		case 'f':
			if (str_to_uint(optarg, &ctx.files_count) < 0)
				i_fatal("Invalid -f parameter: %s", optarg);
			break;
		case 'p':
			if (str_to_uint(optarg, &ctx.max_parallel_ops) < 0)
				i_fatal("Invalid -p parameter: %s", optarg);
			break;
		case 's':
			ctx.sync_only = TRUE;
			break;
		case 'u':
			params.username = optarg;
			break;
		case 't':
			if (str_to_uint(optarg, &timeout_secs) < 0)
				i_fatal("Invalid -t parameter: %s", optarg);
			break;
		default:
			return FATAL_DEFAULT;
		}
	}
	argc -= optind;
	argv += optind;

	if (argc != 1)
		i_fatal("Usage: [-a|-s] [-D] [-f <files#>] [-p <max ops>] [-t <secs>] [-u <user>] -o fs_driver=<name> [-o ...] <prefix>");

	if (master_service_settings_read_simple(master_service, &error) < 0)
		i_fatal("%s", error);

	master_service_init_finish(master_service);
	dict_drivers_register_builtin();

	if (fs_init_auto(master_service_get_event(master_service),
			 &params, &ctx.fs, &error) <= 0)
		i_fatal("fs_init() failed: %s", error);
	ctx.prefix = argv[0];

	root_ioloop = current_ioloop;
	test_more(&ctx);
	if (timeout_secs != 0)
		to_stop = timeout_add(timeout_secs*1000, io_loop_stop, current_ioloop);
	struct timeout *to_stats = timeout_add(1000, stats_output, &ctx);
	io_loop_run(current_ioloop);
	timeout_remove(&to_stats);
	timeout_remove(&to_stop);

	while (ctx.writes != NULL)
		test_write_finish(ctx.writes);
	while (ctx.iters != NULL)
		test_iter_finish(ctx.iters);
	while (ctx.reads != NULL)
		test_read_finish(ctx.reads);
	while (ctx.deletes != NULL)
		test_delete_finish(ctx.deletes);

	stats_output(&ctx);
	timeout_remove(&ctx.to);
	fs_deinit(&ctx.fs);
	master_service_deinit(&master_service);
}
