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

#include "lib.h"
#include "test-common.h"
#include "test-dir.h"
#include "test-subprocess.h"
#include "istream.h"
#include "istream-unix.h"
#include "strescape.h"
#include "path-util.h"
#include "unlink-directory.h"
#include "settings-parser.h"
#include "settings.h"
#include "master-service.h"
#include "smtp-submit.h"
#include "mail-storage-service.h"
#include "mail-storage-private.h"
#include "imap-common.h"
#include "imap-settings.h"
#include "imap-client.h"

#include <sys/stat.h>

#define TEMP_DIRNAME ".test-ich"
#define SERVER_KILL_TIMEOUT_SECS 10

#define EVILSTR "\t\r\n\001"

struct test_imap_client_hibernate {
	struct client *client;
	int fd_listen;
	bool has_mailbox;
	const char *reply;
};

imap_client_created_func_t *hook_client_created = NULL;
bool imap_debug = FALSE;
bool verbose_proctitle = FALSE;

static struct mail_storage_service_ctx *storage_service;

void imap_refresh_proctitle(void) { }
void imap_refresh_proctitle_delayed(void) { }
int client_create_from_input(const struct mail_storage_service_input *input ATTR_UNUSED,
			     const struct imap_logout_stats *stats ATTR_UNUSED,
			     int fd_in ATTR_UNUSED, int fd_out ATTR_UNUSED,
			     enum client_create_flags flags ATTR_UNUSED,
			     struct client **client_r ATTR_UNUSED,
			     const char **error_r ATTR_UNUSED) { return -1; }

static int imap_hibernate_server(struct test_imap_client_hibernate *ctx)
{
	i_set_failure_prefix("SERVER: ");

	int fd = net_accept(ctx->fd_listen, NULL, NULL);
	i_assert(fd > 0);
	struct istream *input = i_stream_create_unix(fd, SIZE_MAX);
	i_stream_unix_set_read_fd(input);

	/* send handshake */
	const char *str = "VERSION\timap-hibernate\t1\t0\n";
	if (write(fd, str, strlen(str)) != (ssize_t)strlen(str))
		i_fatal("write(imap-hibernate client handshake) failed: %m");

	/* read handshake */
	const char *line;
	if ((line = i_stream_read_next_line(input)) == NULL)
		i_fatal("read(imap-hibernate client handshake) failed: %s",
			i_stream_get_error(input));
	if (strcmp(line, "VERSION\timap-hibernate\t1\t0") != 0)
		i_fatal("VERSION not received");
	/* read command */
	if ((line = i_stream_read_next_line(input)) == NULL)
		i_fatal("read(imap-hibernate client command) failed: %s",
			i_stream_get_error(input));
	int fd2 = i_stream_unix_get_read_fd(input);
	test_assert(fd2 != -1);
	i_close_fd(&fd2);
	const char *const *args = t_strsplit_tabescaped(line);

	/* write reply */
	if (write(fd, ctx->reply, strlen(ctx->reply)) != (ssize_t)strlen(ctx->reply))
		i_fatal("write(imap-hibernate client command) failed: %m");

	if (ctx->has_mailbox) {
		/* read mailbox notify fd */
		i_stream_unix_set_read_fd(input);
		if (i_stream_read_next_line(input) == NULL)
			i_fatal("read(imap-hibernate notify fd) failed: %s",
				i_stream_get_error(input));

		fd2 = i_stream_unix_get_read_fd(input);
		test_assert(fd2 != -1);
		i_close_fd(&fd2);

		if (write(fd, "+\n", 2) != 2)
			i_fatal("write(imap-hibernate client command) failed: %m");
	}

	unsigned int i = 0;
	test_assert_strcmp(args[i++], EVILSTR"testuser");
	test_assert_strcmp(args[i++], EVILSTR"%u");
	test_assert_strcmp(args[i++], "idle_notify_interval=120");
	test_assert(str_begins_with(args[i++], "peer_dev_major="));
	test_assert(str_begins_with(args[i++], "peer_dev_minor="));
	test_assert(str_begins_with(args[i++], "peer_ino="));
	test_assert_strcmp(args[i++], "session="EVILSTR"session");
	test_assert(str_begins_with(args[i++], "session_created="));
	test_assert_strcmp(args[i++], "lip=127.0.0.1");
	test_assert_strcmp(args[i++], "lport=1234");
	test_assert_strcmp(args[i++], "rip=127.0.0.2");
	test_assert_strcmp(args[i++], "rport=5678");
	test_assert(str_begins_with(args[i++], "uid="));
	test_assert(str_begins_with(args[i++], "gid="));
	if (ctx->has_mailbox)
		test_assert_strcmp(args[i++], "mailbox="EVILSTR"mailbox");
	test_assert_strcmp(args[i++], "tag="EVILSTR"tag");
	test_assert_strcmp(args[i++], "auth_token=asdf");
	test_assert(str_begins_with(args[i++], "session_pid="));
	test_assert(args[i] != NULL && str_begins_with(args[i++], "stats="));
	test_assert_strcmp(args[i++], "idle-cmd");
	if (ctx->has_mailbox)
		test_assert_strcmp(args[i++], "notify_fd");

	const char *const stats_prefixes[] = {
		"state=",
		"fetch_hdr_count=",
		"fetch_hdr_bytes=",
		"fetch_body_count=",
		"fetch_body_bytes=",
		"deleted_count=",
		"expunged_count=",
		"trashed_count=",
		"autoexpunged_count=",
		"append_count=",
		"input_bytes_extra=",
		"output_bytes_extra=",
	};
	for (size_t p = 0; p < N_ELEMENTS(stats_prefixes); p++)
		test_assert(str_begins_with(args[i++], stats_prefixes[p]));

	test_assert(args[i] == NULL);

	i_stream_unref(&input);
	i_close_fd(&ctx->fd_listen);
	i_close_fd(&fd);

	ctx->client->hibernated = TRUE; /* prevent disconnect Info message */
	client_destroy(ctx->client, NULL);

	mail_storage_service_deinit(&storage_service);
	master_service_deinit_forked(&master_service);
	return test_has_failed() ? 1 : 0;
}

static void
mailbox_notify_callback(struct mailbox *box ATTR_UNUSED,
			struct client *client ATTR_UNUSED)
{
}

static void test_imap_client_hibernate(void)
{
	struct client *client;
	struct smtp_submit_settings smtp_set;
	struct mail_user *mail_user;
	struct test_imap_client_hibernate ctx;
	const char *error;

	storage_service = mail_storage_service_init(master_service,
		MAIL_STORAGE_SERVICE_FLAG_ALLOW_ROOT |
		MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT |
		MAIL_STORAGE_SERVICE_FLAG_NO_CHDIR |
		MAIL_STORAGE_SERVICE_FLAG_NO_RESTRICT_ACCESS);

	const char *const input_userdb[] = {
		"mailbox_list_index=no",
		"mail_driver=mbox",
		t_strdup_printf("mail_path=%s/mbox", test_dir_get()),
		t_strdup_printf("base_dir=%s", test_dir_get()),
		"mail_log_prefix="EVILSTR"%u",
		"auth_token=asdf",
		NULL
	};
	struct mail_storage_service_input input = {
		.username = EVILSTR"testuser",
		.local_port = 1234,
		.remote_port = 5678,
		.userdb_fields = input_userdb,
		.session_id = EVILSTR"session",
	};
	test_assert(net_addr2ip("127.0.0.1", &input.local_ip) == 0);
	test_assert(net_addr2ip("127.0.0.2", &input.remote_ip) == 0);
	test_assert(mail_storage_service_lookup_next(storage_service, &input,
						     &mail_user, &error) == 1);
	i_zero(&smtp_set);
	i_zero(&ctx);

	struct event *event = event_create(NULL);
	int client_fd = dup(dev_null_fd);
	client = client_create(client_fd, client_fd, 0, event, mail_user,
			       imap_setting_parser_info.defaults, &smtp_set);
	ctx.client = client;

	/* can't hibernate without IDLE */
	test_begin("imap client hibernate: non-IDLE");
	test_assert(!imap_client_hibernate(&client, &error));
	test_assert_strcmp(error, "Non-IDLE connections not supported currently");
	test_end();

	struct client_command_context *cmd = client_command_alloc(client);
	cmd->tag = EVILSTR"tag";
	cmd->name = "IDLE";
	event_unref(&event);

	/* imap-hibernate socket doesn't exist */
	test_begin("imap client hibernate: socket not found");
	test_expect_error_string("/imap-hibernate) failed: No such file or directory");
	test_assert(!imap_client_hibernate(&client, &error));
	test_expect_no_more_errors();
	test_assert(strstr(error, "net_connect_unix") != NULL);
	test_end();

	/* imap-hibernate socket times out */
	const char *socket_path = t_strdup_printf("%s/imap-hibernate",
						  test_dir_get());
	ctx.fd_listen = net_listen_unix(socket_path, 1);
	if (ctx.fd_listen == -1)
		i_fatal("net_listen_unix(%s) failed: %m", socket_path);
	fd_set_nonblock(ctx.fd_listen, FALSE);

	/* imap-hibernate socket returns failure */
	test_begin("imap client hibernate: error returned");
	ctx.reply = "-notgood\n";
	test_subprocess_fork(imap_hibernate_server, &ctx, FALSE);

	test_expect_error_string("/imap-hibernate returned failure: notgood");
	test_assert(!imap_client_hibernate(&client, &error));
	test_expect_no_more_errors();
	test_assert(strstr(error, "notgood") != NULL);
	test_end();

	/* create and open evil mailbox */
	client->mailbox = mailbox_alloc(client->user->namespaces->list,
					"testbox", 0);
	struct mailbox_update update = {
		.uid_validity = 12345678,
	};
	memset(update.mailbox_guid, 0x12, sizeof(update.mailbox_guid));
	test_assert(mailbox_create(client->mailbox, &update, FALSE) == 0);
	test_assert(mailbox_open(client->mailbox) == 0);
	client->mailbox->vname = EVILSTR"mailbox";

	/* successful hibernation */
	test_begin("imap client hibernate: success");
	ctx.reply = "+\n";
	ctx.has_mailbox = TRUE;
	test_subprocess_fork(imap_hibernate_server, &ctx, FALSE);
	/* start notification only after forking or we'll have trouble
	   deinitializing cleanly */
	mailbox_notify_changes(client->mailbox, mailbox_notify_callback, client);
	test_assert(imap_client_hibernate(&client, &error));
	test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS);

	test_end();

	i_close_fd(&ctx.fd_listen);
	mail_storage_service_deinit(&storage_service);
}

struct test_service_settings {
	pool_t pool;
	ARRAY_TYPE(const_string) services;
};

static const struct setting_define test_service_setting_defines[] = {
	{ .type = SET_FILTER_ARRAY, .key = "service",
	  .offset = offsetof(struct test_service_settings, services) },
	SETTING_DEFINE_LIST_END
};

static const struct setting_parser_info test_service_setting_parser_info = {
	.name = "test_service",

	.defines = test_service_setting_defines,

	.struct_size = sizeof(struct test_service_settings),
	.pool_offset1 = 1 + offsetof(struct test_service_settings, pool),
};

int main(int argc, char *argv[])
{
	const enum master_service_flags service_flags =
		MASTER_SERVICE_FLAG_CONFIG_BUILTIN |
		MASTER_SERVICE_FLAG_STANDALONE |
		MASTER_SERVICE_FLAG_STD_CLIENT |
		MASTER_SERVICE_FLAG_DONT_SEND_STATS;
	int ret;

	master_service = master_service_init("test-imap-client-hibernate",
					     service_flags, &argc, &argv, "D");

	/* imap default settings use service/imap/imap_capability, so we need
	   to register "service" named list filter. */
	settings_info_register(&test_service_setting_parser_info);

	master_service_init_finish(master_service);

	test_dir_init("ich");
	test_subprocesses_init();

	static void (*const test_functions[])(void) = {
		test_imap_client_hibernate,
		NULL
	};
	ret = test_run(test_functions);

	master_service_deinit(&master_service);
	return ret;
}
