/******************************************************************************
*
* Copyright (C) 2026  Brian Berlin
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
* 
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef _WIN32
#include <direct.h>
#include <process.h>
#define chdir _chdir
#define getcwd _getcwd
#define getpid _getpid
#define rmdir _rmdir
static int test_mkdir(const char *path) { return _mkdir(path); }
#else
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
static int test_mkdir(const char *path) { return mkdir(path, 0700); }
#endif

#include "../src/interface/katconfig.h"
#include "../src/interface/logger.h"
#include "../src/interface/palette.h"
#include "../src/interface/toml.h"
#include "../src/interface/util.h"

static int failure = 0;

static void output_result(int result, const char *test, const char *value,
                          const char *error)
{
    if (result) {
        printf("\e[32m[PASS]\e[m %s Result=%s\n", test, value);
    } else {
        failure++;
        printf("\e[31m[FAIL]\e[m %s %s Result=%s\n", test, error, value);
    }
}

static void check_int(const char *test, int value, int answer)
{
    char value_string[65];
    char error[257];

    sprintf(value_string, "%d", value);
    if (value == answer) {
        output_result(1, test, value_string, "");
    } else {
        sprintf(error, "Value not %d", answer);
        output_result(0, test, value_string, error);
    }
}

static void check_string_null(const char *test, const char *value,
                              const char *answer)
{
    char error[257];

    if ((value == NULL && answer == NULL) ||
            (value && answer && !strcmp(value, answer))) {
        output_result(1, test, value ? value : "NULL", "");
    } else {
        sprintf(error, "Value not %s", answer ? answer : "NULL");
        output_result(0, test, value ? value : "NULL", error);
    }
}

static void check_palette_value(const char *test, int value, int answer)
{
    char value_string[65];
    char error[257];

    sprintf(value_string, "0x%06x", value);
    if (value == answer) {
        output_result(1, test, value_string, "");
    } else {
        sprintf(error, "Value not 0x%06x", answer);
        output_result(0, test, value_string, error);
    }
}

static int copy_file(const char *src, const char *dst)
{
    FILE *in = fopen(src, "rb");
    FILE *out;
    unsigned char buf[4096];
    size_t n;

    if (!in)
        return -1;

    out = fopen(dst, "wb");
    if (!out) {
        fclose(in);
        return -1;
    }

    while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
        if (fwrite(buf, 1, n, out) != n) {
            fclose(in);
            fclose(out);
            return -1;
        }
    }

    fclose(in);
    fclose(out);
    return 0;
}

static int write_palette_file(const char *path, int bytes)
{
    FILE *fp = fopen(path, "wb");

    if (!fp)
        return -1;

    for (int i = 0; i < bytes; i++)
        fputc(i & 0xff, fp);

    fclose(fp);
    return 0;
}

static int file_contains(const char *path, const char *needle)
{
    FILE *fp = fopen(path, "rb");
    char buf[4096];
    size_t needle_len;
    size_t carry = 0;

    if (!fp || !needle)
        return 0;

    needle_len = strlen(needle);
    if (!needle_len) {
        fclose(fp);
        return 1;
    }

    while (!feof(fp)) {
        size_t n = fread(buf + carry, 1, sizeof(buf) - carry, fp);
        size_t total = carry + n;

        if (total >= needle_len) {
            for (size_t i = 0; i + needle_len <= total; i++) {
                if (!memcmp(buf + i, needle, needle_len)) {
                    fclose(fp);
                    return 1;
                }
            }

            carry = needle_len - 1;
            memmove(buf, buf + total - carry, carry);
        } else {
            carry = total;
        }

        if (n == 0)
            break;
    }

    fclose(fp);
    return 0;
}

static void join_path(char *out, size_t out_size, const char *dir,
                      const char *file)
{
    snprintf(out, out_size, "%s%c%s", dir, util_get_dir_seperator_character(),
             file);
}

static const char *active_palette_config_value(void)
{
    t_toml_node *video_tbl = toml_get_table_array_by_key(
            config_get_global_table()->child, "video", "tag", "All");
    const char *key = config_get_global_cfg()->machine.tv_standard == NTSC
        ? "ntsc.palette" : "pal.palette";

    return video_tbl ? toml_get_key_value(video_tbl, key) : NULL;
}

static void check_active_palette_key(const char *test, const char *answer)
{
    check_string_null(test, active_palette_config_value(), answer);
}

static void set_active_palette_for_test(const char *name)
{
    t_config *cfg = config_get_global_cfg();

    if (name && name[0]) {
        util_alloc_string(&cfg->video.palette_name, name);
        config_load_palette(config_get_global_table(), cfg->video.palette_name,
                            cfg->video.palette);
        config_save_active_palette_name(config_get_global_table(), "All",
                                        cfg->video.palette_name);
    } else {
        free(cfg->video.palette_name);
        cfg->video.palette_name = NULL;
        make_default_palette(cfg->video.palette);
        config_save_active_palette_name(config_get_global_table(), "All", "");
    }
}

int main(int argc, char *argv[])
{
    char cwd[KAT_PATH_MAX];
    char workdir[KAT_PATH_MAX];
    char conf_dst[KAT_PATH_MAX];
    char *testv[1] = {"test_palette"};
    int imported[256] = {0};
    int default_before[256] = {0};
    int default_after[256] = {0};
    int generated[256] = {0};
    int result;

    (void)argc;
    (void)argv;

    if (!getcwd(cwd, sizeof(cwd))) {
        output_result(0, "Get CWD", "NULL", "getcwd failed");
        return failure;
    }

    for (int attempt = 0; attempt < 100; attempt++) {
        if (attempt == 0)
            snprintf(workdir, sizeof(workdir), "%s%cpalette_test_%ld", cwd,
                     util_get_dir_seperator_character(), (long)getpid());
        else
            snprintf(workdir, sizeof(workdir), "%s%cpalette_test_%ld_%d", cwd,
                     util_get_dir_seperator_character(), (long)getpid(),
                     attempt);

        if (!test_mkdir(workdir))
            break;

        if (attempt == 99) {
            output_result(0, "Create Workdir", workdir, "mkdir failed");
            return failure;
        }
    }

    if (!workdir[0]) {
        output_result(0, "Create Workdir", workdir, "mkdir failed");
        return failure;
    }

    join_path(conf_dst, sizeof(conf_dst), workdir, "kat5200.conf");
    if (copy_file("kat5200.conf", conf_dst)) {
        output_result(0, "Copy Config", conf_dst, "copy failed");
        return failure;
    }

    if (chdir(workdir)) {
        output_result(0, "Enter Workdir", workdir, "chdir failed");
        return failure;
    }

    logger_init();
    config_init(1, testv);

    {
        char *names[1] = {NULL};

        check_int("Null Palette Table Is Empty",
                  config_get_palette_names(NULL, names, 1), 0);
        check_int("Null Palette Output Is Empty",
                  config_get_palette_names(config_get_global_table(), NULL, 1),
                  0);
        check_int("Zero Palette Capacity Is Empty",
                  config_get_palette_names(config_get_global_table(), names, 0),
                  0);
    }

    check_int("Palette Exists Exact", config_palette_exists(
              config_get_global_table(), "Default"), 1);
    check_int("Palette Exists Case Sensitive", config_palette_exists(
              config_get_global_table(), "default"), 0);

    {
        t_toml_node *blank_video_tbl;
        int add_blank_tag_result = 0;

        set_active_palette_for_test("Default");
        check_palette_value("Named Default Palette Active",
                            config_get_global_cfg()->video.palette[1],
                            0x1e1e1e);

        blank_video_tbl = toml_add_table(config_get_global_table(), "video",
                                         TOML_TABLE_ARY, "");
        if (!blank_video_tbl) {
            add_blank_tag_result = -1;
        } else {
            add_blank_tag_result |= toml_add_key(blank_video_tbl, "tag",
                    TOML_DQ_STRING, "blank_override", "", " ");
            add_blank_tag_result |= toml_add_key(blank_video_tbl,
                    "ntsc.palette", TOML_DQ_STRING, "", "", " ");
        }
        check_int("Blank Palette Tag Created", add_blank_tag_result, 0);

        if (!add_blank_tag_result) {
            make_default_palette(generated);
            config_load_video(config_get_global_table(), "blank_override");
            check_string_null("Blank Palette Tag Active Name",
                              config_get_global_cfg()->video.palette_name, "");
            check_palette_value("Blank Palette Tag Generated Default",
                                config_get_global_cfg()->video.palette[1],
                                generated[1]);
        }

        if (!add_blank_tag_result) {
            t_toml_node *missing_video_tbl;
            int add_missing_tag_result = 0;

            missing_video_tbl = toml_add_table(config_get_global_table(),
                                               "video", TOML_TABLE_ARY, "");
            if (!missing_video_tbl) {
                add_missing_tag_result = -1;
            } else {
                add_missing_tag_result |= toml_add_key(missing_video_tbl,
                        "tag", TOML_DQ_STRING, "missing_override", "", " ");
                add_missing_tag_result |= toml_add_key(missing_video_tbl,
                        "ntsc.palette", TOML_DQ_STRING, "missing_palette",
                        "", " ");
            }
            check_int("Missing Palette Tag Created", add_missing_tag_result, 0);

            if (!add_missing_tag_result) {
                make_default_palette(generated);
                config_load_video(config_get_global_table(),
                                  "missing_override");
                check_string_null("Missing Palette Tag Active Name",
                                  config_get_global_cfg()->video.palette_name,
                                  NULL);
                check_palette_value("Missing Palette Generated Default",
                                    config_get_global_cfg()->video.palette[1],
                                    generated[1]);
                check_active_palette_key("Missing Palette Key Not Saved",
                                         "Default");
                check_int("Missing Palette Tag Not Written",
                          file_contains("kat5200.conf",
                                        "missing_override"), 0);
            }
        }
    }

    if (write_palette_file("valid.pal", 256 * 3)) {
        output_result(0, "Write Valid Palette", "valid.pal", "write failed");
    } else {
        t_toml_node *video_tbl = toml_get_table_array_by_key(
                config_get_global_table()->child, "video", "tag", "All");
        int fallback_default[256] = {0};

        fallback_default[1] = -1;
        config_load_palette(video_tbl, "Default", fallback_default);
        check_palette_value("Palette Load Falls Back To Global",
                            fallback_default[1], 0x1e1e1e);

        config_load_palette(config_get_global_table(), "Default",
                            default_before);
        result = convert_palette("valid.pal", "Default");
        check_int("Duplicate Palette Rejected", result,
                  PALETTE_IMPORT_ERR_EXISTS);
        config_load_palette(config_get_global_table(), "Default",
                            default_after);
        check_palette_value("Duplicate Palette Unchanged", default_after[1],
                            default_before[1]);

        result = convert_palette("valid.pal", "unit_import");
        check_int("Palette Import Result", result, PALETTE_IMPORT_OK);
        check_int("Imported Palette Exists", config_palette_exists(
                  config_get_global_table(), "unit_import"), 1);
        check_int("Imported Palette Exact Case", config_palette_exists(
                  config_get_global_table(), "Unit_import"), 0);
        config_load_palette(config_get_global_table(), "unit_import",
                            imported);
        check_palette_value("Imported Palette Color 0", imported[0],
                            0x000102);
        check_palette_value("Imported Palette Color 1", imported[1],
                            0x030405);

        result = config_delete_palette(config_get_global_table(),
                                       "unit_import");
        check_int("Delete Imported Palette", result, 0);
        check_int("Deleted Palette Missing", config_palette_exists(
                  config_get_global_table(), "unit_import"), 0);
        check_int("Deleted Palette Missing From File",
                  file_contains("kat5200.conf", "unit_import"), 0);

        result = config_delete_palette(config_get_global_table(),
                                       "unit_import");
        check_int("Delete Missing Palette", result, -1);

        result = convert_palette("valid.pal", "unit_import");
        check_int("Reimport Palette Result", result, PALETTE_IMPORT_OK);

        {
            t_toml_node *video_tbl = toml_get_table_array_by_key(
                    config_get_global_table()->child, "video", "tag", "All");
            config_save_palette(video_tbl, "unit_import", imported);
            check_int("Nested Palette Duplicate Created",
                      file_contains("kat5200.conf", "unit_import"), 1);
        }

        set_active_palette_for_test("unit_import");
        result = config_delete_palette(config_get_global_table(),
                                       "unit_import");
        check_int("Delete Active Imported Palette", result, 0);
        check_int("Active Deleted Palette Missing", config_palette_exists(
                  config_get_global_table(), "unit_import"), 0);
        check_int("Active Imported Fallback Changed",
                  config_fallback_active_palette_after_delete(
                      config_get_global_table(), "unit_import"), 1);
        check_string_null("Active Imported Fallback Name",
                          config_get_global_cfg()->video.palette_name,
                          NULL);
        check_active_palette_key("Active Imported Fallback Saved", "");
        make_default_palette(generated);
        check_palette_value("Active Imported Generated Default",
                            config_get_global_cfg()->video.palette[1],
                            generated[1]);
        check_int("Active Deleted Palette Missing From File",
                  file_contains("kat5200.conf", "unit_import"), 0);

        set_active_palette_for_test("Default");
        result = config_delete_palette(config_get_global_table(), "Default");
        check_int("Delete Active Default Palette", result, 0);
        check_int("Active Default Fallback Changed",
                  config_fallback_active_palette_after_delete(
                      config_get_global_table(), "Default"), 1);
        check_string_null("Active Default Fallback Name",
                          config_get_global_cfg()->video.palette_name,
                          NULL);
        check_active_palette_key("Active Default Fallback Saved",
                                 "");
        make_default_palette(generated);
        check_palette_value("Active Default Generated Default",
                            config_get_global_cfg()->video.palette[1],
                            generated[1]);
    }

    result = convert_palette("missing.pal", "missing_import");
    check_int("Missing Palette File", result, PALETTE_IMPORT_ERR_OPEN);

    if (write_palette_file("short.pal", 10)) {
        output_result(0, "Write Short Palette", "short.pal", "write failed");
    } else {
        result = convert_palette("short.pal", "short_import");
        check_int("Short Palette File", result, PALETTE_IMPORT_ERR_READ);
        check_int("Short Palette Not Saved", config_palette_exists(
                  config_get_global_table(), "short_import"), 0);
    }

    {
        char *names[24];
        char last_name[128];
        int count;

        set_active_palette_for_test("");

        while ((count = config_get_palette_names(config_get_global_table(),
                         names, 24)) > 1) {
            snprintf(last_name, sizeof(last_name), "%s", names[0]);
            check_int("Delete Palette While Reducing",
                      config_delete_palette(config_get_global_table(),
                          last_name), 0);
        }

        count = config_get_palette_names(config_get_global_table(), names, 24);
        check_int("One Palette Remains Before Final Delete", count, 1);
        if (count == 1 && names[0]) {
            snprintf(last_name, sizeof(last_name), "%s", names[0]);
            set_active_palette_for_test(last_name);
            check_int("Delete Final Active Palette",
                      config_delete_palette(config_get_global_table(),
                          last_name), 0);
            check_int("Final Active Fallback Changed",
                      config_fallback_active_palette_after_delete(
                          config_get_global_table(), last_name), 1);
            check_string_null("Final Active Palette Cleared",
                              config_get_global_cfg()->video.palette_name,
                              NULL);
            check_active_palette_key("Final Active Palette Key Cleared", "");
            make_default_palette(generated);
            check_palette_value("Final Active Generated Default",
                                config_get_global_cfg()->video.palette[1],
                                generated[1]);
            check_int("No Palettes Remain", config_get_palette_names(
                      config_get_global_table(), names, 24), 0);
        }
    }

    config_exit();
    logger_exit();

    remove("valid.pal");
    remove("short.pal");
    remove("video");
    remove("kat5200.conf");
    remove("kat5200.log");

    if (chdir(cwd)) {
        output_result(0, "Restore CWD", cwd, "chdir failed");
        return failure;
    }
    rmdir(workdir);

    return failure;
}
