/* Copyright (c) Dovecot authors, see top-level COPYING file */

#include "lib.h"
#include "array.h"
#include "hostpid.h"
#include "net.h"
#include "istream.h"
#include "ostream.h"
#include "ioloop.h"
#include "unlink-directory.h"
#include "sleep.h"
#include "test-common.h"
#include "test-dir.h"
#include "test-subprocess.h"
#include "imapc-client-private.h"
#include "settings.h"

#include <stdio.h>
#include <unistd.h>

#define SERVER_KILL_TIMEOUT_SECS    20

#define IMAPC_COMMAND_STATE_INVALID (enum imapc_command_state)-1

typedef void test_server_init_t(void);
typedef void test_client_init_t(void);

struct test_server {
	in_port_t port;
	pid_t pid;

	int fd_listen, fd;
	struct istream *input;
	struct ostream *output;
};

static struct ip_addr bind_ip;
static struct test_server server;
static struct imapc_client *imapc_client;
static enum imapc_command_state imapc_login_last_reply;
static ARRAY(enum imapc_command_state) imapc_cmd_last_replies;
static bool debug = FALSE;

static struct settings_simple test_set;

static void main_deinit(void);

/*
 * Test client
 */

static const char **
test_generate_settings(const char *port, const char *connection_timeout_interval)
{
	static const char *settings[] = {
		"imapc_host", "127.0.0.1",
		"imapc_port", "",
		"imapc_user", "testuser",
		"imapc_password", "testpass",
		"imapc_connection_timeout_interval", "5s",
		"imapc_connection_retry_count", "3",
		"imapc_connection_retry_interval", "10ms",
		"imapc_max_idle_time", "10s",

		NULL,
	};
	if (port != NULL)
		settings[3] = port;
	if (connection_timeout_interval != NULL)
		settings[9] = connection_timeout_interval;

	return settings;
}

static enum imapc_command_state test_imapc_cmd_last_reply_pop(void)
{
	const enum imapc_command_state *replies;
	enum imapc_command_state reply;
	unsigned int count;

	replies = array_get(&imapc_cmd_last_replies, &count);
	if (count == 0)
		return IMAPC_COMMAND_STATE_INVALID;
	reply = replies[0];
	array_pop_front(&imapc_cmd_last_replies);
	return reply;
}

static bool test_imapc_cmd_last_reply_expect(enum imapc_command_state state)
{
	if (array_count(&imapc_cmd_last_replies) == 0)
		imapc_client_run(imapc_client);
	return test_imapc_cmd_last_reply_pop() == state;
}

static void imapc_login_callback(const struct imapc_command_reply *reply,
				 void *context ATTR_UNUSED)
{
	if (debug) {
		i_debug("Login reply: %s %s",
			imapc_command_state_names[reply->state],
			reply->text_full);
	}
	imapc_login_last_reply = reply->state;
	imapc_client_stop(imapc_client);
}

static void imapc_command_callback(const struct imapc_command_reply *reply,
				   void *context ATTR_UNUSED)
{
	if (debug) {
		i_debug("Command reply: %s %s",
			imapc_command_state_names[reply->state],
			reply->text_full);
	}
	array_push_back(&imapc_cmd_last_replies, &reply->state);
	imapc_client_stop(imapc_client);
}

static void imapc_reopen_callback(void *context)
{
	struct imapc_client_mailbox *box = context;
	struct imapc_command *cmd;

	cmd = imapc_client_mailbox_cmd(box, imapc_command_callback, NULL);
	imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_SELECT);
	imapc_command_send(cmd, "SELECT");
}

/*
 * Test server
 */

static bool
test_imapc_server_expect_full(struct test_server *server,
			      const char *expected_line)
{
	const char *line = i_stream_read_next_line(server->input);

	if (debug)
		i_debug("Received: %s", (line == NULL ? "<EOF>" : line));

	if (line == NULL) {
		printf("imapc client disconnected unexpectedly: %s\n",
		       i_stream_get_error(server->input));
		return FALSE;
	} else if (strcmp(line, expected_line) != 0) {
		printf("imapc client sent '%s' when expecting '%s'\n",
		       line, expected_line);
		return FALSE;
	} else {
		return TRUE;
	}
}

static bool test_imapc_server_expect(const char *expected_line)
{
	return test_imapc_server_expect_full(&server, expected_line);
}

static void
test_server_wait_connection_common(struct test_server *server)
{
	if (debug)
		i_debug("Waiting for connection");

	server->fd = net_accept(server->fd_listen, NULL, NULL);
	i_assert(server->fd >= 0);

	if (debug)
		i_debug("Client connected");

	fd_set_nonblock(server->fd, FALSE);
	server->input = i_stream_create_fd(server->fd, SIZE_MAX);
	server->output = o_stream_create_fd(server->fd, SIZE_MAX);
	o_stream_set_no_error_handling(server->output, TRUE);
}

static void
test_server_wait_connection(struct test_server *server, bool send_banner)
{
	test_server_wait_connection_common(server);
	if (send_banner) {
		o_stream_nsend_str(server->output,
			"* OK [CAPABILITY IMAP4rev1 UNSELECT QUOTA] ready\r\n");
	}
}

#ifdef EXPERIMENTAL_IMAP4REV2
static void
test_server_wait_connection_imap4rev2(struct test_server *server, bool send_banner)
{
	test_server_wait_connection_common(server);

	if (send_banner) {
		o_stream_nsend_str(server->output,
			"* OK [CAPABILITY IMAP4rev1 IMAP4rev2 UNSELECT QUOTA QRESYNC CONDSTORE] ready\r\n");
	}
}
#endif

static void test_server_disconnect(struct test_server *server)
{
	if (debug)
		i_debug("Disconnecting client");

	i_stream_unref(&server->input);
	o_stream_unref(&server->output);
	i_close_fd(&server->fd);
}

static void test_server_disconnect_and_wait(bool send_banner)
{
	test_server_disconnect(&server);
	test_server_wait_connection(&server, send_banner);
}

/*
 * Test processes
 */

static int test_open_server_fd(in_port_t *bind_port)
{
	int fd = net_listen(&bind_ip, bind_port, 128);
	if (debug)
		i_debug("server listening on %u", *bind_port);
	if (fd == -1) {
		i_fatal("listen(%s:%u) failed: %m",
			net_ip2addr(&bind_ip), *bind_port);
	}
	fd_set_nonblock(fd, FALSE);
	return fd;
}

static int test_run_server(test_server_init_t *server_test)
{
	struct ioloop *ioloop;

	i_set_failure_prefix("SERVER: ");

	if (debug)
		i_debug("PID=%s", my_pid);

	ioloop = io_loop_create();
	if (server_test != NULL)
		server_test();
	test_server_disconnect(&server);
	io_loop_destroy(&ioloop);

	if (debug)
		i_debug("Terminated");

	i_close_fd(&server.fd_listen);
	main_deinit();

	/* Cleanup the test settings in the server process as well.
	   See test_run_client_server() for the appropriate cleanup call in the
	   main process. */
	settings_simple_deinit(&test_set);
	return 0;
}

static void
test_run_client(test_client_init_t *client_test, const char *temp_path_prefix)
{
	struct ioloop *ioloop;

	i_set_failure_prefix("CLIENT: ");

	if (debug)
		i_debug("PID=%s", my_pid);

	i_sleep_msecs(100); /* wait a little for server setup */

	struct imapc_parameters imapc_params = {
		.temp_path_prefix = temp_path_prefix,
	};
	ioloop = io_loop_create();
	imapc_client = imapc_client_init(&imapc_params, test_set.event);
	client_test();
	imapc_client_logout(imapc_client);
	test_assert(array_count(&imapc_cmd_last_replies) == 0);
	if (imapc_client != NULL)
		imapc_client_deinit(&imapc_client);
	io_loop_destroy(&ioloop);

	if (debug)
		i_debug("Terminated");
}

static void
test_run_client_server(test_client_init_t *client_test,
		       test_server_init_t *server_test,
		       bool reduce_timeout)
{
	static unsigned int run_id = 0;
	const char *error;

	imapc_client_cmd_tag_counter = 0;
	imapc_login_last_reply = IMAPC_COMMAND_STATE_INVALID;
	t_array_init(&imapc_cmd_last_replies, 4);

	i_zero(&server);
	server.pid = (pid_t)-1;
	server.fd = -1;
	server.fd_listen = test_open_server_fd(&server.port);

	const char **settings = test_generate_settings(dec2str(server.port),
						       reduce_timeout ? "500ms" : "5s");
	settings_simple_init(&test_set, settings);

	const char *temp_path_prefix = t_strdup_printf(
		"%s/client-run-%u/", test_dir_get(), run_id++);
	if (mkdir(temp_path_prefix, 0700) < 0 && errno != EEXIST)
		i_fatal("mkdir(%s) failed: %m", temp_path_prefix);

	if (server_test != NULL) {
		/* Fork server */
		test_subprocess_fork(test_run_server, server_test, TRUE);
	}
	i_close_fd(&server.fd_listen);

	/* Run client */
	test_run_client(client_test, temp_path_prefix);

	i_unset_failure_prefix();
	test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS);
	if (unlink_directory(temp_path_prefix, UNLINK_DIRECTORY_FLAG_RMDIR,
			     &error) < 0)
		i_fatal("%s", error);

	/* Cleanup the test settings in the main process.
	   Note: This needs to be called as well in the server process,
	   otherwise it will leak it's event and the looked up settings
	   struct. See test_run_server() for the appropriate cleanup call in
	   the server process. */
	settings_simple_deinit(&test_set);
}

/*
 * imapc connect failed
 */

static void test_imapc_connect_failed_client(void)
{
	imapc_client_set_login_callback(imapc_client,
					imapc_login_callback, NULL);
	test_expect_errors(2);
	imapc_client_login(imapc_client);
	/* connection refused & one reconnect */
	imapc_client_run(imapc_client);
	test_expect_no_more_errors();
	test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_DISCONNECTED);
}

static void test_imapc_connect_failed(void)
{
	test_begin("imapc connect failed");
	test_run_client_server(test_imapc_connect_failed_client, NULL, FALSE);
	test_end();
}

/*
 * imapc banner hang
 */

static void test_imapc_banner_hangs_client(void)
{
	imapc_client_set_login_callback(imapc_client,
					imapc_login_callback, NULL);
	imapc_client_login(imapc_client);
	test_expect_errors(2);
	imapc_client_run(imapc_client);
	test_expect_no_more_errors();
	test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_DISCONNECTED);
}

static void test_imapc_banner_hangs_server(void)
{
	struct test_server server2 = { .fd_listen = server.fd_listen };

	test_server_wait_connection(&server, FALSE);
	test_server_wait_connection(&server2, FALSE);
	test_assert(i_stream_read_next_line(server2.input) == NULL);
	test_server_disconnect(&server2);
}

static void test_imapc_banner_hangs(void)
{
	test_begin("imapc banner hangs");
	test_run_client_server(test_imapc_banner_hangs_client,
			       test_imapc_banner_hangs_server,
			       TRUE);
	test_end();
}

/*
 * imapc login hangs
 */

static void test_imapc_login_hangs_client(void)
{
	imapc_client_set_login_callback(imapc_client,
					imapc_login_callback, NULL);
	imapc_client_login(imapc_client);
	/* run the first login */
	test_expect_error_string("Authentication timed out");
	imapc_client_run(imapc_client);
	test_expect_no_more_errors();
	/* imapc_login_callback() has stopped us. run the second reconnect
	   login. */
	test_expect_error_string("Authentication timed out");
	imapc_client_run(imapc_client);
	test_expect_no_more_errors();
	test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_DISCONNECTED);
}

static void test_imapc_login_hangs_server(void)
{
	struct test_server server2 = { .fd_listen = server.fd_listen };

	test_server_wait_connection(&server, TRUE);
	test_assert(test_imapc_server_expect(
		"1 LOGIN \"testuser\" \"testpass\""));

	test_server_wait_connection(&server2, TRUE);
	test_assert(test_imapc_server_expect_full(
		&server2, "2 LOGIN \"testuser\" \"testpass\""));

	test_assert(i_stream_read_next_line(server2.input) == NULL);
	test_server_disconnect(&server2);
}

static void test_imapc_login_hangs(void)
{
	test_begin("imapc login hangs");
	test_run_client_server(test_imapc_login_hangs_client,
			       test_imapc_login_hangs_server,
			       TRUE);
	test_end();
}

/*
 * imapc login fails
 */

static void test_imapc_login_fails_client(void)
{
	imapc_client_set_login_callback(imapc_client,
					imapc_login_callback, NULL);
	imapc_client_login(imapc_client);
	test_expect_error_string("Authentication failed: Test login failed");
	imapc_client_run(imapc_client);
	test_expect_no_more_errors();
	test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_AUTH_FAILED);
}

static void test_imapc_login_fails_server(void)
{
	test_server_wait_connection(&server, TRUE);
	test_assert(test_imapc_server_expect(
		"1 LOGIN \"testuser\" \"testpass\""));
	o_stream_nsend_str(server.output, "1 NO Test login failed\r\n");
}

static void test_imapc_login_fails(void)
{
	test_begin("imapc login fails");
	test_run_client_server(test_imapc_login_fails_client,
			       test_imapc_login_fails_server,
			       FALSE);
	test_end();
}

/*
 * imapc reconnect
 */

static void test_imapc_reconnect_client(void)
{
	struct imapc_command *cmd;

	/* login to server */
	imapc_client_set_login_callback(imapc_client,
					imapc_login_callback, NULL);
	imapc_client_login(imapc_client);
	imapc_client_run(imapc_client);
	test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_OK);
	imapc_login_last_reply = IMAPC_COMMAND_STATE_INVALID;

	/* disconnect */
	cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
	imapc_command_send(cmd, "DISCONNECT");
	test_expect_error_string("reconnecting");
	imapc_client_run(imapc_client);
	test_expect_no_more_errors();
	test_assert(test_imapc_cmd_last_reply_pop() ==
		    IMAPC_COMMAND_STATE_DISCONNECTED);

	/* we should be reconnected now. try a command. */
	cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
	imapc_command_send(cmd, "NOOP");
	imapc_client_run(imapc_client);
	test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_INVALID);
	test_assert(test_imapc_cmd_last_reply_pop() == IMAPC_COMMAND_STATE_OK);
}

static void test_imapc_reconnect_server(void)
{
	test_server_wait_connection(&server, TRUE);
	test_assert(test_imapc_server_expect(
		"1 LOGIN \"testuser\" \"testpass\""));
	o_stream_nsend_str(server.output, "1 OK \r\n");

	test_assert(test_imapc_server_expect("2 DISCONNECT"));
	test_server_disconnect_and_wait(TRUE);

	test_assert(test_imapc_server_expect(
		"4 LOGIN \"testuser\" \"testpass\""));
	o_stream_nsend_str(server.output, "4 OK \r\n");
	test_assert(test_imapc_server_expect("3 NOOP"));
	o_stream_nsend_str(server.output, "3 OK \r\n");

	test_assert(test_imapc_server_expect("5 LOGOUT"));
	o_stream_nsend_str(server.output, "5 OK \r\n");

	test_assert(i_stream_read_next_line(server.input) == NULL);
}

/*
 * imapc short tagged reply (IMAP4rev2)
 */

static void test_imapc_short_reply_client(void)
{
	imapc_client_set_login_callback(imapc_client, imapc_login_callback, NULL);
	imapc_client_login(imapc_client);
	imapc_client_run(imapc_client);
	test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_OK);
}

static void test_imapc_short_reply_server(void)
{
	test_server_wait_connection(&server, TRUE);
	test_assert(test_imapc_server_expect(
		"1 LOGIN \"testuser\" \"testpass\""));
	/* short reply: no human-readable text (IMAP4rev2 section 6.1) */
	o_stream_nsend_str(server.output, "1 OK\r\n");

	test_assert(test_imapc_server_expect("2 LOGOUT"));
	o_stream_nsend_str(server.output, "* BYE\r\n"
					  "2 OK\r\n");

	test_assert(i_stream_read_next_line(server.input) == NULL);
}

static void test_imapc_short_reply(void)
{
	test_begin("imapc short tagged reply (IMAP4rev2)");
	test_run_client_server(test_imapc_short_reply_client,
			       test_imapc_short_reply_server,
			       FALSE);
	test_end();
}

/*
 * imapc short tagged NO reply (IMAP4rev2)
 */

static void test_imapc_short_reply_no_client(void)
{
	imapc_client_set_login_callback(imapc_client, imapc_login_callback, NULL);
	imapc_client_login(imapc_client);
	test_expect_error_string("Authentication failed");
	imapc_client_run(imapc_client);
	test_expect_no_more_errors();
	test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_AUTH_FAILED);
}

static void test_imapc_short_reply_no_server(void)
{
	test_server_wait_connection(&server, TRUE);
	test_assert(test_imapc_server_expect(
		"1 LOGIN \"testuser\" \"testpass\""));
	/* short reply: no human-readable text (IMAP4rev2 section 6.1) */
	o_stream_nsend_str(server.output, "1 NO\r\n");
}

static void test_imapc_short_reply_no(void)
{
	test_begin("imapc short tagged NO reply (IMAP4rev2)");
	test_run_client_server(test_imapc_short_reply_no_client,
			       test_imapc_short_reply_no_server,
			       FALSE);
	test_end();
}

/*
 * imapc short tagged BAD reply (IMAP4rev2)
 */

static void test_imapc_short_reply_bad_client(void)
{
	imapc_client_set_login_callback(imapc_client, imapc_login_callback, NULL);
	imapc_client_login(imapc_client);
	/* BAD logs 2 messages, both containing "failed":
	   "Command X failed with BAD" + "Authentication failed" */
	test_expect_error_string_n_times("failed", 2);
	imapc_client_run(imapc_client);
	test_expect_no_more_errors();
}

static void test_imapc_short_reply_bad_server(void)
{
	test_server_wait_connection(&server, TRUE);
	test_assert(test_imapc_server_expect(
		"1 LOGIN \"testuser\" \"testpass\""));
	/* short reply: no human-readable text (IMAP4rev2 section 6.1) */
	o_stream_nsend_str(server.output, "1 BAD\r\n");
}

static void test_imapc_short_reply_bad(void)
{
	test_begin("imapc short tagged BAD reply (IMAP4rev2)");
	test_run_client_server(test_imapc_short_reply_bad_client,
			       test_imapc_short_reply_bad_server,
			       FALSE);
	test_end();
}

/*
 * imapc banner with resp-text-code but no human-readable text (IMAP4rev2)
 */

static void test_imapc_short_banner_client(void)
{
	imapc_client_set_login_callback(imapc_client, imapc_login_callback, NULL);
	imapc_client_login(imapc_client);
	imapc_client_run(imapc_client);
	test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_OK);
}

static void test_imapc_short_banner_server(void)
{
	test_server_wait_connection(&server, FALSE);
	/* short banner: resp-text-code present but no human-readable text */
	o_stream_nsend_str(server.output,
		"* OK [CAPABILITY IMAP4rev1]\r\n");

	test_assert(test_imapc_server_expect(
		"1 LOGIN \"testuser\" \"testpass\""));
	o_stream_nsend_str(server.output, "1 OK\r\n");

	test_assert(test_imapc_server_expect("2 LOGOUT"));
	o_stream_nsend_str(server.output, "* BYE\r\n"
					  "2 OK\r\n");

	test_assert(i_stream_read_next_line(server.input) == NULL);
}

static void test_imapc_short_banner(void)
{
	test_begin("imapc banner with short resp-text (IMAP4rev2)");
	test_run_client_server(test_imapc_short_banner_client,
			       test_imapc_short_banner_server,
			       FALSE);
	test_end();
}

static void test_imapc_reconnect(void)
{
	test_begin("imapc reconnect");
	test_run_client_server(test_imapc_reconnect_client,
			       test_imapc_reconnect_server,
			       FALSE);
	test_end();
}

/*
 * imapc reconnect resend commands
 */

static void test_imapc_reconnect_resend_cmds_client(void)
{
	struct imapc_command *cmd;

	/* login to server */
	imapc_client_set_login_callback(imapc_client,
					imapc_login_callback, NULL);
	imapc_client_login(imapc_client);
	imapc_client_run(imapc_client);
	test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_OK);
	imapc_login_last_reply = IMAPC_COMMAND_STATE_INVALID;

	/* send two commands */
	cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
	imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
	imapc_command_send(cmd, "RETRY1");
	cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
	imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
	imapc_command_send(cmd, "RETRY2");

	/* disconnect & reconnect automatically */
	cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
	imapc_command_send(cmd, "DISCONNECT");
	test_expect_error_string("reconnecting");
	imapc_client_run(imapc_client);
	test_expect_no_more_errors();
	test_assert(test_imapc_cmd_last_reply_expect(
		IMAPC_COMMAND_STATE_DISCONNECTED));

	/* continue reconnection */
	test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_OK));
	test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_OK));
}

static void test_imapc_reconnect_resend_cmds_server(void)
{
	test_server_wait_connection(&server, TRUE);
	test_assert(test_imapc_server_expect(
		"1 LOGIN \"testuser\" \"testpass\""));
	o_stream_nsend_str(server.output, "1 OK \r\n");

	test_assert(test_imapc_server_expect("2 RETRY1"));
	test_assert(test_imapc_server_expect("3 RETRY2"));
	test_assert(test_imapc_server_expect("4 DISCONNECT"));
	test_server_disconnect_and_wait(TRUE);

	test_assert(test_imapc_server_expect(
		"5 LOGIN \"testuser\" \"testpass\""));
	o_stream_nsend_str(server.output, "5 OK \r\n");
	test_assert(test_imapc_server_expect("2 RETRY1"));
	o_stream_nsend_str(server.output, "2 OK \r\n");
	test_assert(test_imapc_server_expect("3 RETRY2"));
	o_stream_nsend_str(server.output, "3 OK \r\n");

	test_assert(test_imapc_server_expect("6 LOGOUT"));
	o_stream_nsend_str(server.output, "6 OK \r\n");

	test_assert(i_stream_read_next_line(server.input) == NULL);
}

static void test_imapc_reconnect_resend_commands(void)
{
	test_begin("imapc reconnect resend commands");
	test_run_client_server(test_imapc_reconnect_resend_cmds_client,
			       test_imapc_reconnect_resend_cmds_server,
			       FALSE);
	test_end();
}

/*
 * imapc reconnect resend commands failed
 */

static void test_imapc_reconnect_resend_cmds_failed_client(void)
{
	struct imapc_command *cmd;

	/* login to server */
	imapc_client_set_login_callback(imapc_client,
					imapc_login_callback, NULL);
	imapc_client_login(imapc_client);
	imapc_client_run(imapc_client);
	test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_OK);
	imapc_login_last_reply = IMAPC_COMMAND_STATE_INVALID;

	/* send two commands */
	cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
	imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
	imapc_command_send(cmd, "RETRY1");
	cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
	imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
	imapc_command_send(cmd, "RETRY2");

	/* disconnect & try to reconnect automatically */
	cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
	imapc_command_send(cmd, "DISCONNECT");
	test_expect_error_string("reconnecting");
	imapc_client_run(imapc_client);
	test_expect_no_more_errors();
	test_assert(test_imapc_cmd_last_reply_expect(
		IMAPC_COMMAND_STATE_DISCONNECTED));
	test_expect_error_string("timed out");
	test_assert(test_imapc_cmd_last_reply_expect(
		IMAPC_COMMAND_STATE_DISCONNECTED));
	test_expect_no_more_errors();
	test_assert(test_imapc_cmd_last_reply_expect(
		IMAPC_COMMAND_STATE_DISCONNECTED));
}

static void test_imapc_reconnect_resend_cmds_failed_server(void)
{
	test_server_wait_connection(&server, TRUE);
	test_assert(test_imapc_server_expect(
		"1 LOGIN \"testuser\" \"testpass\""));
	o_stream_nsend_str(server.output, "1 OK \r\n");

	test_assert(test_imapc_server_expect("2 RETRY1"));
	test_assert(test_imapc_server_expect("3 RETRY2"));
	test_assert(test_imapc_server_expect("4 DISCONNECT"));
	test_server_disconnect(&server);

	i_sleep_intr_secs(60);
}

static void test_imapc_reconnect_resend_commands_failed(void)
{
	test_begin("imapc reconnect resend commands failed");
	test_run_client_server(test_imapc_reconnect_resend_cmds_failed_client,
			       test_imapc_reconnect_resend_cmds_failed_server,
			       TRUE);
	test_end();
}

/*
 * imapc reconnect mailbox
 */

static void test_imapc_reconnect_mailbox_client(void)
{
	struct imapc_command *cmd;
	struct imapc_client_mailbox *box;

	/* login to server */
	imapc_client_set_login_callback(imapc_client,
					imapc_login_callback, NULL);
	imapc_client_login(imapc_client);
	imapc_client_run(imapc_client);
	test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_OK);
	imapc_login_last_reply = IMAPC_COMMAND_STATE_INVALID;

	/* select a mailbox */
	box = imapc_client_mailbox_open(imapc_client, NULL, NULL);
	imapc_client_mailbox_set_reopen_cb(box, imapc_reopen_callback, box);

	cmd = imapc_client_mailbox_cmd(box, imapc_command_callback, NULL);
	imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_SELECT);
	imapc_command_send(cmd, "SELECT");
	imapc_client_run(imapc_client);
	test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_OK));

	/* send a command */
	cmd = imapc_client_mailbox_cmd(box, imapc_command_callback, NULL);
	imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
	imapc_command_send(cmd, "RETRY");

	/* disconnect & reconnect automatically */
	cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
	imapc_command_send(cmd, "DISCONNECT");
	test_expect_error_string("reconnecting");
	imapc_client_run(imapc_client);
	test_expect_no_more_errors();
	test_assert(test_imapc_cmd_last_reply_expect(
		IMAPC_COMMAND_STATE_DISCONNECTED));

	/* continue reconnection */
	test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_OK));
	test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_OK));

	imapc_client_mailbox_close(&box);
}

static void test_imapc_reconnect_mailbox_server(void)
{
	test_server_wait_connection(&server, TRUE);
	test_assert(test_imapc_server_expect(
		"1 LOGIN \"testuser\" \"testpass\""));
	o_stream_nsend_str(server.output, "1 OK \r\n");

	test_assert(test_imapc_server_expect("2 SELECT"));
	o_stream_nsend_str(server.output, "2 OK \r\n");

	test_assert(test_imapc_server_expect("3 RETRY"));
	test_assert(test_imapc_server_expect("4 DISCONNECT"));
	test_server_disconnect_and_wait(TRUE);

	test_assert(test_imapc_server_expect(
		"5 LOGIN \"testuser\" \"testpass\""));
	o_stream_nsend_str(server.output, "5 OK \r\n");
	test_assert(test_imapc_server_expect("6 SELECT"));
	o_stream_nsend_str(server.output, "6 OK \r\n");
	test_assert(test_imapc_server_expect("3 RETRY"));
	o_stream_nsend_str(server.output, "3 OK \r\n");

	test_assert(test_imapc_server_expect("7 LOGOUT"));
	o_stream_nsend_str(server.output, "7 OK \r\n");

	test_assert(i_stream_read_next_line(server.input) == NULL);
}

static void test_imapc_reconnect_mailbox(void)
{
	test_begin("imapc reconnect mailbox");
	test_run_client_server(test_imapc_reconnect_mailbox_client,
			       test_imapc_reconnect_mailbox_server,
			       FALSE);
	test_end();
}

/*
 * imapc_client_get_capabilities()
 */

static void test_imapc_client_get_capabilities_client(void)
{
	enum imapc_capability capabilities;

	test_assert(imapc_client_get_capabilities(imapc_client, &capabilities) == 0);
	test_assert(capabilities == (IMAPC_CAPABILITY_IMAP4REV1 |
				     IMAPC_CAPABILITY_UNSELECT |
				     IMAPC_CAPABILITY_QUOTA));
}

static void test_imapc_client_get_capabilities_server(void)
{
	test_server_wait_connection(&server, TRUE);
	test_assert(test_imapc_server_expect(
		"1 LOGIN \"testuser\" \"testpass\""));
	o_stream_nsend_str(server.output, "1 OK \r\n");

	test_assert(test_imapc_server_expect("2 LOGOUT"));
	o_stream_nsend_str(server.output, "2 OK \r\n");

	test_assert(i_stream_read_next_line(server.input) == NULL);
}

static void test_imapc_client_get_capabilities(void)
{
	test_begin("imapc_client_get_capabilities()");
	test_run_client_server(test_imapc_client_get_capabilities_client,
			       test_imapc_client_get_capabilities_server,
			       FALSE);
	test_end();
}

/*
 * imapc_client_get_capabilities() reconnected
 */

static void test_imapc_client_get_capabilities_reconnected_client(void)
{
	enum imapc_capability capabilities;

	test_expect_error_string("Server disconnected unexpectedly");
	test_assert(imapc_client_get_capabilities(imapc_client,
						  &capabilities) == 0);
	test_assert(capabilities == (IMAPC_CAPABILITY_IMAP4REV1 |
				     IMAPC_CAPABILITY_UNSELECT |
				     IMAPC_CAPABILITY_QUOTA));
	test_expect_no_more_errors();
}

static void test_imapc_client_get_capabilities_reconnected_server(void)
{
	test_server_wait_connection(&server, TRUE);
	test_server_disconnect_and_wait(TRUE);

	test_assert(test_imapc_server_expect(
		"2 LOGIN \"testuser\" \"testpass\""));
	o_stream_nsend_str(server.output, "2 OK \r\n");

	test_assert(test_imapc_server_expect("3 LOGOUT"));
	o_stream_nsend_str(server.output, "3 OK \r\n");

	test_assert(i_stream_read_next_line(server.input) == NULL);
}

static void test_imapc_client_get_capabilities_reconnected(void)
{
	test_begin("imapc_client_get_capabilities() reconnected");

	test_run_client_server(
		test_imapc_client_get_capabilities_reconnected_client,
		test_imapc_client_get_capabilities_reconnected_server,
		FALSE);
	test_end();
}

/*
 * imapc_client_get_capabilities() disconnected
 */

static void test_imapc_client_get_capabilities_disconnected_client(void)
{
	enum imapc_capability capabilities;

	test_expect_errors(2);
	test_assert(imapc_client_get_capabilities(imapc_client,
						  &capabilities) < 0);
	test_expect_no_more_errors();
}

static void test_imapc_client_get_capabilities_disconnected_server(void)
{
	test_server_wait_connection(&server, TRUE);
	test_server_disconnect_and_wait(TRUE);
}

static void test_imapc_client_get_capabilities_disconnected(void)
{
	test_begin("imapc_client_get_capabilities() disconnected");

	test_run_client_server(
		test_imapc_client_get_capabilities_disconnected_client,
		test_imapc_client_get_capabilities_disconnected_server,
		FALSE);
	test_end();
}

#ifdef EXPERIMENTAL_IMAP4REV2
/*
 * imapc_client_get_capabilities_imap4rev2()
 */

static void test_imapc_client_get_capabilities_imap4rev2_client(void)
{
	enum imapc_capability capabilities;

	test_assert(imapc_client_get_capabilities(imapc_client, &capabilities) == 0);
	test_assert(capabilities == (IMAPC_CAPABILITY_IMAP4REV1 |
				     IMAPC_CAPABILITY_IMAP4REV2 |
				     IMAPC_CAPABILITY_QRESYNC |
				     IMAPC_CAPABILITY_CONDSTORE |
				     IMAPC_CAPABILITY_UNSELECT |
				     IMAPC_CAPABILITY_QUOTA));
}

static void test_imapc_client_get_capabilities_imap4rev2_server(void)
{
	test_server_wait_connection_imap4rev2(&server, TRUE);
	test_assert(test_imapc_server_expect(
		"1 LOGIN \"testuser\" \"testpass\""));
	o_stream_nsend_str(server.output, "1 OK \r\n");

	test_assert(test_imapc_server_expect("2 ENABLE IMAP4REV2"));
	o_stream_nsend_str(server.output, "2 OK ENABLED\r\n");

	test_assert(test_imapc_server_expect("3 LOGOUT"));
	o_stream_nsend_str(server.output, "3 OK \r\n");

	test_assert(i_stream_read_next_line(server.input) == NULL);

}

static void test_imapc_client_get_capabilities_imap4rev2(void)
{
	test_begin("imapc_client_get_capabilities_imap4rev2()");
	test_run_client_server(test_imapc_client_get_capabilities_imap4rev2_client,
			       test_imapc_client_get_capabilities_imap4rev2_server,
			       FALSE);
	test_end();
}
#endif

/*
 * Main
 */

static void main_init(void)
{
	/* nothing yet */
}

static void main_deinit(void)
{
	/* nothing yet; also called from sub-processes */
}

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

	static void (*const test_functions[])(void) = {
		test_imapc_connect_failed,
		test_imapc_banner_hangs,
		test_imapc_login_hangs,
		test_imapc_login_fails,
		test_imapc_short_reply,
		test_imapc_short_reply_no,
		test_imapc_short_reply_bad,
		test_imapc_short_banner,
		test_imapc_reconnect,
		test_imapc_reconnect_resend_commands,
		test_imapc_reconnect_resend_commands_failed,
		test_imapc_reconnect_mailbox,
		test_imapc_client_get_capabilities,
		test_imapc_client_get_capabilities_reconnected,
		test_imapc_client_get_capabilities_disconnected,
#ifdef EXPERIMENTAL_IMAP4REV2
		test_imapc_client_get_capabilities_imap4rev2,
#endif
		NULL
	};

	lib_init();
	main_init();

	while ((c = getopt(argc, argv, "D")) > 0) {
		switch (c) {
		case 'D':
			debug = TRUE;
			break;
		default:
			i_fatal("Usage: %s [-D]", argv[0]);
		}
	}

	test_init();
	event_set_forced_debug(test_event, debug);
	test_dir_init("test-imapc-client");
	test_subprocesses_init();

	/* listen on localhost */
	i_zero(&bind_ip);
	bind_ip.family = AF_INET;
	bind_ip.u.ip4.s_addr = htonl(INADDR_LOOPBACK);

	ret = test_run(test_functions);

	main_deinit();
	lib_deinit();

	return ret;
}
