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

#include "test-lib.h"
#include "array.h"
#include "buffer.h"
#include "str.h"
#include "str-sanitize.h"
#include "istream.h"
#include "ostream.h"
#include "test-common.h"
#include "smtp-reply-parser.h"

#include <time.h>

struct smtp_reply_parse_valid_test {
	const char *input, *output;
	unsigned int status;
	bool ehlo;
	size_t max_size;
	struct {
		unsigned int x, y, z;
	} enhanced_code;
	const char *const *text_lines;
};

/* Valid reply tests */

static const struct smtp_reply_parse_valid_test
valid_reply_parse_tests[] = {
	{
		.input = "220\r\n",
		.output = "220 \r\n",
		.status = 220,
		.text_lines = (const char *[]){ "", NULL }
	},{
		.input = "220 \r\n",
		.status = 220,
		.text_lines = (const char *[]){ "", NULL }
	},{
		.input = "220 OK\r\n",
		.status = 220,
		.text_lines = (const char *[]){ "OK", NULL }
	},{
		.input = "550 Requested action not taken: mailbox unavailable\r\n",
		.status = 550,
		.text_lines = (const char *[])
			{ "Requested action not taken: mailbox unavailable", NULL }
	},{
		.input =
			"250-smtp.example.com Hello client.example.org [10.0.0.1]\r\n"
			"250-SIZE 52428800\r\n"
			"250-PIPELINING\r\n"
			"250-STARTTLS\r\n"
			"250 HELP\r\n",
		.ehlo = TRUE,
		.status = 250,
		.text_lines = (const char *[]) {
			"smtp.example.com Hello client.example.org [10.0.0.1]",
			"SIZE 52428800",
			"PIPELINING",
			"STARTTLS",
			"HELP",
			NULL
		}
	},{
		.input =
			"250-smtp.example.com We got some nice '\x03' and '\x04'\r\n"
			"250 HELP\r\n",
		.output =
			"250-smtp.example.com We got some nice ' ' and ' '\r\n"
			"250 HELP\r\n",
		.ehlo = TRUE,
		.status = 250,
		.text_lines = (const char *[]) {
			"smtp.example.com We got some nice ' ' and ' '",
			"HELP",
			NULL
		}
	},{
		.input =
			"250 smtp.example.com We got some nice '\x08'\r\n",
		.output =
			"250 smtp.example.com We got some nice ' '\r\n",
		.ehlo = TRUE,
		.status = 250,
		.text_lines = (const char *[]) {
			"smtp.example.com We got some nice ' '",
			NULL
		}
	},{
		.input = "250 2.1.0 Originator <frop@example.com> ok\r\n",
		.status = 250,
		.enhanced_code = { 2, 1, 0 },
		.text_lines = (const char *[]){
			"Originator <frop@example.com> ok", NULL
		}
	},{
		.input =
			"551-5.7.1 Forwarding to remote hosts disabled\r\n"
			"551 5.7.1 Select another host to act as your forwarder\r\n",
		.status = 551,
		.enhanced_code = { 5, 7, 1 },
		.text_lines = (const char *[])	{
			"Forwarding to remote hosts disabled",
			"Select another host to act as your forwarder",
			NULL
		}
	}
};

unsigned int valid_reply_parse_test_count =
	N_ELEMENTS(valid_reply_parse_tests);

static void test_smtp_reply_parse_valid(void)
{
	unsigned int i;

	for (i = 0; i < valid_reply_parse_test_count; i++) T_BEGIN {
		struct istream *input;
		const struct smtp_reply_parse_valid_test *test;
		struct smtp_reply_parser *parser;
		struct smtp_reply *reply;
		const char *error;
		int ret;

		test = &valid_reply_parse_tests[i];
		input = i_stream_create_from_data(test->input,
						  strlen(test->input));
		parser = smtp_reply_parser_init(input, test->max_size);
		i_stream_unref(&input);

		test_begin(t_strdup_printf("smtp reply valid [%d]", i));

		if (test->ehlo) {
			while ((ret=smtp_reply_parse_ehlo
				(parser, &reply, &error)) > 0) {
			}
		} else {
			while ((ret=smtp_reply_parse_next
				(parser, test->enhanced_code.x > 0, &reply, &error)) > 0) {
			}
		}

		test_out_reason("parse success", ret == 0, error);

		if (ret == 0) {
			const char *output;
			string_t *encoded;

			/* verify last response only */
			test_out(t_strdup_printf("reply->status = %d", test->status),
					reply->status == test->status);
			if (test->enhanced_code.x > 0) {
				test_out(t_strdup_printf("reply->enhanced_code = %d.%d.%d",
					test->enhanced_code.x, test->enhanced_code.y, test->enhanced_code.z),
					(reply->enhanced_code.x == test->enhanced_code.x &&
						reply->enhanced_code.y == test->enhanced_code.y &&
						reply->enhanced_code.z == test->enhanced_code.z));
			}
			if (test->text_lines != NULL) {
				const char *const *line = test->text_lines;
				const char *const *reply_line = reply->text_lines;
				unsigned int index = 0;

				while (*line != NULL) {
					if (*reply_line == NULL) {
						test_out(
							t_strdup_printf("reply->text_lines[%d] = NULL", index),
							FALSE);
						break;
					}
					test_out(t_strdup_printf(
						"reply->text_lines[%d] = \"%s\"", index, *reply_line),
						strcmp(*line, *reply_line) == 0);
					line++;
					reply_line++;
					index++;
				}
			} else {
				test_out("reply->text_lines = NULL", reply->text_lines == NULL);
			}

			encoded = t_str_new(512);
			smtp_reply_write(encoded, reply);

			output = (test->output == NULL ? test->input : test->output);
			test_out("write() = input",
				strcmp(str_c(encoded), output) == 0);
		}
		test_end();
		smtp_reply_parser_deinit(&parser);
	} T_END;
}

struct smtp_reply_parse_invalid_test {
	const char *reply;
	bool ehlo;
	size_t max_size;
};

static const struct smtp_reply_parse_invalid_test
	invalid_reply_parse_tests[] = {
	{
		.reply = "22X OK\r\n"
	},{
		.reply = "220OK\r\n"
	},{
		.reply =
			"200-This is\r\n"
			"250 inconsistent.\r\n"
	},{
		.reply = "400 This \r is wrong\r\n"
	},{
		.reply = "500 This is \x03 worse\r\n"
	},{
		.reply = "699 Obscure\r\n"
	},{
		.reply = "100 Invalid\r\n"
	},{
		.reply = "400 Interrupted\r"
	},{
		.reply = "251 example.com We got '\x04'\r\n",
		.ehlo = TRUE
	},{
		.reply =
			"250-example.com Hello\r\n"
			"250 We got some '\x08' for you\r\n",
		.ehlo = TRUE
	},{
		.reply =
			"556-This is a very long reply\r\n"
			"556 that exceeds the very low limit.\r\n",
		.max_size = 50
	}
};

unsigned int invalid_reply_parse_test_count =
	N_ELEMENTS(invalid_reply_parse_tests);

static void test_smtp_reply_parse_invalid(void)
{
	unsigned int i;

	for (i = 0; i < invalid_reply_parse_test_count; i++) T_BEGIN {
		const struct smtp_reply_parse_invalid_test *test;
		struct istream *input;
		struct smtp_reply_parser *parser;
		struct smtp_reply *reply;
		const char *reply_text, *error;
		int ret;

		test = &invalid_reply_parse_tests[i];
		reply_text = test->reply;
		input = i_stream_create_from_data(reply_text, strlen(reply_text));
		parser = smtp_reply_parser_init(input, test->max_size);
		i_stream_unref(&input);

		test_begin(t_strdup_printf("smtp reply invalid [%d]", i));

		if (test->ehlo)
			while ((ret=smtp_reply_parse_ehlo(parser, &reply, &error)) > 0);
		else
			while ((ret=smtp_reply_parse_next(parser, FALSE, &reply, &error)) > 0);
		test_out_reason(t_strdup_printf("parse(\"%s\")",
			str_sanitize(reply_text, 80)), ret < 0, error);
		test_end();
		smtp_reply_parser_deinit(&parser);
	} T_END;
}

static void test_smtp_reply_parse_enhanced_code(void)
{
	struct smtp_reply_enhanced_code enh_code;
	const char *pos;

	test_begin("smtp reply parse enhanced code");

	/* short/empty input must be rejected without reading past the end of
	   the string (the p[0]/p[1] reads used to happen before any length
	   check) */
	test_assert(!smtp_reply_parse_enhanced_code("", &enh_code, &pos));
	test_assert(!smtp_reply_parse_enhanced_code("2", &enh_code, &pos));
	test_assert(!smtp_reply_parse_enhanced_code("2.", &enh_code, &pos));
	test_assert(!smtp_reply_parse_enhanced_code("x.y.z", &enh_code, &pos));

	/* a valid code is still parsed correctly */
	test_assert(smtp_reply_parse_enhanced_code("5.7.1 text",
						   &enh_code, &pos));
	test_assert(enh_code.x == 5 && enh_code.y == 7 && enh_code.z == 1);
	test_assert_strcmp(pos, " text");

	test_end();
}

int main(void)
{
	static void (*test_functions[])(void) = {
		test_smtp_reply_parse_valid,
		test_smtp_reply_parse_invalid,
		test_smtp_reply_parse_enhanced_code,
		NULL
	};
	return test_run(test_functions);
}
