/*
 * event_test.c - unit tests for new_event subject/object cache behavior
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdatomic.h>
#include <sys/fanotify.h>

#include "attr-lookup-metrics.h"
#include "event.h"
#include "decision-context.h"
#include "conf.h"
#include "process.h"
#include "object.h"
#include "subject.h"
#include "fapolicyd-backend.h"

/*
 * Test doubles
 * ------------
 * The tests below replace the process and file fingerprint helpers used by
 * new_event().  Each stub returns deterministic data so we can control cache
 * reuse and eviction without touching /proc or real file descriptors.
 */

/*
 * Test strategy
 * -------------
 * The fixtures configure small, deterministic caches so that each test can
 * exercise a specific branch inside new_event().  The helpers below provide
 * stable process and file identities which lets us trigger subject cache
 * reuse, deliberate evictions, and skip-path behavior without relying on
 * kernel state.  Each scenario asserts the resulting event_t contents as well
 * as the cache side effects (state transitions and cache pointer reuse).
 *
 * Extending the suite is straightforward: add new rows to the stub tables or
 * new helper routines that model additional metadata, then write another test
 * that seeds fanotify_event_metadata with the desired pid/fd pair.  Tests can
 * reuse init_caches() to size caches appropriately and the CHECK macro to
 * report deterministic failures.  Future scenarios to consider include
 * multi-object fanotify events, cross-context cache flush generations, or
 * validating that trust-database results propagate into the event fields.
 */

struct proc_info *stat_proc_entry(pid_t pid);
void clear_proc_info(struct proc_info *info);
int compare_proc_infos(const struct proc_info *p1, const struct proc_info *p2);
struct file_info *stat_file_entry(int fd);
int compare_file_infos(const struct file_info *p1, const struct file_info *p2);
char *get_file_from_fd(int fd, pid_t pid, size_t blen, char *buf);
uint32_t gather_elf(int fd, off_t size);
void msg(int priority, const char *fmt, ...);
unsigned int policy_get_rules_proc_status_mask(void);
unsigned int policy_get_syslog_proc_status_mask(void);
int read_proc_status(pid_t pid, unsigned int fields,
		struct proc_status_info *info);
char *get_program_from_pid(pid_t pid, size_t blen, char *buf);
char *get_type_from_pid(pid_t pid, size_t blen, char *buf);
uid_t get_program_auid_from_pid(pid_t pid);
int get_program_sessionid_from_pid(pid_t pid);
int check_trust_database(const char *exe, const char *digest, int mode);
char *get_device_from_stat(unsigned int device, size_t blen, char *buf);
char *get_file_type_from_fd(int fd, const struct file_info *i, const char *path,
			     size_t blen, char *buf);
char *get_hash_from_fd2(int fd, size_t size, file_hash_alg_t alg);

struct stub_proc_record {
	pid_t pid;
	dev_t device;
	ino_t inode;
	long nsec;
};

static const struct stub_proc_record proc_table[] = {
	{ 100, 1,  111, 100 },
	{ 200, 2,  222, 200 },
	{ 201, 3,  333, 300 },
	{ 202, 4,  444, 400 },
	{ 300, 5,  555, 500 },
	{ 301, 6,  666, 600 },
	{ 400, 7,  777, 700 },
};

struct stub_file_record {
	int fd;
	dev_t device;
	ino_t inode;
	off_t size;
	long nsec;
	const char *path;
};

static const struct stub_file_record file_table[] = {
	{ 10, 11, 1010, 4096, 101, "/stub/bin/first" },
	{ 11, 12, 1111, 4096, 111, "/stub/bin/first-open" },
	{ 20, 21, 2020, 2048, 202, "/stub/bin/second" },
	{ 21, 22, 2121, 1024, 212, "/stub/bin/third" },
	{ 30, 31, 3030, 512,  303, "/stub/bin/fourth" },
	{ 31, 32, 3131, 512,  313, "/stub/bin/fifth" },
	{ 40, 41, 4040, 256,  404, "/stub/bin/sixth" },
};

static pid_t traced_proc_pid = -1;

/* --- Stub implementations ------------------------------------------------ */

/*
 * Locate the stubbed proc entry for the given pid or NULL when missing.
 */
static const struct stub_proc_record *find_proc(pid_t pid)
{
	size_t i;

	for (i = 0; i < sizeof(proc_table)/sizeof(proc_table[0]); i++)
		if (proc_table[i].pid == pid)
			return &proc_table[i];
	return NULL;
}

/*
 * Locate the stubbed file entry for the given descriptor or NULL when absent.
 */
static const struct stub_file_record *find_file(int fd)
{
	size_t i;

	for (i = 0; i < sizeof(file_table)/sizeof(file_table[0]); i++)
		if (file_table[i].fd == fd)
			return &file_table[i];
	return NULL;
}

/*
 * file_hash_length - return digest lengths for the unit test stubs.
 * @alg: digest algorithm requested by the event code under test.
 * Returns a constant binary length for the supported algorithms.
 */
size_t file_hash_length(file_hash_alg_t alg)
{
	switch (alg) {
	case FILE_HASH_ALG_SHA1:
		return SHA1_LEN;
	case FILE_HASH_ALG_SHA256:
		return SHA256_LEN;
	case FILE_HASH_ALG_SHA512:
		return SHA512_LEN;
	case FILE_HASH_ALG_MD5:
		return 16;
	default:
		return 0;
	}
}

file_hash_alg_t file_hash_alg(unsigned len)
{
	switch (len) {
	case MD5_LEN * 2:
		return FILE_HASH_ALG_MD5;
	case SHA1_LEN * 2:
		return FILE_HASH_ALG_SHA1;
	case SHA256_LEN * 2:
		return FILE_HASH_ALG_SHA256;
	case SHA512_LEN * 2:
		return FILE_HASH_ALG_SHA512;
	default:
		return FILE_HASH_ALG_NONE;
	}
}

/*
 * file_info_reset_digest - clear cached digest metadata for unit tests.
 * @info: cached file entry supplied by the test harness.
 */
void file_info_reset_digest(struct file_info *info)
{
	if (info == NULL)
		return;

	info->digest_alg = FILE_HASH_ALG_NONE;
	info->digest[0] = 0;
}

/*
 * file_info_cache_digest - store digest metadata for unit test file entries.
 * @info: cached file entry supplied by the test harness.
 * @alg: algorithm associated with the cached digest string.
 * Tests derive digest length with file_hash_length(@alg) when necessary.
 */
void file_info_cache_digest(struct file_info *info, file_hash_alg_t alg)
{
	if (info == NULL)
		return;

	info->digest_alg = alg;
}

/*
 * Allocate a proc_info populated from the stub table, emulating /proc stats.
 */
struct proc_info *stat_proc_entry(pid_t pid)
{
	const struct stub_proc_record *rec = find_proc(pid);
	struct proc_info *info;

	if (rec == NULL)
		return NULL;

	info = malloc(sizeof(*info));
	if (info == NULL)
		return NULL;

	info->pid = rec->pid;
	info->device = rec->device;
	info->inode = rec->inode;
	info->time.tv_sec = 0;
	info->time.tv_nsec = rec->nsec;
	info->state = STATE_COLLECTING;
	info->path1 = NULL;
	info->path2 = NULL;
	info->building_started_ns = 0;
	info->building_event_count = 0;
	info->elf_info = 0;
	return info;
}

/*
 * Release any heap-allocated strings contained inside the stub proc_info.
 */
void clear_proc_info(struct proc_info *info)
{
	if (info == NULL)
		return;
	free(info->path1);
	free(info->path2);
	info->path1 = NULL;
	info->path2 = NULL;
}

/*
 * Provide the equality predicate required by the subject cache machinery.
 */
int compare_proc_infos(const struct proc_info *p1, const struct proc_info *p2)
{
	if (p1 == NULL || p2 == NULL)
		return 1;
	if (p1->pid != p2->pid)
		return 1;
	if (p1->device != p2->device)
		return 1;
	if (p1->inode != p2->inode)
		return 1;
	if (p1->time.tv_sec != p2->time.tv_sec)
		return 1;
	if (p1->time.tv_nsec != p2->time.tv_nsec)
		return 1;
	return 0;
}

/*
 * Allocate a file_info populated from the stub table for the supplied fd.
 */
struct file_info *stat_file_entry(int fd)
{
	const struct stub_file_record *rec = find_file(fd);
	struct file_info *info;

	if (rec == NULL)
		return NULL;

	info = malloc(sizeof(*info));
	if (info == NULL)
		return NULL;

	info->device = rec->device;
	info->inode = rec->inode;
	info->mode = 0;
	info->size = rec->size;
	info->time.tv_sec = 0;
	info->time.tv_nsec = rec->nsec;
	file_info_reset_digest(info);
	return info;
}

/*
 * Implement the object cache equality predicate using stub file metadata.
 */
int compare_file_infos(const struct file_info *p1, const struct file_info *p2)
{
	if (p1 == NULL || p2 == NULL)
		return 1;
	if (p1->device != p2->device)
		return 1;
	if (p1->inode != p2->inode)
		return 1;
	if (p1->size != p2->size)
		return 1;
	if (p1->time.tv_sec != p2->time.tv_sec)
		return 1;
	if (p1->time.tv_nsec != p2->time.tv_nsec)
		return 1;
	return 0;
}

/*
 * Return a synthetic path for the provided fd so path collection can succeed.
 */
char *get_file_from_fd(int fd, pid_t pid, size_t blen, char *buf)
{
	const struct stub_file_record *rec = find_file(fd);

	(void)pid;
	if (rec == NULL)
		return NULL;

	if (strlen(rec->path) + 1 > blen)
		return NULL;

	strcpy(buf, rec->path);
	return buf;
}

/*
 * Produce a deterministic ELF signature based on the stubbed fd and size.
 */
uint32_t gather_elf(int fd, off_t size)
{
	return ((uint32_t)fd << 8) ^ (uint32_t)size;
}

/*
 * Stub out the logging hook invoked by new_event(); nothing to record here.
 */
void msg(int priority, const char *fmt, ...)
{
	(void)priority;
	(void)fmt;
}

/* Return zero to disable reading of /proc status fields during tests. */
unsigned int policy_get_rules_proc_status_mask(void)
{
	return 0;
}

/* Avoid requesting additional /proc status fields in this isolated harness. */
unsigned int policy_get_syslog_proc_status_mask(void)
{
	return 0;
}

/* Return zero for the combined policy proc-status field request. */
unsigned int policy_get_proc_status_mask(void)
{
	return 0;
}

/*
 * Provide an inert implementation for read_proc_status() that always succeeds.
 */
int read_proc_status(pid_t pid, unsigned int fields,
		struct proc_status_info *info)
{
	(void)pid;
	if (info == NULL)
		return -1;
	info->ppid = -1;
	if (fields & PROC_STAT_TRACER)
		info->tracer_state = pid == traced_proc_pid ?
			PROC_TRACER_TRACED : PROC_TRACER_NOT_TRACED;
	else
		info->tracer_state = PROC_TRACER_UNKNOWN;
	info->uid = NULL;
	info->groups = NULL;
	info->comm = NULL;
	return 0;
}

/*
 * Fabricate a program path based on pid so subject attributes remain stable.
 */
char *get_program_from_pid(pid_t pid, size_t blen, char *buf)
{
	if (snprintf(buf, blen, "/proc/%d/exe", pid) < 0)
		return NULL;
	return buf;
}

/* Fabricate a subject type string that is unique per pid. */
char *get_type_from_pid(pid_t pid, size_t blen, char *buf)
{
	if (snprintf(buf, blen, "type-%d", pid) < 0)
		return NULL;
	return buf;
}

/* Return a deterministic audit uid derived from the pid. */
uid_t get_program_auid_from_pid(pid_t pid)
{
	return (uid_t)pid;
}

/* Return a deterministic session id derived from the pid. */
int get_program_sessionid_from_pid(pid_t pid)
{
	return (int)pid;
}

/* Bypass trust database lookups while keeping the signature intact. */
int check_trust_database(const char *exe, const char *digest, int mode)
{
	(void)exe;
	(void)digest;
	(void)mode;
	return 0;
}

/*
 * Report a stringified device identifier to satisfy object attribute updates.
 */
char *get_device_from_stat(unsigned int device, size_t blen, char *buf)
{
	if (snprintf(buf, blen, "dev-%u", device) < 0)
		return NULL;
	return buf;
}

/*
 * Provide a deterministic object type string incorporating the fd and path.
 */
char *get_file_type_from_fd(int fd, const struct file_info *i, const char *path,
			    size_t blen, char *buf)
{
	(void)i;
	if (snprintf(buf, blen, "ftype-%d-%s", fd, path ? path : "?") < 0)
		return NULL;
	return buf;
}

/*
 * Produce a fake digest string so new_event() can populate hash attributes.
 */
char *get_hash_from_fd2(int fd, size_t size, file_hash_alg_t alg)
{
	char *out = malloc(64);
	if (out == NULL)
		return NULL;
	snprintf(out, 64, "hash-%d-%zu-%d", fd, (size_t)size, (int)alg);
	return out;
}

/* --- Test helpers -------------------------------------------------------- */

#define CHECK(cond, code, msg) \
	do { \
		if (!(cond)) { \
			fprintf(stderr, "%s\n", msg); \
			return code; \
		} \
	} while (0)

struct metric_expectation {
	unsigned long long requests;
	unsigned long long lookups;
};

/*
 * reset_attr_lookup_metrics - clear all attribute lookup counters.
 * Returns nothing.
 */
static void reset_attr_lookup_metrics(void)
{
	struct attr_lookup_metric_snapshot snapshot;
	unsigned int type;

	for (type = SUBJ_START; type <= SUBJ_END; type++)
		attr_lookup_metrics_subject_snapshot(type, &snapshot, 1);
	for (type = OBJ_START; type <= OBJ_END; type++)
		attr_lookup_metrics_object_snapshot(type, &snapshot, 1);
}

/*
 * check_subject_metric - assert one subject attribute metric snapshot.
 * @type: subject attribute to inspect.
 * @expected: expected request and lookup counts.
 * @code: test failure code.
 * Return codes:
 * 0 - counters match.
 * @code - counters differ or snapshot failed.
 */
static int check_subject_metric(subject_type_t type,
		const struct metric_expectation *expected, int code)
{
	struct attr_lookup_metric_snapshot snapshot;
	const char *name = subj_val_to_name(type, RULE_FMT_COLON);

	if (attr_lookup_metrics_subject_snapshot(type, &snapshot, 0)) {
		fprintf(stderr, "[ERROR:%d] subject metric snapshot failed\n",
			code);
		return code;
	}
	if (snapshot.requests != expected->requests ||
	    snapshot.lookups != expected->lookups) {
		fprintf(stderr,
			"[ERROR:%d] subject %s metrics %llu/%llu != %llu/%llu\n",
			code, name ? name : "unknown", snapshot.requests,
			snapshot.lookups, expected->requests,
			expected->lookups);
		return code;
	}

	return 0;
}

/*
 * check_object_metric - assert one object attribute metric snapshot.
 * @type: object attribute to inspect.
 * @expected: expected request and lookup counts.
 * @code: test failure code.
 * Return codes:
 * 0 - counters match.
 * @code - counters differ or snapshot failed.
 */
static int check_object_metric(object_type_t type,
		const struct metric_expectation *expected, int code)
{
	struct attr_lookup_metric_snapshot snapshot;
	const char *name = obj_val_to_name(type);

	if (attr_lookup_metrics_object_snapshot(type, &snapshot, 0)) {
		fprintf(stderr, "[ERROR:%d] object metric snapshot failed\n",
			code);
		return code;
	}
	if (snapshot.requests != expected->requests ||
	    snapshot.lookups != expected->lookups) {
		fprintf(stderr,
			"[ERROR:%d] object %s metrics %llu/%llu != %llu/%llu\n",
			code, name ? name : "unknown", snapshot.requests,
			snapshot.lookups, expected->requests,
			expected->lookups);
		return code;
	}

	return 0;
}

/*
 * check_attr_metric_report_line - verify aligned attribute metric output.
 * @needle: expected report line.
 * @code: test failure code.
 * Return codes:
 * 0 - report contains @needle.
 * @code - report generation failed or @needle is absent.
 */
static int check_attr_metric_report_line(const char *needle, int code)
{
	char report_buf[2048];
	FILE *report;
	size_t used;

	report = tmpfile();
	if (report == NULL) {
		fprintf(stderr, "[ERROR:%d] tmpfile failed\n", code);
		return code;
	}

	attr_lookup_metrics_report(report, 0);
	fflush(report);
	rewind(report);
	used = fread(report_buf, 1, sizeof(report_buf) - 1, report);
	report_buf[used] = 0;
	fclose(report);

	if (strstr(report_buf, needle) == NULL) {
		fprintf(stderr, "[ERROR:%d] missing metric line: %s\n",
			code, needle);
		return code;
	}

	return 0;
}

struct lmdb_record {
	unsigned int tsource;
	off_t size;
	char digest[FILE_DIGEST_STRING_MAX];
	size_t digest_len;
	file_hash_alg_t alg;
};

static int parse_record(const char *record, struct lmdb_record *parsed)
{
	size_t expected_len;
	trustdb_size_t stored_size;

	if (sscanf(record, DATA_FORMAT_IN, &parsed->tsource, &stored_size,
		parsed->digest) != 3 ||
	    trustdb_size_to_off_t(stored_size, &parsed->size))
		return 1;

	parsed->digest_len = strlen(parsed->digest);
	parsed->alg = file_hash_alg(parsed->digest_len);

	expected_len = file_hash_length(parsed->alg) * 2;
	if (expected_len == 0 || parsed->digest_len != expected_len)
		return 2;

	if (parsed->tsource != SRC_RPM && parsed->alg != FILE_HASH_ALG_SHA256)
		return 2;

	return 0;
}

static int test_rpm_accepts_sha512(void)
{
	struct lmdb_record parsed;
	char record[256];
	const char *sha512 =
		"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
		"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";

	snprintf(record, sizeof(record), DATA_FORMAT, (unsigned int)SRC_RPM,
		 UINT64_C(8192), sha512);
	CHECK(parse_record(record, &parsed) == 0, 40,
	      "[ERROR:40] parse failed for RPM SHA512 digest");
	CHECK(parsed.alg == FILE_HASH_ALG_SHA512, 41,
	      "[ERROR:41] RPM digest algorithm not inferred as SHA512");
	CHECK(parsed.digest_len == strlen(sha512), 42,
	      "[ERROR:42] RPM digest length not preserved");
	return 0;
}

static int test_filedb_rejects_sha512(void)
{
	struct lmdb_record parsed;
	char record[256];
	const char *sha512 =
		"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
		"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd";

	snprintf(record, sizeof(record), DATA_FORMAT, (unsigned int)SRC_FILE_DB,
		 UINT64_C(4096), sha512);
	CHECK(parse_record(record, &parsed) != 0, 50,
	      "[ERROR:50] filedb SHA512 digest unexpectedly accepted");
	return 0;
}

static int test_filedb_accepts_sha256(void)
{
	struct lmdb_record parsed;
	char record[256];
	const char *sha256 =
		"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";

	snprintf(record, sizeof(record), DATA_FORMAT, (unsigned int)SRC_FILE_DB,
		 UINT64_C(1024), sha256);
	CHECK(parse_record(record, &parsed) == 0, 60,
	      "[ERROR:60] filedb SHA256 digest rejected");
	CHECK(parsed.alg == FILE_HASH_ALG_SHA256, 61,
	      "[ERROR:61] filedb digest algorithm not forced to SHA256");
	return 0;
}

static int init_caches(unsigned int subj_size, unsigned int obj_size)
{
	conf_t cfg = (conf_t){ 0 };

	cfg.subj_cache_size = subj_size;
	cfg.obj_cache_size = obj_size;
	return init_event_system(&cfg);
}

/*
 * test_attribute_replacement_preserves_count - verify subject/object array
 * replacement keeps cnt as an occupied-slot count.
 * Returns 0 on success, test error code otherwise.
 */
static int test_attribute_replacement_preserves_count(void)
{
	s_array subjects;
	o_array objects;
	subject_attr_t old_exe = { .type = EXE, .str = strdup("/old-exe") };
	subject_attr_t new_exe = { .type = EXE, .str = strdup("/new-exe") };
	object_attr_t old_path = { .type = PATH, .o = strdup("/old-path") };
	object_attr_t new_path = { .type = PATH, .o = strdup("/new-path") };
	subject_attr_t *cached_subject;
	object_attr_t *cached_object;

	CHECK(old_exe.str && new_exe.str && old_path.o && new_path.o, 130,
	      "[ERROR:130] failed to allocate replacement attributes");
	CHECK(subject_create(&subjects) == 0, 131,
	      "[ERROR:131] failed to allocate subject array");
	CHECK(object_create(&objects) == 0, 132,
	      "[ERROR:132] failed to allocate object array");

	CHECK(subject_add(&subjects, &old_exe) == 0, 133,
	      "[ERROR:133] failed to add original subject attribute");
	CHECK(subject_add(&subjects, &new_exe) == 0, 134,
	      "[ERROR:134] failed to replace subject attribute");
	CHECK(subjects.cnt == 1, 135,
	      "[ERROR:135] subject replacement changed slot count");
	cached_subject = subject_access(&subjects, EXE);
	CHECK(cached_subject != NULL, 136,
	      "[ERROR:136] replacement subject attribute missing");
	CHECK(strcmp(cached_subject->str, "/new-exe") == 0, 137,
	      "[ERROR:137] replacement subject attribute not cached");

	CHECK(object_add(&objects, &old_path) == 0, 138,
	      "[ERROR:138] failed to add original object attribute");
	CHECK(object_add(&objects, &new_path) == 0, 139,
	      "[ERROR:139] failed to replace object attribute");
	CHECK(objects.cnt == 1, 140,
	      "[ERROR:140] object replacement changed slot count");
	cached_object = object_access(&objects, PATH);
	CHECK(cached_object != NULL, 141,
	      "[ERROR:141] replacement object attribute missing");
	CHECK(strcmp(cached_object->o, "/new-path") == 0, 142,
	      "[ERROR:142] replacement object attribute not cached");

	subject_clear(&subjects);
	object_clear(&objects);
	return 0;
}

/*
 * test_attr_lookup_metrics - verify request, miss, dependency, and reset data.
 * Returns 0 on success, test error code otherwise.
 */
static int test_attr_lookup_metrics(void)
{
	event_t e = { 0 };
	s_array subjects;
	o_array objects;
	struct attr_lookup_metric_snapshot snapshot;
	subject_attr_t pid_attr = { .type = PID, .pid = 123 };
	struct metric_expectation expected;
	int rc;

	reset_attr_lookup_metrics();
	CHECK(attr_lookup_metrics_subject_snapshot(PATTERN, &snapshot, 0),
	      99, "[ERROR:99] pattern should not have attr metrics");
	CHECK(subject_create(&subjects) == 0, 100,
	      "[ERROR:100] failed to allocate subject array");
	CHECK(object_create(&objects) == 0, 101,
	      "[ERROR:101] failed to allocate object array");
	e.pid = 123;
	e.fd = 10;
	e.s = &subjects;
	e.o = &objects;
	e.o->info = stat_file_entry(e.fd);
	CHECK(e.o->info != NULL, 102,
	      "[ERROR:102] failed to allocate test file info");
	CHECK(subject_add(e.s, &pid_attr) == 0, 103,
	      "[ERROR:103] failed to add cached pid");

	CHECK(get_subj_attr(&e, PID) != NULL, 104,
	      "[ERROR:104] cached pid lookup failed");
	CHECK(get_subj_attr(&e, EXE) != NULL, 105,
	      "[ERROR:105] exe lookup failed");
	CHECK(get_subj_attr(&e, EXE) != NULL, 106,
	      "[ERROR:106] cached exe lookup failed");
	CHECK(get_subj_attr(&e, SUBJ_TRUST) != NULL, 107,
	      "[ERROR:107] subject trust lookup failed");
	CHECK(get_obj_attr(&e, PATH) != NULL, 108,
	      "[ERROR:108] path lookup failed");
	CHECK(get_obj_attr(&e, PATH) != NULL, 109,
	      "[ERROR:109] cached path lookup failed");
	CHECK(get_obj_attr(&e, FTYPE) != NULL, 110,
	      "[ERROR:110] ftype lookup failed");

	expected = (struct metric_expectation){ 1, 0 };
	rc = check_subject_metric(PID, &expected, 111);
	if (rc)
		return rc;
	expected = (struct metric_expectation){ 3, 1 };
	rc = check_subject_metric(EXE, &expected, 112);
	if (rc)
		return rc;
	expected = (struct metric_expectation){ 1, 1 };
	rc = check_subject_metric(SUBJ_TRUST, &expected, 113);
	if (rc)
		return rc;
	expected = (struct metric_expectation){ 3, 1 };
	rc = check_object_metric(PATH, &expected, 114);
	if (rc)
		return rc;
	expected = (struct metric_expectation){ 1, 1 };
	rc = check_object_metric(FTYPE, &expected, 115);
	if (rc)
		return rc;
	rc = check_attr_metric_report_line(
		"Subject attr: pid       requests=1"
		"          lookups=0\n", 118);
	if (rc)
		return rc;
	rc = check_attr_metric_report_line(
		"Object attr:  path      requests=3"
		"          lookups=1\n", 119);
	if (rc)
		return rc;

	reset_attr_lookup_metrics();
	expected = (struct metric_expectation){ 0, 0 };
	rc = check_subject_metric(EXE, &expected, 116);
	if (rc)
		return rc;
	rc = check_object_metric(PATH, &expected, 117);
	if (rc)
		return rc;

	subject_clear(&subjects);
	object_clear(&objects);
	return 0;
}

/*
 * read_cache_report_line - report one cache counter from event.c.
 * @label: printable counter label from do_cache_reports().
 * @value: output location for the parsed counter.
 *
 * Returns 0 when @value is populated and 1 on parse or I/O failure.
 */
static int read_cache_report_line(const char *label, unsigned long *value)
{
	FILE *report;
	char line[256];
	char pattern[96];
	unsigned long parsed;

	if (label == NULL || value == NULL)
		return 1;

	report = tmpfile();
	if (report == NULL)
		return 1;

	do_cache_reports(report);
	rewind(report);
	snprintf(pattern, sizeof(pattern), "%s: %%lu", label);

	while (fgets(line, sizeof(line), report)) {
		if (sscanf(line, pattern, &parsed) == 1) {
			fclose(report);
			*value = parsed;
			return 0;
		}
	}

	fclose(report);
	return 1;
}

/*
 * reset_cache_report_counters - clear interval counters in event.c reports.
 * Returns 0 on success and 1 on I/O failure.
 */
static int reset_cache_report_counters(void)
{
	FILE *report = tmpfile();

	if (report == NULL)
		return 1;

	do_cache_reports_reset(report, 1);
	fclose(report);
	return 0;
}

/*
 * cache_report_contains - check whether do_cache_reports() emits text.
 * @needle: text expected in the cache report.
 * Returns 1 when found, 0 otherwise.
 */
static int cache_report_contains(const char *needle)
{
	FILE *report;
	char buf[4096];
	size_t used;

	if (needle == NULL)
		return 0;

	report = tmpfile();
	if (report == NULL)
		return 0;

	do_cache_reports(report);
	fflush(report);
	rewind(report);
	used = fread(buf, 1, sizeof(buf) - 1, report);
	buf[used] = 0;
	fclose(report);

	return strstr(buf, needle) != NULL;
}

/*
 * read_object_cache_metric - report an object-cache counter from event.c.
 * @metric: printable counter label from do_cache_reports(), e.g. "hits".
 * @value: output location for the parsed counter.
 *
 * The helper captures do_cache_reports() into a temporary stream and extracts
 * a single "Object <metric>: <number>" line for assertions.
 *
 * Returns 0 when @value is populated and 1 on parse or I/O failure.
 */
static int read_object_cache_metric(const char *metric, unsigned long *value)
{
	char label[64];

	if (metric == NULL || value == NULL)
		return 1;

	snprintf(label, sizeof(label), "Object %s", metric);
	return read_cache_report_line(label, value);
}

/*
 * Verify that a second FAN_OPEN_PERM event for the same pid reuses the cached
 * subject, transitions STATE_COLLECTING -> STATE_REOPEN, and skips path
 * collection.
 */
static int test_reopen_skip_path(void)
{
	struct fanotify_event_metadata meta = { 0 };
	event_t first = { 0 };
	event_t reopen = { 0 };
	object_attr_t *path;
	object_attr_t *digest;

	CHECK(init_caches(4, 4) == 0, 1, "[ERROR:1] init_event_system failed");

	meta.mask = FAN_OPEN_EXEC_PERM;
	meta.fd = 10;
	meta.pid = 100;
	CHECK(new_event(&meta, &first) == 0, 2, "[ERROR:2] first new_event failed");

	CHECK(first.pid == 100, 3, "[ERROR:3] pid not copied");
	CHECK(first.fd == 10, 4, "[ERROR:4] fd not copied");
	CHECK((first.type & FAN_OPEN_EXEC_PERM) != 0, 5,
	      "[ERROR:5] mask missing FAN_OPEN_EXEC_PERM");
	CHECK(first.s && first.s->info, 6, "[ERROR:6] missing subject info");
	CHECK(first.o && first.o->info, 7, "[ERROR:7] missing object info");

	path = object_access(first.o, PATH);
	CHECK(path != NULL, 8, "[ERROR:8] path attribute missing");
	CHECK(strcmp(path->o, "/stub/bin/first") == 0, 9,
	      "[ERROR:9] unexpected path1");
	CHECK(first.s->info->path1 &&
	      strcmp(first.s->info->path1, "/stub/bin/first") == 0, 10,
	      "[ERROR:10] subject path1 not captured");
	CHECK(first.s->info->state == STATE_COLLECTING, 11,
	      "[ERROR:11] initial state mutated");

	digest = get_obj_attr(&first, FILE_HASH);
	CHECK(digest != NULL, 17, "[ERROR:17] missing digest attribute");
	CHECK(first.o->info->digest_alg == FILE_HASH_ALG_SHA256, 18,
	      "[ERROR:18] digest algorithm not cached as SHA256");

	meta.mask = FAN_OPEN_PERM;
	CHECK(new_event(&meta, &reopen) == 0, 12,
	      "[ERROR:12] reopen new_event failed");
	CHECK(reopen.s == first.s, 13, "[ERROR:13] subject cache miss");
	CHECK(reopen.o == first.o, 14, "[ERROR:14] object cache miss");
	CHECK(reopen.s->info->state == STATE_REOPEN, 15,
	      "[ERROR:15] state did not transition to STATE_REOPEN");
	CHECK(reopen.s->info->path2 == NULL, 16,
	      "[ERROR:16] path2 collected despite skip_path");

	destroy_event_system();
	return 0;
}

/*
 * Ensure that a tiny subject cache evicts the previous entry when a different
 * pid hashes to the same slot.
 */
static int test_subject_eviction(void)
{
	struct fanotify_event_metadata meta = { 0 };
	event_t first = { 0 };
	event_t second = { 0 };
	int first_pid;

	CHECK(init_caches(1, 2) == 0, 20,
	      "[ERROR:20] init_event_system failed");

	meta.mask = FAN_OPEN_EXEC_PERM;
	meta.fd = 30;
	meta.pid = 300;
	CHECK(new_event(&meta, &first) == 0, 21,
	      "[ERROR:21] first new_event failed");
	CHECK(first.s && first.s->info, 22, "[ERROR:22] subject missing");
	first_pid = first.s->info->pid;

	meta.fd = 31;
	meta.pid = 301;
	CHECK(new_event(&meta, &second) == 0, 23,
	      "[ERROR:23] second new_event failed");
	CHECK(second.s && second.s->info, 24,
	      "[ERROR:24] subject info missing after eviction");
	CHECK(second.s->info->pid != first_pid, 25,
	      "[ERROR:25] subject cache did not evict prior entry");

	destroy_event_system();
	return 0;
}

/*
 * Verify that a different pid colliding with a pre-STATE_FULL occupant blocks
 * instead of evicting the occupant, while same-pid and terminal occupants do
 * not block.
 */
static int test_subject_slot_blocks_pre_full_collision(void)
{
	struct fanotify_event_metadata meta = { 0 };
	event_t first = { 0 };
	unsigned int slot;

	CHECK(init_caches(1, 2) == 0, 70,
	      "[ERROR:70] init_event_system failed");

	meta.mask = FAN_OPEN_EXEC_PERM;
	meta.fd = 30;
	meta.pid = 300;
	CHECK(new_event(&meta, &first) == 0, 71,
	      "[ERROR:71] first new_event failed");
	CHECK(first.s && first.s->info, 72,
	      "[ERROR:72] subject missing");
	CHECK(first.s->info->state < STATE_FULL, 73,
	      "[ERROR:73] subject unexpectedly reached terminal state");

	slot = event_subject_slot(301);
	CHECK(slot == event_subject_slot(300), 74,
	      "[ERROR:74] tiny cache did not force a slot collision");
	CHECK(event_subject_slot_is_blocked(slot, 301) == 1, 75,
	      "[ERROR:75] colliding BUILDING subject did not block");
	CHECK(first.s->info->pid == 300, 76,
	      "[ERROR:76] blocked collision evicted the occupant");
	CHECK(event_subject_slot_is_blocked(slot, 300) == 0, 77,
	      "[ERROR:77] same pid should not block itself");
	CHECK(event_subject_slot_is_unblocked(slot) == 0, 78,
	      "[ERROR:78] pre-FULL subject reported unblocked");

	first.s->info->state = STATE_FULL;
	CHECK(event_subject_slot_is_blocked(slot, 301) == 0, 79,
	      "[ERROR:79] STATE_FULL subject still blocked collision");
	CHECK(event_subject_slot_is_unblocked(slot) == 1, 80,
	      "[ERROR:80] STATE_FULL subject did not release slot");

	destroy_event_system();
	return 0;
}

/*
 * Verify that a traced BUILDING occupant is evicted instead of deferring
 * incoming work indefinitely, and that the eviction is counted.
 */
static int test_traced_building_occupant_eviction(void)
{
	struct fanotify_event_metadata meta = { 0 };
	event_t first = { 0 };
	unsigned long tracer_evictions = 0;
	unsigned int slot;

	CHECK(init_caches(1, 2) == 0, 90,
	      "[ERROR:90] init_event_system failed");
	CHECK(reset_cache_report_counters() == 0, 91,
	      "[ERROR:91] failed resetting cache report counters");

	meta.mask = FAN_OPEN_EXEC_PERM;
	meta.fd = 30;
	meta.pid = 300;
	CHECK(new_event(&meta, &first) == 0, 92,
	      "[ERROR:92] first new_event failed");
	CHECK(first.s && first.s->info, 93,
	      "[ERROR:93] subject missing");

	slot = event_subject_slot(301);
	traced_proc_pid = 300;
	CHECK(event_subject_slot_is_blocked(slot, 301) == 0, 94,
	      "[ERROR:94] traced BUILDING occupant blocked collision");
	traced_proc_pid = -1;
	CHECK(event_subject_slot_is_unblocked(slot) == 1, 95,
	      "[ERROR:95] traced BUILDING occupant was not evicted");
	CHECK(read_cache_report_line("Subject BUILDING tracer evictions",
				     &tracer_evictions) == 0, 96,
	      "[ERROR:96] failed reading tracer eviction counter");
	CHECK(tracer_evictions == 1, 97,
	      "[ERROR:97] tracer eviction counter mismatch");

	destroy_event_system();
	return 0;
}

/*
 * Verify that a flush generation resets the object cache so the next lookup
 * allocates a fresh entry.
 */
static int test_flush_generation_resets_object_cache(void)
{
	struct fanotify_event_metadata meta = { 0 };
	event_t first = { 0 };
	event_t second = { 0 };
	unsigned long object_hits = 0, object_misses = 0;
	unsigned long generation;

	CHECK(init_caches(4, 1) == 0, 30,
	      "[ERROR:30] init_event_system failed");

	meta.mask = FAN_OPEN_EXEC_PERM;
	meta.fd = 40;
	meta.pid = 400;
	CHECK(new_event(&meta, &first) == 0, 31,
	      "[ERROR:31] first new_event failed");
	CHECK(first.o != NULL, 32, "[ERROR:32] object missing");

	generation = object_cache_flush_generation_snapshot();
	CHECK(request_object_cache_flush() == generation + 1, 34,
	      "[ERROR:34] flush generation did not advance");
	meta.mask = FAN_OPEN_PERM;
	CHECK(new_event(&meta, &second) == 0, 33,
	      "[ERROR:33] second new_event failed");
	CHECK(second.s == first.s, 35,
	      "[ERROR:35] subject cache should reuse same entry");
	CHECK(read_object_cache_metric("hits", &object_hits) == 0, 36,
	      "[ERROR:36] failed reading object cache hits");
	CHECK(read_object_cache_metric("misses", &object_misses) == 0, 37,
	      "[ERROR:37] failed reading object cache misses");
	CHECK(object_hits == 0, 38,
	      "[ERROR:38] object cache flush should reset hit counter");
	CHECK(object_misses == 1, 39,
	      "[ERROR:39] object cache flush should force a cache miss");

	destroy_event_system();
	return 0;
}

/*
 * Verify that one trust DB update flushes every registered worker cache.
 */
static int test_flush_generation_reaches_all_contexts(void)
{
	conf_t cfg = { 0 };
	struct decision_context *ctx0, *ctx1;
	struct fanotify_event_metadata meta = { 0 };
	event_t event = { 0 };
	unsigned long object_hits = 0, object_misses = 0;

	cfg.subj_cache_size = 4;
	cfg.obj_cache_size = 1;
	CHECK(init_event_system(&cfg) == 0, 150,
	      "[ERROR:150] init_event_system failed");
	ctx0 = decision_context_current();
	decision_context_set_worker_id(ctx0, 0);
	ctx1 = decision_context_create(&cfg);
	CHECK(ctx1 != NULL, 151,
	      "[ERROR:151] second decision context allocation failed");
	decision_context_set_worker_id(ctx1, 1);

	meta.mask = FAN_OPEN_EXEC_PERM;
	meta.fd = 40;
	meta.pid = 400;
	decision_context_set_current(ctx0);
	CHECK(new_event(&meta, &event) == 0, 152,
	      "[ERROR:152] first context prime failed");
	decision_context_set_current(ctx1);
	CHECK(new_event(&meta, &event) == 0, 153,
	      "[ERROR:153] second context prime failed");
	CHECK(reset_cache_report_counters() == 0, 154,
	      "[ERROR:154] failed resetting cache counters");

	request_object_cache_flush();
	meta.mask = FAN_OPEN_PERM;
	decision_context_set_current(ctx0);
	CHECK(new_event(&meta, &event) == 0, 155,
	      "[ERROR:155] first context post-flush event failed");
	decision_context_set_current(ctx1);
	CHECK(new_event(&meta, &event) == 0, 156,
	      "[ERROR:156] second context post-flush event failed");

	CHECK(read_object_cache_metric("hits", &object_hits) == 0, 157,
	      "[ERROR:157] failed reading object cache hits");
	CHECK(read_object_cache_metric("misses", &object_misses) == 0, 158,
	      "[ERROR:158] failed reading object cache misses");
	CHECK(object_hits == 0, 159,
	      "[ERROR:159] one worker reused stale object cache");
	CHECK(object_misses == 2, 160,
	      "[ERROR:160] both worker caches were not refreshed");

	destroy_event_system();
	return 0;
}

/*
 * Verify that cache reports retain aggregate counters and add worker detail.
 */
static int test_cache_worker_report(void)
{
	CHECK(init_caches(2, 2) == 0, 120,
	      "[ERROR:120] init_event_system failed");

	CHECK(cache_report_contains("Subject hits:"), 121,
	      "[ERROR:121] aggregate subject counters missing");
	CHECK(cache_report_contains("Per-worker cache effectiveness:"), 122,
	      "[ERROR:122] per-worker cache section missing");
	CHECK(cache_report_contains("  Decision worker 0 Subject hits:"), 123,
	      "[ERROR:123] worker subject counters missing");
	CHECK(cache_report_contains("  Decision worker 0 Object hits:"), 124,
	      "[ERROR:124] worker object counters missing");

	destroy_event_system();
	return 0;
}

/* Run each scenario in sequence, propagating the first non-zero error code. */
int main(void)
{
	int rc;

	rc = test_rpm_accepts_sha512();
	if (rc)
		return rc;

	rc = test_filedb_rejects_sha512();
	if (rc)
		return rc;

	rc = test_filedb_accepts_sha256();
	if (rc)
		return rc;

	rc = test_attribute_replacement_preserves_count();
	if (rc)
		return rc;

	rc = test_attr_lookup_metrics();
	if (rc)
		return rc;

	rc = test_reopen_skip_path();
	if (rc)
		return rc;

	rc = test_subject_eviction();
	if (rc)
		return rc;

	rc = test_subject_slot_blocks_pre_full_collision();
	if (rc)
		return rc;

	rc = test_traced_building_occupant_eviction();
	if (rc)
		return rc;

	rc = test_flush_generation_resets_object_cache();
	if (rc)
		return rc;

	rc = test_flush_generation_reaches_all_contexts();
	if (rc)
		return rc;

	rc = test_cache_worker_report();
	if (rc)
		return rc;

	return 0;
}
