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

#include "test-lib.h"
#include "istream.h"
#include "istream-crlf.h"

static void test_istream_children(void)
{
	struct istream *parent, *child1, *child2;
	const unsigned char *data;
	size_t size;

	test_begin("istream children");

	parent = test_istream_create_data("123456789", 9);
	test_istream_set_max_buffer_size(parent, 3);

	child1 = i_stream_create_limit(parent, UOFF_T_MAX);
	child2 = i_stream_create_limit(parent, UOFF_T_MAX);

	/* child1 read beginning */
	test_assert(i_stream_read(child1) == 3);
	data = i_stream_get_data(child1, &size);
	test_assert(size == 3 && memcmp(data, "123", 3) == 0);
	i_stream_skip(child1, 3);
	/* child1 read middle.. */
	test_assert(i_stream_read(child1) == 3);
	data = i_stream_get_data(child1, &size);
	test_assert(size == 3 && memcmp(data, "456", 3) == 0);
	/* child2 read beginning.. */
	test_assert(i_stream_read(child2) == 3);
	data = i_stream_get_data(child2, &size);
	test_assert(size == 3 && memcmp(data, "123", 3) == 0);
	/* child1 check middle again.. the parent has been modified,
	   so it can't return the original data (without some code changes). */
	test_assert(i_stream_get_data_size(child1) == 0);
	i_stream_skip(child1, 3);
	/* child1 read end */
	test_assert(i_stream_read(child1) == 3);
	data = i_stream_get_data(child1, &size);
	test_assert(size == 3 && memcmp(data, "789", 3) == 0);
	i_stream_skip(child1, 3);
	test_assert(i_stream_read(child1) == -1);
	/* child2 check beginning again.. */
	test_assert(i_stream_get_data_size(child1) == 0);
	i_stream_skip(child2, 3);
	/* child2 read middle */
	test_assert(i_stream_read(child2) == 3);
	data = i_stream_get_data(child2, &size);
	test_assert(size == 3 && memcmp(data, "456", 3) == 0);
	i_stream_skip(child2, 3);

	i_stream_destroy(&child1);
	i_stream_destroy(&child2);
	i_stream_destroy(&parent);

	test_end();
}

static void test_istream_next_line_expect(struct istream *is, const char *expect,
					  unsigned int i)
{
	const char *line = i_stream_next_line(is);
	test_assert_strcmp_idx(line, expect, i);
}

static void test_istream_next_line(void)
{
	/* single line cases */
#define TEST_CASE(a, s, b) { \
	.input = (const unsigned char*)((a)), .input_len = sizeof((a)), \
	.skip = s, \
	.output = b }
	const struct test_case_sl {
		const unsigned char *input;
		size_t input_len;
		size_t skip;
		const char *output;
	} test_cases_sl[] = {
		TEST_CASE("", 0, NULL),
		TEST_CASE("a\n", 1, ""),
		TEST_CASE("a\r\n", 0, "a"),
		TEST_CASE("a\r\n", 1, ""),
		TEST_CASE("a\r\n", 2, ""),
		TEST_CASE("hello\nworld\n", 6, "world"),
		TEST_CASE("hello\nworld", 6, NULL),
		TEST_CASE("hello\n\n\n\n", 6, ""),
		TEST_CASE("wrong\n\r\n\n", 0, "wrong"),
		TEST_CASE("wrong\n\r\r\n", 6, "\r"),
		TEST_CASE("wrong\n\r\r\n", 7, ""),
	};

	test_begin("i_stream_next_line");

	for(unsigned int i = 0; i < N_ELEMENTS(test_cases_sl); i++) {
		const struct test_case_sl *test_case = &test_cases_sl[i];
		struct istream *input =
			i_stream_create_copy_from_data(test_case->input, test_case->input_len);
		test_assert_idx(i_stream_read(input) >= 0 ||
				(input->stream_errno == 0 && input->eof), i);
		i_stream_skip(input, test_case->skip);
		test_assert_strcmp_idx(i_stream_next_line(input), test_case->output, i);
		test_assert_idx(input->stream_errno == 0, i);
		i_stream_unref(&input);

		input = test_istream_create_data(test_case->input, test_case->input_len);
		test_assert_idx(i_stream_read(input) >= 0 ||
				(input->stream_errno == 0 && input->eof), i);
		i_stream_skip(input, test_case->skip);
		test_assert_strcmp_idx(i_stream_next_line(input), test_case->output, i);
		test_assert_idx(input->stream_errno == 0, i);
		i_stream_unref(&input);
	}

#undef TEST_CASE
#define TEST_CASE(a) test_istream_create_data((a), sizeof(a))
	/* multiline tests */
	struct istream *is = TEST_CASE("\n\n\n\n\n\n");
	size_t i;
	test_assert(i_stream_read(is) >= 0 || (is->stream_errno == 0 && is->eof));
	for(i = 0; i < 6; i++)
		test_istream_next_line_expect(is, "", i);
	test_assert(is->stream_errno == 0);
	i_stream_unref(&is);

	is = TEST_CASE(
		"simple\r\n"
		"multiline\n"
		"test with\0"
		"some exciting\n"
		"things\r\n\0");
	test_assert(i_stream_read(is) >= 0 || (is->stream_errno == 0 && is->eof));
	test_istream_next_line_expect(is, "simple", 0);
	test_istream_next_line_expect(is, "multiline", 1);
	test_istream_next_line_expect(is, "test with", 2);
	test_istream_next_line_expect(is, "things", 3);
	test_istream_next_line_expect(is, NULL, 4);
	test_assert(is->stream_errno == 0);
	i_stream_unref(&is);

	is = TEST_CASE(
		"NUL\0"
		"test\n");
	test_assert(i_stream_read(is) >= 0 || (is->stream_errno == 0 && is->eof));
	test_istream_next_line_expect(is, "NUL", 0);
	test_istream_next_line_expect(is,  NULL, 1);
	test_assert(is->stream_errno == 0);
	i_stream_unref(&is);

	const char test_data_1[] =
		"this is some data\n"
		"written like this\n"
		"to attempt and induce\n"
		"errors or flaws\n";

	is = TEST_CASE(test_data_1);
	size_t n = 0;
	const char *const *lines = t_strsplit(test_data_1, "\n");
	for(i = 0; i < sizeof(test_data_1); i++) {
		test_istream_set_size(is, i);
		test_assert(i_stream_read(is) >= 0);
		const char *line = i_stream_next_line(is);
		if (line != NULL) {
			test_assert_strcmp_idx(lines[n], line, n);
			n++;
		}
	}
	test_assert(n == 4);
	test_assert(is->stream_errno == 0);
	i_stream_unref(&is);

	const char test_data_2[] =
		"this is some data\n"
		"written like this\n"
		"to attempt and induce\n"
		"errors or flaws";

	is = TEST_CASE(test_data_2);
	lines = t_strsplit(test_data_2, "\n");
	i_stream_set_return_partial_line(is, TRUE);
	n = 0;

	/* requires one extra read to get the last line */
	for(i = 0; i < sizeof(test_data_1) + 1; i++) {
		test_istream_set_size(is, I_MIN(sizeof(test_data_1), i));
		(void)i_stream_read(is);
		const char *line = i_stream_next_line(is);
		if (line != NULL) {
			test_assert_strcmp_idx(lines[n], line, n);
			n++;
		}
		(void)i_stream_read(is);
	}
	test_assert(n == 4);
	test_assert(is->stream_errno == 0);
	i_stream_unref(&is);

	const char test_data_3[] =
		"this is some data\r\n"
		"written like this\r\n"
		"to attempt and induce\r\n"
		"errors or flaws\r\n";

	struct istream *is_1 = TEST_CASE(test_data_3);
	is = i_stream_create_crlf(is_1);
	i_stream_unref(&is_1);

	lines = t_strsplit_spaces(test_data_3, "\r\n");
	n = 0;

	for(i = 0; i < sizeof(test_data_3); i++) {
		test_istream_set_size(is, i);
		test_assert(i_stream_read(is) >= 0);
		const char *line = i_stream_next_line(is);
		if (line != NULL) {
			test_assert_strcmp_idx(lines[n], line, n);
			n++;
		}
	}
	test_assert(n == 4);
	test_assert(is->stream_errno == 0);
	i_stream_unref(&is);

	test_end();
}

static void test_istream_read_next_line(void)
{
	/* single line cases */
#undef TEST_CASE
#define TEST_CASE(a, s, b) { \
	.input = (const unsigned char*)((a)), .input_len = sizeof((a)), \
	.skip = s, \
	.output = b }
	const struct test_case_sl {
		const unsigned char *input;
		size_t input_len;
		size_t skip;
		const char *output;
	} test_cases_sl[] = {
		TEST_CASE("", 0, NULL),
		TEST_CASE("a\n", 1, ""),
		TEST_CASE("a\r\n", 0, "a"),
		TEST_CASE("a\r\n", 1, ""),
		TEST_CASE("a\r\n", 2, ""),
		TEST_CASE("hello\nworld\n", 6, "world"),
		TEST_CASE("hello\nworld", 6, NULL),
		TEST_CASE("hello\n\n\n\n", 6, ""),
		TEST_CASE("wrong\n\r\n\n", 0, "wrong"),
		TEST_CASE("wrong\n\r\r\n", 6, "\r"),
		TEST_CASE("wrong\n\r\r\n", 7, ""),
	};

	test_begin("i_stream_read_next_line");
	for(unsigned int i = 0; i < N_ELEMENTS(test_cases_sl); i++) {
		const struct test_case_sl *test_case = &test_cases_sl[i];
		struct istream *input =
			i_stream_create_copy_from_data(test_case->input, test_case->input_len);
		i_stream_skip(input, test_case->skip);
		test_assert_strcmp_idx(i_stream_read_next_line(input), test_case->output, i);
		test_assert_idx(input->stream_errno == 0, i);
		i_stream_unref(&input);

		input = test_istream_create_data(test_case->input, test_case->input_len);
		i_stream_skip(input, test_case->skip);
		test_assert_strcmp_idx(i_stream_read_next_line(input), test_case->output, i);
		test_assert_idx(input->stream_errno == 0, i);
		i_stream_unref(&input);
	}

	const char test_data_1[] =
		"this is some data\n"
		"written like this\n"
		"to attempt and induce\n"
		"errors or flaws\n";

#undef TEST_CASE
#define TEST_CASE(a) test_istream_create_data((a), sizeof(a))
	/* multiline tests */
	struct istream *is = TEST_CASE("\n\n\n\n\n\n");
	size_t i;
	for(i = 0; i < 6; i++)
		test_assert_strcmp_idx(i_stream_read_next_line(is), "", i);
	test_assert(is->stream_errno == 0);
	i_stream_unref(&is);

	is = TEST_CASE(
		"simple\r\n"
		"multiline\n"
		"test with\0"
		"some exciting\n"
		"things\r\n\0");
	test_assert_strcmp_idx(i_stream_read_next_line(is), "simple", 0);
	test_assert_strcmp_idx(i_stream_read_next_line(is), "multiline", 1);
	test_assert_strcmp_idx(i_stream_read_next_line(is), "test with", 2);
	test_assert_strcmp_idx(i_stream_read_next_line(is), "things", 3);
	test_assert_strcmp_idx(i_stream_read_next_line(is), NULL, 4);
	test_assert(is->stream_errno == 0);
	i_stream_unref(&is);

	is = TEST_CASE(
		"NUL\0"
		"test\n");
	test_assert_strcmp_idx(i_stream_read_next_line(is), "NUL", 0);
	test_assert_strcmp_idx(i_stream_read_next_line(is),  NULL, 1);
	test_assert(is->stream_errno == 0);
	i_stream_unref(&is);

	is = TEST_CASE(test_data_1);
	size_t n = 0;
	const char *const *lines = t_strsplit(test_data_1, "\n");
	for(i = 0; i < sizeof(test_data_1); i++) {
		test_istream_set_size(is, i);
		const char *line = i_stream_read_next_line(is);
		if (line != NULL) {
			test_assert_strcmp_idx(lines[n], line, n);
			n++;
		}
	}
	test_assert(n == 4);
	test_assert(is->stream_errno == 0);
	i_stream_unref(&is);

	const char test_data_2[] =
		"this is some data\n"
		"written like this\n"
		"to attempt and induce\n"
		"errors or flaws";

	is = TEST_CASE(test_data_2);
	lines = t_strsplit(test_data_2, "\n");
	i_stream_set_return_partial_line(is, TRUE);
	n = 0;

	for(i = 0; i < sizeof(test_data_1); i++) {
		test_istream_set_size(is, i);
		const char *line = i_stream_read_next_line(is);
		if (line != NULL) {
			test_assert_strcmp_idx(lines[n], line, n);
			n++;
		}
	}
	test_assert(n == 4);
	test_assert(is->stream_errno == 0);
	i_stream_unref(&is);

	const char test_data_3[] =
		"this is some data\r\n"
		"written like this\r\n"
		"to attempt and induce\r\n"
		"errors or flaws\r\n";


	struct istream *is_1 = TEST_CASE(test_data_3);
	is = i_stream_create_crlf(is_1);
	i_stream_unref(&is_1);

	lines = t_strsplit_spaces(test_data_3, "\r\n");
	n = 0;

	for(i = 0; i < sizeof(test_data_3); i++) {
		test_istream_set_size(is, i);
		const char *line = i_stream_read_next_line(is);
		if (line != NULL) {
			test_assert_strcmp_idx(lines[n], line, n);
			n++;
		}
	}
	test_assert(n == 4);
	test_assert(is->stream_errno == 0);
	i_stream_unref(&is);

	test_end();
}

void test_istream(void)
{
	test_istream_children();
	test_istream_next_line();
	test_istream_read_next_line();
}
