/* SPDX-License-Identifier: MIT
 * Copyright (c) 2026 Ivan Kovmir */
#ifdef NDEBUG
#error "Enable assert() for unit tests."
#endif
#include <assert.h>

#include <fts.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#include "tldr.h"

#define URL_SIZE 2048
#define URL_PROTO "file://"
#define MKTEMP_TEMPLATE "/tmp/tinytldr_XXXXXX"
#define FETCH_PAYLOAD "abcdef666\n"

struct Config {
	char *pages_url;
	char *user_agent;
	char *pages_home;
	char *heading_style;
	char *summary_style;
	char *comment_style;
	char *command_style;
	char *reset_style;
	int skip_empty;
	int apply_styles;
	FILE *out;
};

static void test_fetch_pages(void);
static void test_extract_pages(void);
static void test_find_page(void);
static void test_print_page(void);
static void test_list_pages(void);
static int remove_directory(const char *path);

int
remove_directory(const char *path)
{
	char *const paths[] = { (char *)path, NULL };
	FTS *fts = fts_open(paths, FTS_PHYSICAL | FTS_NOCHDIR, NULL);
	if (fts == NULL)
		return -1;

	FTSENT *entry;
	int ret = 0;

	while ((entry = fts_read(fts)) != NULL) {
		switch (entry->fts_info) {
		case FTS_DP:  /* Directory (children already visited). */
			if (rmdir(entry->fts_accpath) == -1) {
				ret = -1;
			}
			break;
		/* Files, links, etc. */
		case FTS_F:
		case FTS_SL:
		case FTS_SLNONE:
		case FTS_DEFAULT:
			if (unlink(entry->fts_accpath) == -1) {
				ret = -1;
			}
			break;
		case FTS_DNR:
		case FTS_ERR:
		case FTS_NS:
			ret = -1;
			break;
		default:
			/* Skip FTS_D, FTS_DC, etc. */
			break;
		}
	}

	fts_close(fts);
	return ret;
}

void
test_fetch_pages(void)
{
	char tmpl[] = MKTEMP_TEMPLATE;
	char url_buf[PATH_MAX];
	const char *payload = FETCH_PAYLOAD;
	const size_t payload_len = strlen(FETCH_PAYLOAD);
	char *out_buf = calloc(payload_len, sizeof(char));
	FILE *out = fmemopen(out_buf, payload_len, "rb+");
	Config *cfg;
	size_t n_written;
	int fd;

	/* Create temporary file. */
	fd = mkstemp(tmpl);
	assert(fd > 0);

	/* Fill temporary file with data. */
	n_written = write(fd, payload, payload_len);
	assert(n_written == payload_len);
	assert(close(fd) == 0);

	/* Construct file URL. */
	snprintf(url_buf, URL_SIZE, "file://%s", tmpl);

	/* Fetch payload. */
	cfg = create_cfg(&(ConfigOpts){
		.pages_url     = url_buf,
		.pages_home    = "nil",
		.user_agent    = "tinytldr/"GIT_VERSION,
		.heading_style = "nil",
		.summary_style = "nil",
		.comment_style = "nil",
		.command_style = "nil",
		.reset_style   = "nil",
		.skip_empty    = -1,
		.apply_styles  = -1,
		.out           = NULL,
	});
	assert(cfg != NULL);
	assert(fetch_pages(cfg, out) == 0);

	/* Verify payload. */
	rewind(out);
	assert(memcmp(out_buf, payload, payload_len) == 0);

	/* Clean up. */
	free(out_buf);
	destroy_cfg(cfg);
	assert(fclose(out) == 0);
	assert(unlink(tmpl) == 0);
}

void
test_extract_pages(void)
{
	unsigned char test_archive_zip[] = {
		0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0xb4,
		0x04, 0x5d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x2e, 0x2f, 0x50, 0x4b, 0x03, 0x04,
		0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0xb4, 0x04, 0x5d, 0xe5, 0x4b,
		0x35, 0x9a, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x0b, 0x00,
		0x00, 0x00, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x2e, 0x7a, 0x69,
		0x70, 0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46,
		0xb4, 0x04, 0x5d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x2e, 0x2f, 0x50, 0x4b, 0x03,
		0x04, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0xb4, 0x04, 0x5d, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04,
		0x00, 0x00, 0x00, 0x62, 0x62, 0x62, 0x2f, 0x50, 0x4b, 0x03, 0x04, 0x14,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0xb4, 0x04, 0x5d, 0x22, 0x90, 0xae,
		0xa0, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00,
		0x00, 0x62, 0x62, 0x62, 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x33, 0x2e, 0x74,
		0x78, 0x74, 0x61, 0x68, 0x6f, 0x6a, 0x0a, 0x50, 0x4b, 0x03, 0x04, 0x14,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0xb4, 0x04, 0x5d, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
		0x00, 0x61, 0x61, 0x61, 0x2f, 0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x32, 0xb4, 0x04, 0x5d, 0xb7, 0xc5, 0x14, 0x13, 0x04,
		0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x61,
		0x61, 0x61, 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x32, 0x2e, 0x74, 0x78, 0x74,
		0x62, 0x79, 0x65, 0x0a, 0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x2d, 0xb4, 0x04, 0x5d, 0x20, 0x30, 0x3a, 0x36, 0x06, 0x00,
		0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x61, 0x61,
		0x61, 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x31, 0x2e, 0x74, 0x78, 0x74, 0x68,
		0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x50, 0x4b, 0x01, 0x02, 0x14, 0x03, 0x14,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0xb4, 0x04, 0x5d, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0xed, 0x41, 0x00,
		0x00, 0x00, 0x00, 0x2e, 0x2f, 0x50, 0x4b, 0x01, 0x02, 0x14, 0x03, 0x14,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0xb4, 0x04, 0x5d, 0xe5, 0x4b, 0x35,
		0x9a, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x81, 0x20,
		0x00, 0x00, 0x00, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x2e, 0x7a,
		0x69, 0x70, 0x50, 0x4b, 0x01, 0x02, 0x14, 0x03, 0x14, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x3c, 0xb4, 0x04, 0x5d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0xed, 0x41, 0x69, 0x00, 0x00, 0x00,
		0x62, 0x62, 0x62, 0x2f, 0x50, 0x4b, 0x01, 0x02, 0x14, 0x03, 0x14, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x3c, 0xb4, 0x04, 0x5d, 0x22, 0x90, 0xae, 0xa0,
		0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x81, 0x8b, 0x00,
		0x00, 0x00, 0x62, 0x62, 0x62, 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x33, 0x2e,
		0x74, 0x78, 0x74, 0x50, 0x4b, 0x01, 0x02, 0x14, 0x03, 0x14, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x32, 0xb4, 0x04, 0x5d, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0xed, 0x41, 0xbb, 0x00, 0x00,
		0x00, 0x61, 0x61, 0x61, 0x2f, 0x50, 0x4b, 0x01, 0x02, 0x14, 0x03, 0x14,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0xb4, 0x04, 0x5d, 0xb7, 0xc5, 0x14,
		0x13, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x81, 0xdd,
		0x00, 0x00, 0x00, 0x61, 0x61, 0x61, 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x32,
		0x2e, 0x74, 0x78, 0x74, 0x50, 0x4b, 0x01, 0x02, 0x14, 0x03, 0x14, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x2d, 0xb4, 0x04, 0x5d, 0x20, 0x30, 0x3a, 0x36,
		0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x81, 0x0c, 0x01,
		0x00, 0x00, 0x61, 0x61, 0x61, 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x31, 0x2e,
		0x74, 0x78, 0x74, 0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x07,
		0x00, 0x07, 0x00, 0x7e, 0x01, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, 0x00,
		0x00
	};
	unsigned int test_archive_zip_len = 721;

	char tmpl[] = MKTEMP_TEMPLATE;
	char path_buf[PATH_MAX];
	Config *cfg;
	FILE *archive;

	/* Create a temporary directory. */
	assert(mkdtemp(tmpl) != NULL);
	cfg = create_cfg(&(ConfigOpts){
		.pages_url     = "nil",
		.pages_home    = tmpl,
		.user_agent    = "nil",
		.heading_style = "nil",
		.summary_style = "nil",
		.comment_style = "nil",
		.command_style = "nil",
		.reset_style   = "nil",
		.skip_empty    = -1,
		.apply_styles  = -1,
		.out           = NULL,
	});
	assert(cfg != NULL);
	archive = fmemopen((void *)test_archive_zip, test_archive_zip_len, "rb");
	/* Extract the archive to the temporary directory. */
	assert(extract_pages(cfg, archive) == 0);

	/* Is what we need extracted? */
	snprintf(path_buf, PATH_MAX, "%s%s", tmpl, "/aaa/file1.txt");
	assert(access(path_buf, F_OK) == 0);
	snprintf(path_buf, PATH_MAX, "%s%s", tmpl, "/aaa/file2.txt");
	assert(access(path_buf, F_OK) == 0);
	snprintf(path_buf, PATH_MAX, "%s%s", tmpl, "/bbb/file3.txt");
	assert(access(path_buf, F_OK) == 0);

	/* Clean up */
	assert(fclose(archive) == 0);
	assert(remove_directory(tmpl) == 0);
	destroy_cfg(cfg);
}

void
test_find_page(void)
{
	char tmpl[] = MKTEMP_TEMPLATE;
	char path_buf[PATH_MAX];
	Config *cfg;
	char *found;
	FILE *f;

	/* Create dummy tree structure. */
	assert(mkdtemp(tmpl) != NULL);
	snprintf(path_buf, PATH_MAX, "%s/%s", tmpl, "aaa");
	assert(mkdir(path_buf, 0755) == 0);
	snprintf(path_buf, PATH_MAX, "%s/%s", tmpl, "bbb");
	assert(mkdir(path_buf, 0755) == 0);
	/* Create files. */
	snprintf(path_buf, PATH_MAX, "%s/%s/%s", tmpl, "aaa", "file.txt");
	f = fopen(path_buf, "w");
	assert(f != NULL);
	assert(fclose(f) == 0);
	snprintf(path_buf, PATH_MAX, "%s/%s/%s", tmpl, "bbb", "file.txt");
	f = fopen(path_buf, "w");
	assert(f != NULL);
	assert(fclose(f) == 0);

	cfg = create_cfg(&(ConfigOpts){
		.pages_url     = "nil",
		.pages_home    = tmpl,
		.user_agent    = "nil",
		.heading_style = "nil",
		.summary_style = "nil",
		.comment_style = "nil",
		.command_style = "nil",
		.reset_style   = "nil",
		.skip_empty    = -1,
		.apply_styles  = -1,
		.out           = NULL,
	});
	assert(cfg != NULL);

	/* Conduct searches. */
	found = find_page(cfg, "file.txt", NULL);
	assert(found != NULL);
	assert(strncmp(found, path_buf, strlen(path_buf)) != 0);
	free(found);

	found = find_page(cfg, "file.txt", "aaa");
	assert(found != NULL);
	assert(strncmp(found, path_buf, strlen(path_buf)) != 0);
	free(found);

	found = find_page(cfg, "file.txt", "bbb");
	assert(found != NULL);
	assert(strncmp(found, path_buf, strlen(path_buf)) == 0);
	free(found);

	assert(find_page(cfg, "does-not-exist", "bbb") == NULL);
	assert(find_page(cfg, "does-not-exist!", NULL) == NULL);

	/* Clean up. */
	assert(remove_directory(tmpl) == 0);
	destroy_cfg(cfg);
}

void
test_print_page(void)
{
	Config *cfg;

	char raw_page[]    = "# heading\n\n> summary\n- comment\n`command`\n";
	char styled_page[] = "1# heading@\n2> summary@\n3- comment@\n4`command`@\n";

	size_t in_len  = strlen(raw_page);
	size_t out_len = strlen(styled_page);
	char *in_buf  = calloc(in_len, sizeof(char));
	char *out_buf = calloc(out_len, sizeof(char));
	FILE *in_page;
	FILE *out_page;

	/* Prepare in and out FILEs. */
	memcpy(in_buf, raw_page, in_len);
	in_page  = fmemopen(in_buf, in_len, "rb");
	out_page = fmemopen(out_buf, out_len, "rb+");

	/* Prepare Config. */
	cfg = create_cfg(&(ConfigOpts){
		.pages_url     = "nil",
		.pages_home    = "nil",
		.user_agent    = "nil",
		.heading_style = "1",
		.summary_style = "2",
		.comment_style = "3",
		.command_style = "4",
		.reset_style   = "@",
		.skip_empty    = 1,
		.apply_styles  = 1,
		.out           = out_page,
	});
	assert(cfg != NULL);
	/* Print page and verify styling. */
	assert(print_page(cfg, in_page) == 0); /* Print. */
	assert(fflush(out_page) == 0);
	assert(memcmp(out_buf, styled_page, out_len) == 0);
	/* Disable styling and verify. */
	destroy_cfg(cfg);
	cfg = create_cfg(&(ConfigOpts){
		.pages_url     = "nil",
		.pages_home    = "nil",
		.user_agent    = "nil",
		.heading_style = "1",
		.summary_style = "2",
		.comment_style = "3",
		.command_style = "4",
		.reset_style   = "@",
		.skip_empty    = 0,
		.apply_styles  = 0,
		.out           = out_page,
	});
	assert(cfg != NULL);
	rewind(in_page);
	rewind(out_page);
	assert(print_page(cfg, in_page) == 0); /* Print. */
	assert(fflush(out_page) == 0);
	assert(memcmp(out_buf, in_buf, in_len) == 0);

	/* Clean up. */
	assert(fclose(in_page) == 0);
	assert(fclose(out_page) == 0);
	destroy_cfg(cfg);
	free(in_buf);
	free(out_buf);
}

void
test_list_pages(void)
{
	char tmpl[] = MKTEMP_TEMPLATE;
	char path_buf[PATH_MAX];
	FILE *tmp, *got_file;
	Config *cfg;

	char want[] = "aaa/file1.md\nbbb/file2.md\n";
	size_t got_len = strlen(want);
	char *got_buf = calloc(got_len, sizeof(char));
	got_file = fmemopen(got_buf, got_len, "rb+");

	/* Create dummy tree structure. */
	assert(mkdtemp(tmpl) != NULL);
	snprintf(path_buf, PATH_MAX, "%s/%s", tmpl, "aaa");
	assert(mkdir(path_buf, 0755) == 0);
	snprintf(path_buf, PATH_MAX, "%s/%s", tmpl, "bbb");
	assert(mkdir(path_buf, 0755) == 0);
	/* Create files. */
	snprintf(path_buf, PATH_MAX, "%s/%s/%s", tmpl, "aaa", "file1.md");
	tmp = fopen(path_buf, "w");
	assert(tmp != NULL);
	assert(fclose(tmp) == 0);
	snprintf(path_buf, PATH_MAX, "%s/%s/%s", tmpl, "bbb", "file2.md");
	tmp = fopen(path_buf, "w");
	assert(tmp != NULL);
	assert(fclose(tmp) == 0);
	snprintf(path_buf, PATH_MAX, "%s/%s/%s", tmpl, "bbb", "file3.txt");
	tmp = fopen(path_buf, "w");
	assert(tmp != NULL);
	assert(fclose(tmp) == 0);

	cfg = create_cfg(&(ConfigOpts){
		.pages_url     = "nil",
		.pages_home    = tmpl,
		.user_agent    = "nil",
		.heading_style = "nil",
		.summary_style = "nil",
		.comment_style = "nil",
		.command_style = "nil",
		.reset_style   = "nil",
		.skip_empty    = -1,
		.apply_styles  = -1,
		.out           = got_file,
	});
	/* List */
	assert(list_pages(cfg) == 0);
	fflush(got_file);
	assert(memcmp(got_buf, want, got_len) == 0);

	/* Clean up. */
	fclose(got_file);
	free(got_buf);
	destroy_cfg(cfg);
	remove_directory(tmpl);
}

int
main(void)
{
	test_fetch_pages();
	test_extract_pages();
	test_find_page();
	test_print_page();
	test_list_pages();
	return 0;
}
