/*
 * Copyright © 2025 Red Hat, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

#include "config.h"

#include <fcntl.h>
#include <inttypes.h>
#include <valgrind/valgrind.h>

#include "util-files.h"
#include "util-strings.h"
#include "util-time.h"

#include "libinput.h"
#include "litest.h"

static char *
_litest_write_plugin(const char *tmpdir, const char *filename, const char *content)
{
	static int counter = 0;
	counter += 10;

	char *path = strdup_printf("%s/%d-%s.lua", tmpdir, counter, filename);
	_autoclose_ int fd = open(path, O_WRONLY | O_CREAT, 0644);
	litest_assert_errno_success(fd);

	if (content) {
		write(fd, content, strlen(content));
		fsync(fd);
	}

	return path;
}

#define litest_write_plugin(tmpdir_, content_) \
	_litest_write_plugin(tmpdir_, __func__, content_)

START_TEST(lua_load_failure)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	const char *lua = "asfasdk1298'..asdfasdf'123@2;asd"; /* invalid lua */
	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		size_t index = 0;
		litest_assert(
			strv_find_substring(capture->errors, "Failed to load", &index));
		litest_assert_str_in(path, capture->errors[index]);
	}
}
END_TEST

enum content {
	EMPTY,
	NOTHING,
	BASIC,
	COMMENT,
	DUPLICATE_CALL,
	VERSION_NOT_A_TABLE,
	MISSING_REGISTER,
};

START_TEST(lua_load_success_but_no_register)
{
	enum content content = litest_test_param_get_i32(test_env->params, "content");

	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);

	const char *lua = NULL;
	switch (content) {
	case DUPLICATE_CALL:
		lua = "v1 = libinput:register({1})\np2 = libinput:register({2})\n";
		break;
	case VERSION_NOT_A_TABLE:
		lua = "v1 = libinput:register(1)\n";
		break;
	case MISSING_REGISTER:
		lua = "libinput:connect(\"new-evdev-device\", function(device) assert(false) end)\n";
		break;
	case BASIC:
		lua = "a = 1 + 10";
		break;
	case COMMENT:
		lua = "-- near-empty file";
		break;
	case NOTHING:
		lua = "";
		break;
	case EMPTY:
		break;
	}

	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		switch (content) {
		case VERSION_NOT_A_TABLE:
			litest_assert_strv_substring(capture->errors,
						     "unloading after error");
			break;
		case DUPLICATE_CALL:
			litest_assert_strv_substring(capture->errors,
						     "plugin already registered");
			break;
		default:
			litest_assert_strv_substring(capture->errors,
						     "plugin never registered");
			break;
		}
	}
}
END_TEST

START_TEST(lua_register_noop)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	const char *lua = "libinput:register({1})";
	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);
		litest_assert_logcapture_no_errors(capture);
	}
}
END_TEST

START_TEST(lua_unregister_is_last)
{
	const char *when = litest_test_param_get_string(test_env->params, "when");

	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	_autofree_ char *lua = strdup_printf(
		"libinput:register({1})\n"
		"libinput:connect(\"new-evdev-device\", function(device)\n  %s\n  libinput:log_error(\"abort abort\")\nend)\n"
		"%slibinput:log_error(\"must not happen\")",
		streq(when, "connect") ? "libinput:unregister()" : "",
		streq(when, "run") ? "libinput:unregister()\n" : "--");
	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);

	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		_destroy_(litest_device) *device = litest_add_device(li, LITEST_MOUSE);
		litest_drain_events(li);
		litest_assert_logcapture_no_errors(capture);
	}
}
END_TEST

START_TEST(lua_test_logging)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);

	enum libinput_log_priority priority =
		litest_test_param_get_i32(test_env->params, "priority");

	const char *lua = NULL;
	switch (priority) {
	case LIBINPUT_LOG_PRIORITY_DEBUG:
		lua = "libinput:log_debug(\"deb-ug\");";
		break;
	case LIBINPUT_LOG_PRIORITY_INFO:
		lua = "libinput:log_info(\"inf-o\");";
		break;
	case LIBINPUT_LOG_PRIORITY_ERROR:
		lua = "libinput:log_error(\"err-or\");";
		break;
	default:
		litest_assert_not_reached();
		break;
	}

	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	libinput_log_set_priority(li, priority);

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		switch (priority) {
		case LIBINPUT_LOG_PRIORITY_DEBUG:
			litest_assert(
				strv_find_substring(capture->debugs, "deb-ug", NULL));
			litest_assert(
				!strv_find_substring(capture->infos, "inf-o", NULL));
			litest_assert(
				!strv_find_substring(capture->errors, "err-or", NULL));
			break;
		case LIBINPUT_LOG_PRIORITY_INFO:
			litest_assert(
				!strv_find_substring(capture->debugs, "deb-ug", NULL));
			litest_assert(
				strv_find_substring(capture->infos, "inf-o", NULL));
			litest_assert(
				!strv_find_substring(capture->errors, "err-or", NULL));
			break;
		case LIBINPUT_LOG_PRIORITY_ERROR:
			litest_assert(
				!strv_find_substring(capture->debugs, "deb-ug", NULL));
			litest_assert(
				!strv_find_substring(capture->infos, "inf-o", NULL));
			litest_assert(
				strv_find_substring(capture->errors, "err-or", NULL));
			break;
		}
	}
}
END_TEST

START_TEST(lua_test_evdev_global)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);

	/* This is generated, if a few of them work the
	 * rest should work too */
	const char *lua =
		"libinput:register({1}); a = evdev.ABS_X\nb = evdev.REL_X\nc = evdev.KEY_A\nd = evdev.EV_SYN\n";
	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		litest_assert_logcapture_no_errors(capture);
	}
}
END_TEST

START_TEST(lua_test_libinput_now)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	const char *lua = "libinput:log_error(\">>>\" .. libinput:now())";
	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		usec_t test_now;
		int rc = now_in_us(&test_now);
		litest_assert_neg_errno_success(rc);

		size_t index = 0;
		litest_assert(strv_find_substring(capture->errors, ">>>", &index));
		size_t nelem = 0;
		_autostrvfree_ char **tokens =
			strv_from_string(capture->errors[index], ">>>", &nelem);
		litest_assert_int_eq(nelem, 2U);

		uint64_t plugin_now = strtoull(tokens[1], NULL, 10);

		litest_assert_int_le(plugin_now, usec_as_uint64_t(test_now));
		/* Even a slow test runner hopefully doesn't take >300ms to get to the
		 * log print */
		litest_assert_int_gt(
			plugin_now,
			usec_as_uint64_t(usec_sub(test_now, usec_from_millis(300))));
	}
}
END_TEST

START_TEST(lua_test_libinput_timer)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);

	const char *mode = litest_test_param_get_string(test_env->params, "mode");
	bool reschedule = litest_test_param_get_bool(test_env->params, "reschedule");

	_autofree_ char *timeout =
		strdup_printf("%s%" PRIu64,
			      streq(mode, "absolute") ? "libinput:now() + " : "",
			      usec_as_uint64_t(usec_from_millis(100)));
	_autofree_ char *reschedule_timeout =
		strdup_printf("libinput:timer_set_%s(%s%" PRIu64 ")\n",
			      mode,
			      streq(mode, "absolute") ? "t + " : "",
			      usec_as_uint64_t(usec_from_millis(100)));
	_autofree_ char *lua = strdup_printf(
		"libinput:register({1})\n"
		"libinput:connect(\"timer-expired\",\n"
		"          function(t)\n"
		"               libinput:log_error(\">>>\" .. t)\n"
		"               %s\n"
		"          end)\n"
		"libinput:timer_set_%s(%s)\n",
		reschedule ? reschedule_timeout : "",
		mode,
		timeout);

	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		litest_assert_logcapture_no_errors(capture);

		size_t nloops = reschedule ? 4 : 1;
		for (size_t i = 0; i < nloops; i++) {
			libinput_dispatch(li);
			msleep(100);
			libinput_dispatch(li);

			usec_t test_now;
			int rc = now_in_us(&test_now);
			litest_assert_neg_errno_success(rc);

			_autostrvfree_ char **msg = steal(&capture->errors);
			litest_assert_ptr_notnull(msg);
			size_t index;
			litest_assert(strv_find_substring(msg, ">>>", &index));

			size_t nelem = 0;
			_autostrvfree_ char **tokens =
				strv_from_string(msg[index], ">>>", &nelem);
			litest_assert_int_eq(nelem, 2U);

			uint64_t plugin_now = strtoull(tokens[1], NULL, 10);
			litest_assert_int_le(plugin_now, usec_as_uint64_t(test_now));
			/* Even a slow test runner hopefully doesn't take >300ms between
			 * dispatch and now_in_us */
			litest_assert_int_gt(
				plugin_now,
				usec_as_uint64_t(
					usec_sub(test_now, usec_from_millis(300))));
		}

		if (!reschedule) {
			libinput_dispatch(li);
			msleep(120);
			libinput_dispatch(li);
		}

		litest_assert_logcapture_no_errors(capture);
	}
}
END_TEST

enum connect_error {
	BAD_TYPE,
	TOO_FEW_ARGS,
	TOO_MANY_ARGS,
};

START_TEST(lua_bad_connect)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);

	const char *handler = litest_test_param_get_string(test_env->params, "handler");
	enum connect_error error = litest_test_param_get_i32(test_env->params, "error");

	const char *func = NULL;
	switch (error) {
	case BAD_TYPE:
		func = "a";
		break;
	case TOO_FEW_ARGS:
		func = "function(p) libinput:log_debug(\"few\"); end";
		break;
	case TOO_MANY_ARGS:
		func = "function(p, a, b) libinput:log_debug(\"many\"); end";
		break;
	}

	_autofree_ char *lua = strdup_printf(
		"libinput:register({1})\n"
		"a = 10\n"
		"libinput:connect(\"%s\", %s)\n",
		handler,
		func);

	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		switch (error) {
		/* These don't trigger a lua erro so we just test they don't segfault us
		 */
		case TOO_FEW_ARGS:
		case TOO_MANY_ARGS:
			litest_assert_logcapture_no_errors(capture);
			break;
		case BAD_TYPE:
			litest_assert_strv_substring(capture->errors,
						     "bad argument #2 to 'connect'");
			break;
		}
	}
}
END_TEST

START_TEST(lua_register_multiversions)
{

	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	const char *lua =
		"v = libinput:register({1, 3, 4, 10, 15})\nlibinput:log_info(\"VERSION:\" .. v)\n";
	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	if (libinput_log_get_priority(li) > LIBINPUT_LOG_PRIORITY_INFO)
		libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_INFO);

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		litest_assert_logcapture_no_errors(capture);
		litest_assert_strv_substring(capture->infos, "VERSION:1");
	}
}
END_TEST

START_TEST(lua_allowed_functions)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	/* This tests on the assumption that if some of these work,
	 * then the others we allow will work too. */
	const char *lua =
		"\n"
		"libinput:register({1})\n"
		"a = {10, 20}\n"
		"for _, v in ipairs(a) do\n"
		"   v = v + 1\n"
		"end\n"
		"b = {foo = 1}"
		"for k, v in pairs(a) do\n"
		"   v = v + 1\n"
		"end\n"
		"print(math.maxinteger)\n"
		"table.sort({10, 2, 4})\n"
		"assert(true)\n"
		"";
	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		litest_assert_logcapture_no_errors(capture);
	}
}
END_TEST

START_TEST(lua_disallowed_functions)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	/* This tests on the assumption that if some of these work,
	 * then the others we allow will work too. */
	const char *lua =
		"\n"
		"libinput:register({1})\n"
		"assert(io == nil)\n"
		"assert(require == nil)\n"
		"assert(rawget == nil)\n"
		"assert(rawset == nil)\n"
		"assert(setfenv == nil)\n"
		"assert(getmetatable == nil)\n"
		"assert(setmetatable == nil)\n"
		"assert(package == nil)\n"
		"assert(os == nil)\n"
		"assert(debug == nil)\n"
		"";
	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		litest_assert_logcapture_no_errors(capture);
	}
}
END_TEST

START_TEST(lua_gc_not_accessible)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	const char *lua =
		"libinput:register({1})\n"
		"assert(libinput.__gc == nil)\n"
		"function check_device_gc(device)\n"
		"  assert(device.__gc == nil)\n"
		"  libinput:log_info(\"gc_not_accessible: ok\")\n"
		"end\n"
		"libinput:connect(\"new-evdev-device\", check_device_gc)\n";

	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	if (libinput_log_get_priority(li) > LIBINPUT_LOG_PRIORITY_INFO)
		libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_INFO);

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		_destroy_(litest_device) *device = litest_add_device(li, LITEST_MOUSE);
		litest_drain_events(li);

		litest_assert_logcapture_no_errors(capture);
		litest_assert_strv_substring(capture->infos, "gc_not_accessible: ok");
	}
}
END_TEST

START_TEST(lua_frame_handler)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	const char *lua =
		"libinput:register({1})\n"
		"function frame_handler(_, frame, timestamp)\n"
		"  libinput:log_info(\"T:\" .. timestamp)\n"
		"  for _, e in ipairs(frame) do\n"
		"	libinput:log_info(\"E:\" .. e.usage .. \":\" .. e.value)\n"
		"  end\n"
		"end\n"
		"libinput:connect(\"new-evdev-device\", function(device) device:connect(\"evdev-frame\", frame_handler) end)\n";

	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	if (libinput_log_get_priority(li) > LIBINPUT_LOG_PRIORITY_INFO)
		libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_INFO);

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		_destroy_(litest_device) *device = litest_add_device(li, LITEST_MOUSE);
		litest_drain_events(li);

		usec_t before, after;
		now_in_us(&before);
		msleep(1);
		litest_button_click_debounced(device, li, BTN_LEFT, 1);
		litest_button_click_debounced(device, li, BTN_LEFT, 0);
		litest_assert_logcapture_no_errors(capture);
		msleep(1);
		now_in_us(&after);

		/* EV_KEY << 16 | BTN_LEFT -> 65808 */

		litest_assert_strv_substring(capture->infos, "E:65808:1");
		litest_assert_strv_substring(capture->infos, "E:65808:0");
		/* SYN_REPORT shouldn't show up in the frame */
		litest_assert(!strv_find_substring(capture->infos, "E:0:0", NULL));

		size_t idx;
		litest_assert(strv_find_substring(capture->infos, "T:", &idx));

		_autofree_ char *str = safe_strdup(capture->infos[idx]);
		for (size_t i = 0; str[i]; i++) {
			if (str[i] == '\n') {
				str[i] = '\0';
				break;
			}
		}

		size_t nelems;
		_autostrvfree_ char **split = strv_from_string(str, ":", &nelems);
		litest_assert_int_gt(nelems, 1U);
		char *strtime = split[nelems - 1];
		uint64_t timestamp = 0;
		litest_assert(safe_atou64(strtime, &timestamp));
		litest_assert_int_gt(timestamp, usec_as_uint64_t(before));
		litest_assert_int_lt(timestamp, usec_as_uint64_t(after));
	}
}
END_TEST

START_TEST(lua_device_info)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	const char *lua =
		"libinput:register({1})\n"
		"function info_printer(device)\n"
		"  local info = device:info()\n"
		"  libinput:log_info(\"BUS:\" .. info.bustype)\n"
		"  libinput:log_info(\"VID:\" .. info.vid)\n"
		"  libinput:log_info(\"PID:\" .. info.pid)\n"
		"end\n"
		"libinput:connect(\"new-evdev-device\", info_printer)\n";

	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	if (libinput_log_get_priority(li) > LIBINPUT_LOG_PRIORITY_INFO)
		libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_INFO);

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		_destroy_(litest_device) *device = litest_add_device(li, LITEST_MOUSE);
		litest_drain_events(li);

		litest_assert_strv_substring(capture->infos, "BUS:3");
		litest_assert_strv_substring(capture->infos, "VID:6127");
		litest_assert_strv_substring(capture->infos, "PID:24601");
	}
}
END_TEST

START_TEST(lua_set_absinfo)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	const char *lua =
		"libinput:register({1})\n"
		"function absinfo_setter(device)\n"
		"  local absinfos = device:absinfos()\n"
		"  for u, a in pairs(absinfos) do\n"
		"	libinput:log_info(\"A:\" .. u .. \":\" .. a.minimum .. \":\" .. a.maximum .. \":\" .. a.resolution .. \":\" .. a.fuzz .. \":\" .. a.flat)\n"
		"  end\n"
		"  device:set_absinfo(evdev.ABS_X, { minimum = 0, maximum = 1000, resolution = 100 })\n"
		"  device:set_absinfo(evdev.ABS_Y, { minimum = 0, maximum = 200, resolution = 10 })\n"
		"  device:set_absinfo(evdev.ABS_MT_POSITION_X, { minimum = 0, maximum = 1000, resolution = 100 })\n"
		"  device:set_absinfo(evdev.ABS_MT_POSITION_Y, { minimum = 0, maximum = 200, resolution = 10 })\n"
		"end\n"
		"libinput:connect(\"new-evdev-device\", absinfo_setter)\n";

	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	if (libinput_log_get_priority(li) > LIBINPUT_LOG_PRIORITY_INFO)
		libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_INFO);

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		_destroy_(litest_device) *device =
			litest_add_device(li, LITEST_GENERIC_MULTITOUCH_SCREEN);
		litest_drain_events(li);

		litest_assert_logcapture_no_errors(capture);

		for (int code = 0; code <= ABS_MAX; code++) {
			if (!libevdev_has_event_code(device->evdev, EV_ABS, code)) {
				_autofree_ char *prefix =
					strdup_printf("A:%u", (EV_ABS << 16) | code);
				litest_assert(!strv_find_substring(capture->infos,
								   prefix,
								   NULL));
				continue;
			}

			const struct input_absinfo *absinfo =
				libevdev_get_abs_info(device->evdev, code);
			_autofree_ char *message = strdup_printf("A:%u:%d:%d:%d:%d:%d",
								 (EV_ABS << 16) | code,
								 absinfo->minimum,
								 absinfo->maximum,
								 absinfo->resolution,
								 absinfo->fuzz,
								 absinfo->flat);
			litest_assert_strv_substring(capture->infos, message);
		}

		/* If the absinfo worked, our device is 10x20mm big */
		double w, h;
		libinput_device_get_size(device->libinput_device, &w, &h);
		litest_assert_double_eq(w, 10.0);
		litest_assert_double_eq(h, 20.0);
	}
}
END_TEST

START_TEST(lua_enable_disable_evdev_usage)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	/* We have two plugins here, one that enables codes and one that prints
	 * the frame.
	 *
	 * The first plugin also inserts a REL_Z event into the frame since we
	 * can't send that through the kernel.
	 */
	const char *lua1 =
		"libinput:register({1})\n"
		"function frame_handler(_, frame, timestamp)\n"
		"  table.insert(frame, { usage = evdev.REL_Z, value = 3 })\n"
		"  return frame\n"
		"end\n"
		"function enabler(device)\n"
		"  device:enable_evdev_usage(evdev.REL_Z)\n"
		"  device:enable_evdev_usage(evdev.BTN_STYLUS2)\n"
		"  device:disable_evdev_usage(evdev.REL_WHEEL)\n"
		"  device:connect(\"evdev-frame\", frame_handler)\n"
		"end\n"
		"libinput:connect(\"new-evdev-device\", enabler)\n";

	const char *lua2 =
		"libinput:register({1})\n"
		"function frame_handler(_, frame, timestamp)\n"
		"  libinput:log_info(\"frame\")\n"
		"  for _, e in ipairs(frame) do\n"
		"	libinput:log_info(\"E:\" .. e.usage .. \":\" .. e.value)\n"
		"  end\n"
		"end\n"
		"function f(device)\n"
		"  libinput:log_info(\"F: \" .. device:name())\n"
		"  device:connect(\"evdev-frame\", frame_handler)\n"
		"end\n"
		"libinput:connect(\"new-evdev-device\", f)\n";

	_autofree_ char *p1 = litest_write_plugin(tmpdir->path, lua1);
	_autofree_ char *p2 = litest_write_plugin(tmpdir->path, lua2);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	if (libinput_log_get_priority(li) > LIBINPUT_LOG_PRIORITY_INFO)
		libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_INFO);

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		_destroy_(litest_device) *device = litest_add_device(li, LITEST_MOUSE);
		litest_drain_events(li);

		/* We enabled that one ourselves */
		litest_assert(
			libinput_device_pointer_has_button(device->libinput_device,
							   BTN_STYLUS2));

		litest_assert_logcapture_no_errors(capture);

		litest_event(device, EV_REL, REL_X, 1);
		litest_event(device, EV_REL, REL_Y, 2);
		litest_event(device, EV_REL, REL_WHEEL, -1);
		litest_event(device, EV_SYN, SYN_REPORT, 0);
		litest_dispatch(li);

		litest_assert_logcapture_no_errors(capture);

		litest_assert_strv_substring(capture->infos, "E:131072:1");
		litest_assert_strv_substring(capture->infos, "E:131073:2");
		litest_assert_strv_substring(capture->infos, "E:131074:3");
		litest_assert(!strv_find_substring(capture->infos, "E:131080", NULL));
	}
}
END_TEST

START_TEST(lua_udev_properties)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	const char *lua =
		"libinput:register({1})\n"
		"function prop_printer(device)\n"
		"  local properties = device:udev_properties()\n"
		"  for k, v in pairs(properties) do\n"
		"	libinput:log_info(k .. \"=\" .. v)\n"
		"  end\n"
		"end\n"
		"libinput:connect(\"new-evdev-device\", prop_printer)\n";

	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	if (libinput_log_get_priority(li) > LIBINPUT_LOG_PRIORITY_INFO)
		libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_INFO);

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		enum litest_device_type which =
			litest_test_param_get_i32(test_env->params, "which");
		_destroy_(litest_device) *device = litest_add_device(li, which);
		litest_drain_events(li);

		litest_assert_logcapture_no_errors(capture);

		switch (which) {
		case LITEST_TRACKPOINT:
			litest_assert_strv_substring(capture->infos,
						     "ID_INPUT_POINTINGSTICK=1");
			_fallthrough_;
		case LITEST_MOUSE:
			litest_assert_strv_substring(capture->infos,
						     "ID_INPUT_MOUSE=1");
			break;
		case LITEST_GENERIC_MULTITOUCH_SCREEN:
			litest_assert_strv_substring(capture->infos,
						     "ID_INPUT_TOUCHSCREEN=1");
			break;
		default:
			litest_assert_not_reached();
			break;
		}
		litest_assert(!strv_find_substring(capture->infos,
						   "ID_INPUT_WIDTH_MM",
						   NULL));
		litest_assert(!strv_find_substring(capture->infos,
						   "ID_INPUT_WIDTH_MM",
						   NULL));
	}
}
END_TEST

START_TEST(lua_append_prepend_frame)
{
	bool append = litest_test_param_get_bool(test_env->params, "append");
	bool in_timer = litest_test_param_get_bool(test_env->params, "in_timer");
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	_autofree_ char *lua = strdup_printf(
		"libinput:register({1})\n"
		"mydev = nil\n"
		"function frame_handler(device, frame, timestamp)\n"
		"    device:%s_frame({{ usage = evdev.BTN_LEFT, value = 1}})\n" /* commented
										   out
										   if
										   !in_timer
										 */
		"    return nil\n"
		"end\n"
		"libinput:connect(\"new-evdev-device\", function(device)\n"
		"    mydev = device\n"
		"    %sdevice:connect(\"evdev-frame\", frame_handler)\n"
		"    %slibinput:timer_set_relative(200000)\n" /* commented out if
								 !in_timer */
		"end)\n"
		"function timer_expired(t)\n"
		"    mydev:%s_frame({{ usage = evdev.BTN_LEFT, value = 1 }})\n"
		"end\n"
		"libinput:connect(\"timer-expired\", timer_expired)\n",
		append ? "append" : "prepend",
		in_timer ? "-- " : "",
		in_timer ? "" : "-- ",
		append ? "append" : "prepend");
	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
	litest_drain_events(li);

	_destroy_(litest_device) *device = litest_add_device(li, LITEST_MOUSE);
	litest_drain_events(li);
	msleep(10); /* trigger the timer, if any */
	litest_dispatch(li);

	if (in_timer) {
		litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED);
	}

	litest_event(device, EV_REL, REL_X, 1);
	litest_event(device, EV_REL, REL_Y, 2);
	litest_event(device, EV_SYN, SYN_REPORT, 0);
	litest_dispatch(li);
	litest_timeout_debounce(li);
	litest_dispatch(li);

	if (!in_timer && !append) {
		litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED);
	}

	_destroy_(libinput_event) *ev = libinput_get_event(li);
	litest_is_motion_event(ev);

	if (!in_timer && append) {
		litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED);
	}

	litest_assert_empty_queue(li);
}
END_TEST

START_TEST(lua_ignore_unsupported_codes)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	_autofree_ char *lua = strdup_printf(
		"libinput:register({1})\n"
		"function frame_handler(device, frame, timestamp)\n"
		"    local events = {}\n"
		"    for _, v in ipairs(frame) do\n"
		"       table.insert(events, { usage = v.usage, value = v.value })\n"
		"    end\n"
		"    table.insert(events, { usage = evdev.ABS_X, value = 1000 })\n"
		"    table.insert(events, { usage = evdev.ABS_Y, value = 100 })\n"
		"    table.insert(events, { usage = evdev.BTN_BACK, value = 1 })\n"
		"    table.insert(events, { usage = evdev.BTN_LEFT, value = 1 })\n" /* this
										       one actually exists */
		"    return events\n"
		"end\n"
		"libinput:connect(\"new-evdev-device\", function(device)\n"
		"    device:connect(\"evdev-frame\", frame_handler)\n"
		"end)\n");
	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
	litest_drain_events(li);

	_destroy_(litest_device) *device = litest_add_device(li, LITEST_MOUSE);
	litest_drain_events(li);

	litest_event(device, EV_REL, REL_X, 1);
	litest_event(device, EV_REL, REL_Y, 2);
	litest_event(device, EV_SYN, SYN_REPORT, 0);
	litest_dispatch(li);
	litest_timeout_debounce(li);
	litest_dispatch(li);

	_destroy_(libinput_event) *ev = libinput_get_event(li);
	litest_is_motion_event(ev);
	litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED);

	litest_assert_empty_queue(li);
}
END_TEST

enum when {
	DEVICE_NEW,
	FIRST_FRAME,
};

START_TEST(lua_disable_button_debounce)
{
	enum when when = litest_test_param_get_i32(test_env->params, "when");
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	_autofree_ char *lua = strdup_printf(
		"libinput:register({1})\n"
		"function frame_handler(device, _, _)\n"
		"  device:disable_feature(\"button-debouncing\")\n"
		"end\n"
		"function new_device(device)\n"
		"  %s device:disable_feature(\"button-debouncing\")\n"
		"  %s device:connect(\"evdev-frame\", frame_handler)\n"
		"end\n"
		"libinput:connect(\"new-evdev-device\", new_device)\n",
		when == DEVICE_NEW ? "" : "--",
		when == FIRST_FRAME ? "" : "--");
	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);

	if (libinput_log_get_priority(li) > LIBINPUT_LOG_PRIORITY_DEBUG)
		libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_DEBUG);

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		_destroy_(litest_device) *dev = litest_add_device(li, LITEST_MOUSE);
		litest_drain_events(li);

		litest_disable_middleemu(dev);

		litest_event(dev, EV_KEY, BTN_LEFT, 1);
		litest_event(dev, EV_SYN, SYN_REPORT, 0);
		litest_event(dev, EV_KEY, BTN_LEFT, 0);
		litest_event(dev, EV_SYN, SYN_REPORT, 0);
		litest_event(dev, EV_KEY, BTN_LEFT, 1);
		litest_event(dev, EV_SYN, SYN_REPORT, 0);
		litest_event(dev, EV_KEY, BTN_LEFT, 0);
		litest_event(dev, EV_SYN, SYN_REPORT, 0);
		litest_timeout_debounce(li);

		litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED);
		litest_assert_button_event(li,
					   BTN_LEFT,
					   LIBINPUT_BUTTON_STATE_RELEASED);
		litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED);
		litest_assert_button_event(li,
					   BTN_LEFT,
					   LIBINPUT_BUTTON_STATE_RELEASED);
		litest_assert_empty_queue(li);

		litest_assert_strv_substring(capture->debugs,
					     "disabled button debouncing on request");
	}
}
END_TEST

START_TEST(lua_disable_touchpad_jump_detection)
{
	enum when when = litest_test_param_get_i32(test_env->params, "when");
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	_autofree_ char *lua = strdup_printf(
		"libinput:register({1})\n"
		"function frame_handler(dev, f, ts)\n"
		"  dev:disable_feature(\"touchpad-jump-detection\")\n"
		"end\n"
		"function new_device(device)\n"
		"  %sdevice:disable_feature(\"touchpad-jump-detection\")\n"
		"  %sdevice:connect(\"evdev-frame\", frame_handler)\n"
		"end\n"
		"libinput:connect(\"new-evdev-device\", new_device)\n",
		when == DEVICE_NEW ? "" : "-- ",
		when == FIRST_FRAME ? "" : "-- ");

	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);

	if (libinput_log_get_priority(li) > LIBINPUT_LOG_PRIORITY_DEBUG)
		libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_DEBUG);

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		_destroy_(litest_device) *dev =
			litest_add_device(li, LITEST_SYNAPTICS_RMI4);
		litest_drain_events(li);

		litest_touch_down(dev, 0, 40, 50);
		litest_touch_move(dev, 0, 80, 80);
		litest_touch_move(dev, 0, 90, 90);
		litest_touch_up(dev, 0);

		litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION);
		litest_assert_empty_queue(li);

		litest_assert(!strv_find_substring(capture->infos,
						   "Touch jump detected and discarded",
						   NULL));
	}
}
END_TEST

START_TEST(lua_disable_wheel_debouncing)
{
	enum when when = litest_test_param_get_i32(test_env->params, "when");
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	_autofree_ char *lua = strdup_printf(
		"libinput:register({1})\n"
		"function frame_handler(device, _, _)\n"
		"  device:disable_feature(\"wheel-debouncing\")\n"
		"end\n"
		"function new_device(device)\n"
		"  %s device:disable_feature(\"wheel-debouncing\")\n"
		"  %s device:connect(\"evdev-frame\", frame_handler)\n"
		"end\n"
		"libinput:connect(\"new-evdev-device\", new_device)\n",
		when == DEVICE_NEW ? "" : "--",
		when == FIRST_FRAME ? "" : "--");
	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);

	if (libinput_log_get_priority(li) > LIBINPUT_LOG_PRIORITY_DEBUG)
		libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_DEBUG);

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);

		_destroy_(litest_device) *dev = litest_add_device(li, LITEST_MOUSE);
		litest_drain_events(li);

		for (size_t i = 0; i < 4; i++) {
			/* Send a small wheel events - when debouncing is disabled, they
			 * should all be delivered immediately without delay */
			litest_event(dev, EV_REL, REL_WHEEL_HI_RES, 10);
			litest_event(dev, EV_SYN, SYN_REPORT, 0);
			litest_dispatch(li);

			_destroy_(libinput_event) *ev = libinput_get_event(li);
			auto ptrev = litest_is_axis_event(
				ev,
				LIBINPUT_EVENT_POINTER_SCROLL_WHEEL,
				LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL,
				0);
			double v120 = libinput_event_pointer_get_scroll_value_v120(
				ptrev,
				LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL);
			litest_assert_double_eq(v120, -10);
		}
		litest_assert_empty_queue(li);
		litest_assert_strv_substring(capture->debugs,
					     "disabled wheel debouncing on request");
	}
}
END_TEST

START_TEST(lua_remove_plugin_on_timeout)
{
	enum when when = litest_test_param_get_i32(test_env->params, "when");
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
	_autofree_ char *lua = strdup_printf(
		"libinput:register({1})\n"
		"function infinite_loop(device)\n"
		"  local i = 0\n"
		"  while true do\n"
		"    i = i + 1\n"
		"  end\n"
		"end\n"
		"function new_device(device)\n"
		"   device:connect(\"evdev-frame\", infinite_loop)\n"
		"end\n"
		"%s libinput:connect(\"new-evdev-device\", infinite_loop)\n"
		"%s libinput:connect(\"new-evdev-device\", new_device)\n",
		when == DEVICE_NEW ? "" : "--",
		when == FIRST_FRAME ? "" : "--");

	_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);
	_destroy_(litest_device) *dev = NULL;

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		usec_t before = usec_from_now();

		dev = litest_add_device(li, LITEST_MOUSE);
		litest_drain_events(li);

		if (when == FIRST_FRAME) {
			litest_event(dev, EV_KEY, BTN_LEFT, 1);
			litest_event(dev, EV_SYN, SYN_REPORT, 0);
			litest_dispatch(li);
			litest_event(dev, EV_KEY, BTN_LEFT, 0);
			litest_event(dev, EV_SYN, SYN_REPORT, 0);
			litest_dispatch(li);
		}

		usec_t after = usec_from_now();
		usec_t elapsed = usec_delta(after, before);

		/* verify the timeout kicked in around ~1 second (but less than 2s) */
		litest_assert_int_ge(usec_to_millis(elapsed), 1000U);
		if (!RUNNING_ON_VALGRIND)
			litest_assert_int_lt(usec_to_millis(elapsed), 2000U);

		size_t index = 0;
		litest_assert(strv_find_substring(capture->errors,
						  "Plugin execution timeout",
						  &index));
		litest_assert_str_in("exceeded 1 second", capture->errors[index]);

		litest_assert(strv_find_substring(capture->errors,
						  "unloading after error",
						  NULL));
	}
	/* device events should go through now */
	litest_event(dev, EV_KEY, BTN_LEFT, 1);
	litest_event(dev, EV_SYN, SYN_REPORT, 0);
	litest_dispatch(li);
	litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED);
}
END_TEST

/* Pre-compiled Lua 5.4 bytecode for the following source:
 *
 *   libinput:register({1})
 *   libinput:connect("new-evdev-device", function(device)
 *       libinput:log_info("loaded from binary lua file")
 *   end)
 *
 * To regenerate:
 *   luac5.4 -o /dev/stdout /tmp/plugin.lua | xxd -i
 */
static const unsigned char binary_lua_plugin[] = {
	0x1b, 0x4c, 0x75, 0x61, 0x54, 0x00, 0x19, 0x93, 0x0d, 0x0a, 0x1a, 0x0a, 0x04,
	0x08, 0x08, 0x78, 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x28, 0x77, 0x40, 0x01, 0x9b, 0x40, 0x74, 0x65, 0x73, 0x74, 0x2f,
	0x31, 0x30, 0x2d, 0x62, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x2d, 0x70, 0x6c, 0x75,
	0x67, 0x69, 0x6e, 0x2e, 0x6c, 0x75, 0x61, 0x80, 0x80, 0x00, 0x01, 0x04, 0x8e,
	0x51, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x14, 0x80, 0x00, 0x01, 0x13,
	0x01, 0x00, 0x01, 0x52, 0x00, 0x00, 0x00, 0x81, 0x01, 0x00, 0x80, 0x4e, 0x01,
	0x01, 0x00, 0x44, 0x00, 0x03, 0x01, 0x0b, 0x00, 0x00, 0x00, 0x14, 0x80, 0x00,
	0x02, 0x03, 0x81, 0x01, 0x00, 0xcf, 0x01, 0x00, 0x00, 0x44, 0x00, 0x04, 0x01,
	0x46, 0x00, 0x01, 0x01, 0x84, 0x04, 0x89, 0x6c, 0x69, 0x62, 0x69, 0x6e, 0x70,
	0x75, 0x74, 0x04, 0x89, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x04,
	0x88, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x04, 0x91, 0x6e, 0x65, 0x77,
	0x2d, 0x65, 0x76, 0x64, 0x65, 0x76, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65,
	0x81, 0x01, 0x00, 0x00, 0x81, 0x80, 0x8d, 0x8f, 0x01, 0x00, 0x04, 0x85, 0x8b,
	0x00, 0x00, 0x00, 0x94, 0x80, 0x01, 0x01, 0x83, 0x01, 0x01, 0x00, 0xc4, 0x00,
	0x03, 0x01, 0xc7, 0x00, 0x01, 0x00, 0x83, 0x04, 0x89, 0x6c, 0x69, 0x62, 0x69,
	0x6e, 0x70, 0x75, 0x74, 0x04, 0x89, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x6e, 0x66,
	0x6f, 0x04, 0x9c, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x20, 0x66, 0x72, 0x6f,
	0x6d, 0x20, 0x62, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x20, 0x6c, 0x75, 0x61, 0x20,
	0x66, 0x69, 0x6c, 0x65, 0x81, 0x00, 0x00, 0x00, 0x80, 0x85, 0x01, 0x00, 0x00,
	0x00, 0x01, 0x80, 0x81, 0x87, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x80, 0x85,
	0x81, 0x85, 0x5f, 0x45, 0x4e, 0x56, 0x8e, 0x01, 0x0b, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0xfe, 0x02, 0x80, 0x80, 0x81, 0x85, 0x5f,
	0x45, 0x4e, 0x56,
};

START_TEST(lua_reject_precompiled_files)
{
	_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);

	/* Write the binary bytecode to a .lua file in the tmpdir.
	 * Binary (pre-compiled) Lua files must be rejected by the
	 * plugin loader for security reasons. */
	_autofree_ char *path = strdup_printf("%s/10-binary-plugin.lua", tmpdir->path);
	_autoclose_ int fd = open(path, O_WRONLY | O_CREAT, 0644);
	litest_assert_errno_success(fd);

	ssize_t written = write(fd, binary_lua_plugin, sizeof(binary_lua_plugin));
	litest_assert_int_eq((int)written, (int)sizeof(binary_lua_plugin));
	fsync(fd);

	_litest_context_destroy_ struct libinput *li =
		litest_create_context_with_plugindir(tmpdir->path);

	litest_with_logcapture(li, capture) {
		libinput_plugin_system_load_plugins(li,
						    LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
		litest_drain_events(li);

		size_t index = 0;
		litest_assert(
			strv_find_substring(capture->errors, "Failed to load", &index));
		litest_assert_str_in(path, capture->errors[index]);
	}
}
END_TEST

TEST_COLLECTION(lua)
{
	/* clang-format off */
	litest_add_no_device(lua_load_failure);
	litest_add_no_device(lua_reject_precompiled_files);
	litest_with_parameters(params,
			       "content", 'I', 6,
					litest_named_i32(EMPTY),
					litest_named_i32(BASIC),
					litest_named_i32(COMMENT),
					litest_named_i32(DUPLICATE_CALL),
					litest_named_i32(MISSING_REGISTER),
					litest_named_i32(VERSION_NOT_A_TABLE)) {
		litest_add_parametrized_no_device(lua_load_success_but_no_register,
						  params);
	}
	litest_add_no_device(lua_register_noop);
	litest_with_parameters(params, "when", 's', 2, "run", "connect") {
		litest_add_parametrized_no_device(lua_unregister_is_last, params);
	}
	litest_add_no_device(lua_test_evdev_global);
	litest_add_no_device(lua_test_libinput_now);
	litest_with_parameters(params,
			       "mode", 's', 2, "absolute", "relative",
			       "reschedule", 'b') {
		litest_add_parametrized_no_device(lua_test_libinput_timer, params);
	}

	litest_with_parameters(params,
			       "priority", 'I', 3,
					litest_named_i32(LIBINPUT_LOG_PRIORITY_DEBUG),
					litest_named_i32(LIBINPUT_LOG_PRIORITY_INFO),
					litest_named_i32(LIBINPUT_LOG_PRIORITY_ERROR)) {
		litest_add_parametrized_no_device(lua_test_logging, params);
	}

	litest_with_parameters(params,
			       "handler", 's', 2, "new-evdev-device", "timer-expired",
			       "error", 'I', 3,
					litest_named_i32(BAD_TYPE),
					litest_named_i32(TOO_FEW_ARGS),
					litest_named_i32(TOO_MANY_ARGS)) {
		litest_add_parametrized_no_device(lua_bad_connect, params);
	}

	litest_add_no_device(lua_register_multiversions);
	litest_add_no_device(lua_allowed_functions);
	litest_add_no_device(lua_disallowed_functions);
	litest_add_no_device(lua_gc_not_accessible);

	litest_add_no_device(lua_frame_handler);
	litest_add_no_device(lua_device_info);
	litest_add_no_device(lua_set_absinfo);
	litest_add_no_device(lua_enable_disable_evdev_usage);
	litest_add_no_device(lua_ignore_unsupported_codes);

	litest_with_parameters(params,
			       "which", 'I', 3,
					litest_named_i32(LITEST_MOUSE),
					litest_named_i32(LITEST_TRACKPOINT),
					litest_named_i32(LITEST_GENERIC_MULTITOUCH_SCREEN)) {
		litest_add_parametrized_no_device(lua_udev_properties, params);
	}

	litest_with_parameters(params, "append", 'b', "in_timer", 'b') {
		litest_add_parametrized_no_device(lua_append_prepend_frame, params);
	}

	litest_with_parameters(params, "when", 'I', 2,
					litest_named_i32(DEVICE_NEW),
					litest_named_i32(FIRST_FRAME)) {
		litest_add_parametrized_no_device(lua_disable_button_debounce, params);
		litest_add_parametrized_no_device(lua_disable_touchpad_jump_detection, params);
		litest_add_parametrized_no_device(lua_disable_wheel_debouncing, params);

		litest_add_parametrized_no_device(lua_remove_plugin_on_timeout, params);
	}
	/* clang-format on */
}
