/* SPDX-License-Identifier: MIT */
/*
 * Copyright © 2020 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 <unistd.h>
#include <fcntl.h>

#include "util-munit.h"
#include "util-time.h"
#include "util-strings.h"
#include "eierpecken.h"

MUNIT_TEST(test_ei_ref_unref)
{
	struct ei *ei = ei_new(NULL);

	struct ei *refd = ei_ref(ei);
	munit_assert_ptr_equal(ei, refd);

	struct ei *unrefd = ei_unref(ei);
	munit_assert_ptr_null(unrefd);
	unrefd = ei_unref(ei);
	munit_assert_ptr_null(unrefd);

	/* memleak only shows up in valgrind */

	return MUNIT_OK;
}

MUNIT_TEST(test_ei_disconnect_immediately)
{
	_unref_(peck) *peck = peck_new();

	/* Client is immediately rejected */
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_REJECT_CLIENT);
	peck_dispatch_until_stable(peck);

	/* Expect the client to get a disconnect event */
	with_client(peck) {
		ei_dispatch(ei);
		_unref_(ei_event) *disconnect =
			peck_ei_next_event(ei, EI_EVENT_DISCONNECT);
	}

	return MUNIT_OK;
}

MUNIT_TEST(test_ei_disconnect_self_immediately)
{
	_unref_(peck) *peck = peck_new();

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_NONE);
	peck_dispatch_until_stable(peck);

	/* Disconnect before server processed CONNECT */
	with_client(peck) {
		peck_drop_ei(peck);
		ei_unref(ei);
	}

	peck_dispatch_until_stable(peck);

	/* Expect the client to get a disconnect event */
	with_server(peck) {
		_unref_(eis_event) *connect =
			peck_eis_next_event(eis, EIS_EVENT_CLIENT_CONNECT);
		_unref_(eis_event) *disconnect =
			peck_eis_next_event(eis, EIS_EVENT_CLIENT_DISCONNECT);
	}

	return MUNIT_OK;
}


MUNIT_TEST(test_ei_disconnect_after_connect)
{
	_unref_(peck) *peck = peck_new();
	_unref_(eis_client) *client = NULL;

	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_NONE);

	peck_dispatch_until_stable(peck);

	with_server(peck) {
		eis_dispatch(eis);
		_unref_(eis_event) *e =
			peck_eis_next_event(eis, EIS_EVENT_CLIENT_CONNECT);
		client = eis_client_ref(eis_event_get_client(e));
		eis_client_connect(client);
	}

	with_client(peck) {
		ei_dispatch(ei);
		_unref_(ei_event) *e =
			peck_ei_next_event(ei, EI_EVENT_CONNECT);
	}

	with_server(peck) {
		eis_client_disconnect(client);
	}

	with_client(peck) {
		ei_dispatch(ei);
		_unref_(ei_event) *e =
			peck_ei_next_event(ei, EI_EVENT_DISCONNECT);
	}

	return MUNIT_OK;
}

MUNIT_TEST(test_ei_disconnect_self_after_connect)
{
	_unref_(peck) *peck = peck_new();

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_NONE);
	peck_dispatch_until_stable(peck);

	with_client(peck) {
		peck_drop_ei(peck);
		ei_unref(ei);
	}

	peck_dispatch_until_stable(peck);

	with_server(peck) {
		_unref_(eis_event) *connect =
			peck_eis_next_event(eis, EIS_EVENT_CLIENT_CONNECT);
		_unref_(eis_event) *disconnect =
			peck_eis_next_event(eis, EIS_EVENT_CLIENT_DISCONNECT);
	}

	return MUNIT_OK;
}

MUNIT_TEST(test_ei_disconnect_after_seat)
{
	_unref_(peck) *peck = peck_new();

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_CLIENT);
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_DEFAULT_SEAT);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_NONE);

	peck_dispatch_until_stable(peck);

	with_client(peck) {
		_unref_(ei_event) *connect =
			peck_ei_next_event(ei, EI_EVENT_CONNECT);
		_unref_(ei_event) *seat =
			peck_ei_next_event(ei, EI_EVENT_SEAT_ADDED);
	}

	with_server(peck) {
		struct eis_client *client = peck_eis_get_default_client(peck);
		eis_client_disconnect(client);
	}

	peck_dispatch_until_stable(peck);

	with_client(peck) {
		_unref_(ei_event) *seat =
			peck_ei_next_event(ei, EI_EVENT_SEAT_REMOVED);
		_unref_(ei_event) *disconnect =
			peck_ei_next_event(ei, EI_EVENT_DISCONNECT);
	}

	return MUNIT_OK;
}

MUNIT_TEST(test_ei_disconnect_self_after_seat)
{
	_unref_(peck) *peck = peck_new();

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_CLIENT);
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_DEFAULT_SEAT);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_NONE);

	peck_dispatch_until_stable(peck);

	with_client(peck) {
		_unref_(ei_event) *connect =
			peck_ei_next_event(ei, EI_EVENT_CONNECT);
		_unref_(ei_event) *seat =
			peck_ei_next_event(ei, EI_EVENT_SEAT_ADDED);
		peck_drop_ei(peck);
		/* Disconnect from client */
		ei_unref(ei);

		/* There is no way to disconnect from the server without
		 * destroying the context, so we don't care about the actual
		 * events here
		 */
	}

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_NONE);
	peck_dispatch_until_stable(peck);

	with_server(peck) {
		_unref_(eis_event) *disconnect =
			peck_eis_next_event(eis, EIS_EVENT_CLIENT_DISCONNECT);
	}

	return MUNIT_OK;
}


MUNIT_TEST(test_ei_disconnect_after_bind_before_received)
{
	_unref_(peck) *peck = peck_new();

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_CLIENT);
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_DEFAULT_SEAT);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_NONE);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_CONNECT);
	peck_dispatch_until_stable(peck);

	with_client(peck) {
		_unref_(ei_event) *event = peck_ei_next_event(ei, EI_EVENT_SEAT_ADDED);
		struct ei_seat *seat = ei_event_get_seat(event);
		ei_seat_bind_capabilities(seat, EI_DEVICE_CAP_POINTER, NULL);
	}

	/* We have *not* called eis_dispatch, so the seat bind hasn't been
	 * processed by the server yet */
	with_server(peck) {
		struct eis_client *client = peck_eis_get_default_client(peck);
		eis_client_disconnect(client);
	}

	with_client(peck) {
		ei_dispatch(ei);
		_unref_(ei_event) *seat_removed =
			peck_ei_next_event(ei, EI_EVENT_SEAT_REMOVED);
		_unref_(ei_event) *disconnect =
			peck_ei_next_event(ei, EI_EVENT_DISCONNECT);
	}

	return MUNIT_OK;
}

MUNIT_TEST(test_ei_disconnect_self_after_bind_before_received)
{
	_unref_(peck) *peck = peck_new();

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_CLIENT);
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_DEFAULT_SEAT);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_NONE);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_CONNECT);
	peck_dispatch_until_stable(peck);

	with_client(peck) {
		_unref_(ei_event) *event = peck_ei_next_event(ei, EI_EVENT_SEAT_ADDED);
		struct ei_seat *seat = ei_event_get_seat(event);
		ei_seat_bind_capabilities(seat, EI_DEVICE_CAP_POINTER, NULL);
		/* Disconnect before the server can process the bind event */
		peck_drop_ei(peck);
		ei_unref(ei);
	}

	peck_dispatch_eis(peck);

	with_server(peck) {
		/* Server got the bind event but client disconnected
		 * immediately after */
		_unref_(eis_event) *bind =
			peck_eis_next_event(eis, EIS_EVENT_SEAT_BIND);
		_unref_(eis_event) *unbind =
			peck_eis_next_event(eis, EIS_EVENT_SEAT_BIND);
		munit_assert_false(eis_event_seat_has_capability(unbind, EIS_DEVICE_CAP_POINTER));
		munit_assert_false(eis_event_seat_has_capability(unbind, EIS_DEVICE_CAP_POINTER_ABSOLUTE));
		munit_assert_false(eis_event_seat_has_capability(unbind, EIS_DEVICE_CAP_KEYBOARD));
		munit_assert_false(eis_event_seat_has_capability(unbind, EIS_DEVICE_CAP_TOUCH));
		_unref_(eis_event) *disconnect =
			peck_eis_next_event(eis, EIS_EVENT_CLIENT_DISCONNECT);
	}

	return MUNIT_OK;
}

MUNIT_TEST(test_ei_disconnect_after_bind_after_received)
{
	_unref_(peck) *peck = peck_new();

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_CLIENT);
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_DEFAULT_SEAT);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_NONE);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_CONNECT);
	peck_dispatch_until_stable(peck);

	with_client(peck) {
		_unref_(ei_event) *event = peck_ei_next_event(ei, EI_EVENT_SEAT_ADDED);
		struct ei_seat *seat = ei_event_get_seat(event);
		ei_seat_bind_capabilities(seat, EI_DEVICE_CAP_POINTER, NULL);
	}

	/* Receive the Bind event but don't actually add any devices,
	 * disconnect the client instead */
	peck_dispatch_eis(peck);

	with_server(peck) {
		struct eis_client *client = peck_eis_get_default_client(peck);
		eis_client_disconnect(client);
	}

	with_client(peck) {
		ei_dispatch(ei);
		_unref_(ei_event) *seat_removed =
			peck_ei_next_event(ei, EI_EVENT_SEAT_REMOVED);
		_unref_(ei_event) *disconnect =
			peck_ei_next_event(ei, EI_EVENT_DISCONNECT);
	}

	return MUNIT_OK;
}

MUNIT_TEST(test_ei_disconnect_self_after_bind_after_received)
{
	_unref_(peck) *peck = peck_new();

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_CLIENT);
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_DEFAULT_SEAT);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_NONE);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_CONNECT);
	peck_dispatch_until_stable(peck);

	with_client(peck) {
		_unref_(ei_event) *event = peck_ei_next_event(ei, EI_EVENT_SEAT_ADDED);
		struct ei_seat *seat = ei_event_get_seat(event);
		ei_seat_bind_capabilities(seat, EI_DEVICE_CAP_POINTER, NULL);
	}

	/* Make sure server sees Bind, then disconnect from server */
	peck_dispatch_eis(peck);

	with_client(peck) {
		peck_drop_ei(peck);
		ei_unref(ei);
	}

	peck_dispatch_eis(peck);

	with_server(peck) {
		_unref_(eis_event) *bind =
			peck_eis_next_event(eis, EIS_EVENT_SEAT_BIND);
		_unref_(eis_event) *unbind =
			peck_eis_next_event(eis, EIS_EVENT_SEAT_BIND);
		munit_assert_false(eis_event_seat_has_capability(unbind, EIS_DEVICE_CAP_POINTER));
		munit_assert_false(eis_event_seat_has_capability(unbind, EIS_DEVICE_CAP_POINTER_ABSOLUTE));
		munit_assert_false(eis_event_seat_has_capability(unbind, EIS_DEVICE_CAP_KEYBOARD));
		munit_assert_false(eis_event_seat_has_capability(unbind, EIS_DEVICE_CAP_TOUCH));
		_unref_(eis_event) *disconnect =
			peck_eis_next_event(eis, EIS_EVENT_CLIENT_DISCONNECT);
	}

	return MUNIT_OK;
}

MUNIT_TEST(test_ei_disconnect_after_unbind_before_received)
{
	_unref_(peck) *peck = peck_new();
	_unref_(ei_seat) *seat = NULL;

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_NONE);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_CONNECT);
	peck_dispatch_until_stable(peck);

	with_client(peck) {
		_unref_(ei_event) *event = peck_ei_next_event(ei, EI_EVENT_SEAT_ADDED);
		seat = ei_seat_ref(ei_event_get_seat(event));
		ei_seat_bind_capabilities(seat, EI_DEVICE_CAP_POINTER, NULL);
	}

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL);

	/* server has the Bind event now */
	peck_dispatch_until_stable(peck);

	with_client(peck) {
		ei_seat_unbind_capabilities(seat, EI_DEVICE_CAP_POINTER,
					    NULL);
	}

	/* No server dispatch here so the server isn't aware of the
	 * ei_seat_unbind() call. Disconnect the client
	 */
	with_server(peck) {
		struct eis_client *client = peck_eis_get_default_client(peck);
		eis_client_disconnect(client);
	}

	with_client(peck) {
		ei_dispatch(ei);
		_unref_(ei_event) *seat_removed =
			peck_ei_next_event(ei, EI_EVENT_SEAT_REMOVED);
		_unref_(ei_event) *disconnect =
			peck_ei_next_event(ei, EI_EVENT_DISCONNECT);
	}

	return MUNIT_OK;
}

MUNIT_TEST(test_ei_disconnect_after_unbind_after_received)
{
	_unref_(peck) *peck = peck_new();
	_unref_(ei_seat) *seat = NULL;

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_NONE);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_CONNECT);
	peck_dispatch_until_stable(peck);

	with_client(peck) {
		_unref_(ei_event) *event = peck_ei_next_event(ei, EI_EVENT_SEAT_ADDED);
		seat = ei_seat_ref(ei_event_get_seat(event));
		ei_seat_bind_capabilities(seat, EI_DEVICE_CAP_POINTER, NULL);
	}

	/* server has the Bind event now */
	peck_dispatch_until_stable(peck);

	with_client(peck) {
		ei_seat_unbind_capabilities(seat, EI_DEVICE_CAP_POINTER,
					    NULL);
	}

	/* Dispatch, server is aware of the ei_seat_unbind() */
	peck_dispatch_eis(peck);
	with_server(peck) {
		struct eis_client *client = peck_eis_get_default_client(peck);
		eis_client_disconnect(client);
	}

	peck_dispatch_ei(peck);
	with_client(peck) {
		_unref_(ei_event) *seat_removed =
			peck_ei_next_event(ei, EI_EVENT_SEAT_REMOVED);

		_unref_(ei_event) *disconnect =
			peck_ei_next_event(ei, EI_EVENT_DISCONNECT);
	}

	return MUNIT_OK;
}

MUNIT_TEST(test_client_is_sender)
{
	_unref_(peck) *peck = peck_new_context(PECK_EI_SENDER);

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_NONE);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_NONE);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_CONNECT);
	peck_dispatch_until_stable(peck);

	with_server(peck) {
		_unref_(eis_event) *connect = peck_eis_next_event(eis, EIS_EVENT_CLIENT_CONNECT);
		struct eis_client *client = eis_event_get_client(connect);
		munit_assert_true(eis_client_is_sender(client));
	}

	return MUNIT_OK;
}

MUNIT_TEST(test_client_is_receiver)
{
	_unref_(peck) *peck = peck_new_context(PECK_EI_RECEIVER);

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_NONE);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_NONE);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_CONNECT);
	peck_dispatch_until_stable(peck);

	with_server(peck) {
		_unref_(eis_event) *connect = peck_eis_next_event(eis, EIS_EVENT_CLIENT_CONNECT);
		struct eis_client *client = eis_event_get_client(connect);
		munit_assert_false(eis_client_is_sender(client));
	}

	return MUNIT_OK;
}


/* Emulates the XWayland behavior for calling
 * xdotool mousemove_relative -- -1 10
 */
MUNIT_TEST(test_xdotool_rel_motion)
{
	_unref_(peck) *peck = peck_new();

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL);
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ADD_POINTER);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_AUTODEVICES);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_AUTOSTART);
	peck_dispatch_until_stable(peck);

	with_client(peck) {
		struct ei_device *device = peck_ei_get_default_pointer(peck);
		ei_device_pointer_motion(device, -1, 10);
		ei_device_frame(device, peck_ei_now(peck));
		ei_device_close(device);
		ei_unref(ei);
		peck_drop_ei(peck);
	}

	peck_dispatch_eis(peck);

	with_server(peck) {
		_unref_(eis_event) *motion =
			peck_eis_next_event(eis, EIS_EVENT_POINTER_MOTION);
		_unref_(eis_event) *stop =
			peck_eis_next_event(eis, EIS_EVENT_DEVICE_STOP_EMULATING);
		_unref_(eis_event) *close =
			peck_eis_next_event(eis, EIS_EVENT_DEVICE_CLOSED);
		_unref_(eis_event) *unbind =
			peck_eis_next_event(eis, EIS_EVENT_SEAT_BIND);
		munit_assert_false(eis_event_seat_has_capability(unbind, EIS_DEVICE_CAP_POINTER));
		munit_assert_false(eis_event_seat_has_capability(unbind, EIS_DEVICE_CAP_POINTER_ABSOLUTE));
		munit_assert_false(eis_event_seat_has_capability(unbind, EIS_DEVICE_CAP_KEYBOARD));
		munit_assert_false(eis_event_seat_has_capability(unbind, EIS_DEVICE_CAP_TOUCH));
		_unref_(eis_event) *disconnect =
			peck_eis_next_event(eis, EIS_EVENT_CLIENT_DISCONNECT);

		peck_assert_no_eis_events(eis);
	}

	return MUNIT_OK;
}

MUNIT_TEST(test_ei_exceed_write_buffer)
{
	_unref_(peck) *peck = peck_new();

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_NONE);
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL);
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ADD_POINTER);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_AUTODEVICES);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_AUTOSTART);
	peck_dispatch_until_stable(peck);

	uint64_t toffset = peck_ei_now(peck);
	unsigned int count = 10000; /* Large enough to require several flushes */
	struct eis_event *events[count];

	with_server(peck) {
		peck_drain_eis(eis);
	}

	with_client(peck) {
		struct ei_device *device = peck_ei_get_default_pointer(peck);
		for (unsigned int i = 0; i < count/2; i++) {
			ei_device_pointer_motion(device, -1, 10);
			ei_device_frame(device, toffset + i);
		}
	}

	peck_dispatch_eis(peck);

	unsigned int before_buffer = 0;
	with_server(peck) {
		struct eis_event *next;
		while ((next = eis_get_event(eis))) {
			events[before_buffer++] = next;
			munit_assert_uint(before_buffer, <=, count);
		}
	}

	unsigned int nevents = before_buffer;
	if (before_buffer < count) {
		/* We sent >socket buffersize events, so we don't expect to receive all of those */
		/* Calling dispatch (on both) should flush some more */
		peck_dispatch_until_stable(peck);

		with_server(peck) {
			struct eis_event *next;
			while ((next = eis_get_event(eis))) {
				events[nevents++] = next;

				munit_assert_uint(nevents, <=, count);

				if (nevents % 50 == 0)
					peck_dispatch_ei(peck);

				_unref_(eis_event) *next = eis_peek_event(eis);
				if (!next)
					peck_dispatch_eis(peck);
			}
		}
	};

	munit_assert_uint(nevents, ==, count);

	for (unsigned int i = 0; i < count; i += 2) {
		_unref_(eis_event) *motion = events[i];
		_unref_(eis_event) *frame = events[i+1];

		uint64_t time = eis_event_get_time(frame);

		munit_assert_string_equal(peck_eis_event_name(motion),
					  peck_eis_event_type_name(EIS_EVENT_POINTER_MOTION));
		munit_assert_string_equal(peck_eis_event_name(frame),
					  peck_eis_event_type_name(EIS_EVENT_FRAME));
		munit_assert_int64(time, ==, toffset + i/2);
	}

	/* Our events are as expected but we never got EAGAIN on
	 * the buffer, so let's count this test as skipped */
	if (before_buffer == count)
		return MUNIT_SKIP;

	return MUNIT_OK;
}

MUNIT_TEST(test_ei_exceed_write_buffer_cleanup)
{
	_unref_(peck) *peck = peck_new();
	struct ei *ei = peck_get_ei(peck);

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_NONE);
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL);
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ADD_POINTER);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_AUTODEVICES);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_AUTOSTART);
	peck_dispatch_until_stable(peck);

	uint64_t toffset = peck_ei_now(peck);
	unsigned int count = 10000; /* Large enough to require several flushes */

	with_server(peck) {
		peck_drain_eis(eis);
	}

	with_client(peck) {
		struct ei_device *device = peck_ei_get_default_pointer(peck);
		for (unsigned int i = 0; i < count/2; i++) {
			ei_device_pointer_motion(device, -1, 10);
			ei_device_frame(device, toffset + i);
		}
	}

	peck_dispatch_eis(peck);

	unsigned int before_buffer = 0;
	with_server(peck) {
		struct eis_event *next;
		while ((next = eis_get_event(eis))) {
			munit_assert_uint(before_buffer, <=, count);
			eis_event_unref(next);
		}
	}

	/* Our events are as expected but we never got EAGAIN on
	 * the buffer, so let's count this test as skipped */
	if (before_buffer == count)
		return MUNIT_SKIP;

	/* Make sure cleanup is handled properly */
	peck_drop_ei(peck);
	ei_unref(ei);

	return MUNIT_OK;
}

MUNIT_TEST(test_ei_invalid_object_ids)
{
	_unref_(peck) *peck = peck_new();
	_unref_(eis_device) *eis_device = NULL;
	_unref_(eis_device) *dummy = NULL;

	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_NONE);
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_AUTODEVICES);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_AUTOSTART);

	peck_dispatch_until_stable(peck);

	with_server(peck) {
		struct eis_seat *seat = peck_eis_get_default_seat(peck);
		eis_device = eis_seat_new_device(seat);
		eis_device_configure_name(eis_device, __func__);
		eis_device_configure_capability(eis_device, EIS_DEVICE_CAP_POINTER);
		eis_device_add(eis_device);
		eis_device_resume(eis_device);
	}

	/* Client has device now, remove it from server but don't tell client yet */
	peck_dispatch_until_stable(peck);

	with_server(peck) {
		eis_device_pause(eis_device);
		eis_device_remove(eis_device);
	}
	peck_dispatch_eis(peck);

	with_client(peck) {
		struct ei_device *device = peck_ei_get_default_pointer(peck);
		ei_device_start_emulating(device, 0);
		ei_device_pointer_motion(device, -1, 1);
		ei_device_frame(device, ei_now(ei));
		ei_device_stop_emulating(device);
	}

	/* This call should trigger invalid object messages */
	peck_dispatch_eis(peck);

	/* This call should receive the invalid object messages */
	peck_dispatch_ei(peck);

	/* Create a second device to test the defunct cleaning code, this one is
	 * just used to send messages that will trigger ei_dispatch() */
	with_server(peck) {
		struct eis_seat *seat = peck_eis_get_default_seat(peck);
		dummy = eis_seat_new_device(seat);
		eis_device_configure_name(dummy, __func__);
		eis_device_configure_capability(dummy, EIS_DEVICE_CAP_POINTER);
		eis_device_add(dummy);
		eis_device_resume(dummy);
	}

	peck_dispatch_until_stable(peck);

	/* trigger the defunct object code - we can't actually test anything here
	 * beyond hoping it crashes if there's a bug */
	peck_ei_add_time_offset(peck, s2us(6));

	for (int i = 0; i < 30; i++) {
		with_server(peck) {
			eis_device_pause(dummy);
			eis_device_resume(dummy);
		}
		peck_dispatch_ei(peck);
	}

	return MUNIT_OK;
}
