/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "istream.h"
#include "ostream.h"
#include "test-common.h"
#include "fs-test.h"

static struct fs *fs_test_alloc(void)
{
	struct test_fs *fs;

	fs = i_new(struct test_fs, 1);
	fs->fs = fs_class_test;
	i_array_init(&fs->iter_files, 32);
	return &fs->fs;
}

static int
fs_test_init(struct fs *_fs ATTR_UNUSED,
	     const struct fs_parameters *params ATTR_UNUSED,
	     const char **error_r ATTR_UNUSED)
{
	return 0;
}

static void fs_test_free(struct fs *_fs)
{
	struct test_fs *fs = (struct test_fs *)_fs;

	array_free(&fs->iter_files);
	i_free(fs);
}

static enum fs_properties fs_test_get_properties(struct fs *_fs)
{
	struct test_fs *fs = (struct test_fs *)_fs;

	return fs->properties;
}

static struct fs_file *fs_test_file_alloc(void)
{
	struct test_fs_file *file = i_new(struct test_fs_file, 1);
	return &file->file;
}

static void
fs_test_file_init(struct fs_file *_file, const char *path,
		  enum fs_open_mode mode, enum fs_open_flags flags)
{
	struct test_fs_file *file = (struct test_fs_file *)_file;

	file->file.path = i_strdup(path);
	file->file.flags = flags;
	file->mode = mode;
	file->contents = buffer_create_dynamic(default_pool, 1024);
	file->exists = TRUE;
	file->seekable = TRUE;
	file->wait_async = (flags & FS_OPEN_FLAG_ASYNC) != 0;
}

static void fs_test_file_deinit(struct fs_file *_file)
{
	struct test_fs_file *file = (struct test_fs_file *)_file;

	fs_file_free(_file);
	buffer_free(&file->contents);
	i_free(file->file.path);
	i_free(file);
}

static void fs_test_file_close(struct fs_file *_file)
{
	struct test_fs_file *file = (struct test_fs_file *)_file;

	file->closed = TRUE;
}

static const char *fs_test_file_get_path(struct fs_file *_file)
{
	return _file->path;
}

static void
fs_test_set_async_callback(struct fs_file *_file,
			   fs_file_async_callback_t *callback,
			   void *context)
{
	struct test_fs_file *file = (struct test_fs_file *)_file;

	file->async_callback = callback;
	file->async_context = context;
}

static void fs_test_wait_async(struct fs *_fs ATTR_UNUSED)
{
}

static void
fs_test_set_metadata(struct fs_file *_file, const char *key,
		     const char *value)
{
	if (strcmp(key, FS_METADATA_WRITE_FNAME) == 0) {
		i_free(_file->path);
		_file->path = i_strdup(value);
	} else {
		fs_default_set_metadata(_file, key, value);
	}
}

static int
fs_test_get_metadata(struct fs_file *_file,
		     enum fs_get_metadata_flags flags,
		     const ARRAY_TYPE(fs_metadata) **metadata_r)
{
	struct test_fs_file *file = (struct test_fs_file *)_file;

	if ((flags & FS_GET_METADATA_FLAG_LOADED_ONLY) != 0) {
		*metadata_r = &_file->metadata;
		return 0;
	}

	if (file->wait_async) {
		fs_file_set_error_async(_file);
		return -1;
	}
	if (file->io_failure) {
		errno = EIO;
		return -1;
	}
	fs_metadata_init(_file);
	*metadata_r = &_file->metadata;
	return 0;
}

static bool fs_test_prefetch(struct fs_file *_file ATTR_UNUSED,
			     uoff_t length ATTR_UNUSED)
{
	struct test_fs_file *file = (struct test_fs_file *)_file;

	file->prefetched = TRUE;
	return TRUE;
}

static void fs_test_stream_destroyed(struct test_fs_file *file)
{
	i_assert(file->input != NULL);
	file->input = NULL;
}

static struct istream *
fs_test_read_stream(struct fs_file *_file, size_t max_buffer_size ATTR_UNUSED)
{
	struct test_fs_file *file = (struct test_fs_file *)_file;
	struct istream *input;

	i_assert(file->input == NULL);

	if (!file->exists)
		return i_stream_create_error(ENOENT);
	if (file->io_failure)
		return i_stream_create_error(EIO);
	input = test_istream_create_data(file->contents->data,
					 file->contents->used);
	i_stream_add_destroy_callback(input, fs_test_stream_destroyed, file);
	if (!file->seekable)
		input->seekable = FALSE;
	file->input = input;
	return input;
}

static void fs_test_write_stream(struct fs_file *_file)
{
	struct test_fs_file *file = (struct test_fs_file *)_file;

	i_assert(_file->output == NULL);

	buffer_set_used_size(file->contents, 0);
	_file->output = o_stream_create_buffer(file->contents);
}

static int fs_test_write_stream_finish(struct fs_file *_file, bool success)
{
	struct test_fs_file *file = (struct test_fs_file *)_file;

	o_stream_destroy(&_file->output);
	if (file->wait_async) {
		fs_file_set_error_async(_file);
		return 0;
	}
	if (file->io_failure)
		success = FALSE;
	if (!success)
		buffer_set_used_size(file->contents, 0);
	return success ? 1 : -1;
}

static int
fs_test_lock(struct fs_file *_file, unsigned int secs ATTR_UNUSED,
	     struct fs_lock **lock_r)
{
	struct test_fs_file *file = (struct test_fs_file *)_file;

	if (file->locked)
		return 0;
	file->locked = TRUE;
	*lock_r = i_new(struct fs_lock, 1);
	(*lock_r)->file = _file;
	return 1;
}

static void fs_test_unlock(struct fs_lock *lock)
{
	struct test_fs_file *file = (struct test_fs_file *)lock->file;

	file->locked = FALSE;
	i_free(lock);
}

static int fs_test_exists(struct fs_file *_file)
{
	struct test_fs_file *file = (struct test_fs_file *)_file;

	if (file->wait_async) {
		fs_file_set_error_async(_file);
		return -1;
	}
	if (file->io_failure) {
		errno = EIO;
		return -1;
	}
	return file->exists ? 1 : 0;
}

static int fs_test_stat(struct fs_file *_file, struct stat *st_r)
{
	struct test_fs_file *file = (struct test_fs_file *)_file;

	if (file->wait_async) {
		fs_file_set_error_async(_file);
		return -1;
	}
	if (file->io_failure) {
		errno = EIO;
		return -1;
	}
	if (!file->exists) {
		errno = ENOENT;
		return -1;
	}
	i_zero(st_r);
	st_r->st_size = file->contents->used;
	return 0;
}

static int fs_test_copy(struct fs_file *_src, struct fs_file *_dest)
{
	struct test_fs_file *src;
	struct test_fs_file *dest = (struct test_fs_file *)_dest;

	if (_src != NULL)
		dest->copy_src = test_fs_file_get(_src->fs, fs_file_path(_src));
	src = dest->copy_src;
	if (dest->wait_async) {
		fs_file_set_error_async(_dest);
		return -1;
	}
	dest->copy_src = NULL;

	if (dest->io_failure) {
		errno = EIO;
		return -1;
	}
	if (!src->exists) {
		errno = ENOENT;
		return -1;
	}
	buffer_set_used_size(dest->contents, 0);
	buffer_append_buf(dest->contents, src->contents, 0, SIZE_MAX);
	dest->exists = TRUE;
	return 0;
}

static int fs_test_rename(struct fs_file *_src, struct fs_file *_dest)
{
	struct test_fs_file *src = (struct test_fs_file *)_src;
	struct test_fs_file *dest = (struct test_fs_file *)_dest;

	if (src->wait_async || dest->wait_async) {
		fs_file_set_error_async(_dest);
		return -1;
	}

	if (fs_test_copy(_src, _dest) < 0)
		return -1;
	src->exists = FALSE;
	return 0;
}

static int fs_test_delete(struct fs_file *_file)
{
	struct test_fs_file *file = (struct test_fs_file *)_file;

	if (file->wait_async) {
		fs_file_set_error_async(_file);
		return -1;
	}

	if (!file->exists) {
		errno = ENOENT;
		return -1;
	}
	return 0;
}

static struct fs_iter *fs_test_iter_alloc(void)
{
	struct test_fs_iter *iter = i_new(struct test_fs_iter, 1);
	return &iter->iter;
}

static void
fs_test_iter_init(struct fs_iter *_iter, const char *path,
		  enum fs_iter_flags flags ATTR_UNUSED)
{
	struct test_fs_iter *iter = (struct test_fs_iter *)_iter;
	struct test_fs *fs = (struct test_fs *)_iter->fs;

	iter->prefix = i_strdup(path);
	iter->prefix_len = strlen(iter->prefix);
	iter->prev_dir = i_strdup("");
	array_sort(&fs->iter_files, i_strcmp_p);
}

static const char *fs_test_iter_next(struct fs_iter *_iter)
{
	struct test_fs_iter *iter = (struct test_fs_iter *)_iter;
	struct test_fs *fs = (struct test_fs *)_iter->fs;
	const char *const *files, *p;
	unsigned int count;
	size_t len, prev_dir_len = strlen(iter->prev_dir);

	files = array_get(&fs->iter_files, &count);
	for (; iter->idx < count; iter->idx++) {
		const char *fname = files[iter->idx];

		if (strncmp(fname, iter->prefix, iter->prefix_len) != 0)
			continue;
		p = strrchr(fname, '/');
		if ((_iter->flags & FS_ITER_FLAG_DIRS) == 0) {
			if (p == NULL)
				return fname;
			if (p[1] == '\0')
				continue; /* dir/ */
			return p+1;
		}

		if (p == NULL)
			continue;
		len = p - fname;
		if (len == 0)
			continue;
		if (len == prev_dir_len &&
		    strncmp(fname, iter->prev_dir, len) == 0)
			continue;
		i_free(iter->prev_dir);
		iter->prev_dir = i_strndup(fname, len);
		return iter->prev_dir;
	}
	return NULL;
}

static int fs_test_iter_deinit(struct fs_iter *_iter)
{
	struct test_fs_iter *iter = (struct test_fs_iter *)_iter;
	int ret = iter->failed ? -1 : 0;

	i_free(iter->prefix);
	return ret;
}

struct test_fs *test_fs_get(struct fs *fs)
{
	while (strcmp(fs->name, "test") != 0) {
		i_assert(fs->parent != NULL);
		fs = fs->parent;
	}
	return (struct test_fs *)fs;
}

struct test_fs_file *test_fs_file_get(struct fs *fs, const char *path)
{
	struct fs_file *file;

	fs = &test_fs_get(fs)->fs;

	for (file = fs->files;; file = file->next) {
		i_assert(file != NULL);
		if (strcmp(fs_file_path(file), path) == 0)
			break;
	}
	return (struct test_fs_file *)file;
}

const struct fs fs_class_test = {
	.name = "test",
	.v = {
		.alloc = fs_test_alloc,
		.init = fs_test_init,
		.deinit = NULL,
		.free = fs_test_free,
		.get_properties = fs_test_get_properties,
		.file_alloc = fs_test_file_alloc,
		.file_init = fs_test_file_init,
		.file_deinit = fs_test_file_deinit,
		.file_close = fs_test_file_close,
		.get_path = fs_test_file_get_path,
		.set_async_callback = fs_test_set_async_callback,
		.wait_async = fs_test_wait_async,
		.set_metadata = fs_test_set_metadata,
		.get_metadata = fs_test_get_metadata,
		.prefetch = fs_test_prefetch,
		.read = NULL,
		.read_stream = fs_test_read_stream,
		.write = NULL,
		.write_stream = fs_test_write_stream,
		.write_stream_finish = fs_test_write_stream_finish,
		.lock = fs_test_lock,
		.unlock = fs_test_unlock,
		.exists = fs_test_exists,
		.stat = fs_test_stat,
		.copy = fs_test_copy,
		.rename = fs_test_rename,
		.delete_file = fs_test_delete,
		.iter_alloc = fs_test_iter_alloc,
		.iter_init = fs_test_iter_init,
		.iter_next = fs_test_iter_next,
		.iter_deinit = fs_test_iter_deinit,
		.switch_ioloop = NULL,
		.get_nlinks = NULL,
	}
};
