// oe state notification unit test: when not consumed, each type keeps only its first queue position; after a pop a new change is queued again.
// The three messages only signal "query the current state" and carry no historical value. Expected order/state are hand-written, not generated by the implementation.
#ifndef HLE_OE_SOURCE
#define HLE_OE_SOURCE "../hle_oe.c"
#endif
#include HLE_OE_SOURCE
#include <signal.h>
#include <sys/wait.h>
void dm_thread_native_wait(int entering) { (void)entering; } // queue-only test, without the guest scheduler
uint64_t now_ns(void) { return 0; }
void dm_audio_suspend(int on) { (void)on; }   // stubs for the bg item of DM_OE_SCRIPT (hle_audio.c / hle_os.c / hle_nvn.c)
void guest_clock_freeze_for(int reason, int on) { (void)reason; (void)on; }
void dm_gfx_suspend(int on) { (void)on; }
void dm_host_shutdown_then(const char *why, void (*then)(void *), void *arg) { (void)why; (void)then; (void)arg; }   // oe::ExecuteProgram, which this test does not call
void dm_host_shutdown(const char *why) { (void)why; }
// The notification event (hle_thread.c binds a kernel event): the unit test only tracks whether it is signalled
static int ev_bound, ev_sig;
void hle_bind_system_event(void *g, int autoclear) { (void)g; (void)autoclear; ev_bound = 1; }
void hle_signal_system_event(void *g) { (void)g; ev_sig = 1; }
void hle_clear_system_event(void *g) { (void)g; ev_sig = 0; }
static int checks, fails;
#define CHECK(c, s) do { checks++; int ok = !!(c); fails += !ok; printf("%s %s\n", ok ? "✓" : "✗", s); } while (0)
static void reset(void) { g_mqh = g_mqn = 0; g_focus = 1; g_seen = 1; g_applets = 0; g_opmode = 1; g_ntf_opmode = g_ntf_perfmode = 0; g_focus_mode = -1; }
static int pop_is(uint32_t want) { uint32_t m = 999; return h_oe_TryPopMsg(&m) && m == want; }
static int empty(void) { uint32_t m = 999; return !h_oe_TryPopMsg(&m) && m == 999; }
static int child_kind;
static void abort_exit(int sig) { (void)sig; dprintf(2, "[RED] t_oe child kind=%d: 10 s watchdog timeout\n", child_kind); _exit(97); } // the old-version die in the child process can be checked, avoiding a crash report or core
static void *focus_burst(void *p) { (void)p; for (int i = 0; i < 17; i++) { dm_focus_set(2); dm_focus_set(1); } return NULL; }
static void *mode_burst(void *p) { (void)p; for (int i = 0; i < 17; i++) dm_set_opmode(i % 2); return NULL; }
static pthread_mutex_t stage_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t stage_cond = PTHREAD_COND_INITIALIZER;
static int stage, interleaved_ok;
static void stage_set(int n) { pthread_mutex_lock(&stage_lock); stage = n; pthread_cond_broadcast(&stage_cond); pthread_mutex_unlock(&stage_lock); }
static void stage_wait(int n) { pthread_mutex_lock(&stage_lock); while (stage < n) pthread_cond_wait(&stage_cond, &stage_lock); pthread_mutex_unlock(&stage_lock); }
static void *consume_interleaved(void *p) {
    (void)p; stage_wait(1); int ok = pop_is(15); stage_set(2); // popped but not yet queried: let the producer update exactly here
    stage_wait(3); interleaved_ok = ok && h_oe_GetFocus() == 1 && pop_is(15) && pop_is(30) && pop_is(31) && empty() && h_oe_GetOpMode() == 0 && h_oe_GetPerfMode() == 0;
    return NULL;
}
static int child_burst(int kind) {
    fflush(stdout); pid_t p = fork(); if (p < 0) return 0;
    if (!p) {
        child_kind = kind; signal(SIGABRT, abort_exit); signal(SIGALRM, abort_exit); alarm(10); reset();
        g_ntf_opmode = g_ntf_perfmode = 1;
        if (kind == 0) { focus_burst(NULL); _exit(h_oe_GetFocus() == 1 && pop_is(15) && empty() ? 0 : 1); }
        if (kind == 1) { mode_burst(NULL); _exit(h_oe_GetOpMode() == 0 && h_oe_GetPerfMode() == 0 && pop_is(30) && pop_is(31) && empty() ? 0 : 1); }
        if (kind == 3) {
            pthread_t c; pthread_create(&c, NULL, consume_interleaved, NULL);
            dm_focus_set(2); stage_set(1); stage_wait(2);
            dm_focus_set(1); dm_set_opmode(0); stage_set(3); pthread_join(c, NULL);
            _exit(interleaved_ok ? 0 : 1);
        }
        pthread_t f, m; pthread_create(&f, NULL, focus_burst, NULL); pthread_create(&m, NULL, mode_burst, NULL);
        pthread_join(f, NULL); pthread_join(m, NULL);
        uint32_t msg; int nf = 0, no = 0, np = 0, order = 1;
        while (h_oe_TryPopMsg(&msg)) { if (msg == 15) nf++; else if (msg == 30) no++; else if (msg == 31) { order &= no == 1; np++; } else order = 0; }
        _exit(nf == 1 && no == 1 && np == 1 && order && h_oe_GetFocus() == 1 && h_oe_GetOpMode() == 0 ? 0 : 1);
    }
    int st; if (waitpid(p, &st, 0) != p) return 0;
    return WIFEXITED(st) && WEXITSTATUS(st) == 0;
}
int main(void) {
    unsetenv("DM_OE_SCRIPT"); reset();
    CHECK(h_oe_InactivityMode() == 0 && !h_oe_InactivityExtQ(), "inactivity extension starts disabled");
    h_oe_InactivityExtSafe(1);
    CHECK(h_oe_InactivityMode() == 1 && h_oe_InactivityExtQ(), "normal inactivity extension is mode 1");
    h_oe_InactivityExt(1);
    CHECK(h_oe_InactivityMode() == 2 && h_oe_InactivityExtQ(), "unsafe inactivity extension is mode 2");
    h_oe_InactivityExtSafe(0x100);
    CHECK(h_oe_InactivityMode() == 0 && !h_oe_InactivityExtQ(), "clearing the extension restores mode 0");
    CHECK(empty(), "empty queue at start: returns false and leaves the output alone");
    dm_focus_set(1); dm_set_opmode(1); CHECK(empty(), "same state sends no notification");
    dm_focus_set(3); CHECK(h_oe_GetFocus() == 3, "background state 3 can be queried before popping");
    CHECK(pop_is(15) && empty(), "background change notification 15 exactly once");
    dm_focus_set(2); dm_focus_set(1);
    CHECK(pop_is(15) && empty() && h_oe_GetFocus() == 1, "unpopped focus-lost/resume merged, final focus queried as 1");
    reset(); g_ntf_opmode = g_ntf_perfmode = 1;
    dm_focus_set(2); dm_set_opmode(0); dm_focus_set(3); dm_set_opmode(1);
    CHECK(pop_is(15) && pop_is(30) && pop_is(31) && empty(), "first positions of the three kinds FIFO=15,30,31, repeats do not move them");
    CHECK(h_oe_GetFocus() == 3 && h_oe_GetOpMode() == 1 && h_oe_GetPerfMode() == 1, "repeated notifications keep no stale value, latest 3/1/1 queried");
    reset(); g_ntf_opmode = g_ntf_perfmode = 1;
    dm_set_opmode(0); dm_focus_set(2); dm_set_opmode(1);
    CHECK(pop_is(30) && pop_is(31) && pop_is(15) && empty(), "when the mode changes first FIFO=30,31,15, the pair order is unchanged");
    reset(); g_ntf_opmode = g_ntf_perfmode = 1;
    dm_focus_set(2); CHECK(pop_is(15), "focus notification pops first");
    dm_set_opmode(0); dm_focus_set(1);
    CHECK(pop_is(30) && pop_is(31) && pop_is(15) && empty(), "a resume after the pop notifies again and queues after the pending mode messages");
    CHECK(h_oe_GetFocus() == 1, "current focus after resume=1");
    reset(); dm_set_opmode(0);
    CHECK(empty() && h_oe_GetOpMode() == 0, "notifications off only update state, nothing queued");
    h_oe_SetOpModeNtf(0x100); h_oe_SetPerfModeNtf(0x101); dm_set_opmode(1);
    CHECK(pop_is(31) && empty(), "notification bool reads only the low 8 bits, performance alone sends only 31");
    h_oe_SetOpModeNtf(0x101); h_oe_SetPerfModeNtf(0x100); dm_set_opmode(0);
    CHECK(pop_is(30) && empty(), "operation mode alone sends only 30");
    reset(); mq_push(16); mq_push(16);
    CHECK(pop_is(16) && pop_is(16) && empty(), "per-occurrence event 16 not merged, both kept in FIFO");
    reset(); g_ntf_opmode = g_ntf_perfmode = 1; dm_set_opmode(0);
    CHECK(pop_is(30), "of the mode pair 30 popped first, 31 not yet");
    dm_set_opmode(1);
    CHECK(pop_is(31) && pop_is(30) && empty() && h_oe_GetOpMode() == 1, "popping across a new change: old 31 keeps its place, popped 30 queued again, final mode 1");
    CHECK(child_burst(0), "34 focus changes across the old 32 slots, no abort, final resume 1 / one 15");
    CHECK(child_burst(1), "17 mode changes across the old 32 slots, no abort, final 0 / 30,31");
    CHECK(child_burst(2), "two concurrent producers, exactly one of each kind, 30 before 31, final focus 1 / mode 0");
    reset(); g_ntf_opmode = g_ntf_perfmode = 1; int rotated = 1;
    for (int i = 0; i < 31; i++) { dm_focus_set(2 - i % 2); rotated &= pop_is(15) && empty(); }
    CHECK(rotated, "31 pops one at a time, moving the ring head to the last slot");
    dm_focus_set(1); dm_set_opmode(0); dm_focus_set(3); dm_set_opmode(1);
    CHECK(pop_is(15) && pop_is(30) && pop_is(31) && empty(), "after wrapping across 32 slots still deduplicated by 15,30,31 in first-seen order");
    CHECK(child_burst(3), "new change after the consumer pops and before it queries: the new resume is not missed, final focus 1 / mode 0");
    // The notification event follows the queue, as the am service's does: signalled while a message is queued, cleared when the last one is popped. Monster Hunter Rise's
    // MessageSuperviseThread (main+0x6bc8fc4) waits on it with WaitAny, which does not clear it, then pops until the queue is empty: a signal left behind made it spin on a core
    reset(); g_ntf_opmode = g_ntf_perfmode = 1;
    h_oe_GetMsgEv();
    CHECK(ev_bound && !ev_sig, "notification event: bound on first use, not signalled while the queue is empty");
    dm_focus_set(2);
    CHECK(ev_sig, "notification event: signalled when a notification is queued");
    CHECK(pop_is(15) && !ev_sig, "notification event: cleared when the last notification is popped");
    dm_focus_set(1); dm_set_opmode(0);
    CHECK(pop_is(15) && ev_sig, "notification event: still signalled after a pop that leaves notifications queued");
    CHECK(pop_is(30) && ev_sig && pop_is(31) && !ev_sig, "notification event: cleared only when the queue is empty");
    ev_sig = 1;
    CHECK(empty() && !ev_sig, "notification event: a pop from an empty queue clears a signal left behind");
    // Focus handling modes 0 and 3 get no focus notifications (the system suspends the game instead); 1 and 2 do. Super Mario Galaxy 2 sets 3 and deadlocked in its own pause on focus 2
    reset(); g_ntf_opmode = g_ntf_perfmode = 1; h_oe_SetFocusMode(3);
    dm_focus_set(2); dm_focus_set(3);
    CHECK(empty() && h_oe_GetFocus() == 3, "focus mode 3: focus changes queue nothing, the state is still queried as 3");
    dm_set_opmode(0); dm_focus_set(1);
    CHECK(pop_is(30) && pop_is(31) && empty() && h_oe_GetFocus() == 1, "focus mode 3: mode notifications still queued, no 15");
    h_oe_SetFocusMode(0); dm_focus_set(2);
    CHECK(empty(), "focus mode 0: no focus notification");
    h_oe_SetFocusMode(2); dm_focus_set(1);
    CHECK(pop_is(15) && empty(), "focus mode 2: focus notification 15");
    h_oe_SetFocusMode(1); dm_focus_set(2);
    CHECK(pop_is(15) && empty(), "focus mode 1: focus notification 15");
    // Library applets (DM_APPLET): on Horizon an applet the application opens takes its focus, 15 with OutOfFocus on the way in and 15 with InFocus on the way out. Mario Kart 8
    // Deluxe applies the controller applet's answer only after that InFocus notification (GitHub issue #550). The front end's own state is kept apart from the applet's
    reset();
    dm_applet_enter("test"); CHECK(h_oe_GetFocus() == 2 && pop_is(15) && empty(), "applet opens over a game in focus: 15, state 2");
    dm_applet_leave(); CHECK(h_oe_GetFocus() == 1 && pop_is(15) && empty(), "applet closes: 15, state 1");
    dm_applet_enter("test"); dm_applet_leave();
    CHECK(pop_is(15) && empty() && h_oe_GetFocus() == 1, "an applet answered at once: one 15 (not popped in between), queried as 1");
    CHECK(dm_focus_now() == 1, "an applet leaves the front end's own state alone");
    dm_applet_enter("test"); pop_is(15);
    dm_focus_set(2); CHECK(empty() && h_oe_GetFocus() == 2 && dm_focus_now() == 2, "window loses focus while an applet is open: no change the game sees");
    dm_focus_set(1); CHECK(empty() && h_oe_GetFocus() == 2, "window regains focus while an applet is open: still 2, the applet keeps focus");
    dm_applet_leave(); CHECK(pop_is(15) && empty() && h_oe_GetFocus() == 1, "then the applet closes: 15, state 1");
    dm_applet_enter("test"); pop_is(15); dm_focus_set(2); dm_applet_leave();
    CHECK(empty() && h_oe_GetFocus() == 2, "applet closes while the window is out of focus: still 2, no notification");
    dm_focus_set(1); CHECK(pop_is(15) && empty() && h_oe_GetFocus() == 1, "window regains focus after the applet closed: 15, state 1");
    dm_applet_enter("test"); pop_is(15); dm_focus_set(3);
    CHECK(pop_is(15) && empty() && h_oe_GetFocus() == 3, "background while an applet is open: 15, state 3");
    dm_focus_set(2); dm_focus_set(1); CHECK(pop_is(15) && empty() && h_oe_GetFocus() == 2, "back to the foreground with the applet still open: 15, state 2");
    dm_applet_enter("test"); dm_applet_leave(); CHECK(empty() && h_oe_GetFocus() == 2, "a nested applet: the outer one keeps the game out of focus");
    dm_applet_leave(); CHECK(pop_is(15) && empty() && h_oe_GetFocus() == 1, "the outer applet closes: 15, state 1");
    h_oe_SetFocusMode(3); dm_applet_enter("test");
    CHECK(empty() && h_oe_GetFocus() == 2, "focus mode 3: an applet changes the state without a notification");
    dm_applet_leave(); CHECK(empty() && h_oe_GetFocus() == 1, "focus mode 3: back to 1 without a notification");
    printf("== oe %d checks, %d failed ==\n", checks, fails); return fails != 0;
}
