#if !defined(_DEBUG)
 #define _DEBUG
#endif
#undef NDEBUG

#include "../log.h"

#include "../config.c"

#define ALEN(v) (sizeof(v) / sizeof((v)[0]))

/*
 * Stubs
 */

void
user_notification_add_fmt(user_notifications_t *notifications,
                          enum user_notification_kind kind,
                          const char *fmt, ...)
{
}

static void
test_invalid_key(struct context *ctx, bool (*parse_fun)(struct context *ctx),
                 const char *key)
{
    ctx->key = key;
    ctx->value = "value for invalid key";

    if (parse_fun(ctx)) {
        BUG("[%s].%s: did not fail to parse as expected"
            "(key should be invalid)", ctx->section, ctx->key);
    }
}

static void
test_string(struct context *ctx, bool (*parse_fun)(struct context *ctx),
             const char *key, char *const *ptr)
{
    ctx->key = key;

    static const struct {
        const char *option_string;
        const char *value;
        bool invalid;
    } input[] = {
        {"a string", "a string"},
    };

    for (size_t i = 0; i < ALEN(input); i++) {
        ctx->value = input[i].option_string;

        if (input[i].invalid) {
            if (parse_fun(ctx)) {
                BUG("[%s].%s=%s: did not fail to parse as expected",
                    ctx->section, ctx->key, ctx->value);
            }
        } else {
            if (!parse_fun(ctx)) {
                BUG("[%s].%s=%s: failed to parse",
                    ctx->section, ctx->key, ctx->value);
            }
            if (!streq(*ptr, input[i].value)) {
                BUG("[%s].%s=%s: set value (%s) not the expected one (%s)",
                    ctx->section, ctx->key, ctx->value,
                    *ptr, input[i].value);
            }
        }
    }
}

static void
test_c32string(struct context *ctx, bool (*parse_fun)(struct context *ctx),
               const char *key, char32_t *const *ptr)
{
    ctx->key = key;

    static const struct {
        const char *option_string;
        const char32_t *value;
        bool invalid;
    } input[] = {
        {"a string", U"a string"},
    };

    for (size_t i = 0; i < ALEN(input); i++) {
        ctx->value = input[i].option_string;

        if (input[i].invalid) {
            if (parse_fun(ctx)) {
                BUG("[%s].%s=%s: did not fail to parse as expected",
                    ctx->section, ctx->key, ctx->value);
            }
        } else {
            if (!parse_fun(ctx)) {
                BUG("[%s].%s=%s: failed to parse",
                    ctx->section, ctx->key, ctx->value);
            }
            if (c32cmp(*ptr, input[i].value) != 0) {
                BUG("[%s].%s=%s: set value (%ls) not the expected one (%ls)",
                    ctx->section, ctx->key, ctx->value,
                    (const wchar_t *)*ptr,
                    (const wchar_t *)input[i].value);
            }
        }
    }
}

static void
test_protocols(struct context *ctx, bool (*parse_fun)(struct context *ctx),
               const char *key, char32_t **const *ptr)
{
    ctx->key = key;

    static const struct {
        const char *option_string;
        int count;
        const char32_t *value[2];
        bool invalid;
    } input[] = {
        {""},
        {"http", 1, {U"http://"}},
        {" http", 1, {U"http://"}},
        {"http, https", 2, {U"http://", U"https://"}},
        {"longprotocolislong", 1, {U"longprotocolislong://"}},
    };

    for (size_t i = 0; i < ALEN(input); i++) {
        ctx->value = input[i].option_string;

        if (input[i].invalid) {
            if (parse_fun(ctx)) {
                BUG("[%s].%s=%s: did not fail to parse as expected",
                    ctx->section, ctx->key, &ctx->value[0]);
            }
        } else {
            if (!parse_fun(ctx)) {
                BUG("[%s].%s=%s: failed to parse",
                    ctx->section, ctx->key, &ctx->value[0]);
            }
            for (int c = 0; c < input[i].count; c++) {
                if (c32cmp((*ptr)[c], input[i].value[c]) != 0) {
                    BUG("[%s].%s=%s: set value[%d] (%ls) not the expected one (%ls)",
                        ctx->section, ctx->key, &ctx->value[c], c,
                        (const wchar_t *)(*ptr)[c],
                        (const wchar_t *)input[i].value[c]);
                }
            }
        }
    }
}

static void
test_boolean(struct context *ctx, bool (*parse_fun)(struct context *ctx),
             const char *key, const bool *ptr)
{
    ctx->key = key;

    static const struct {
        const char *option_string;
        bool value;
        bool invalid;
    } input[] = {
        {"1", true}, {"0", false},
        {"on", true}, {"off", false},
        {"true", true}, {"false", false},
        {"unittest-invalid-boolean-value", false, true},
    };

    for (size_t i = 0; i < ALEN(input); i++) {
        ctx->value = input[i].option_string;

        if (input[i].invalid) {
            if (parse_fun(ctx)) {
                BUG("[%s].%s=%s: did not fail to parse as expected",
                    ctx->section, ctx->key, ctx->value);
            }
        } else {
            if (!parse_fun(ctx)) {
                BUG("[%s].%s=%s: failed to parse",
                    ctx->section, ctx->key, ctx->value);
            }
            if (*ptr != input[i].value) {
                BUG("[%s].%s=%s: set value (%s) not the expected one (%s)",
                    ctx->section, ctx->key, ctx->value,
                    *ptr ? "true" : "false",
                    input[i].value ? "true" : "false");
            }
        }
    }
}

static void
test_uint16(struct context *ctx, bool (*parse_fun)(struct context *ctx),
            const char *key, const uint16_t *ptr)
{
    ctx->key = key;

    static const struct {
        const char *option_string;
        uint16_t value;
        bool invalid;
    } input[] = {
        {"0", 0}, {"65535", 65535}, {"65536", 0, true},
        {"abc", 0, true}, {"true", 0, true},
    };

    for (size_t i = 0; i < ALEN(input); i++) {
        ctx->value = input[i].option_string;

        if (input[i].invalid) {
            if (parse_fun(ctx)) {
                BUG("[%s].%s=%s: did not fail to parse as expected",
                    ctx->section, ctx->key, ctx->value);
            }
        } else {
            if (!parse_fun(ctx)) {
                BUG("[%s].%s=%s: failed to parse",
                    ctx->section, ctx->key, ctx->value);
            }
            if (*ptr != input[i].value) {
                BUG("[%s].%s=%s: set value (%hu) not the expected one (%hu)",
                    ctx->section, ctx->key, ctx->value,
                    *ptr, input[i].value);
            }
        }
    }
}

static void
test_uint32(struct context *ctx, bool (*parse_fun)(struct context *ctx),
            const char *key, const uint32_t *ptr)
{
    ctx->key = key;

    static const struct {
        const char *option_string;
        uint32_t value;
        bool invalid;
    } input[] = {
        {"0", 0}, {"65536", 65536}, {"4294967295", 4294967295},
         {"4294967296", 0, true}, {"abc", 0, true}, {"true", 0, true},
    };

    for (size_t i = 0; i < ALEN(input); i++) {
        ctx->value = input[i].option_string;

        if (input[i].invalid) {
            if (parse_fun(ctx)) {
                BUG("[%s].%s=%s: did not fail to parse as expected",
                    ctx->section, ctx->key, ctx->value);
            }
        } else {
            if (!parse_fun(ctx)) {
                BUG("[%s].%s=%s: failed to parse",
                    ctx->section, ctx->key, ctx->value);
            }
            if (*ptr != input[i].value) {
                BUG("[%s].%s=%s: set value (%u) not the expected one (%u)",
                    ctx->section, ctx->key, ctx->value,
                    *ptr, input[i].value);
            }
        }
    }
}

static void
test_float(struct context *ctx, bool (*parse_fun)(struct context *ctx),
            const char *key, const float *ptr)
{
    ctx->key = key;

    static const struct {
        const char *option_string;
        float value;
        bool invalid;
    } input[] = {
        {"0", 0}, {"0.1", 0.1}, {"1e10", 1e10}, {"-10.7", -10.7},
        {"abc", 0, true}, {"true", 0, true},
    };

    for (size_t i = 0; i < ALEN(input); i++) {
        ctx->value = input[i].option_string;

        if (input[i].invalid) {
            if (parse_fun(ctx)) {
                BUG("[%s].%s=%s: did not fail to parse as expected",
                    ctx->section, ctx->key, ctx->value);
            }
        } else {
            if (!parse_fun(ctx)) {
                BUG("[%s].%s=%s: failed to parse",
                    ctx->section, ctx->key, ctx->value);
            }
            if (*ptr != input[i].value) {
                BUG("[%s].%s=%s: set value (%f) not the expected one (%f)",
                    ctx->section, ctx->key, ctx->value,
                    *ptr, input[i].value);
            }
        }
    }
}

static void
test_pt_or_px(struct context *ctx, bool (*parse_fun)(struct context *ctx),
              const char *key, const struct pt_or_px *ptr)
{
    ctx->key = key;

    static const struct {
        const char *option_string;
        struct pt_or_px value;
        bool invalid;
    } input[] = {
        {"12", {.pt = 12}}, {"12px", {.px = 12}},
        {"unittest-invalid-pt-or-px-value", {0}, true},
    };

    for (size_t i = 0; i < ALEN(input); i++) {
        ctx->value = input[i].option_string;

        if (input[i].invalid) {
            if (parse_fun(ctx)) {
                BUG("[%s].%s=%s: did not fail to parse as expected",
                    ctx->section, ctx->key, ctx->value);
            }
        } else {
            if (!parse_fun(ctx)) {
                BUG("[%s].%s=%s: failed to parse",
                    ctx->section, ctx->key, ctx->value);
            }
            if (memcmp(ptr, &input[i].value, sizeof(*ptr)) != 0) {
                BUG("[%s].%s=%s: "
                    "set value (pt=%f, px=%d) not the expected one (pt=%f, px=%d)",
                    ctx->section, ctx->key, ctx->value,
                    ptr->pt, ptr->px,
                    input[i].value.pt, input[i].value.px);
            }
        }
    }
}

static void
test_spawn_template(struct context *ctx, bool (*parse_fun)(struct context *ctx),
                    const char *key, const struct config_spawn_template *ptr)
{
    static const char *const args[] = {
        "command", "arg1", "arg2", "arg3 has spaces"};

    ctx->key = key;
    ctx->value = "command arg1 arg2 \"arg3 has spaces\"";

    if (!parse_fun(ctx))
        BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value);

    if (ptr->argv.args == NULL)
        BUG("[%s].%s=%s: argv is NULL", ctx->section, ctx->key, ctx->value);

    for (size_t i = 0; i < ALEN(args); i++) {
        if (ptr->argv.args[i] == NULL || !streq(ptr->argv.args[i], args[i])) {
            BUG("[%s].%s=%s: set value not the expected one: "
                "mismatch of arg #%zu: expected=\"%s\", got=\"%s\"",
                ctx->section, ctx->key, ctx->value, i,
                args[i], ptr->argv.args[i]);
        }
    }

    if (ptr->argv.args[ALEN(args)] != NULL) {
        BUG("[%s].%s=%s: set value not the expected one: "
            "expected NULL terminator at arg #%zu, got=\"%s\"",
            ctx->section, ctx->key, ctx->value,
            ALEN(args), ptr->argv.args[ALEN(args)]);
    }

    /* Trigger parse failure */
    ctx->value = "command with \"unterminated quote";
    if (parse_fun(ctx)) {
        BUG("[%s].%s=%s: did not fail to parse as expected",
            ctx->section, ctx->key, ctx->value);
    }
}

static void
test_enum(struct context *ctx, bool (*parse_fun)(struct context *ctx),
          const char *key, size_t count, const char *enum_strings[static count],
          int enum_values[static count], int *ptr)
{
    ctx->key = key;

    for (size_t i = 0; i < count; i++) {
        ctx->value = enum_strings[i];
        if (!parse_fun(ctx)) {
            BUG("[%s].%s=%s: failed to parse",
                ctx->section, ctx->key, ctx->value);
        }

        if (*ptr != enum_values[i]) {
            BUG("[%s].%s=%s: set value not the expected one: expected %d, got %d",
                ctx->section, ctx->key, ctx->value, enum_values[i], *ptr);
        }
    }

    ctx->value = "invalid-enum-value";
    if (parse_fun(ctx)) {
        BUG("[%s].%s=%s: did not fail to parse as expected",
            ctx->section, ctx->key, ctx->value);
    }
}


static void
test_color(struct context *ctx, bool (*parse_fun)(struct context *ctx),
           const char *key, bool alpha_allowed, uint32_t *ptr)
{
    ctx->key = key;

    const struct {
        const char *option_string;
        uint32_t color;
        bool invalid;
    } input[] = {
        {"000000", 0},
        {"999999", 0x999999},
        {"ffffff", 0xffffff},
        {"ffffffff", 0xffffffff, !alpha_allowed},
        {"aabbccdd", 0xaabbccdd, !alpha_allowed},
        {"00", 0, true},
        {"0000", 0, true},
        {"00000", 0, true},
        {"000000000", 0, true},
        {"unittest-invalid-color", 0, true},
    };

    for (size_t i = 0; i < ALEN(input); i++) {
        ctx->value = input[i].option_string;
        if (input[i].invalid) {
            if (parse_fun(ctx)) {
                BUG("[%s].%s=%s: did not fail to parse as expected",
                    ctx->section, ctx->key, ctx->value);
            }
        } else {
            if (!parse_fun(ctx)) {
                BUG("[%s].%s=%s: failed to parse",
                    ctx->section, ctx->key, ctx->value);
            }
        }
    }
}

static void
test_two_colors(struct context *ctx, bool (*parse_fun)(struct context *ctx),
                const char *key, bool alpha_allowed,
                uint32_t *ptr1, uint32_t *ptr2)
{
    ctx->key = key;

    const struct {
        const char *option_string;
        uint32_t color1;
        uint32_t color2;
        bool invalid;
    } input[] = {
        {"000000 000000", 0, 0},

        /* No alpha */
        {"999999 888888", 0x999999, 0x888888},
        {"ffffff aaaaaa", 0xffffff, 0xaaaaaa},

        /* Both colors have alpha component */
        {"ffffffff 00000000", 0xffffffff, 0x00000000, !alpha_allowed},
        {"aabbccdd, ee112233", 0xaabbccdd, 0xee112233, !alpha_allowed},

        /* Only one color has alpha component */
        {"ffffffff 112233", 0xffffffff, 0x112233, !alpha_allowed},
        {"ffffff ff112233", 0x00ffffff, 0xff112233, !alpha_allowed},

        {"unittest-invalid-color", 0, 0, true},
    };

    for (size_t i = 0; i < ALEN(input); i++) {
        ctx->value = input[i].option_string;
        if (input[i].invalid) {
            if (parse_fun(ctx)) {
                BUG("[%s].%s=%s: did not fail to parse as expected",
                    ctx->section, ctx->key, ctx->value);
            }
        } else {
            if (!parse_fun(ctx)) {
                BUG("[%s].%s=%s: failed to parse",
                    ctx->section, ctx->key, ctx->value);
            }
        }
    }
}

static void
test_section_main(void)
{
    struct config conf = {0};
    struct context ctx = {.conf = &conf, .section = "main", .path = "unittest"};

    test_invalid_key(&ctx, &parse_section_main, "invalid-key");

    test_string(&ctx, &parse_section_main, "shell", &conf.shell);
    test_string(&ctx, &parse_section_main, "term", &conf.term);
    test_string(&ctx, &parse_section_main, "app-id", &conf.app_id);
    test_string(&ctx, &parse_section_main, "utmp-helper", &conf.utmp_helper_path);

    test_c32string(&ctx, &parse_section_main, "word-delimiters", &conf.word_delimiters);

    test_boolean(&ctx, &parse_section_main, "login-shell", &conf.login_shell);
    test_boolean(&ctx, &parse_section_main, "box-drawings-uses-font-glyphs", &conf.box_drawings_uses_font_glyphs);
    test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title);
    test_boolean(&ctx, &parse_section_main, "notify-focus-inhibit", &conf.notify_focus_inhibit);
    test_boolean(&ctx, &parse_section_main, "dpi-aware", &conf.dpi_aware);

    test_pt_or_px(&ctx, &parse_section_main, "font-size-adjustment", &conf.font_size_adjustment.pt_or_px);  /* TODO: test ‘N%’ values too */
    test_pt_or_px(&ctx, &parse_section_main, "line-height", &conf.line_height);
    test_pt_or_px(&ctx, &parse_section_main, "letter-spacing", &conf.letter_spacing);
    test_pt_or_px(&ctx, &parse_section_main, "horizontal-letter-offset", &conf.horizontal_letter_offset);
    test_pt_or_px(&ctx, &parse_section_main, "vertical-letter-offset", &conf.vertical_letter_offset);
    test_pt_or_px(&ctx, &parse_section_main, "underline-thickness", &conf.underline_thickness);

    test_uint16(&ctx, &parse_section_main, "resize-delay-ms", &conf.resize_delay_ms);
    test_uint16(&ctx, &parse_section_main, "workers", &conf.render_worker_count);

    test_spawn_template(&ctx, &parse_section_main, "notify", &conf.notify);

    test_enum(&ctx, &parse_section_main, "selection-target",
              4,
              (const char *[]){"none", "primary", "clipboard", "both"},
              (int []){SELECTION_TARGET_NONE,
                       SELECTION_TARGET_PRIMARY,
                       SELECTION_TARGET_CLIPBOARD,
                       SELECTION_TARGET_BOTH},
              (int *)&conf.selection_target);

    test_enum(
        &ctx, &parse_section_main, "initial-window-mode",
        3,
        (const char *[]){"windowed", "maximized", "fullscreen"},
        (int []){STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN},
        (int *)&conf.startup_mode);

    /* TODO: font (custom) */
    /* TODO: include (custom) */
    /* TODO: bold-text-in-bright (enum/boolean) */
    /* TODO: pad (geometry + optional string)*/
    /* TODO: initial-window-size-pixels (geometry) */
    /* TODO: initial-window-size-chars (geometry) */

    config_free(&conf);
}

static void
test_section_bell(void)
{
    struct config conf = {0};
    struct context ctx = {.conf = &conf, .section = "bell", .path = "unittest"};

    test_invalid_key(&ctx, &parse_section_bell, "invalid-key");

    test_boolean(&ctx, &parse_section_bell, "urgent", &conf.bell.urgent);
    test_boolean(&ctx, &parse_section_bell, "notify", &conf.bell.notify);
    test_boolean(&ctx, &parse_section_bell, "command-focused",
                 &conf.bell.command_focused);
    test_spawn_template(&ctx, &parse_section_bell, "command",
                        &conf.bell.command);

    config_free(&conf);
}

static void
test_section_scrollback(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "scrollback", .path = "unittest"};

    test_invalid_key(&ctx, &parse_section_scrollback, "invalid-key");

    test_uint32(&ctx, &parse_section_scrollback, "lines",
                &conf.scrollback.lines);
    test_float(&ctx, parse_section_scrollback, "multiplier", &conf.scrollback.multiplier);

    test_enum(
        &ctx, &parse_section_scrollback, "indicator-position",
        3,
        (const char *[]){"none", "fixed", "relative"},
        (int []){SCROLLBACK_INDICATOR_POSITION_NONE,
            SCROLLBACK_INDICATOR_POSITION_FIXED,
            SCROLLBACK_INDICATOR_POSITION_RELATIVE},
        (int *)&conf.scrollback.indicator.position);

    /* TODO: indicator-format (enum, sort-of) */

    config_free(&conf);
}

static void
test_section_url(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "url", .path = "unittest"};

    test_invalid_key(&ctx, &parse_section_url, "invalid-key");

    test_spawn_template(&ctx, &parse_section_url, "launch", &conf.url.launch);
    test_enum(&ctx, &parse_section_url, "osc8-underline",
              2,
              (const char *[]){"url-mode", "always"},
              (int []){OSC8_UNDERLINE_URL_MODE, OSC8_UNDERLINE_ALWAYS},
              (int *)&conf.url.osc8_underline);
    test_c32string(&ctx, &parse_section_url, "label-letters", &conf.url.label_letters);
    test_protocols(&ctx, &parse_section_url, "protocols", &conf.url.protocols);

    /* TODO: uri-characters (wchar string, but sorted) */

    config_free(&conf);
}

static void
test_section_cursor(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "cursor", .path = "unittest"};

    test_invalid_key(&ctx, &parse_section_cursor, "invalid-key");

    test_enum(
        &ctx, &parse_section_cursor, "style",
        3,
        (const char *[]){"block", "beam", "underline"},
        (int []){CURSOR_BLOCK, CURSOR_BEAM, CURSOR_UNDERLINE},
        (int *)&conf.cursor.style);
    test_enum(
        &ctx, &parse_section_cursor, "unfocused-style",
        3,
        (const char *[]){"unchanged", "hollow", "none"},
        (int []){CURSOR_UNFOCUSED_UNCHANGED, CURSOR_UNFOCUSED_HOLLOW, CURSOR_UNFOCUSED_NONE},
        (int *)&conf.cursor.unfocused_style);
    test_boolean(&ctx, &parse_section_cursor, "blink", &conf.cursor.blink);
    test_pt_or_px(&ctx, &parse_section_cursor, "beam-thickness",
                  &conf.cursor.beam_thickness);
    test_pt_or_px(&ctx, &parse_section_cursor, "underline-thickness",
                  &conf.cursor.underline_thickness);

    /* TODO: color (two RRGGBB values) */

    config_free(&conf);
}

static void
test_section_mouse(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "mouse", .path = "unittest"};

    test_invalid_key(&ctx, &parse_section_mouse, "invalid-key");

    test_boolean(&ctx, &parse_section_mouse, "hide-when-typing",
                 &conf.mouse.hide_when_typing);
    test_boolean(&ctx, &parse_section_mouse, "alternate-scroll-mode",
                 &conf.mouse.alternate_scroll_mode);

    config_free(&conf);
}

static void
test_section_touch(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "touch", .path = "unittest"};

    test_invalid_key(&ctx, &parse_section_touch, "invalid-key");

    test_uint32(&ctx, &parse_section_touch, "long-press-delay",
                &conf.touch.long_press_delay);

    config_free(&conf);
}

static void
test_section_colors(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "colors", .path = "unittest"};

    test_invalid_key(&ctx, &parse_section_colors, "invalid-key");

    test_color(&ctx, &parse_section_colors, "foreground", false, &conf.colors.fg);
    test_color(&ctx, &parse_section_colors, "background", false, &conf.colors.bg);
    test_color(&ctx, &parse_section_colors, "regular0", false, &conf.colors.table[0]);
    test_color(&ctx, &parse_section_colors, "regular1", false, &conf.colors.table[1]);
    test_color(&ctx, &parse_section_colors, "regular2", false, &conf.colors.table[2]);
    test_color(&ctx, &parse_section_colors, "regular3", false, &conf.colors.table[3]);
    test_color(&ctx, &parse_section_colors, "regular4", false, &conf.colors.table[4]);
    test_color(&ctx, &parse_section_colors, "regular5", false, &conf.colors.table[5]);
    test_color(&ctx, &parse_section_colors, "regular6", false, &conf.colors.table[6]);
    test_color(&ctx, &parse_section_colors, "regular7", false, &conf.colors.table[7]);
    test_color(&ctx, &parse_section_colors, "bright0", false, &conf.colors.table[8]);
    test_color(&ctx, &parse_section_colors, "bright1", false, &conf.colors.table[9]);
    test_color(&ctx, &parse_section_colors, "bright2", false, &conf.colors.table[10]);
    test_color(&ctx, &parse_section_colors, "bright3", false, &conf.colors.table[11]);
    test_color(&ctx, &parse_section_colors, "bright4", false, &conf.colors.table[12]);
    test_color(&ctx, &parse_section_colors, "bright5", false, &conf.colors.table[13]);
    test_color(&ctx, &parse_section_colors, "bright6", false, &conf.colors.table[14]);
    test_color(&ctx, &parse_section_colors, "bright7", false, &conf.colors.table[15]);
    test_color(&ctx, &parse_section_colors, "dim0", false, &conf.colors.dim[0]);
    test_color(&ctx, &parse_section_colors, "dim1", false, &conf.colors.dim[1]);
    test_color(&ctx, &parse_section_colors, "dim2", false, &conf.colors.dim[2]);
    test_color(&ctx, &parse_section_colors, "dim3", false, &conf.colors.dim[3]);
    test_color(&ctx, &parse_section_colors, "dim4", false, &conf.colors.dim[4]);
    test_color(&ctx, &parse_section_colors, "dim5", false, &conf.colors.dim[5]);
    test_color(&ctx, &parse_section_colors, "dim6", false, &conf.colors.dim[6]);
    test_color(&ctx, &parse_section_colors, "dim7", false, &conf.colors.dim[7]);
    test_color(&ctx, &parse_section_colors, "selection-foreground", false, &conf.colors.selection_fg);
    test_color(&ctx, &parse_section_colors, "selection-background", false, &conf.colors.selection_bg);
    test_color(&ctx, &parse_section_colors, "urls", false, &conf.colors.url);
    test_two_colors(&ctx, &parse_section_colors, "jump-labels", false,
                    &conf.colors.jump_label.fg,
                    &conf.colors.jump_label.bg);
    test_two_colors(&ctx, &parse_section_colors, "scrollback-indicator", false,
                    &conf.colors.scrollback_indicator.fg,
                    &conf.colors.scrollback_indicator.bg);
    test_two_colors(&ctx, &parse_section_colors, "search-box-no-match", false,
                    &conf.colors.search_box.no_match.fg,
                    &conf.colors.search_box.no_match.bg);
    test_two_colors(&ctx, &parse_section_colors, "search-box-match", false,
                    &conf.colors.search_box.match.fg,
                    &conf.colors.search_box.match.bg);

    for (size_t i = 0; i < 255; i++) {
        char key_name[4];
        sprintf(key_name, "%zu", i);
        test_color(&ctx, &parse_section_colors, key_name, false,
                   &conf.colors.table[i]);
    }

    test_invalid_key(&ctx, &parse_section_colors, "256");

    /* TODO: alpha (float in range 0-1, converted to uint16_t) */

    config_free(&conf);
}

static void
test_section_csd(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "csd", .path = "unittest"};

    test_invalid_key(&ctx, &parse_section_csd, "invalid-key");

    test_enum(
        &ctx, &parse_section_csd, "preferred",
        3,
        (const char *[]){"none", "client", "server"},
        (int []){CONF_CSD_PREFER_NONE,
                 CONF_CSD_PREFER_CLIENT,
                 CONF_CSD_PREFER_SERVER},
        (int *)&conf.csd.preferred);
    test_uint16(&ctx, &parse_section_csd, "size", &conf.csd.title_height);
    test_color(&ctx, &parse_section_csd, "color", true, &conf.csd.color.title);
    test_uint16(&ctx, &parse_section_csd, "border-width",
                &conf.csd.border_width_visible);
    test_color(&ctx, &parse_section_csd, "border-color", true,
               &conf.csd.color.border);
    test_uint16(&ctx, &parse_section_csd, "button-width",
                &conf.csd.button_width);
    test_color(&ctx, &parse_section_csd, "button-color", true,
               &conf.csd.color.buttons);
    test_color(&ctx, &parse_section_csd, "button-minimize-color", true,
               &conf.csd.color.minimize);
    test_color(&ctx, &parse_section_csd, "button-maximize-color", true,
               &conf.csd.color.maximize);
    test_color(&ctx, &parse_section_csd, "button-close-color", true,
               &conf.csd.color.quit);
    test_boolean(&ctx, &parse_section_csd, "hide-when-maximized",
                 &conf.csd.hide_when_maximized);
    test_boolean(&ctx, &parse_section_csd, "double-click-to-maximize",
                 &conf.csd.double_click_to_maximize);

    /* TODO: verify the ‘set’ bit is actually set for colors */
    /* TODO: font */

    config_free(&conf);
}

static bool
have_modifier(const config_modifier_list_t *mods, const char *mod)
{
    tll_foreach(*mods, it) {
        if (strcmp(it->item, mod) == 0)
            return true;
    }

    return false;
}

static void
test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx),
                 int action, int max_action, const char *const *map,
                 struct config_key_binding_list *bindings,
                 enum key_binding_type type)
{
    xassert(map[action] != NULL);
    xassert(bindings->count == 0);

    const char *key = map[action];

    /* “Randomize” which modifiers to enable */
    const bool ctrl = action % 2;
    const bool alt = action % 3;
    const bool shift = action % 4;
    const bool super = action % 5;
    const bool argv = action % 6;

    static const char *const args[] = {
        "command", "arg1", "arg2", "arg3 has spaces"};

    /* Generate the modifier part of the ‘value’ */
    char modifier_string[32];
    sprintf(modifier_string, "%s%s%s%s",
            ctrl ? XKB_MOD_NAME_CTRL "+" : "",
            alt ? XKB_MOD_NAME_ALT "+" : "",
            shift ? XKB_MOD_NAME_SHIFT "+" : "",
            super ? XKB_MOD_NAME_LOGO "+" : "");

    /* Use a unique symbol for this action (key bindings) */
    const xkb_keysym_t sym = XKB_KEY_a + action;

    /* Mouse button (mouse bindings) */
    const int button_idx = action % ALEN(button_map);
    const int button = button_map[button_idx].code;
    const int click_count = action % 3 + 1;

    /* Finally, generate the ‘value’ (e.g. “Control+shift+x”) */
    char value[128] = {0};

    ctx->key = key;
    ctx->value = value;

    /* First, try setting the empty string */
    if (parse_fun(ctx)) {
        BUG("[%s].%s=<empty>: did not fail to parse as expected",
            ctx->section, ctx->key);
    }

    switch (type) {
    case KEY_BINDING: {
        char sym_name[16];
        xkb_keysym_get_name(sym, sym_name, sizeof(sym_name));

        snprintf(value, sizeof(value), "%s%s%s",
                 argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : "",
                 modifier_string, sym_name);
        break;
    }

    case MOUSE_BINDING: {
        const char *const button_name = button_map[button_idx].name;
        int chars = snprintf(
            value, sizeof(value), "%s%s%s",
            argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : "",
            modifier_string, button_name);

        xassert(click_count > 0);
        if (click_count > 1)
            snprintf(&value[chars], sizeof(value) - chars, "-%d", click_count);
        break;
    }
    }

    if (!parse_fun(ctx)) {
        BUG("[%s].%s=%s failed to parse",
            ctx->section, ctx->key, ctx->value);
    }

    const struct config_key_binding *binding =
        &bindings->arr[bindings->count - 1];

    if (argv) {
        if (binding->aux.pipe.args == NULL) {
            BUG("[%s].%s=%s: pipe argv is NULL",
                ctx->section, ctx->key, ctx->value);
        }

        for (size_t i = 0; i < ALEN(args); i++) {
            if (binding->aux.pipe.args[i] == NULL ||
                !streq(binding->aux.pipe.args[i], args[i]))
            {
                BUG("[%s].%s=%s: pipe argv not the expected one: "
                    "mismatch of arg #%zu: expected=\"%s\", got=\"%s\"",
                    ctx->section, ctx->key, ctx->value, i,
                    args[i], binding->aux.pipe.args[i]);
            }
        }

        if (binding->aux.pipe.args[ALEN(args)] != NULL) {
            BUG("[%s].%s=%s: pipe argv not the expected one: "
                "expected NULL terminator at arg #%zu, got=\"%s\"",
                ctx->section, ctx->key, ctx->value,
                ALEN(args), binding->aux.pipe.args[ALEN(args)]);
        }
    } else {
        if (binding->aux.pipe.args != NULL) {
            BUG("[%s].%s=%s: pipe argv not NULL",
                ctx->section, ctx->key, ctx->value);
        }
    }

    if (binding->action != action) {
        BUG("[%s].%s=%s: action mismatch: %d != %d",
            ctx->section, ctx->key, ctx->value, binding->action, action);
    }

    bool have_ctrl = have_modifier(&binding->modifiers, XKB_MOD_NAME_CTRL);
    bool have_alt = have_modifier(&binding->modifiers, XKB_MOD_NAME_ALT);
    bool have_shift = have_modifier(&binding->modifiers, XKB_MOD_NAME_SHIFT);
    bool have_super = have_modifier(&binding->modifiers, XKB_MOD_NAME_LOGO);

    if (have_ctrl != ctrl || have_alt != alt ||
        have_shift != shift || have_super != super)
    {
        BUG("[%s].%s=%s: modifier mismatch:\n"
            "  have:     ctrl=%d, alt=%d, shift=%d, super=%d\n"
            "  expected: ctrl=%d, alt=%d, shift=%d, super=%d",
            ctx->section, ctx->key, ctx->value,
            have_ctrl, have_alt, have_shift, have_super,
            ctrl, alt, shift, super);
    }

    switch (type) {
    case KEY_BINDING:
        if (binding->k.sym != sym) {
            BUG("[%s].%s=%s: key symbol mismatch: %d != %d",
                ctx->section, ctx->key, ctx->value, binding->k.sym, sym);
        }
        break;

    case MOUSE_BINDING:;
        if (binding->m.button != button) {
            BUG("[%s].%s=%s: mouse button mismatch: %d != %d",
                ctx->section, ctx->key, ctx->value, binding->m.button, button);
        }

        if (binding->m.count != click_count) {
            BUG("[%s].%s=%s: mouse button click count mismatch: %d != %d",
                ctx->section, ctx->key, ctx->value,
                binding->m.count, click_count);
        }
        break;
    }


    free_key_binding_list(bindings);
}

enum collision_test_mode {
    FAIL_DIFFERENT_ACTION,
    FAIL_DIFFERENT_ARGV,
    FAIL_MOUSE_OVERRIDE,
    SUCCEED_SAME_ACTION_AND_ARGV,
};

static void
_test_binding_collisions(struct context *ctx,
                         int max_action, const char *const *map,
                         enum key_binding_type type,
                         enum collision_test_mode test_mode)
{
    struct config_key_binding *bindings_array =
        xcalloc(2, sizeof(bindings_array[0]));

    struct config_key_binding_list bindings = {
        .count = 2,
        .arr = bindings_array,
    };

    /* First, verify we get a collision when trying to assign the same
     * key combo to multiple actions */
    bindings.arr[0] = (struct config_key_binding){
        .action = (test_mode == FAIL_DIFFERENT_ACTION
                   ? max_action - 1 : max_action),
        .modifiers = tll_init(),
        .path = "unittest",
    };
    tll_push_back(bindings.arr[0].modifiers, xstrdup(XKB_MOD_NAME_CTRL));

    bindings.arr[1] = (struct config_key_binding){
        .action = max_action,
        .modifiers = tll_init(),
        .path = "unittest",
    };
    tll_push_back(bindings.arr[1].modifiers, xstrdup(XKB_MOD_NAME_CTRL));

    switch (type) {
    case KEY_BINDING:
        bindings.arr[0].k.sym = XKB_KEY_a;
        bindings.arr[1].k.sym = XKB_KEY_a;
        break;

    case MOUSE_BINDING:
        bindings.arr[0].m.button = BTN_LEFT;
        bindings.arr[0].m.count = 1;
        bindings.arr[1].m.button = BTN_LEFT;
        bindings.arr[1].m.count = 1;
        break;
    }

    switch (test_mode) {
    case FAIL_DIFFERENT_ACTION:
        break;

    case FAIL_MOUSE_OVERRIDE:
        tll_free_and_free(ctx->conf->mouse.selection_override_modifiers, free);
        tll_push_back(ctx->conf->mouse.selection_override_modifiers, xstrdup(XKB_MOD_NAME_CTRL));
        break;

    case FAIL_DIFFERENT_ARGV:
    case SUCCEED_SAME_ACTION_AND_ARGV:
        bindings.arr[0].aux.type = BINDING_AUX_PIPE;
        bindings.arr[0].aux.master_copy = true;
        bindings.arr[0].aux.pipe.args = xcalloc(
            4, sizeof(bindings.arr[0].aux.pipe.args[0]));
        bindings.arr[0].aux.pipe.args[0] = xstrdup("/usr/bin/foobar");
        bindings.arr[0].aux.pipe.args[1] = xstrdup("hello");
        bindings.arr[0].aux.pipe.args[2] = xstrdup("world");

        bindings.arr[1].aux.type = BINDING_AUX_PIPE;
        bindings.arr[1].aux.master_copy = true;
        bindings.arr[1].aux.pipe.args = xcalloc(
            4, sizeof(bindings.arr[1].aux.pipe.args[0]));
        bindings.arr[1].aux.pipe.args[0] = xstrdup("/usr/bin/foobar");
        bindings.arr[1].aux.pipe.args[1] = xstrdup("hello");

        if (test_mode == SUCCEED_SAME_ACTION_AND_ARGV)
            bindings.arr[1].aux.pipe.args[2] = xstrdup("world");
        break;
    }

    bool expected_result =
        test_mode == SUCCEED_SAME_ACTION_AND_ARGV ? true : false;

    if (resolve_key_binding_collisions(
            ctx->conf, ctx->section, map, &bindings, type) != expected_result)
    {
        BUG("[%s].%s vs. %s: %s",
            ctx->section, map[max_action - 1], map[max_action],
            (expected_result == true
             ? "invalid key combo collision detected"
             : "key combo collision not detected"));
    }

    if (expected_result == false) {
        if (bindings.count != 1)
            BUG("[%s]: colliding binding not removed", ctx->section);

        if (bindings.arr[0].action !=
            (test_mode == FAIL_DIFFERENT_ACTION ? max_action - 1 : max_action))
        {
            BUG("[%s]: wrong binding removed", ctx->section);
        }
    }

    free_key_binding_list(&bindings);
}

static void
test_binding_collisions(struct context *ctx,
                         int max_action, const char *const *map,
                        enum key_binding_type type)
{
    _test_binding_collisions(ctx, max_action, map, type, FAIL_DIFFERENT_ACTION);
    _test_binding_collisions(ctx, max_action, map, type, FAIL_DIFFERENT_ARGV);
    _test_binding_collisions(ctx, max_action, map, type, SUCCEED_SAME_ACTION_AND_ARGV);

    if (type == MOUSE_BINDING) {
        _test_binding_collisions(
            ctx, max_action, map, type, FAIL_MOUSE_OVERRIDE);
    }
}

static void
test_section_key_bindings(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "key-bindings", .path = "unittest"};

    test_invalid_key(&ctx, &parse_section_key_bindings, "invalid-key");

    for (int action = 0; action < BIND_ACTION_KEY_COUNT; action++) {
        if (binding_action_map[action] == NULL)
            continue;

        test_key_binding(
            &ctx, &parse_section_key_bindings,
            action, BIND_ACTION_KEY_COUNT - 1,
            binding_action_map, &conf.bindings.key, KEY_BINDING);
    }

    config_free(&conf);
}

static void
test_section_key_bindings_collisions(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "key-bindings", .path = "unittest"};

    test_binding_collisions(
        &ctx, BIND_ACTION_KEY_COUNT - 1, binding_action_map, KEY_BINDING);

    config_free(&conf);
}

static void
test_section_search_bindings(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "search-bindings", .path = "unittest"};

    test_invalid_key(&ctx, &parse_section_search_bindings, "invalid-key");

    for (int action = 0; action < BIND_ACTION_SEARCH_COUNT; action++) {
        if (search_binding_action_map[action] == NULL)
            continue;

        test_key_binding(
            &ctx, &parse_section_search_bindings,
            action, BIND_ACTION_SEARCH_COUNT - 1,
            search_binding_action_map, &conf.bindings.search, KEY_BINDING);
    }

    config_free(&conf);
}

static void
test_section_search_bindings_collisions(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "search-bindings", .path = "unittest"};

    test_binding_collisions(
        &ctx,
        BIND_ACTION_SEARCH_COUNT - 1, search_binding_action_map, KEY_BINDING);

    config_free(&conf);
}

static void
test_section_url_bindings(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "rul-bindings", .path = "unittest"};

    test_invalid_key(&ctx, &parse_section_url_bindings, "invalid-key");

    for (int action = 0; action < BIND_ACTION_URL_COUNT; action++) {
        if (url_binding_action_map[action] == NULL)
            continue;

        test_key_binding(
            &ctx, &parse_section_url_bindings,
            action, BIND_ACTION_URL_COUNT - 1,
            url_binding_action_map, &conf.bindings.url, KEY_BINDING);
    }

    config_free(&conf);
}

static void
test_section_url_bindings_collisions(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "url-bindings", .path = "unittest"};

    test_binding_collisions(
        &ctx,
        BIND_ACTION_URL_COUNT - 1, url_binding_action_map, KEY_BINDING);

    config_free(&conf);
}

static void
test_section_mouse_bindings(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "mouse-bindings", .path = "unittest"};

    test_invalid_key(&ctx, &parse_section_mouse_bindings, "invalid-key");

    for (int action = 0; action < BIND_ACTION_COUNT; action++) {
        if (binding_action_map[action] == NULL)
            continue;

        test_key_binding(
            &ctx, &parse_section_mouse_bindings,
            action, BIND_ACTION_COUNT - 1,
            binding_action_map, &conf.bindings.mouse, MOUSE_BINDING);
    }

    config_free(&conf);
}

static void
test_section_mouse_bindings_collisions(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "mouse-bindings", .path = "unittest"};

    test_binding_collisions(
        &ctx,
        BIND_ACTION_COUNT - 1, binding_action_map, MOUSE_BINDING);

    config_free(&conf);
}

static void
test_section_text_bindings(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "text-bindings", .path = "unittest"};

    ctx.key = "abcd";
    ctx.value = XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT "+x";
    xassert(parse_section_text_bindings(&ctx));

    ctx.key = "\\x07";
    xassert(parse_section_text_bindings(&ctx));

    ctx.key = "\\x1g";
    xassert(!parse_section_text_bindings(&ctx));

    ctx.key = "\\x1";
    xassert(!parse_section_text_bindings(&ctx));

    ctx.key = "\\x";
    xassert(!parse_section_text_bindings(&ctx));

    ctx.key = "\\";
    xassert(!parse_section_text_bindings(&ctx));

    ctx.key = "\\y";
    xassert(!parse_section_text_bindings(&ctx));

#if 0
    /* Invalid modifier and key names are detected later, when a
     * layout is applied */
    ctx.key = "abcd";
    ctx.value = "InvalidMod+y";
    xassert(!parse_section_text_bindings(&ctx));
#endif
    config_free(&conf);
}

static void
test_section_environment(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "environment", .path = "unittest"};

    /* A single variable */
    ctx.key = "FOO";
    ctx.value = "bar";
    xassert(parse_section_environment(&ctx));
    xassert(tll_length(conf.env_vars) == 1);
    xassert(streq(tll_front(conf.env_vars).name, "FOO"));
    xassert(streq(tll_front(conf.env_vars).value, "bar"));

    /* Add a second variable */
    ctx.key = "BAR";
    ctx.value = "123";
    xassert(parse_section_environment(&ctx));
    xassert(tll_length(conf.env_vars) == 2);
    xassert(streq(tll_back(conf.env_vars).name, "BAR"));
    xassert(streq(tll_back(conf.env_vars).value, "123"));

    /* Replace the *value* of the first variable */
    ctx.key = "FOO";
    ctx.value = "456";
    xassert(parse_section_environment(&ctx));
    xassert(tll_length(conf.env_vars) == 2);
    xassert(streq(tll_front(conf.env_vars).name, "FOO"));
    xassert(streq(tll_front(conf.env_vars).value, "456"));
    xassert(streq(tll_back(conf.env_vars).name, "BAR"));
    xassert(streq(tll_back(conf.env_vars).value, "123"));

    config_free(&conf);
}

static void
test_section_tweak(void)
{
    struct config conf = {0};
    struct context ctx = {
        .conf = &conf, .section = "tweak", .path = "unittest"};

    test_invalid_key(&ctx, &parse_section_tweak, "invalid-key");

    test_enum(
        &ctx, &parse_section_tweak, "scaling-filter",
        5,
        (const char *[]){"none", "nearest", "bilinear", "cubic", "lanczos3"},
        (int []){FCFT_SCALING_FILTER_NONE,
                 FCFT_SCALING_FILTER_NEAREST,
                 FCFT_SCALING_FILTER_BILINEAR,
                 FCFT_SCALING_FILTER_CUBIC,
                 FCFT_SCALING_FILTER_LANCZOS3},
        (int *)&conf.tweak.fcft_filter);

    test_boolean(&ctx, &parse_section_tweak, "overflowing-glyphs",
                 &conf.tweak.overflowing_glyphs);

    test_enum(
        &ctx, &parse_section_tweak, "render-timer",
        4,
        (const char *[]){"none", "osd", "log", "both"},
        (int []){RENDER_TIMER_NONE,
                 RENDER_TIMER_OSD,
                 RENDER_TIMER_LOG,
                 RENDER_TIMER_BOTH},
        (int *)&conf.tweak.render_timer);

    test_float(&ctx, &parse_section_tweak, "box-drawing-base-thickness",
                &conf.tweak.box_drawing_base_thickness);
    test_boolean(&ctx, &parse_section_tweak, "box-drawing-solid-shades",
        &conf.tweak.box_drawing_solid_shades);

#if 0  /* Must be less than 16ms */
    test_uint32(&ctx, &parse_section_tweak, "delayed-render-lower",
                &conf.tweak.delayed_render_lower_ns);
    test_uint32(&ctx, &parse_section_tweak, "delayed-render-upper",
                &conf.tweak.delayed_render_upper_ns);
#endif
    test_boolean(&ctx, &parse_section_tweak, "damage-whole-window",
                 &conf.tweak.damage_whole_window);

#if defined(FOOT_GRAPHEME_CLUSTERING)
    test_boolean(&ctx, &parse_section_tweak, "grapheme-shaping",
                 &conf.tweak.grapheme_shaping);
#else
    /* TODO: the setting still exists, but is always forced to ‘false’. */
#endif

    test_enum(
        &ctx, &parse_section_tweak, "grapheme-width-method",
        3,
        (const char *[]){"wcswidth", "double-width", "max"},
        (int []){GRAPHEME_WIDTH_WCSWIDTH,
                 GRAPHEME_WIDTH_DOUBLE,
                 GRAPHEME_WIDTH_MAX},
        (int *)&conf.tweak.grapheme_width_method);

    test_boolean(&ctx, &parse_section_tweak, "font-monospace-warn",
                 &conf.tweak.font_monospace_warn);

    test_float(&ctx, &parse_section_tweak, "bold-text-in-bright-amount",
               &conf.bold_in_bright.amount);

#if 0 /* Must be equal to, or less than INT32_MAX */
    test_uint32(&ctx, &parse_section_tweak, "max-shm-pool-size-mb",
                &conf.tweak.max_shm_pool_size);
#endif

    config_free(&conf);
}

int
main(int argc, const char *const *argv)
{
    FcInit();
    log_init(LOG_COLORIZE_AUTO, false, 0, LOG_CLASS_ERROR);
    test_section_main();
    test_section_bell();
    test_section_scrollback();
    test_section_url();
    test_section_cursor();
    test_section_mouse();
    test_section_touch();
    test_section_colors();
    test_section_csd();
    test_section_key_bindings();
    test_section_key_bindings_collisions();
    test_section_search_bindings();
    test_section_search_bindings_collisions();
    test_section_url_bindings();
    test_section_url_bindings_collisions();
    test_section_mouse_bindings();
    test_section_mouse_bindings_collisions();
    test_section_text_bindings();
    test_section_environment();
    test_section_tweak();
    log_deinit();
    FcFini();
    return 0;
}
