/*
 * Copyright © 2012 Ran Benita <ran234@gmail.com>
 * SPDX-License-Identifier: MIT
 */

#include "config.h"

#include <locale.h>

#include "test.h"
#include "context.h"
#include "messages-codes.h"

#ifdef __GNUC__
#pragma GCC diagnostic ignored "-Wmissing-format-attribute"
#endif

static const char *
log_level_to_string(enum xkb_log_level level)
{
    switch (level) {
    case XKB_LOG_LEVEL_CRITICAL:
        return "critical";
    case XKB_LOG_LEVEL_ERROR:
        return "error";
    case XKB_LOG_LEVEL_WARNING:
        return "warning";
    case XKB_LOG_LEVEL_INFO:
        return "info";
    case XKB_LOG_LEVEL_DEBUG:
        return "debug";
    }

    return "unknown";
}

ATTR_PRINTF(3, 0) static void
log_fn(struct xkb_context *ctx, enum xkb_log_level level,
       const char *fmt, va_list args)
{
    char *s;
    int size;
    darray_char *ls = xkb_context_get_user_data(ctx);
    assert(ls);

    size = vasprintf(&s, fmt, args);
    assert(size != -1);

    darray_append_string(*ls, log_level_to_string(level));
    darray_append_lit(*ls, ": ");
    darray_append_string(*ls, s);
    free(s);
}

static void
test_basic(void)
{
    int ret;
    ret = setenv("XKB_LOG_LEVEL", "warn", 1);
    assert(ret == 0);
    ret = setenv("XKB_LOG_VERBOSITY", "5", 1);
    assert(ret == 0);

    struct xkb_context *ctx = test_get_context(CONTEXT_NO_FLAG);
    assert(ctx);

    darray_char log_string;
    darray_init(log_string);
    xkb_context_set_user_data(ctx, &log_string);
    xkb_context_set_log_fn(ctx, log_fn);

    log_warn(ctx, XKB_LOG_MESSAGE_NO_ID, "first warning: %d\n", 87);
    log_info(ctx, XKB_LOG_MESSAGE_NO_ID, "first info\n");
    log_dbg(ctx, XKB_LOG_MESSAGE_NO_ID, "first debug: %s\n", "hello");
    log_err(ctx, XKB_LOG_MESSAGE_NO_ID, "first error: %lu\n", 115415UL);
    log_vrb(ctx, 5, XKB_LOG_MESSAGE_NO_ID, "first verbose 5\n");

    xkb_context_set_log_level(ctx, XKB_LOG_LEVEL_DEBUG);
    log_warn(ctx, XKB_LOG_MESSAGE_NO_ID, "second warning: %d\n", 87);
    log_dbg(ctx, XKB_LOG_MESSAGE_NO_ID, "second debug: %s %s\n", "hello", "world");
    log_info(ctx, XKB_LOG_MESSAGE_NO_ID, "second info\n");
    log_err(ctx, XKB_ERROR_MALFORMED_NUMBER_LITERAL, "second error: %lu\n", 115415UL);
    log_vrb(ctx, 6, XKB_LOG_MESSAGE_NO_ID, "second verbose 6\n");

    xkb_context_set_log_verbosity(ctx, XKB_LOG_VERBOSITY_MINIMAL);
    xkb_context_set_log_level(ctx, XKB_LOG_LEVEL_CRITICAL);
    log_warn(ctx, XKB_LOG_MESSAGE_NO_ID, "third warning: %d\n", 87);
    log_dbg(ctx, XKB_LOG_MESSAGE_NO_ID, "third debug: %s %s\n", "hello", "world");
    log_info(ctx, XKB_LOG_MESSAGE_NO_ID, "third info\n");
    log_err(ctx, XKB_LOG_MESSAGE_NO_ID, "third error: %lu\n", 115415UL);
    log_vrb(ctx, 0, XKB_LOG_MESSAGE_NO_ID, "third verbose 0\n");

    printf("%s", darray_items(log_string));

    assert(streq(darray_items(log_string),
                 "warning: first warning: 87\n"
                 "error: first error: 115415\n"
                 "warning: first verbose 5\n"
                 "warning: second warning: 87\n"
                 "debug: second debug: hello world\n"
                 "info: second info\n"
                 "error: [XKB-034] second error: 115415\n"));

    xkb_context_unref(ctx);
    darray_free(log_string);
}

struct test_data {
    const char * const input;
    const char * const log;
    bool error;
};

static void
test_keymaps(void)
{
    struct xkb_context *ctx = test_get_context(CONTEXT_NO_FLAG);
    assert(ctx);

    darray_char log_string;
    darray_init(log_string);
    xkb_context_set_user_data(ctx, &log_string);
    xkb_context_set_log_fn(ctx, log_fn);

    xkb_context_set_log_level(ctx, XKB_LOG_LEVEL_WARNING);
    xkb_context_set_log_verbosity(ctx, XKB_LOG_VERBOSITY_COMPREHENSIVE);

    const struct test_data keymaps[] = {
        {
            .input = "",
            .log = "error: [XKB-822] Failed to parse input xkb string\n",
            .error = true
        },
        {
            .input = " ",
            .log = "error: [XKB-822] Failed to parse input xkb string\n",
            .error = true
        },
        {
            .input = "\n",
            .log = "error: [XKB-822] Failed to parse input xkb string\n",
            .error = true
        },
        {
            .input = "xkb_keymap {\n",
            .log =
                "error: [XKB-769] (input string):1:12: syntax error, unexpected end of file\n"
                "error: [XKB-822] Failed to parse input xkb string\n",
            .error = true
        },
        {
            .input =
                "xkb_keymap \"\\j\"\n"
                " { xkb_symbols = {};\n"
                "};",
            .log =
                "warning: [XKB-645] (input string):1:12: unknown escape sequence \"\\j\" in string literal\n"
                "error: [XKB-769] (input string):2:16: syntax error, unexpected =, expecting {\n"
                "error: [XKB-822] Failed to parse input xkb string\n",
            .error = true
        },
        {
            .input =
                "xkb_keymap {\n"
                " xkb_keycodes { <> = 1; };\n"
                " xkb_symbols { key <> { [\"\xC3\xBC\xff\"] }; };\n"
                "};",
            .log =
                "error: [XKB-542] (input string):3:26: Cannot convert string to keysyms: Invalid UTF-8 encoding starting at byte position 3 (code point position: 2).\n"
                "error: [XKB-822] Failed to parse input xkb string\n",
            .error = true
        },
        /* Extra lines & spaces intented to check the file positions */
        {
            .input =
                "xkb_keymap {\n"
                "  xkb_keycodes {\n"
                "    <> = 1;\n"
                "\n"
                "    alias <1> = <>;\n"
                "    alias <1> =\n"
                "                <>;\n"
                "  };\n"
                "  xkb_types \"\\400x\\j\" { };\n"
                "  xkb_compat {\n"
                "    interpret invalidKeysym +\n"
                "                              Any { repeat = true; };\n"
                "  };\n"
                "  xkb_symbols { key <> {[0x30, leftshoe]}; };\n"
                "};",
            .log =
                "warning: [XKB-193] (input string):9:13: invalid octal escape sequence \"\\400\" in string literal\n"
                "warning: [XKB-645] (input string):9:13: unknown escape sequence \"\\j\" in string literal\n"
                "warning: [XKB-107] (input string):11:15: unrecognized keysym \"invalidKeysym\"\n"
                "warning: [XKB-489] (input string):14:26: numeric keysym \"0x0030\" (48)\n"
                "warning: [XKB-301] (input string):14:32: deprecated keysym \"leftshoe\".\n"
                "warning: [XKB-433] No map in include statement, but \"(input string)\" contains several; Using first defined map, \"(unnamed map)\"\n"
                "warning: [XKB-523] Alias of <1> for <> declared more than once; First definition ignored\n"
                "warning: [XKB-286] The type \"TWO_LEVEL\" for key '<>' group 1 was not previously defined; Using the default type\n"
                "warning: [XKB-516] Type \"ONE_LEVEL\" has 1 levels, but <> has 2 levels; Ignoring extra symbols\n",
            .error = false
        },
        /* Invalid action fields */
        {
            .input =
                "default xkb_keymap {\n"
                "  xkb_compat {\n"
                "    NoAction.data = 1;\n"
                "    interpret VoidSymbol { action= VoidAction(data=<garbage>); };\n"
                "  };\n"
                "};",
            .log =
                "error: [XKB-563] The \"NoAction\" action takes no argument, but got \"data\" field; Action definition ignored\n"
                "error: [XKB-563] The \"VoidAction\" action takes no argument, but got \"data\" field; Action definition ignored\n"
                "error: Failed to compile xkb_compatibility\n"
                "error: [XKB-822] Failed to compile keymap\n",
            .error = true
        },
        /* Invalid action fields */
        {
            .input =
                "default xkb_keymap {\n"
                "  xkb_keycodes { <> = 1; };\n"
                "  xkb_symbols {\n"
                "    NoAction.data = 1;\n"
                "    key <> { [TerminateServer(data=<garbage>)] };\n"
                "  };\n"
                "};",
            .log =
                "error: [XKB-563] The \"NoAction\" action takes no argument, but got \"data\" field; Action definition ignored\n"
                "error: [XKB-563] The \"Terminate\" action takes no argument, but got \"data\" field; Action definition ignored\n"
                "error: [XKB-796] Illegal action definition for <>; Action for group 1/level 1 ignored\n"
                "error: Failed to compile xkb_symbols\n"
                "error: [XKB-822] Failed to compile keymap\n",
            .error = true
        },
        /* Unsupported legacy X11 actions */
        {
            .input =
                "default xkb_keymap {\n"
                "  xkb_compat {\n"
                /* This is a valid entry */
                "    interpret ISO_Lock+AnyOf(all) {\n"
                "      action= ISOLock(modifiers=modMapMods,affect=all);\n"
                "    };\n"
                /* This uses invalid field, but we do not check fields of
                 * legacy actions */
                "    interpret VoidSymbol+AnyOf(all) {\n"
                "      action= RedirectKey(data=<garbage>);\n"
                "    };\n"
                "  };\n"
                "};",
            .log =
                "warning: [XKB-362] Unsupported legacy action type \"ISOLock\".\n"
                "warning: [XKB-362] Unsupported legacy action type \"RedirectKey\".\n",
            .error = false
        },
        /* Invalid global var */
        {
            .input =
                "default xkb_keymap {\n"
                "  xkb_keycodes { foobar; };\n"
                "};",
            .log =
                "error: [XKB-639] Default defined for unknown field \"foobar\"; Ignored\n"
                "error: Failed to compile xkb_keycodes\n"
                "error: [XKB-822] Failed to compile keymap\n",
            .error = true
        },
        {
            .input =
                "default xkb_keymap {\n"
                "  xkb_types { foobar; };\n"
                "};",
            .log =
                "error: [XKB-639] Default defined for unknown field \"foobar\"; Ignored\n"
                "error: Failed to compile xkb_types\n"
                "error: [XKB-822] Failed to compile keymap\n",
            .error = true
        },
        {
            .input =
                "default xkb_keymap {\n"
                "  xkb_compat { foobar; };\n"
                "};",
            .log =
                "error: [XKB-639] Default defined for unknown field \"foobar\"; Ignored\n"
                "error: Failed to compile xkb_compatibility\n"
                "error: [XKB-822] Failed to compile keymap\n",
            .error = true
        },
        {
            .input =
                "default xkb_keymap {\n"
                "  xkb_symbols { foobar; };\n"
                "};",
            .log =
                "error: [XKB-639] Default defined for unknown field \"foobar\"; Ignored\n"
                "error: Failed to compile xkb_symbols\n"
                "error: [XKB-822] Failed to compile keymap\n",
            .error = true
        },
    };

    for (unsigned int k = 0; k < ARRAY_SIZE(keymaps); k++) {
        fprintf(stderr, "------\n*** %s: #%u ***\n", __func__, k);
        struct xkb_keymap *keymap =
            test_compile_buffer(ctx, XKB_KEYMAP_FORMAT_TEXT_V1,
                                keymaps[k].input, strlen(keymaps[k].input));
        assert_printf(keymaps[k].error ^ !!keymap,
                      "%s\n", darray_items(log_string));
        xkb_keymap_unref(keymap);
        assert_printf(streq_not_null(darray_items(log_string), keymaps[k].log),
                      "Expected:\n%s\nGot:\n%s\n",
                      keymaps[k].log, darray_items(log_string));
        darray_free(log_string);
    }

    xkb_context_unref(ctx);
}

static void
test_compose(void)
{
    struct xkb_context *ctx = test_get_context(CONTEXT_NO_FLAG);
    assert(ctx);

    darray_char log_string;
    darray_init(log_string);
    xkb_context_set_user_data(ctx, &log_string);
    xkb_context_set_log_fn(ctx, log_fn);

    xkb_context_set_log_level(ctx, XKB_LOG_LEVEL_WARNING);
    xkb_context_set_log_verbosity(ctx, XKB_LOG_VERBOSITY_VERBOSE);

    const struct test_data composes[] = {
        {
            .input = "",
            .log = NULL,
            .error = false
        },
        {
            .input = "\n",
            .log = NULL,
            .error = false
        },
        {
            .input = "\xff\n",
            .log =
                "error: [XKB-542] (input string):1:1: unexpected non-ASCII character.\n"
                "error: [XKB-542] (input string):1:1: This could be a file encoding issue. Supported file encodings are ASCII and UTF-8.\n"
                "error: (input string):1:1: failed to parse file\n",
            .error = true
        },
        {
            .input =
                "<leftshoe> : x\n"
                "include \"x\"\n",
            .log =
                "warning: [XKB-301] (input string):1:1: deprecated keysym \"leftshoe\".\n"
                "error: (input string):2:9: failed to open included Compose file \"x\": No such file or directory\n"
                "error: (input string):2:9: failed to parse file\n",
            .error = true
        },
        {
            .input =
                "<a> : \"a\"\n"
                "\n"
                "<b> : \"i\\j\\xk\n"
                "<0x30> : \"\\400\" invalidKeysym\n"
                "<0> <1> <2> <3> <4> <5> <6> <7> <8> <9> <leftshoe> : \"\"\n",
            .log =
                "warning: [XKB-645] (input string):3:7: unknown escape sequence \"\\j\" in string literal\n"
                "warning: [XKB-193] (input string):3:7: illegal hexadecimal escape sequence \"\\x\" in string literal\n"
                "error: [XKB-685] (input string):3:7: unterminated string literal\n"
                "warning: [XKB-193] (input string):4:10: illegal octal escape sequence \"\\400\" in string literal\n"
                "error: (input string):4:17: unrecognized keysym \"invalidKeysym\" on right-hand side\n"
                "warning: [XKB-301] (input string):5:41: deprecated keysym \"leftshoe\".\n"
                "warning: [XKB-685] (input string):5:41: too many keysyms (11) on left-hand side; skipping line\n",
            .error = false
        },
        {
            .input =
                ":\n"
                "<a> :\n"
                "#\n"
                "<c> : \"a\" \"b\"\n"
                "<d> : a b\n",
            .log =
                "warning: (input string):1:1: expected at least one keysym on left-hand side; skipping line\n"
                "warning: [XKB-685] (input string):2:5: right-hand side must have at least one of string or keysym; skipping line\n"
                "warning: (input string):4:11: right-hand side can have at most one string; skipping line\n"
                "error: [XKB-685] (input string):5:9: unrecognized modifier \"b\"\n",
            .error = false
        },
        {
            .input =
                "<a> : a\n"
                "<a> : a\n"
                "<b>     : b\n"
                "<b> <c> : x\n"
                "<c> <d> : y\n"
                "<c>     : c\n",
            .log =
                "warning: (input string):2:7: this compose sequence is a duplicate of another; skipping line\n"
                "warning: (input string):4:11: a sequence already exists which is a prefix of this sequence; overriding\n"
                "warning: (input string):6:11: this compose sequence is a prefix of another; overriding\n",
            .error = false
        }
    };

    for (unsigned int k = 0; k < ARRAY_SIZE(composes); k++) {
        fprintf(stderr, "------\n*** %s: #%u ***\n", __func__, k);
        struct xkb_compose_table *table = xkb_compose_table_new_from_buffer(
            ctx, composes[k].input, strlen(composes[k].input), "",
            XKB_COMPOSE_FORMAT_TEXT_V1,
            XKB_COMPOSE_COMPILE_NO_FLAGS
        );
        assert(composes[k].error ^ !!table);
        xkb_compose_table_unref(table);
        assert_printf(streq_null(darray_items(log_string), composes[k].log),
                      "Expected:\n%s\nGot:\n%s\n",
                      darray_items(log_string), composes[k].log);
        darray_free(log_string);
    }

    xkb_context_unref(ctx);
}

int
main(void)
{
    test_init();

    /* We really need to be locale-independent here */
    setlocale(LC_ALL, "C");

    test_basic();
    test_keymaps();
    test_compose();
    return EXIT_SUCCESS;
}
