/*
 * notify_test.c - unit tests for daemon fanotify metadata handling
 */
#include "config.h"
#include <error.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdatomic.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/fanotify.h>
#include <sys/socket.h>
#include <sys/timerfd.h>
#include <sys/un.h>
#include <time.h>
#include <sys/wait.h>
#include <unistd.h>

#include "failure-action.h"
#include "fanotify-fs-error.h"
#include "notify.h"
#include "decision-context.h"
#include "decision-worker.h"
#include "policy.h"
#include "decision-config.h"
#include "decision-defer.h"
#include "decision-timing.h"
#include "event.h"
#include "lru.h"
#include "process.h"
#include "state-report.h"
#include "subject.h"
#include "systemd-notify.h"

#ifndef FAN_Q_OVERFLOW
#define FAN_Q_OVERFLOW		0x00004000
#endif

#if defined(FAPOLICYD_ENABLE_FANOTIFY_FS_ERROR) && \
	defined(FAN_FS_ERROR) && defined(FAN_REPORT_FID) && \
	defined(FAN_MARK_FILESYSTEM) && \
	defined(FAN_EVENT_INFO_TYPE_ERROR) && \
	defined(FAN_EVENT_INFO_TYPE_FID)
#define TEST_HAVE_FAN_FS_ERROR 1

struct test_fanotify_fs_error_info {
	struct fanotify_event_info_header hdr;
	int32_t error;
	uint32_t error_count;
};
#else
#define TEST_HAVE_FAN_FS_ERROR 0
#endif

extern atomic_bool run_stats;
extern atomic_uint signal_report_requests;
extern conf_t config;

#define DISPATCH_TRACE_CAP 32

#define CHECK(expr, code, msg) \
	do { \
		if (!(expr)) \
			error(1, 0, "%s", msg); \
	} while (0)

struct dispatch_trace_event {
	pid_t pid;
	int fd;
};

struct dispatch_trace_observed {
	unsigned int worker;
	pid_t pid;
	int fd;
};

/*
 * seed_building_subject - place one live BUILDING subject in the cache.
 * @pid: live process id used for the cached subject fingerprint.
 * @slot: subject-cache slot that should be occupied.
 *
 * The deferred-dispatch regression needs event_subject_slot_is_blocked() to
 * take the same branch it takes for a real pre-STATE_FULL subject. Use a live
 * proc fingerprint so the stale-BUILDING guard does not evict the fixture.
 *
 * Returns nothing. Exits on test failure.
 */
static void seed_building_subject(pid_t pid, unsigned int slot)
{
	struct decision_context *ctx = decision_context_current();
	struct proc_info *info;
	s_array *subject;
	QNode *node;

	CHECK(ctx != NULL && ctx->subject_cache != NULL, 151,
	      "[ERROR:151] decision context missing subject cache");

	info = stat_proc_entry(pid);
	CHECK(info != NULL, 152,
	      "[ERROR:152] failed to create subject fingerprint");

	subject = malloc(sizeof(*subject));
	CHECK(subject != NULL, 153,
	      "[ERROR:153] failed to allocate subject fixture");
	CHECK(subject_create(subject) == 0, 154,
	      "[ERROR:154] failed to initialize subject fixture");
	subject->info = info;

	node = check_lru_cache(ctx->subject_cache, slot);
	CHECK(node != NULL && node->item == NULL, 155,
	      "[ERROR:155] failed to reserve subject cache slot");
	node->item = subject;
}

void do_stat_report_reset(FILE *f, int shutdown, int reset)
{
	(void)f;
	(void)shutdown;
	(void)reset;
}

void do_state_report(FILE *f, int shutdown)
{
	(void)f;
	(void)shutdown;
}

void do_metrics_report_reset(FILE *f, int reset)
{
	(void)f;
	(void)reset;
}

/*
 * read_decision_report - capture decision_report output for assertions.
 * @buf: destination buffer.
 * @size: size of @buf.
 * Returns nothing. Exits if the temporary stream cannot be used.
 */
static void read_decision_report(char *buf, size_t size, int reset)
{
	FILE *f = tmpfile();
	size_t used;

	if (f == NULL)
		error(1, 0, "tmpfile failed");

	decision_report_reset(f, reset);
	fflush(f);
	rewind(f);
	used = fread(buf, 1, size - 1, f);
	buf[used] = 0;
	fclose(f);
}

/*
 * read_decision_metrics_report - capture metrics decision header output.
 * @buf: destination buffer.
 * @size: size of @buf.
 * Returns nothing. Exits if the temporary stream cannot be used.
 */
static void read_decision_metrics_report(char *buf, size_t size)
{
	FILE *f = tmpfile();
	size_t used;

	if (f == NULL)
		error(1, 0, "tmpfile failed");

	decision_report_metrics_reset(f, 0);
	fflush(f);
	rewind(f);
	used = fread(buf, 1, size - 1, f);
	buf[used] = 0;
	fclose(f);
}

/*
 * read_fanotify_queue_report - capture fanotify queue metrics output.
 * @buf: destination buffer.
 * @size: size of @buf.
 * @reset: non-zero resets interval queue counters.
 * Returns nothing. Exits if the temporary stream cannot be used.
 */
static void read_fanotify_queue_report(char *buf, size_t size, int reset)
{
	FILE *f = tmpfile();
	size_t used;

	if (f == NULL)
		error(1, 0, "tmpfile failed");

	fanotify_queue_report_reset(f, reset);
	fflush(f);
	rewind(f);
	used = fread(buf, 1, size - 1, f);
	buf[used] = 0;
	fclose(f);
}

/*
 * read_fanotify_health_report - capture fanotify worker health output.
 * @buf: destination buffer.
 * @size: size of @buf.
 * Returns nothing. Exits if the temporary stream cannot be used.
 */
static void read_fanotify_health_report(char *buf, size_t size)
{
	FILE *f = tmpfile();
	size_t used;

	if (f == NULL)
		error(1, 0, "tmpfile failed");

	fanotify_queue_health_report(f);
	fflush(f);
	rewind(f);
	used = fread(buf, 1, size - 1, f);
	buf[used] = 0;
	fclose(f);
}

/*
 * test_nonblocking_handler_returns - run an empty nonblocking read in a child.
 * @set_fd: test hook that installs the empty pipe descriptor.
 * @handler: fanotify read loop under test.
 * @fd: nonblocking read side of an empty pipe.
 *
 * The alarm makes the regression fail rather than hanging the test suite when
 * a handler retries EAGAIN forever. Returns nothing. Exits on test failure.
 */
static void test_nonblocking_handler_returns(void (*set_fd)(int),
		void (*handler)(void), int fd)
{
	int status;
	pid_t pid;

	pid = fork();
	CHECK(pid >= 0, 168, "[ERROR:168] fork failed");
	if (pid == 0) {
		set_fd(fd);
		alarm(1);
		handler();
		_exit(0);
	}

	CHECK(waitpid(pid, &status, 0) == pid, 169,
	      "[ERROR:169] waitpid failed");
	CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0, 170,
	      "[ERROR:170] nonblocking fanotify handler did not return");
}

/*
 * test_nonblocking_fanotify_reads - verify EAGAIN ends both fanotify drains.
 *
 * A poll readiness notification can race with another drain. The next
 * nonblocking read then returns EAGAIN and must return to poll instead of
 * spinning. Returns nothing. Exits on test failure.
 */
static void test_nonblocking_fanotify_reads(void)
{
	int pipe_fds[2];
	int flags;

	CHECK(pipe(pipe_fds) == 0, 171,
	      "[ERROR:171] nonblocking pipe setup failed");
	flags = fcntl(pipe_fds[0], F_GETFL);
	CHECK(flags >= 0, 172, "[ERROR:172] failed reading pipe flags");
	CHECK(fcntl(pipe_fds[0], F_SETFL, flags | O_NONBLOCK) == 0, 173,
	      "[ERROR:173] failed setting nonblocking pipe");

	test_nonblocking_handler_returns(test_notify_set_fanotify_fd,
					 handle_events, pipe_fds[0]);
	test_nonblocking_handler_returns(test_fanotify_fs_error_set_fd,
					 fanotify_fs_error_handle_events, pipe_fds[0]);
	close(pipe_fds[0]);
	close(pipe_fds[1]);
}

/*
 * test_fatal_signal_fanotify_close - verify emergency descriptor ownership.
 *
 * The first close must release the registered descriptor. A later emergency
 * close must not close an unrelated descriptor that reused the same integer.
 * Returns nothing. Exits on test failure.
 */
static void test_fatal_signal_fanotify_close(void)
{
	int pipe_fds[2];
	int group_fd;

	CHECK(pipe(pipe_fds) == 0, 193,
	      "[ERROR:193] emergency close pipe setup failed");
	group_fd = pipe_fds[0];
	test_notify_set_fanotify_fd(group_fd);
	fanotify_close_on_fatal_signal();

	errno = 0;
	CHECK(fcntl(group_fd, F_GETFD) == -1 && errno == EBADF, 194,
	      "[ERROR:194] emergency close retained fanotify descriptor");
	CHECK(dup2(pipe_fds[1], group_fd) == group_fd, 195,
	      "[ERROR:195] failed reusing closed fanotify descriptor");

	fanotify_close_on_fatal_signal();
	CHECK(fcntl(group_fd, F_GETFD) >= 0, 196,
	      "[ERROR:196] repeated emergency close closed reused descriptor");

	test_notify_set_fanotify_fd(-1);
	close(group_fd);
	close(pipe_fds[1]);
}

/*
 * test_shutdown_fanotify_close - keep late replies off a reused group fd.
 *
 * Normal shutdown must close the group before worker joins to release watched
 * self-opens. A late worker still carries the original descriptor number, so
 * prove its cleanup closes the event fd without writing to a pipe that reused
 * the retired group descriptor.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_shutdown_fanotify_close(void)
{
	struct fanotify_event_metadata metadata = {
		.event_len = sizeof(metadata),
		.vers = FANOTIFY_METADATA_VERSION,
		.mask = FAN_OPEN_PERM,
		.pid = 1234,
	};
	char byte;
	int event[2], group[2], reused[2];
	int flags, group_fd;

	CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, group) == 0, 210,
	      "[ERROR:210] shutdown group socket setup failed");
	CHECK(pipe(reused) == 0, 211,
	      "[ERROR:211] shutdown reused pipe setup failed");
	CHECK(pipe(event) == 0, 212,
	      "[ERROR:212] shutdown event pipe setup failed");
	group_fd = group[0];
	metadata.fd = event[0];

	CHECK(reply_event_init(group_fd) == 0, 213,
	      "[ERROR:213] shutdown response registration failed");
	test_notify_set_fanotify_fd(group_fd);
	fanotify_close_for_shutdown();
	errno = 0;
	CHECK(fcntl(group_fd, F_GETFD) == -1 && errno == EBADF, 214,
	      "[ERROR:214] shutdown retained fanotify descriptor");

	CHECK(dup2(reused[1], group_fd) == group_fd, 215,
	      "[ERROR:215] failed reusing shutdown fanotify descriptor");
	flags = fcntl(reused[0], F_GETFL);
	CHECK(flags >= 0 && fcntl(reused[0], F_SETFL, flags | O_NONBLOCK) == 0,
	      216, "[ERROR:216] failed setting reused pipe nonblocking");
	reply_event(group_fd, &metadata, FAN_ALLOW, NULL);
	errno = 0;
	CHECK(read(reused[0], &byte, sizeof(byte)) == -1 && errno == EAGAIN,
	      217, "[ERROR:217] late reply wrote to reused descriptor");
	errno = 0;
	CHECK(close(event[0]) == -1 && errno == EBADF, 218,
	      "[ERROR:218] late reply retained event descriptor");

	fanotify_close_for_shutdown();
	CHECK(fcntl(group_fd, F_GETFD) >= 0, 219,
	      "[ERROR:219] repeated shutdown closed reused descriptor");

	test_notify_set_fanotify_fd(-1);
	close(group_fd);
	close(group[1]);
	close(reused[0]);
	close(reused[1]);
	close(event[1]);
}

/*
 * test_mount_list_reconcile - verify a complete mount snapshot is merged.
 *
 * The active list must retain existing marks, flag disappeared mounts for
 * removal, and transfer newly parsed paths without allocating during the
 * update. Returns nothing. Exits on test failure.
 */
static void test_mount_list_reconcile(void)
{
	mlist current, next;

	mlist_create(&current);
	mlist_create(&next);
	CHECK(mlist_append(&current, "/old") == 0, 185,
	      "[ERROR:185] failed adding old mount fixture");
	CHECK(mlist_append(&current, "/kept") == 0, 186,
	      "[ERROR:186] failed adding kept mount fixture");
	CHECK(mlist_append(&next, "/kept") == 0, 187,
	      "[ERROR:187] failed adding replacement kept fixture");
	CHECK(mlist_append(&next, "/new") == 0, 188,
	      "[ERROR:188] failed adding replacement new fixture");

	mlist_reconcile(&current, &next);
	CHECK(mlist_find(&current, "/old") && current.cur->status == MNT_DELETE,
	      189, "[ERROR:189] disappeared mount was not marked deleted");
	CHECK(mlist_find(&current, "/kept") &&
	      current.cur->status == MNT_NO_CHANGE, 190,
	      "[ERROR:190] retained mount was not preserved");
	CHECK(mlist_find(&current, "/new") && current.cur->status == MNT_ADD,
	      191, "[ERROR:191] new mount was not marked for addition");
	CHECK(mlist_first(&next) == NULL, 192,
	      "[ERROR:192] replacement list retained transferred nodes");

	mlist_clear(&current);
	mlist_clear(&next);
}

/*
 * test_systemd_notify_runtime_gate - verify debug-mode notify suppression.
 *
 * Debug runs can inherit arbitrary NOTIFY_SOCKET/WATCHDOG_* values from a
 * shell. The runtime gate must make the watchdog helper ignore those values
 * before it consults the environment.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_systemd_notify_runtime_gate(void)
{
	char pid[32];

	snprintf(pid, sizeof(pid), "%ld", (long)getpid());
	CHECK(setenv("NOTIFY_SOCKET", "/tmp/fapolicyd-test-notify", 1) == 0,
	      122, "[ERROR:122] failed setting NOTIFY_SOCKET");
	CHECK(setenv("WATCHDOG_USEC", "1234567", 1) == 0,
	      123, "[ERROR:123] failed setting WATCHDOG_USEC");
	CHECK(setenv("WATCHDOG_PID", pid, 1) == 0,
	      124, "[ERROR:124] failed setting WATCHDOG_PID");

	systemd_notify_set_enabled(0);
	CHECK(systemd_watchdog_interval_usec() == 0, 125,
	      "[ERROR:125] disabled notify gate used watchdog environment");
	CHECK(!systemd_watchdog_enabled(), 126,
	      "[ERROR:126] disabled notify gate enabled watchdog");

	systemd_notify_set_enabled(1);
#ifdef FAPOLICYD_ENABLE_SYSTEMD_WATCHDOG
	CHECK(systemd_watchdog_interval_usec() == 1234567, 127,
	      "[ERROR:127] enabled notify gate ignored watchdog environment");
#else
	CHECK(systemd_watchdog_interval_usec() == 0, 127,
	      "[ERROR:127] disabled build returned watchdog interval");
#endif

	unsetenv("WATCHDOG_PID");
	unsetenv("WATCHDOG_USEC");
	unsetenv("NOTIFY_SOCKET");
}

/*
 * test_systemd_notify_full_queue - verify notification sends never block.
 *
 * A full AF_UNIX datagram receive queue must make READY fail with EAGAIN so
 * the daemon can service fanotify events before retrying. Returns nothing.
 * Exits on test failure.
 */
static void test_systemd_notify_full_queue(void)
{
#ifdef FAPOLICYD_ENABLE_SYSTEMD_WATCHDOG
	struct sockaddr_un addr = { .sun_family = AF_UNIX };
	char notify_path[sizeof(addr.sun_path)];
	char buf[8];
	int fill_fds[16];
	socklen_t addr_len;
	size_t path_len;
	unsigned int fill_fd_count = 0, i;
	int receive_fd, rc;

	snprintf(notify_path, sizeof(notify_path),
		 "@fapolicyd-notify-test-%ld", (long)getpid());
	path_len = strlen(notify_path + 1);
	memcpy(addr.sun_path + 1, notify_path + 1, path_len);
	addr_len = offsetof(struct sockaddr_un, sun_path) + 1 + path_len;

	receive_fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
	CHECK(receive_fd >= 0, 193,
	      "[ERROR:193] failed creating notify receive socket");
	CHECK(bind(receive_fd, (const struct sockaddr *)&addr, addr_len) == 0,
	      194, "[ERROR:194] failed binding notify receive socket");
	/* A sender can fill its own buffer before the shared receive queue. */
	for (i = 0; i < 16; i++) {
		int sent = 0;

		fill_fds[i] = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
		CHECK(fill_fds[i] >= 0, 195,
		      "[ERROR:195] failed creating notify fill socket");
		fill_fd_count++;
		do {
			rc = sendto(fill_fds[i], "FILL", 4, MSG_DONTWAIT,
				    (const struct sockaddr *)&addr, addr_len);
			if (rc == 4)
				sent++;
		} while (rc == 4);
		CHECK(rc == -1 && (errno == EAGAIN || errno == EWOULDBLOCK),
		      196, "[ERROR:196] failed filling notify receive queue");
		if (sent == 0)
			break;
	}
	CHECK(i < 16, 201,
	      "[ERROR:201] notify queue exceeds test fill capacity");
	CHECK(setenv("NOTIFY_SOCKET", notify_path, 1) == 0, 197,
	      "[ERROR:197] failed setting full notify socket");

	systemd_notify_set_enabled(1);
	alarm(1);
	rc = systemd_notify_ready();
	alarm(0);
	CHECK(rc == -1 && (errno == EAGAIN || errno == EWOULDBLOCK), 198,
	      "[ERROR:198] full notify queue did not return EAGAIN");
	CHECK(recv(receive_fd, buf, sizeof(buf), MSG_DONTWAIT) == 4, 199,
	      "[ERROR:199] failed draining notify receive queue");
	CHECK(systemd_notify_ready() == 0, 200,
	      "[ERROR:200] READY retry failed after queue drain");

	unsetenv("NOTIFY_SOCKET");
	for (i = 0; i < fill_fd_count; i++)
		close(fill_fds[i]);
	close(receive_fd);
#endif
}

/*
 * read_operating_mode_report - capture the state operating mode section.
 * @buf: destination buffer.
 * @size: size of @buf.
 * Returns nothing. Exits if the temporary stream cannot be used.
 */
static void read_operating_mode_report(char *buf, size_t size)
{
	struct state_report_operating_mode mode = {
		.permissive = false,
		.integrity = "sha256",
		.reset_strategy = "manual",
		.config_generation = 5,
		.ruleset_generation = 7,
		.config_effective_since = 1,
		.ruleset_effective_since = 1,
		.trust_db = {
			.generation = 9,
			.entries = 11,
			.publish_time = 1,
			.lmdb_generation = 3,
			.lmdb_publish_time = 1,
		},
		.config = &config,
	};
	FILE *f = tmpfile();
	size_t used;

	if (f == NULL)
		error(1, 0, "tmpfile failed");

	config.timing_collection = TIMING_COLLECTION_MANUAL;
	state_report_operating_mode(f, &mode);
	fflush(f);
	rewind(f);
	used = fread(buf, 1, size - 1, f);
	buf[used] = 0;
	fclose(f);
}

#if TEST_HAVE_FAN_FS_ERROR
/*
 * read_fs_error_report - capture recent FAN_FS_ERROR detail output.
 * @buf: destination buffer.
 * @size: size of @buf.
 * Returns nothing. Exits if the temporary stream cannot be used.
 */
static void read_fs_error_report(char *buf, size_t size)
{
	FILE *f = tmpfile();
	size_t used;

	if (f == NULL)
		error(1, 0, "tmpfile failed");

	fanotify_fs_error_report(f);
	fflush(f);
	rewind(f);
	used = fread(buf, 1, size - 1, f);
	buf[used] = 0;
	fclose(f);
}
#endif

/*
 * test_operating_mode_report_order - verify state field order.
 *
 * Config generation and ruleset generation are adjacent so readers can see the
 * decision configuration identity beside the active policy identity.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_operating_mode_report_order(void)
{
	const char *config_generation, *ruleset, *trust_db, *lmdb_env;
	const char *timing_mode;
	char report[1024];

	read_operating_mode_report(report, sizeof(report));
	config_generation = strstr(report,
				   "Config generation: 5 "
				   "(effective since ");
	ruleset = strstr(report,
			 "Ruleset generation: 7 "
			 "(effective since ");
	trust_db = strstr(report,
			  "Trust database generation: 9 "
			  "(effective since ");
	lmdb_env = strstr(report,
			  "LMDB environment generation: 3 "
			  "(effective since ");
	timing_mode = strstr(report, "Timing collection mode: manual\n");
	CHECK(config_generation != NULL, 68,
	      "[ERROR:68] operating mode report missing config generation");
	CHECK(ruleset != NULL, 59,
	      "[ERROR:59] operating mode report missing ruleset field");
	CHECK(trust_db != NULL, 69,
	      "[ERROR:69] operating mode report missing trust DB generation");
	CHECK(lmdb_env != NULL, 71,
	      "[ERROR:71] operating mode report missing LMDB generation");
	CHECK(strstr(report, "Trust database entries: 11\n") != NULL, 70,
	      "[ERROR:70] operating mode report missing trust DB entries");
	CHECK(timing_mode != NULL, 58,
	      "[ERROR:58] operating mode report missing timing mode field");
	CHECK(config_generation < ruleset && ruleset < trust_db &&
	      trust_db < lmdb_env && lmdb_env < timing_mode, 60,
	      "[ERROR:60] generation fields were not grouped");
}

/*
 * test_shutdown_deferred_events - verify notify shutdown replies once.
 *
 * Deferred permission events own their metadata fd until shutdown cleanup
 * replies and closes it. Use pipe read ends as stand-ins for fanotify fds so
 * the test can prove the production cleanup path closes each one exactly once.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_shutdown_deferred_events(void)
{
	decision_event_t event;
	int pipes[3][2];
	unsigned int i;

	CHECK(test_notify_defer_reset(1) == 0, 40,
	      "[ERROR:40] notify defer reset failed");

	for (i = 0; i < 3; i++) {
		CHECK(pipe(pipes[i]) == 0, 41,
		      "[ERROR:41] pipe setup failed");
		memset(&event, 0, sizeof(event));
		event.metadata.fd = pipes[i][0];
		event.metadata.pid = 500 + i;
		event.metadata.mask = FAN_OPEN_PERM;
		event.subject_slot = i;
		event.completed_subject_slot = DECISION_EVENT_NO_SLOT;
		CHECK(test_notify_defer_push(&event) == 0, 42,
		      "[ERROR:42] notify defer push failed");
	}

	__atomic_store_n(&config.permissive, true, __ATOMIC_RELAXED);
	CHECK(decision_config_publish(&config) == 0, 66,
	      "[ERROR:66] decision config publish failed");
	CHECK(test_notify_shutdown_deferred_events() == 3, 61,
	      "[ERROR:61] deferred shutdown count mismatch");
	__atomic_store_n(&config.permissive, false, __ATOMIC_RELAXED);
	CHECK(decision_config_publish(&config) == 0, 69,
	      "[ERROR:69] decision config reset failed");
	decision_config_destroy();

	for (i = 0; i < 3; i++) {
		errno = 0;
		CHECK(close(pipes[i][0]) == -1 && errno == EBADF, 43,
		      "[ERROR:43] deferred fd was not closed exactly once");
		close(pipes[i][1]);
	}

	test_notify_defer_destroy();
}

/*
 * test_shutdown_queued_events - verify notify shutdown drains queue fds.
 *
 * The decision worker can observe stop before it processes every event already
 * accepted from fanotify. Those queued permission events must still be answered
 * during shutdown or their requesting tasks can remain blocked.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_shutdown_queued_events(void)
{
	decision_event_t event;
	int pipes[3][2];
	unsigned int i;

	CHECK(test_notify_queue_reset(4) == 0, 62,
	      "[ERROR:62] notify queue reset failed");

	for (i = 0; i < 3; i++) {
		CHECK(pipe(pipes[i]) == 0, 63,
		      "[ERROR:63] queue pipe setup failed");
		memset(&event, 0, sizeof(event));
		event.metadata.fd = pipes[i][0];
		event.metadata.pid = 600 + i;
		event.metadata.mask = FAN_OPEN_PERM;
		event.subject_slot = i;
		event.completed_subject_slot = DECISION_EVENT_NO_SLOT;
		CHECK(test_notify_queue_push(&event) == 0, 64,
		      "[ERROR:64] notify queue push failed");
	}

	__atomic_store_n(&config.permissive, true, __ATOMIC_RELAXED);
	CHECK(decision_config_publish(&config) == 0, 67,
	      "[ERROR:67] decision config publish failed");
	CHECK(test_notify_shutdown_queued_events() == 3, 65,
	      "[ERROR:65] queued shutdown count mismatch");
	__atomic_store_n(&config.permissive, false, __ATOMIC_RELAXED);
	CHECK(decision_config_publish(&config) == 0, 70,
	      "[ERROR:70] decision config reset failed");
	decision_config_destroy();

	for (i = 0; i < 3; i++) {
		errno = 0;
		CHECK(close(pipes[i][0]) == -1 && errno == EBADF, 71,
		      "[ERROR:71] queued fd was not closed exactly once");
		close(pipes[i][1]);
	}

	test_notify_queue_destroy();
}

/*
 * test_shutdown_rejects_late_enqueue - keep queue ownership safe at shutdown.
 *
 * Once shutdown begins, an event that was not already handed to a worker must
 * remain owned by the dispatcher. Events accepted before that boundary still
 * need the normal queue-drain reply. Returns nothing. Exits on test failure.
 */
static void test_shutdown_rejects_late_enqueue(void)
{
	int accepted[2], rejected[2];

	CHECK(test_notify_queue_reset(2) == 0, 174,
	      "[ERROR:174] queue reset failed for shutdown admission test");
	CHECK(pipe(accepted) == 0, 175,
	      "[ERROR:175] accepted pipe setup failed");
	CHECK(test_notify_enqueue_pid_fd(700, accepted[0]) == 0, 176,
	      "[ERROR:176] pre-shutdown event was not queued");

	test_notify_worker_pool_begin_shutdown();
	CHECK(pipe(rejected) == 0, 177,
	      "[ERROR:177] rejected pipe setup failed");
	errno = 0;
	CHECK(test_notify_enqueue_pid_fd(701, rejected[0]) == -1 &&
	      errno == ESHUTDOWN, 178,
	      "[ERROR:178] shutdown accepted a late event");
	CHECK(test_notify_worker_queue_depth(0) == 1, 179,
	      "[ERROR:179] shutdown changed accepted queue ownership");

	__atomic_store_n(&config.permissive, true, __ATOMIC_RELAXED);
	CHECK(decision_config_publish(&config) == 0, 180,
	      "[ERROR:180] decision config publish failed");
	CHECK(test_notify_shutdown_queued_events() == 1, 181,
	      "[ERROR:181] accepted event was not drained at shutdown");
	__atomic_store_n(&config.permissive, false, __ATOMIC_RELAXED);
	CHECK(decision_config_publish(&config) == 0, 182,
	      "[ERROR:182] decision config reset failed");
	decision_config_destroy();

	errno = 0;
	CHECK(close(accepted[0]) == -1 && errno == EBADF, 183,
	      "[ERROR:183] accepted event fd was not closed");
	CHECK(close(rejected[0]) == 0, 184,
	      "[ERROR:184] rejected event fd changed ownership");
	close(accepted[1]);
	close(rejected[1]);
	test_notify_queue_destroy();
}

/*
 * test_dispatcher_worker_routing - verify stable subject worker selection.
 *
 * The dispatcher must not choose workers by queue pressure or round-robin:
 * all events with the same subject key need the same decision owner so the
 * subject cache sees an ordered startup sequence.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_dispatcher_worker_routing(void)
{
	CHECK(test_notify_worker_index(1001, 4) == 1, 77,
	      "[ERROR:77] pid routing did not use stable modulo key");
	CHECK(test_notify_worker_index(1001, 4) ==
	      test_notify_worker_index(1001, 4), 78,
	      "[ERROR:78] same pid did not route to same worker");
	CHECK(test_notify_worker_index(1002, 4) == 2, 79,
	      "[ERROR:79] adjacent pid routing mismatch");
	CHECK(test_notify_worker_index(-1, 4) == 0, 80,
	      "[ERROR:80] invalid pid did not use fallback key");
	CHECK(test_notify_worker_index(1001, 0) == 0, 81,
	      "[ERROR:81] zero worker fallback changed");
}

/*
 * test_dispatcher_same_pid_ordering - verify FIFO ownership for one pid.
 *
 * Same-pid fanotify events must enter the same worker queue in arrival order
 * so the subject startup state machine sees a coherent sequence.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_dispatcher_same_pid_ordering(void)
{
	pid_t pids[4];
	int fds[4];
	unsigned int worker;

	CHECK(test_notify_worker_pool_reset(4, 8) == 0, 91,
	      "[ERROR:91] worker pool reset failed");
	CHECK(fanotify_active_worker_count() == 4, 92,
	      "[ERROR:92] active worker count mismatch");

	worker = test_notify_worker_index(1001, 4);
	CHECK(test_notify_enqueue_pid_fd(1001, 10) == 0, 93,
	      "[ERROR:93] first same-pid enqueue failed");
	CHECK(test_notify_enqueue_pid_fd(1001, 11) == 0, 94,
	      "[ERROR:94] second same-pid enqueue failed");
	CHECK(test_notify_enqueue_pid_fd(1001, 12) == 0, 95,
	      "[ERROR:95] third same-pid enqueue failed");
	CHECK(test_notify_worker_queue_depth(worker) == 3, 96,
	      "[ERROR:96] same-pid events did not share one queue");
	CHECK(test_notify_worker_drain(worker, pids, fds, 4) == 3, 97,
	      "[ERROR:97] same-pid drain count mismatch");
	CHECK(pids[0] == 1001 && pids[1] == 1001 && pids[2] == 1001,
	      98, "[ERROR:98] drained pid changed");
	CHECK(fds[0] == 10 && fds[1] == 11 && fds[2] == 12, 99,
	      "[ERROR:99] same-pid queue order changed");

	test_notify_worker_pool_destroy();
}

/*
 * test_dispatcher_pid_reuse_stability - verify reused pid routing stability.
 *
 * Numeric PID reuse is still handled by the subject fingerprint inside the
 * owning worker. Routing must not send the reused numeric PID to a different
 * worker, or stale/startup state could be split across caches.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_dispatcher_pid_reuse_stability(void)
{
	pid_t pids[4];
	int fds[4];
	unsigned int worker;

	CHECK(test_notify_worker_pool_reset(4, 8) == 0, 100,
	      "[ERROR:100] worker pool reset failed for pid reuse");
	worker = test_notify_worker_index(2209, 4);
	CHECK(test_notify_enqueue_pid_fd(2209, 20) == 0, 101,
	      "[ERROR:101] first reused-pid enqueue failed");
	CHECK(test_notify_enqueue_pid_fd(2210, 30) == 0, 102,
	      "[ERROR:102] adjacent pid enqueue failed");
	CHECK(test_notify_enqueue_pid_fd(2209, 21) == 0, 103,
	      "[ERROR:103] second reused-pid enqueue failed");

	CHECK(test_notify_worker_drain(worker, pids, fds, 4) == 2, 104,
	      "[ERROR:104] reused-pid owner queue count mismatch");
	CHECK(pids[0] == 2209 && pids[1] == 2209, 105,
	      "[ERROR:105] reused pid did not stay on owner queue");
	CHECK(fds[0] == 20 && fds[1] == 21, 106,
	      "[ERROR:106] reused-pid queue order changed");

	test_notify_worker_pool_destroy();
}

/*
 * collect_worker_trace - append one worker queue to an observed trace.
 * @worker_id: worker queue to drain.
 * @observed: destination trace.
 * @used: entries already populated in @observed.
 *
 * Returns the updated trace length. Exits if the fixed unit-test trace buffer
 * would overflow.
 */
static unsigned int collect_worker_trace(unsigned int worker_id,
		struct dispatch_trace_observed *observed, unsigned int used)
{
	pid_t pids[DISPATCH_TRACE_CAP];
	int fds[DISPATCH_TRACE_CAP];
	unsigned int drained, i;

	CHECK(used < DISPATCH_TRACE_CAP, 130,
	      "[ERROR:130] dispatcher trace buffer full");
	drained = test_notify_worker_drain(worker_id, pids, fds,
					   DISPATCH_TRACE_CAP - used);

	for (i = 0; i < drained; i++) {
		observed[used].worker = worker_id;
		observed[used].pid = pids[i];
		observed[used].fd = fds[i];
		used++;
	}

	return used;
}

/*
 * collect_pool_trace - drain every worker queue into an observed trace.
 * @workers: number of synthetic workers to drain.
 * @observed: destination trace.
 *
 * Returns the number of observed events.
 */
static unsigned int collect_pool_trace(unsigned int workers,
		struct dispatch_trace_observed *observed)
{
	unsigned int i, used = 0;

	for (i = 0; i < workers; i++)
		used = collect_worker_trace(i, observed, used);

	return used;
}

/*
 * enqueue_dispatch_trace - enqueue a synthetic replay trace.
 * @trace: PID/fd sequence to enqueue.
 * @count: number of trace entries.
 *
 * Returns nothing. Exits when any enqueue fails.
 */
static void enqueue_dispatch_trace(const struct dispatch_trace_event *trace,
		unsigned int count)
{
	unsigned int i;

	for (i = 0; i < count; i++)
		CHECK(test_notify_enqueue_pid_fd(trace[i].pid,
						 trace[i].fd) == 0,
		      131, "[ERROR:131] dispatcher trace enqueue failed");
}

/*
 * test_dispatcher_single_multi_replay_parity - compare queue replay order.
 *
 * A multi-worker daemon may interleave different worker queues differently
 * from the single-worker daemon, but each worker's input stream must equal the
 * single-worker stream filtered by that worker's stable subject owner. That is
 * the unit-testable invariant behind pattern-detection parity: all events
 * that can mutate one subject startup state arrive at the same worker in the
 * same order they had before worker-pool dispatch.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_dispatcher_single_multi_replay_parity(void)
{
	const struct dispatch_trace_event trace[] = {
		{ 1001, 10 }, { 1002, 11 }, { 1001, 12 },
		{ 1004, 13 }, { 1008, 14 }, { 1002, 15 },
		{ 1005, 16 }, { 1001, 17 }, { 1009, 18 },
		{ -1, 19 }, { 1004, 20 }, { 1005, 21 },
	};
	const unsigned int trace_count = sizeof(trace) / sizeof(trace[0]);
	struct dispatch_trace_observed single[DISPATCH_TRACE_CAP];
	struct dispatch_trace_observed multi[DISPATCH_TRACE_CAP];
	unsigned int single_count, multi_count, worker, i, out;

	CHECK(test_notify_worker_pool_reset(1, trace_count) == 0, 132,
	      "[ERROR:132] single-worker replay reset failed");
	enqueue_dispatch_trace(trace, trace_count);
	single_count = collect_pool_trace(1, single);
	CHECK(single_count == trace_count, 133,
	      "[ERROR:133] single-worker replay count mismatch");
	for (i = 0; i < trace_count; i++) {
		CHECK(single[i].worker == 0, 134,
		      "[ERROR:134] single-worker replay used wrong worker");
		CHECK(single[i].pid == trace[i].pid &&
		      single[i].fd == trace[i].fd, 135,
		      "[ERROR:135] single-worker replay order changed");
	}
	test_notify_worker_pool_destroy();

	CHECK(test_notify_worker_pool_reset(4, trace_count) == 0, 136,
	      "[ERROR:136] multi-worker replay reset failed");
	enqueue_dispatch_trace(trace, trace_count);
	multi_count = collect_pool_trace(4, multi);
	CHECK(multi_count == single_count, 137,
	      "[ERROR:137] multi-worker replay count mismatch");

	out = 0;
	for (worker = 0; worker < 4; worker++) {
		for (i = 0; i < single_count; i++) {
			if (test_notify_worker_index(single[i].pid, 4) !=
			    worker)
				continue;
			CHECK(out < multi_count, 138,
			      "[ERROR:138] multi-worker replay ended early");
			CHECK(multi[out].worker == worker, 139,
			      "[ERROR:139] multi-worker replay worker mismatch");
			CHECK(multi[out].pid == single[i].pid &&
			      multi[out].fd == single[i].fd, 140,
			      "[ERROR:140] multi-worker replay parity failed");
			out++;
		}
	}
	CHECK(out == multi_count, 141,
	      "[ERROR:141] multi-worker replay had unexpected events");

	test_notify_worker_pool_destroy();
}

/*
 * test_dispatcher_hot_worker_queue_full - verify isolated queue pressure.
 *
 * Queue-full handling is per worker. When a hot subject bucket fills one
 * worker queue, other workers must still accept their own events and the
 * metrics report must identify the full worker.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_dispatcher_hot_worker_queue_full(void)
{
	char report[4096];

	CHECK(test_notify_worker_pool_reset(4, 1) == 0, 142,
	      "[ERROR:142] queue-full worker pool reset failed");
	CHECK(test_notify_enqueue_pid_fd(1000, 50) == 0, 143,
	      "[ERROR:143] hot worker first enqueue failed");
	errno = 0;
	CHECK(test_notify_enqueue_pid_fd(1004, 51) == -1 &&
	      errno == ENOSPC, 144,
	      "[ERROR:144] hot worker full enqueue did not fail");
	CHECK(test_notify_enqueue_pid_fd(1001, 52) == 0, 145,
	      "[ERROR:145] idle worker enqueue failed after hot worker full");

	CHECK(test_notify_worker_queue_depth(0) == 1, 146,
	      "[ERROR:146] hot worker depth mismatch");
	CHECK(test_notify_worker_queue_depth(1) == 1, 147,
	      "[ERROR:147] idle worker did not accept event");

	read_fanotify_queue_report(report, sizeof(report), 0);
	CHECK(strstr(report, "Inter-thread queue full count: 1\n") != NULL,
	      148, "[ERROR:148] aggregate queue full count mismatch");
	CHECK(strstr(report,
		     "  Decision worker 0 queue full count: 1\n") != NULL,
	      149, "[ERROR:149] hot worker full count missing");
	CHECK(strstr(report,
		     "  Decision worker 1 queue full count: 0\n") != NULL,
	      150, "[ERROR:150] idle worker full count changed");

	test_notify_worker_pool_destroy();
}

/*
 * test_dispatcher_worker_skew - verify hot PID buckets remain observable.
 *
 * Stable pid modulo routing can skew traffic. The dispatcher must expose that
 * pressure through per-worker queue metrics instead of rebalancing same-pid
 * events onto other workers.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_dispatcher_worker_skew(void)
{
	char report[4096];

	CHECK(test_notify_worker_pool_reset(4, 8) == 0, 107,
	      "[ERROR:107] worker pool reset failed for skew");
	CHECK(test_notify_enqueue_pid_fd(1000, 40) == 0, 108,
	      "[ERROR:108] skew enqueue 1 failed");
	CHECK(test_notify_enqueue_pid_fd(1004, 41) == 0, 109,
	      "[ERROR:109] skew enqueue 2 failed");
	CHECK(test_notify_enqueue_pid_fd(1008, 42) == 0, 110,
	      "[ERROR:110] skew enqueue 3 failed");
	CHECK(test_notify_enqueue_pid_fd(1012, 43) == 0, 111,
	      "[ERROR:111] skew enqueue 4 failed");

	CHECK(test_notify_worker_queue_depth(0) == 4, 112,
	      "[ERROR:112] hot pid bucket did not stay on worker 0");
	CHECK(test_notify_worker_queue_depth(1) == 0 &&
	      test_notify_worker_queue_depth(2) == 0 &&
	      test_notify_worker_queue_depth(3) == 0, 113,
	      "[ERROR:113] skew was rebalanced across workers");

	read_fanotify_queue_report(report, sizeof(report), 0);
	CHECK(strstr(report,
		     "  Decision worker 0 current queue depth: 4\n") != NULL,
	      114, "[ERROR:114] skew report missing hot worker depth");
	CHECK(strstr(report,
		     "  Decision worker 1 current queue depth: 0\n") != NULL,
	      115, "[ERROR:115] skew report missing idle worker depth");

	test_notify_worker_pool_destroy();
}

/*
 * test_notify_queue_report_reset - verify per-worker queue metrics reset.
 *
 * Worker queues own their metrics independently. A metrics reset must clear
 * the full counter for the queue that reported it while preserving the live
 * depth state for future reports.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_notify_queue_report_reset(void)
{
	decision_event_t event = { 0 };
	char report[2048];

	CHECK(test_notify_queue_reset(1) == 0, 82,
	      "[ERROR:82] notify queue reset failed for metrics");
	CHECK(test_notify_queue_push(&event) == 0, 83,
	      "[ERROR:83] queue metrics push failed");
	errno = 0;
	CHECK(test_notify_queue_push(&event) == -1 && errno == ENOSPC, 84,
	      "[ERROR:84] full queue did not reject metrics push");

	read_fanotify_queue_report(report, sizeof(report), 1);
	CHECK(strstr(report, "Inter-thread current queue depth: 1\n") != NULL,
	      85, "[ERROR:85] aggregate queue depth missing");
	CHECK(strstr(report,
		     "  Decision worker 0 current queue depth: 1\n") != NULL,
	      86, "[ERROR:86] worker current depth missing");
	CHECK(strstr(report,
		     "  Decision worker 0 max queue depth: 1\n") != NULL,
	      87, "[ERROR:87] worker max depth missing");
	CHECK(strstr(report,
		     "  Decision worker 0 queue full count: 1\n") != NULL,
	      88, "[ERROR:88] worker full count missing before reset");
	CHECK(strstr(report,
		     "  Decision worker 0 oldest queued age: ") != NULL,
	      89, "[ERROR:89] worker oldest age missing");
	CHECK(strstr(report, "Per-worker subject defer activity:\n") != NULL,
	      116, "[ERROR:116] worker defer section missing");
	CHECK(strstr(report,
		     "  Decision worker 0 Subject defer fallbacks: 0\n") != NULL,
	      117, "[ERROR:117] worker defer fallback missing");

	read_fanotify_health_report(report, sizeof(report));
	CHECK(strstr(report,
		     "  Decision worker 0 health: state=idle heartbeat=") != NULL,
	      118, "[ERROR:118] worker health state missing");
	CHECK(strstr(report, " current_event=idle\n") != NULL,
	      119, "[ERROR:119] worker current event age missing");

	read_fanotify_queue_report(report, sizeof(report), 0);
	CHECK(strstr(report,
		     "  Decision worker 0 queue full count: 0\n") != NULL,
	      90, "[ERROR:90] worker full count did not reset");

	test_notify_queue_destroy();
}

/*
 * test_deferred_dispatch_refreshes_health - verify dequeue liveness.
 *
 * A worker that dequeues an event and parks it behind a BUILDING subject has
 * still made progress. The worker heartbeat must be refreshed before dispatch
 * can return from the successful deferral branch, otherwise the health monitor
 * can misclassify bounded deferral as a stalled worker under queue pressure.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_deferred_dispatch_refreshes_health(void)
{
	conf_t cfg = { 0 };
	decision_event_t event;
	pid_t owner = getpid();
	pid_t blocked = owner + 1;
	unsigned int slot;
	uint64_t before, after;
	int pipes[2];

	cfg.subj_cache_size = 1;
	cfg.obj_cache_size = 1;
	CHECK(init_event_system(&cfg) == 0, 156,
	      "[ERROR:156] failed to initialize event system");
	CHECK(test_notify_queue_reset(2) == 0, 157,
	      "[ERROR:157] notify queue reset failed");

	slot = event_subject_slot(owner);
	seed_building_subject(owner, slot);
	CHECK(slot == event_subject_slot(blocked), 158,
	      "[ERROR:158] subject cache did not force a collision");
	CHECK(event_subject_slot_is_blocked(slot, blocked) == 1, 159,
	      "[ERROR:159] fixture subject did not block collision");

	CHECK(pipe(pipes) == 0, 160,
	      "[ERROR:160] deferred dispatch pipe setup failed");
	memset(&event, 0, sizeof(event));
	event.metadata.fd = pipes[0];
	event.metadata.pid = blocked;
	event.metadata.mask = FAN_OPEN_PERM;
	event.subject_slot = slot;
	event.completed_subject_slot = DECISION_EVENT_NO_SLOT;
	CHECK(test_notify_queue_push(&event) == 0, 161,
	      "[ERROR:161] deferred dispatch queue push failed");

	test_notify_worker_set_heartbeat_ns(0, 1);
	before = test_notify_worker_heartbeat_ns(0);
	CHECK(test_notify_worker_dequeue_dispatch(0) == 1, 162,
	      "[ERROR:162] deferred dispatch did not process one event");
	after = test_notify_worker_heartbeat_ns(0);
	CHECK(after > before, 163,
	      "[ERROR:163] deferred dispatch did not refresh heartbeat");
	CHECK(test_notify_worker_queue_depth(0) == 0, 164,
	      "[ERROR:164] dispatched event remained queued");
	CHECK(test_notify_shutdown_deferred_events() == 1, 166,
	      "[ERROR:166] deferred dispatch cleanup missed event");

	errno = 0;
	CHECK(close(pipes[0]) == -1 && errno == EBADF, 167,
	      "[ERROR:167] deferred dispatch fd was not closed");
	close(pipes[1]);

	test_notify_queue_destroy();
	destroy_event_system();
}

/*
 * test_interval_report_deadline - interval reports must not start at epoch.
 *
 * The queue timeout and timerfd deadline must describe the first configured
 * interval. An absolute value containing only the interval is already in the
 * past, which makes the timer fire as soon as the worker starts.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_interval_report_deadline(void)
{
	struct timespec before, timeout;
	struct itimerspec timer;

	CHECK(clock_gettime(CLOCK_REALTIME, &before) == 0, 168,
	      "[ERROR:168] failed to read report timer clock");
	CHECK(test_notify_report_timer_init(5, &timeout, &timer) == 0, 169,
	      "[ERROR:169] failed to initialize report timer");
	CHECK(timeout.tv_sec >= before.tv_sec + 5, 170,
	      "[ERROR:170] initial report timeout was not in the future");
	CHECK(timer.it_interval.tv_sec == 5 && timer.it_interval.tv_nsec == 0,
	      171, "[ERROR:171] report timer interval changed");
	CHECK(timer.it_value.tv_sec >= 4, 172,
	      "[ERROR:172] report timer expired before its first interval");
	test_notify_report_timer_destroy();
}

/*
 * test_interval_report_defer_deadline - preserve deferred-event liveness.
 *
 * Worker 0 owns interval reports, but its report deadline must not hide the
 * shorter deferred-event deadline. Otherwise a quiet worker never revisits a
 * permission event after the BUILDING subject ahead of it becomes stale.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_interval_report_defer_deadline(void)
{
	decision_event_t event = { 0 };
	struct timespec now, report_timeout, selected_timeout;

	CHECK(test_notify_defer_reset(1) == 0, 202,
	      "[ERROR:202] report deadline defer reset failed");
	CHECK(clock_gettime(CLOCK_REALTIME, &now) == 0, 203,
	      "[ERROR:203] failed reading report deadline clock");
	report_timeout = now;
	report_timeout.tv_sec += 30;

	CHECK(test_notify_report_dequeue_timeout(&report_timeout,
			&selected_timeout) == 0, 204,
	      "[ERROR:204] empty defer queue shortened report deadline");
	CHECK(selected_timeout.tv_sec == report_timeout.tv_sec &&
	      selected_timeout.tv_nsec == report_timeout.tv_nsec, 205,
	      "[ERROR:205] empty defer queue changed report deadline");

	event.metadata.fd = FAN_NOFD;
	event.metadata.pid = 700;
	event.metadata.mask = FAN_OPEN_PERM;
	event.subject_slot = 0;
	event.completed_subject_slot = DECISION_EVENT_NO_SLOT;
	CHECK(test_notify_defer_push(&event) == 0, 206,
	      "[ERROR:206] failed parking report-owner deferred event");
	CHECK(test_notify_report_dequeue_timeout(&report_timeout,
			&selected_timeout) == 1, 207,
	      "[ERROR:207] report wait missed deferred event");
	CHECK(clock_gettime(CLOCK_REALTIME, &now) == 0, 208,
	      "[ERROR:208] failed reading selected deadline clock");
	CHECK(selected_timeout.tv_sec < report_timeout.tv_sec &&
	      selected_timeout.tv_sec <= now.tv_sec + 1, 209,
	      "[ERROR:209] report wait did not use defer recheck deadline");

	test_notify_defer_destroy();
}

/*
 * test_interval_report_disable_closes_timer - invalidate a disabled timer.
 *
 * A later pool cleanup must not close an unrelated descriptor after the
 * operating system reuses the report timer's descriptor number.
 *
 * Returns nothing. Exits on test failure.
 */
static void test_interval_report_disable_closes_timer(void)
{
	struct timespec timeout;
	struct itimerspec timer;

	CHECK(test_notify_report_timer_init(5, &timeout, &timer) == 0, 173,
	      "[ERROR:173] failed to initialize report timer for disable");
	CHECK(test_notify_report_timer_fd() != -1, 174,
	      "[ERROR:174] report timer descriptor was not active");
	test_notify_report_timer_disable();
	CHECK(test_notify_report_timer_fd() == -1, 175,
	      "[ERROR:175] disabled report timer kept its descriptor");
}

/*
 * main - exercise synthetic FAN_NOFD kernel metadata.
 * Returns 0 on success. Exits with error() on test failure.
 */
int main(void)
{
	struct fanotify_event_metadata metadata = {
		.event_len = sizeof(metadata),
		.vers = FANOTIFY_METADATA_VERSION,
		.fd = FAN_NOFD,
		.pid = 0,
	};
	unsigned long before, after;
	unsigned long overflow_after, reply_after;
#if TEST_HAVE_FAN_FS_ERROR
	unsigned long fs_error_after = 0;
#endif
	char report[4096], expected[128];
	int event_pipe[2];

	test_operating_mode_report_order();
	test_dispatcher_worker_routing();
	test_dispatcher_same_pid_ordering();
	test_dispatcher_pid_reuse_stability();
	test_dispatcher_single_multi_replay_parity();
	test_dispatcher_hot_worker_queue_full();
	test_dispatcher_worker_skew();
	test_notify_queue_report_reset();
	test_deferred_dispatch_refreshes_health();
	test_interval_report_deadline();
	test_interval_report_defer_deadline();
	test_interval_report_disable_closes_timer();
	test_nonblocking_fanotify_reads();
	test_shutdown_fanotify_close();
	test_fatal_signal_fanotify_close();
	test_mount_list_reconcile();

	before = getKernelQueueOverflow();
	metadata.mask = 0;
	// A FAN_NOFD event without FAN_Q_OVERFLOW is not a kernel event.
	CHECK(handle_kernel_event(&metadata) == 0, 1,
	      "[ERROR:1] non-overflow FAN_NOFD event was consumed");
	// Ignoring a non-overflow FAN_NOFD event must not change metrics.
	CHECK(getKernelQueueOverflow() == before, 2,
	      "[ERROR:2] non-overflow event changed overflow count");
	CHECK(failure_action_count(FAILURE_REASON_KERNEL_QUEUE_OVERFLOW) ==
	      before, 3, "[ERROR:3] non-overflow event changed failure count");

	atomic_store(&run_stats, false);
	metadata.mask = FAN_Q_OVERFLOW;
	// FAN_Q_OVERFLOW should be consumed as a kernel queue failure.
	CHECK(handle_kernel_event(&metadata) == 1, 4,
	      "[ERROR:4] FAN_Q_OVERFLOW event was not consumed");

	after = getKernelQueueOverflow();
	overflow_after = after;
	// Consuming FAN_Q_OVERFLOW should increment the overflow counter once.
	CHECK(after == before + 1, 5,
	      "[ERROR:5] FAN_Q_OVERFLOW did not increment count");
	CHECK(failure_action_count(FAILURE_REASON_KERNEL_QUEUE_OVERFLOW) ==
	      after, 6, "[ERROR:6] FAN_Q_OVERFLOW failure count mismatch");
	// Queue overflow should request the configured failure action.
	CHECK(atomic_load(&run_stats), 7,
	      "[ERROR:7] FAN_Q_OVERFLOW did not trigger failure action");

	read_decision_report(report, sizeof(report), 0);
	snprintf(expected, sizeof(expected), "Kernel Queue Overflow: %lu",
		 after);
	// The status report should expose the overflow counter value.
	CHECK(strstr(report, expected) != NULL, 8,
	      "[ERROR:8] status report missing Kernel Queue Overflow");
	snprintf(expected, sizeof(expected),
		 "Failure action kernel_queue_overflow (observe): %lu", after);
	CHECK(strstr(report, expected) != NULL, 9,
	      "[ERROR:9] status report missing kernel overflow failure count");

#if TEST_HAVE_FAN_FS_ERROR
	{
		struct {
			struct fanotify_event_metadata metadata;
			struct test_fanotify_fs_error_info error;
		} fs_error_event;

		memset(&fs_error_event, 0, sizeof(fs_error_event));
		fs_error_event.metadata.event_len =
			sizeof(fs_error_event.metadata) +
			sizeof(fs_error_event.error);
		fs_error_event.metadata.vers = FANOTIFY_METADATA_VERSION;
		fs_error_event.metadata.metadata_len =
			sizeof(fs_error_event.metadata);
		fs_error_event.metadata.fd = FAN_NOFD;
		fs_error_event.metadata.pid = 5678;
		fs_error_event.metadata.mask = FAN_FS_ERROR;
		fs_error_event.error.hdr.info_type =
			FAN_EVENT_INFO_TYPE_ERROR;
		fs_error_event.error.hdr.len = sizeof(fs_error_event.error);
		fs_error_event.error.error = EIO;
		fs_error_event.error.error_count = 3;

		before = getFanotifyFilesystemErrors();
		atomic_store(&run_stats, false);
		CHECK(fanotify_fs_error_handle_event(
		      &fs_error_event.metadata) == 1, 46,
		      "[ERROR:46] FAN_FS_ERROR event was not consumed");
		after = getFanotifyFilesystemErrors();
		fs_error_after = after;
		CHECK(after == before + 1, 47,
		      "[ERROR:47] FAN_FS_ERROR did not increment count");
		CHECK(failure_action_count(
		      FAILURE_REASON_FANOTIFY_FS_ERROR) == after, 48,
		      "[ERROR:48] FAN_FS_ERROR failure count mismatch");
		CHECK(atomic_load(&run_stats), 49,
		      "[ERROR:49] FAN_FS_ERROR did not trigger failure action");

		read_decision_report(report, sizeof(report), 0);
		snprintf(expected, sizeof(expected),
			 "Filesystem Errors: %lu", after);
		CHECK(strstr(report, expected) != NULL, 50,
		      "[ERROR:50] status report missing Filesystem Errors");
		snprintf(expected, sizeof(expected),
			 "Failure action fanotify_filesystem_error "
			 "(observe): %lu", after);
		CHECK(strstr(report, expected) != NULL, 51,
		      "[ERROR:51] status report missing FS error failure");

		read_fs_error_report(report, sizeof(report));
		CHECK(strstr(report, "Filesystem error last status: ok") != NULL,
		      52, "[ERROR:52] FS error status missing");
		CHECK(strstr(report, "Filesystem error last errno: 5") != NULL,
		      53, "[ERROR:53] FS error errno missing");
		CHECK(strstr(report,
			     "Filesystem error last suppressed count: 3")
		      != NULL, 54,
		      "[ERROR:54] FS error suppressed count missing");
	}
#endif

	// Use a real event fd so reply_event can prove it still closes once.
	CHECK(pipe(event_pipe) == 0, 10, "[ERROR:10] pipe failed");
	metadata.fd = event_pipe[0];
	metadata.pid = 1234;
	metadata.mask = FAN_OPEN_PERM;
	before = getReplyErrors();

	reply_event(-1, &metadata, FAN_ALLOW, NULL);
	after = getReplyErrors();
	reply_after = after;
	// A failed fanotify response write should increment reply_errors once.
	CHECK(after == before + 1, 11,
	      "[ERROR:11] reply_event did not count EBADF write failure");
	CHECK(failure_action_count(FAILURE_REASON_RESPONSE_WRITE_FAILURE) ==
	      after, 12, "[ERROR:12] response failure count mismatch");

	errno = 0;
	// reply_event should close the event fd even when the response fails.
	CHECK(close(event_pipe[0]) == -1 && errno == EBADF, 13,
	      "[ERROR:13] reply_event did not close event fd");
	close(event_pipe[1]);

	read_decision_report(report, sizeof(report), 0);
	snprintf(expected, sizeof(expected), "Reply Errors: %lu", after);
	// The status report should expose the aggregate reply_errors value.
	CHECK(strstr(report, expected) != NULL, 14,
	      "[ERROR:14] status report missing Reply Errors count");
	snprintf(expected, sizeof(expected),
		 "Failure action response_write_failure (observe): %lu", after);
	CHECK(strstr(report, expected) != NULL, 15,
	      "[ERROR:15] status report missing response failure count");
	CHECK(strstr(report, "Failure action queue_full (observe): ") != NULL,
	      16, "[ERROR:16] status report missing queue full failure count");
	CHECK(strstr(report, "Failure action worker_stall (terminate): ") != NULL,
	      17, "[ERROR:17] status report missing worker stall failure count");
	CHECK(strstr(report, "Failure action rule_reload_failure (observe): ")
	      != NULL, 18,
	      "[ERROR:18] status report missing rule reload failure count");
	CHECK(strstr(report, "Failure action trust_reload_failure (observe): ")
	      != NULL, 19,
	      "[ERROR:19] status report missing trust reload failure count");
	CHECK(strstr(report,
		     "Failure action fanotify_filesystem_error (observe): ")
	      != NULL, 55,
	      "[ERROR:55] status report missing fs error failure count");
	CHECK(strstr(report, "Allowed by rule: ") != NULL, 20,
	      "[ERROR:20] status report missing rule allow count");
	CHECK(strstr(report, "Allowed by fallthrough: ") != NULL, 21,
	      "[ERROR:21] status report missing fallthrough allow count");
	CHECK(strstr(report, "Allowed by fallthrough executable: ") == NULL, 22,
	      "[ERROR:22] zero fallthrough report included ftype detail");
	CHECK(strstr(report, "Ruleset generation: ") != NULL, 23,
	      "[ERROR:23] status report missing ruleset generation");

	read_decision_metrics_report(report, sizeof(report));
	const char *metrics_reset = strstr(report, "Last metrics reset: never\n");
	const char *metrics_config = strstr(report,
					    "Config generation: 0 "
					    "(effective since ");
	const char *metrics_ruleset = strstr(report, "Ruleset generation: ");

	CHECK(metrics_reset != NULL, 44,
	      "[ERROR:44] metrics report missing last reset header");
	CHECK(metrics_config != NULL, 72,
	      "[ERROR:72] metrics report missing config generation header");
	CHECK(metrics_ruleset != NULL, 45,
	      "[ERROR:45] metrics report missing ruleset generation header");
	CHECK(metrics_reset < metrics_config && metrics_config < metrics_ruleset,
	      73, "[ERROR:73] metrics identity headers out of order");

	atomic_store(&run_stats, false);
	atomic_store(&signal_report_requests, 0);
	siginfo_t info;

	memset(&info, 0, sizeof(info));
	info.si_code = SI_QUEUE;
	info.si_pid = 4321;
	info.si_uid = 0;
	info.si_value.sival_int = REPORT_INTENT_TIMING_ARM;
	usr1_handler(SIGUSR1, &info, NULL);
	CHECK(!atomic_load(&run_stats), 24,
	      "[ERROR:24] timing start incorrectly requested state report");
	CHECK(atomic_load(&signal_report_requests) == 0, 25,
	      "[ERROR:25] timing start incremented state report requests");

	config.timing_collection = TIMING_COLLECTION_OFF;
	decision_timing_process_requests(&config);
	FILE *timing = tmpfile();
	CHECK(timing != NULL, 26, "[ERROR:26] tmpfile failed");
	decision_timing_control_report(timing, &config);
	fflush(timing);
	rewind(timing);
	size_t used = fread(report, 1, sizeof(report) - 1, timing);
	report[used] = 0;
	fclose(timing);
	CHECK(strstr(report, "Timing collection mode: off") != NULL, 27,
	      "[ERROR:27] timing mode missing from state report");
	CHECK(strstr(report, "Timing collection armed: false") != NULL, 28,
	      "[ERROR:28] timing unexpectedly armed while configured off");

	config.timing_collection = TIMING_COLLECTION_MANUAL;
	usr1_handler(SIGUSR1, &info, NULL);
	decision_timing_process_requests(&config);
	timing = tmpfile();
	CHECK(timing != NULL, 29, "[ERROR:29] tmpfile failed");
	decision_timing_control_report(timing, &config);
	fflush(timing);
	rewind(timing);
	used = fread(report, 1, sizeof(report) - 1, timing);
	report[used] = 0;
	fclose(timing);
	CHECK(strstr(report, "Timing collection mode: manual") != NULL, 30,
	      "[ERROR:30] manual timing mode missing from state report");
	CHECK(strstr(report, "Timing collection armed: true") != NULL, 31,
	      "[ERROR:31] privileged manual timing start was not applied");
	CHECK(strstr(report, "Timing collection last start requester") == NULL,
	      32, "[ERROR:32] timing start requester still in state report");

	info.si_value.sival_int = REPORT_INTENT_TIMING_STOP;
	usr1_handler(SIGUSR1, &info, NULL);
	decision_timing_process_requests(&config);
	timing = tmpfile();
	CHECK(timing != NULL, 33, "[ERROR:33] tmpfile failed");
	decision_timing_control_report(timing, &config);
	fflush(timing);
	rewind(timing);
	used = fread(report, 1, sizeof(report) - 1, timing);
	report[used] = 0;
	fclose(timing);
	CHECK(strstr(report, "Timing collection armed: false") != NULL, 34,
	      "[ERROR:34] timing stop did not disarm");
	CHECK(strstr(report, "Timing collection last stop requester") == NULL,
	      35, "[ERROR:35] timing stop requester still in state report");

	read_decision_report(report, sizeof(report), 1);
	snprintf(expected, sizeof(expected), "Kernel Queue Overflow: %lu",
		 overflow_after);
	CHECK(strstr(report, expected) != NULL, 36,
	      "[ERROR:36] reset report lost pre-reset overflow count");
	snprintf(expected, sizeof(expected), "Reply Errors: %lu", reply_after);
	CHECK(strstr(report, expected) != NULL, 37,
	      "[ERROR:37] reset report lost pre-reset reply count");
#if TEST_HAVE_FAN_FS_ERROR
	snprintf(expected, sizeof(expected), "Filesystem Errors: %lu",
		 fs_error_after);
	CHECK(strstr(report, expected) != NULL, 56,
	      "[ERROR:56] reset report lost pre-reset fs error count");
#endif
	CHECK(getKernelQueueOverflow() == 0, 38,
	      "[ERROR:38] reset report did not clear overflow count");
	CHECK(getReplyErrors() == 0, 39,
	      "[ERROR:39] reset report did not clear reply count");
#if TEST_HAVE_FAN_FS_ERROR
	CHECK(getFanotifyFilesystemErrors() == 0, 57,
	      "[ERROR:57] reset report did not clear fs error count");
#endif

	test_shutdown_deferred_events();
	test_shutdown_queued_events();
	test_shutdown_rejects_late_enqueue();
	test_systemd_notify_runtime_gate();
	test_systemd_notify_full_queue();

	return 0;
}
