/*
 * proftest.c
 *
 * Copyright (C) 2015 - 2016 James Booth <boothj5@gmail.com>
 * Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
#include <sys/stat.h>
#include <sys/wait.h>
#include <glib.h>

#include <stdlib.h>
#include "prof_cmocka.h"
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <pty.h>
#include <utmp.h>
#include <poll.h>
#include <stdarg.h>
#include <termios.h>

#include <stabber.h>

#include "proftest.h"

int prof_expect_timeout = 10;
int prof_pid = 0;

char* config_orig;
char* data_orig;

int current_port = 5230;

int fd = 0;

static GString* accumulated_output = NULL;

int
prof_expect(int fd, ...)
{
    va_list args;
    va_start(args, fd);

    enum prof_expect_type type = va_arg(args, enum prof_expect_type);
    if (type == prof_expect_end) {
        va_end(args);
        return 0;
    }

    char* pattern = va_arg(args, char*);
    int result_val = va_arg(args, int);
    va_end(args);

    if (accumulated_output == NULL) {
        accumulated_output = g_string_new("");
    }

    usleep(1000 * 100); // Wait 100ms for buffer to settle

    struct pollfd pfd;
    pfd.fd = fd;
    pfd.events = POLLIN;

    GError* error = NULL;
    GRegex* ansi = g_regex_new("\\x1b\\[[0-9;]*[a-zA-Z]|\\x1b\\([a-zA-Z]", 0, 0, &error);
    if (error) {
        g_error_free(error);
        return 0;
    }
    GTimer* timer = g_timer_new();
    while (g_timer_elapsed(timer, NULL) < prof_expect_timeout) {
        // Check if pattern is already in accumulated output
        gchar* clean_output = g_regex_replace(ansi, accumulated_output->str, -1, 0, "", 0, NULL);

        if (type == prof_expect_exact) {
            gchar* escaped = g_regex_escape_string(pattern, -1);
            error = NULL;
            GRegex* regex = g_regex_new(escaped, G_REGEX_DOTALL | G_REGEX_OPTIMIZE, 0, &error);
            if (error) {
                g_error_free(error);
                g_free(escaped);
                g_free(clean_output);
                g_timer_destroy(timer);
                g_regex_unref(ansi);
                return 0;
            }
            g_free(escaped);
            GMatchInfo* match_info;
            if (g_regex_match(regex, clean_output, 0, &match_info)) {
                g_string_truncate(accumulated_output, 0);

                g_match_info_free(match_info);
                g_regex_unref(regex);
                g_free(clean_output);
                g_timer_destroy(timer);
                g_regex_unref(ansi);
                return result_val;
            }
            g_match_info_free(match_info);
            g_regex_unref(regex);
        } else if (type == prof_expect_regexp) {
            GMatchInfo* match_info;
            error = NULL;
            GRegex* regex = g_regex_new(pattern, G_REGEX_DOTALL | G_REGEX_OPTIMIZE, 0, &error);
            if (error) {
                g_error_free(error);
                g_free(clean_output);
                g_timer_destroy(timer);
                g_regex_unref(ansi);
                return 0;
            }
            if (g_regex_match(regex, clean_output, 0, &match_info)) {
                gint start_pos, end_pos;
                g_match_info_fetch_pos(match_info, 0, &start_pos, &end_pos);

                g_string_truncate(accumulated_output, 0);

                g_match_info_free(match_info);
                g_regex_unref(regex);
                g_free(clean_output);
                g_timer_destroy(timer);
                g_regex_unref(ansi);
                return result_val;
            }
            g_match_info_free(match_info);
            g_regex_unref(regex);
        }

        g_free(clean_output);

        int ret = poll(&pfd, 1, 10); // 10ms timeout for poll
        if (ret > 0 && (pfd.revents & POLLIN)) {
            char buf[1024];
            ssize_t n = read(fd, buf, sizeof(buf) - 1);
            if (n > 0) {
                buf[n] = '\0';
                g_string_append(accumulated_output, buf);
            } else if (n == 0) {
                break; // EOF
            }
        } else {
            usleep(1000 * 5); // 5ms sleep if no data
        }
    }

    g_timer_destroy(timer);
    g_regex_unref(ansi);
    return 0; // Timeout or EOF without match
}

gboolean
_create_dir(const char* name)
{
    struct stat sb;

    if (stat(name, &sb) != 0) {
        if (errno != ENOENT || mkdir(name, S_IRWXU) != 0) {
            return FALSE;
        }
    } else {
        if ((sb.st_mode & S_IFDIR) != S_IFDIR) {
            return FALSE;
        }
    }

    return TRUE;
}

gboolean
_mkdir_recursive(const char* dir)
{
    int i;
    gboolean result = TRUE;

    for (i = 1; i <= strlen(dir); i++) {
        if (dir[i] == '/' || dir[i] == '\0') {
            gchar* next_dir = g_strndup(dir, i);
            result = _create_dir(next_dir);
            g_free(next_dir);
            if (!result) {
                break;
            }
        }
    }

    return result;
}

void
_create_config_dir(void)
{
    GString* profanity_dir = g_string_new(XDG_CONFIG_HOME);
    g_string_append(profanity_dir, "/profanity");

    if (!_mkdir_recursive(profanity_dir->str)) {
        assert_true(FALSE);
    }

    g_string_free(profanity_dir, TRUE);
}

void
_create_data_dir(void)
{
    GString* profanity_dir = g_string_new(XDG_DATA_HOME);
    g_string_append(profanity_dir, "/profanity");

    if (!_mkdir_recursive(profanity_dir->str)) {
        assert_true(FALSE);
    }

    g_string_free(profanity_dir, TRUE);
}

void
_create_chatlogs_dir(void)
{
    GString* chatlogs_dir = g_string_new(XDG_DATA_HOME);
    g_string_append(chatlogs_dir, "/profanity/chatlogs");

    if (!_mkdir_recursive(chatlogs_dir->str)) {
        assert_true(FALSE);
    }

    g_string_free(chatlogs_dir, TRUE);
}

void
_create_logs_dir(void)
{
    GString* logs_dir = g_string_new(XDG_DATA_HOME);
    g_string_append(logs_dir, "/profanity/logs");

    if (!_mkdir_recursive(logs_dir->str)) {
        assert_true(FALSE);
    }

    g_string_free(logs_dir, TRUE);
}

void
_cleanup_dirs(void)
{
    system("rm -rf ./tests/functionaltests/files");
}

void
prof_start(void)
{
    if (accumulated_output) {
        g_string_free(accumulated_output, TRUE);
        accumulated_output = NULL;
    }

    // helper script sets terminal columns, avoids assertions failing
    // based on the test runner terminal size
    int master;
    pid_t pid = forkpty(&master, NULL, NULL, NULL);
    if (pid == 0) {
        system("stty sane -echo");
        execlp("sh", "sh", "-c", "./tests/functionaltests/start_profanity.sh", (char*)0);
        _exit(1);
    }
    prof_pid = pid;
    fd = master;
}

int
init_prof_test(void** state)
{
    // Check if profanity binary exists and is executable
    if (access("./build_run/profanity", X_OK) != 0 && access("./profanity", X_OK) != 0) {
        fprintf(stderr, "ERROR: Profanity binary not found or not executable.\n");
        fprintf(stderr, "Ensure you have built the project.\n");
        return -1;
    }

    const char* cfg = getenv("XDG_CONFIG_HOME");
    const char* dat = getenv("XDG_DATA_HOME");
    config_orig = cfg ? g_strdup(cfg) : NULL;
    data_orig = dat ? g_strdup(dat) : NULL;

    setenv("XDG_CONFIG_HOME", XDG_CONFIG_HOME, 1);
    setenv("XDG_DATA_HOME", XDG_DATA_HOME, 1);

    usleep(1000 * 2000); // 2s delay between tests to let OS recover

    int res = -1;
    int port = 0;
    for (int i = 0; i < 5; i++) {
        port = current_port + (i * 10);
        res = stbbr_start(STBBR_LOGDEBUG, &port, 0);
        if (res == 0 && port > 0) {
            break;
        }
    }

    if (res != 0 || port == 0) {
        assert_true(FALSE);
        return -1;
    }
    current_port = port;

    _cleanup_dirs();

    _create_config_dir();
    _create_data_dir();
    _create_chatlogs_dir();
    _create_logs_dir();

    prof_start();
    usleep(1000 * 500); // Wait for profanity to start
    prof_expect_timeout = 30;
    assert_true(prof_output_regex("Profanity"));

    // set UI options to make expect assertions faster and more reliable
    prof_input("/inpblock timeout 5");
    prof_output_regex("Input blocking set to 5 milliseconds");
    prof_input("/inpblock dynamic off");
    prof_output_regex("Dynamic input blocking disabled");
    prof_input("/notify chat off");
    prof_output_regex("Chat notifications disabled");
    prof_input("/notify room off");
    prof_output_regex("Room notifications disabled");
    prof_input("/wrap off");
    prof_output_regex("Word wrap disabled");
    prof_input("/roster hide");
    prof_output_regex("Roster disabled");
    prof_input("/occupants default hide");
    prof_output_regex("Occupant list disabled");
    prof_input("/time console off");
    prof_output_regex("Console time display");
    prof_input("/time chat off");
    prof_output_regex("Chat time display");
    prof_input("/time muc off");
    prof_output_regex("MUC time display");
    prof_input("/time config off");
    prof_output_regex("Config time display");
    prof_input("/time private off");
    prof_output_regex("Private chat time display");
    prof_input("/time xml off");
    prof_output_regex("XML Console time display");

    prof_expect_timeout = 10;
    return 0;
}

int
close_prof_test(void** state)
{
    prof_input("/quit");

    // Give it some time to quit gracefully
    int retries = 0;
    int status;
    while (retries < 50) { // 5 seconds
        pid_t res = waitpid(prof_pid, &status, WNOHANG);
        if (res == prof_pid) {
            break;
        }
        usleep(1000 * 100);
        retries++;
    }

    if (retries == 50) {
        kill(prof_pid, SIGKILL);
        waitpid(prof_pid, &status, 0);
    }

    _cleanup_dirs();

    if (config_orig) {
        setenv("XDG_CONFIG_HOME", config_orig, 1);
        g_free(config_orig);
        config_orig = NULL;
    } else {
        unsetenv("XDG_CONFIG_HOME");
    }

    if (data_orig) {
        setenv("XDG_DATA_HOME", data_orig, 1);
        g_free(data_orig);
        data_orig = NULL;
    } else {
        unsetenv("XDG_DATA_HOME");
    }

    stbbr_stop();
    usleep(1000 * 500); // 500ms delay after stop
    return 0;
}

void
prof_input(const char* input)
{
    GString* inp_str = g_string_new(input);
    g_string_append(inp_str, "\n");
    write(fd, inp_str->str, inp_str->len);
    tcdrain(fd);
    g_string_free(inp_str, TRUE);
    usleep(1000 * 100); // 100ms delay
}

void
prof_send_raw(const char* bytes)
{
    write(fd, bytes, strlen(bytes));
    tcdrain(fd);
    usleep(1000 * 100);
}

int
prof_output_exact(const char* text)
{
    // Use regex to skip potential timestamps [HH:MM:SS] or character noise
    gchar* escaped = g_regex_escape_string(text, -1);
    gchar* pattern = g_strdup_printf(".*%s", escaped);
    int res = prof_expect(fd, prof_expect_regexp, pattern, 1, prof_expect_end);
    g_free(pattern);
    g_free(escaped);
    return res;
}

int
prof_output_regex(const char* text)
{
    gchar* pattern = g_strdup_printf(".*%s", text);
    int res = prof_expect(fd, prof_expect_regexp, pattern, 1, prof_expect_end);
    g_free(pattern);
    return res;
}

void
prof_connect_with_roster(const char* roster)
{
    GString* roster_str = g_string_new(
        "<iq type='result' to='stabber@localhost/profanity'>"
        "<query xmlns='jabber:iq:roster' ver='362'>");
    g_string_append(roster_str, roster);
    g_string_append(roster_str,
                    "</query>"
                    "</iq>");

    stbbr_for_query("jabber:iq:roster", roster_str->str);
    g_string_free(roster_str, TRUE);

    stbbr_for_id("presence:*",
                 "<presence id='prof_presence_*' lang='en' to='stabber@localhost/profanity' from='stabber@localhost/profanity'>"
                 "<priority>0</priority>"
                 "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io/' ver='f8mrtdyAmhnj8Ca+630bThSL718='/>"
                 "</presence>");

    int port = current_port;
    gchar* connect_cmd = g_strdup_printf("/connect stabber@localhost/profanity server 127.0.0.1 port %d tls disable auth legacy", port);
    prof_input(connect_cmd);
    g_free(connect_cmd);

    assert_true(prof_output_regex("Enter password:"));
    usleep(1000 * 200); // 200ms delay
    prof_input("password");
    usleep(1000 * 200); // 200ms delay

    // Allow time for profanity to connect
    prof_expect_timeout = 30;
    assert_true(prof_output_regex("successfully"));
    prof_expect_timeout = 10;
    stbbr_wait_for("presence:*");
    // usleep(1000 * 2000); // 2s delay
}

void
prof_timeout(int timeout)
{
    prof_expect_timeout = timeout;
}

void
prof_timeout_reset(void)
{
    prof_expect_timeout = 10;
}

void
prof_connect(void)
{
    prof_connect_with_roster(
        "<item jid='buddy1@localhost' subscription='both' name='Buddy1'/>"
        "<item jid='buddy2@localhost' subscription='both' name='Buddy2'/>");
}
