#undef NDEBUG

#include <assert.h>
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "csv.h"

#define FMEMOPEN(f, data)                      \
	f = fmemopen(data, sizeof(data) - 1, "r"); \
	assert(f != NULL)

#define FMEMCLOSE(f)  \
	(void) fclose(f); \
	f = NULL

#define UNUSED(x) (void) (x)

void
test_empty_object(void)
{
	{
		csv_t csv = { 0 };
		assert(csv_nfields(&csv) == 0);
	}

	{
		csv_t csv;
		csv_init(&csv);
		assert(csv_nfields(&csv) == 0);
	}
}

void
test_line_endings1(void)
{
	FILE *file = NULL;
	csv_t csv  = { 0 };

	char data[] = "A,B,C\r\n"
	              "D,E,F\n";

	FMEMOPEN(file, data);

	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);
	assert(strcmp(csv_field(&csv, 1), "B") == 0);
	assert(strcmp(csv_field(&csv, 2), "C") == 0);

	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "D") == 0);
	assert(strcmp(csv_field(&csv, 1), "E") == 0);
	assert(strcmp(csv_field(&csv, 2), "F") == 0);

	assert(csv_read(&csv, file) == 0);

	FMEMCLOSE(file);
}

void
test_line_endings2(void)
{
	FILE *file = NULL;
	csv_t csv  = { 0 };

	char data[] = "A,B,C\r\n"
	              "D,E,F";

	FMEMOPEN(file, data);

	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);
	assert(strcmp(csv_field(&csv, 1), "B") == 0);
	assert(strcmp(csv_field(&csv, 2), "C") == 0);

	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "D") == 0);
	assert(strcmp(csv_field(&csv, 1), "E") == 0);
	assert(strcmp(csv_field(&csv, 2), "F") == 0);

	assert(csv_read(&csv, file) == 0);

	FMEMCLOSE(file);
}

void
test_empty_fields(void)
{
	FILE *file = NULL;
	csv_t csv  = { 0 };

	char data[] = "\n"
	              "\r"
	              "\r\n"
	              "\"\"\n"
	              "\"\"\r"
	              "\"\"\r\n"
	              ",\n"
	              ",\r"
	              ",\r\n"
	              "\"\",\"\"\n"
	              "\"\",\"\"\r"
	              "\"\",\"\"\r\n"
	              ",,\n"
	              ",,\r"
	              ",,\r\n"
	              "\"\",\"\",\n"
	              "\"\",\"\",\r"
	              "\"\",\"\",\r\n"
	              ",,";

	FMEMOPEN(file, data);

	// Line 1
	assert(csv_read(&csv, file) == 1);
	assert(csv_nfields(&csv) == 1);
	assert(strcmp(csv_field(&csv, 0), "") == 0);

	// Line 2
	assert(csv_read(&csv, file) == 1);
	assert(csv_nfields(&csv) == 1);
	assert(strcmp(csv_field(&csv, 0), "") == 0);

	// Line 3
	assert(csv_read(&csv, file) == 1);
	assert(csv_nfields(&csv) == 1);
	assert(strcmp(csv_field(&csv, 0), "") == 0);

	// Line 4
	assert(csv_read(&csv, file) == 1);
	assert(csv_nfields(&csv) == 1);
	assert(strcmp(csv_field(&csv, 0), "") == 0);

	// Line 5
	assert(csv_read(&csv, file) == 1);
	assert(csv_nfields(&csv) == 1);
	assert(strcmp(csv_field(&csv, 0), "") == 0);

	// Line 6
	assert(csv_read(&csv, file) == 1);
	assert(csv_nfields(&csv) == 1);
	assert(strcmp(csv_field(&csv, 0), "") == 0);

	// Line 7
	assert(csv_read(&csv, file) == 2);
	assert(csv_nfields(&csv) == 2);
	assert(strcmp(csv_field(&csv, 0), "") == 0);
	assert(strcmp(csv_field(&csv, 1), "") == 0);

	// Line 8
	assert(csv_read(&csv, file) == 2);
	assert(csv_nfields(&csv) == 2);
	assert(strcmp(csv_field(&csv, 0), "") == 0);
	assert(strcmp(csv_field(&csv, 1), "") == 0);

	// Line 9
	assert(csv_read(&csv, file) == 2);
	assert(csv_nfields(&csv) == 2);
	assert(strcmp(csv_field(&csv, 0), "") == 0);
	assert(strcmp(csv_field(&csv, 1), "") == 0);

	// Line 10
	assert(csv_read(&csv, file) == 2);
	assert(csv_nfields(&csv) == 2);
	assert(strcmp(csv_field(&csv, 0), "") == 0);
	assert(strcmp(csv_field(&csv, 1), "") == 0);

	// Line 11
	assert(csv_read(&csv, file) == 2);
	assert(csv_nfields(&csv) == 2);
	assert(strcmp(csv_field(&csv, 0), "") == 0);
	assert(strcmp(csv_field(&csv, 1), "") == 0);

	// Line 12
	assert(csv_read(&csv, file) == 2);
	assert(csv_nfields(&csv) == 2);
	assert(strcmp(csv_field(&csv, 0), "") == 0);
	assert(strcmp(csv_field(&csv, 1), "") == 0);

	// Line 13
	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "") == 0);
	assert(strcmp(csv_field(&csv, 1), "") == 0);
	assert(strcmp(csv_field(&csv, 2), "") == 0);

	// Line 14
	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "") == 0);
	assert(strcmp(csv_field(&csv, 1), "") == 0);
	assert(strcmp(csv_field(&csv, 2), "") == 0);

	// Line 15
	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "") == 0);
	assert(strcmp(csv_field(&csv, 1), "") == 0);
	assert(strcmp(csv_field(&csv, 2), "") == 0);

	// Line 16
	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "") == 0);
	assert(strcmp(csv_field(&csv, 1), "") == 0);
	assert(strcmp(csv_field(&csv, 2), "") == 0);

	// Line 17
	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "") == 0);
	assert(strcmp(csv_field(&csv, 1), "") == 0);
	assert(strcmp(csv_field(&csv, 2), "") == 0);

	// Line 18
	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "") == 0);
	assert(strcmp(csv_field(&csv, 1), "") == 0);
	assert(strcmp(csv_field(&csv, 2), "") == 0);

	// Line 19
	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "") == 0);
	assert(strcmp(csv_field(&csv, 1), "") == 0);
	assert(strcmp(csv_field(&csv, 2), "") == 0);

	// EOF
	assert(csv_read(&csv, file) == 0);

	FMEMCLOSE(file);
}

void
test_simple_fields(void)
{
	FILE *file = NULL;
	csv_t csv  = { 0 };

	char data[] = "A\n"
	              "A\r"
	              "A\r\n"
	              "\"A\"\n"
	              "\"A\"\r"
	              "\"A\"\r\n"
	              "A,B\n"
	              "A,B\r"
	              "A,B\r\n"
	              "\"A\",\"B\"\n"
	              "\"A\",\"B\"\r"
	              "\"A\",\"B\"\r\n"
	              "A,B,C\n"
	              "A,B,C\r"
	              "A,B,C\r\n"
	              "\"A\",\"B\",C\n"
	              "\"A\",\"B\",C\r"
	              "\"A\",\"B\",C\r\n"
	              "A,B,C";

	FMEMOPEN(file, data);

	// Line 1
	assert(csv_read(&csv, file) == 1);
	assert(csv_nfields(&csv) == 1);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);

	// Line 2
	assert(csv_read(&csv, file) == 1);
	assert(csv_nfields(&csv) == 1);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);

	// Line 3
	assert(csv_read(&csv, file) == 1);
	assert(csv_nfields(&csv) == 1);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);

	// Line 4
	assert(csv_read(&csv, file) == 1);
	assert(csv_nfields(&csv) == 1);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);

	// Line 5
	assert(csv_read(&csv, file) == 1);
	assert(csv_nfields(&csv) == 1);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);

	// Line 6
	assert(csv_read(&csv, file) == 1);
	assert(csv_nfields(&csv) == 1);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);

	// Line 7
	assert(csv_read(&csv, file) == 2);
	assert(csv_nfields(&csv) == 2);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);
	assert(strcmp(csv_field(&csv, 1), "B") == 0);

	// Line 8
	assert(csv_read(&csv, file) == 2);
	assert(csv_nfields(&csv) == 2);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);
	assert(strcmp(csv_field(&csv, 1), "B") == 0);

	// Line 9
	assert(csv_read(&csv, file) == 2);
	assert(csv_nfields(&csv) == 2);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);
	assert(strcmp(csv_field(&csv, 1), "B") == 0);

	// Line 10
	assert(csv_read(&csv, file) == 2);
	assert(csv_nfields(&csv) == 2);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);
	assert(strcmp(csv_field(&csv, 1), "B") == 0);

	// Line 11
	assert(csv_read(&csv, file) == 2);
	assert(csv_nfields(&csv) == 2);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);
	assert(strcmp(csv_field(&csv, 1), "B") == 0);

	// Line 12
	assert(csv_read(&csv, file) == 2);
	assert(csv_nfields(&csv) == 2);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);
	assert(strcmp(csv_field(&csv, 1), "B") == 0);

	// Line 13
	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);
	assert(strcmp(csv_field(&csv, 1), "B") == 0);
	assert(strcmp(csv_field(&csv, 2), "C") == 0);

	// Line 14
	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);
	assert(strcmp(csv_field(&csv, 1), "B") == 0);
	assert(strcmp(csv_field(&csv, 2), "C") == 0);

	// Line 15
	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);
	assert(strcmp(csv_field(&csv, 1), "B") == 0);
	assert(strcmp(csv_field(&csv, 2), "C") == 0);

	// Line 16
	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);
	assert(strcmp(csv_field(&csv, 1), "B") == 0);
	assert(strcmp(csv_field(&csv, 2), "C") == 0);

	// Line 17
	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);
	assert(strcmp(csv_field(&csv, 1), "B") == 0);
	assert(strcmp(csv_field(&csv, 2), "C") == 0);

	// Line 18
	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);
	assert(strcmp(csv_field(&csv, 1), "B") == 0);
	assert(strcmp(csv_field(&csv, 2), "C") == 0);

	// Line 19
	assert(csv_read(&csv, file) == 3);
	assert(csv_nfields(&csv) == 3);
	assert(strcmp(csv_field(&csv, 0), "A") == 0);
	assert(strcmp(csv_field(&csv, 1), "B") == 0);
	assert(strcmp(csv_field(&csv, 2), "C") == 0);

	// EOF
	assert(csv_read(&csv, file) == 0);

	FMEMCLOSE(file);
}

void
test_quoted_fields(void)
{
	FILE *file = NULL;
	csv_t csv  = { 0 };

	char data[] = "foo \"baz\" bar,foo \"\"baz\"\" bar,\"foo \"\"baz\"\" bar\",\"foo \"baz\" bar\",\"foo \"\"baz\"\", bar\"";

	FMEMOPEN(file, data);

	assert(csv_read(&csv, file) == 5);
	assert(csv_nfields(&csv) == 5);
	assert(strcmp(csv_field(&csv, 0), "foo \"baz\" bar") == 0);
	assert(strcmp(csv_field(&csv, 1), "foo \"\"baz\"\" bar") == 0);
	assert(strcmp(csv_field(&csv, 2), "foo \"baz\" bar") == 0);
	assert(strcmp(csv_field(&csv, 3), "foo \"baz\" bar") == 0);
	assert(strcmp(csv_field(&csv, 4), "foo \"baz\", bar") == 0);

	assert(csv_read(&csv, file) == 0);

	FMEMCLOSE(file);
}

void
test_wrong_quoted_field(void)
{
	FILE *file = NULL;
	csv_t csv  = { 0 };

	char data[] = "\"foo";

	FMEMOPEN(file, data);

	assert(csv_read(&csv, file) == 1);
	assert(csv_nfields(&csv) == 1);
	assert(strcmp(csv_field(&csv, 0), "foo") == 0);

	assert(csv_read(&csv, file) == 0);

	FMEMCLOSE(file);
}

static void
cb_error(csv_err_t err, void *cb_arg)
{
	longjmp(*((jmp_buf *) cb_arg), err);
}

void
test_out_of_range_error(void)
{
	jmp_buf       env;
	csv_options_t csv_options = csv_default_options;
	csv_options.cb_error      = cb_error;
	csv_options.cb_error_arg  = &env;

	csv_t csv;
	csv_init_opt(&csv, &csv_options);

	FILE *file   = NULL;
	char  data[] = ",";

	FMEMOPEN(file, data);

	switch ( setjmp(env) ) {
	case CSV_ERR_OK:
		assert(csv_read(&csv, file) == 2);
		assert(csv_nfields(&csv) == 2);

		// force an "out of range"-Exception!
		assert(strcmp(csv_field(&csv, 2), "will fail") == 0);
		break;

	case CSV_ERR_OUT_OF_RANGE:
		csv_cleanup(&csv);
		break;

	default:
		assert(!"this should not be happen!");
	}

	FMEMCLOSE(file);
}

void
test_read_error(void)
{
	jmp_buf       env;
	csv_options_t csv_options = csv_default_options;
	csv_options.cb_error      = cb_error;
	csv_options.cb_error_arg  = &env;

	csv_t csv;
	csv_init_opt(&csv, &csv_options);

	FILE *file = fopen("/dev/null", "r");
	assert(file != NULL);
	(void) fputc('T', file); // Set Error-Flag!

	switch ( setjmp(env) ) {
	case CSV_ERR_OK:
		assert(csv_read(&csv, file) == 0);
		break;

	case CSV_ERR_IO_READ:
		csv_cleanup(&csv);
		break;

	default:
		assert(!"this should not be happen!");
	}

	(void) fclose(file);
}

void *
my_allocate(size_t n, size_t size, void *cb_arg)
{
	UNUSED(cb_arg);

	void *ptr = calloc(n, size);

	return ptr;
}

void *
my_allocate_null(size_t n, size_t size, void *cb_arg)
{
	UNUSED(n);
	UNUSED(size);
	UNUSED(cb_arg);

	return NULL;
}

void *
my_reallocate(void *ptr, size_t n, size_t size, void *cb_arg)
{
	UNUSED(ptr);
	UNUSED(n);
	UNUSED(size);
	UNUSED(cb_arg);

	return NULL;
}

void
my_free(void *ptr, size_t n, size_t size, void *cb_arg)
{
	UNUSED(n);
	UNUSED(size);
	UNUSED(cb_arg);

	free(ptr);
}

void
test_allocation_error1(void)
{
	jmp_buf env;

	csv_options_t csv_options = csv_default_options;
	csv_options.cb_error      = cb_error;
	csv_options.cb_error_arg  = &env;
	csv_options.cb_allocate   = my_allocate;
	csv_options.cb_reallocate = my_reallocate;
	csv_options.cb_free       = my_free;

	csv_t csv;
	csv_init_opt(&csv, &csv_options);

	char data[] = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T";

	FILE *file = NULL;
	FMEMOPEN(file, data);
	assert(file != NULL);

	switch ( setjmp(env) ) {
	case CSV_ERR_OK:
		assert(csv_read(&csv, file) == 20);
		assert(!"this should not be happen!");
		break;

	case CSV_ERR_OUT_OF_MEMORY:
		csv_cleanup(&csv);
		break;

	default:
		assert(!"this should not be happen!");
	}

	FMEMCLOSE(file);
}

void
show_version(void)
{
	const char *version = csv_version;

	printf("libcsv - version: ");
	for ( ; *version; ++version ) {
		putchar(*version);
	}

	printf(", build date: ");
	for ( ++version; *version; ++version ) {
		putchar(*version);
	}

	printf(", build time: ");
	for ( ++version; *version; ++version ) {
		putchar(*version);
	}
	putchar('\n');
}

int
main(void)
{
	show_version();

	test_empty_object();
	test_line_endings1();
	test_line_endings2();
	test_empty_fields();
	test_simple_fields();
	test_quoted_fields();
	test_wrong_quoted_field();

	test_out_of_range_error();
	test_read_error();

	test_allocation_error1();

	puts("all tests passed...");

	return EXIT_SUCCESS;
}
