//! HLE kernel export implementations (xam.xex).

use crate::objects::KernelObject;
use crate::state::{GuestMemoryPcr, KernelState, ModuleId};
use crate::thread::allocate_thread_image;
use xenia_cpu::PpcContext;
use xenia_cpu::scheduler::SpawnParams;
use xenia_memory::{GuestMemory, MemoryAccess};

pub fn register_exports(state: &mut KernelState) {
    use ModuleId::Xam;

    // Net
    state.register_export(Xam, 0x01, "NetDll_WSAStartup", stub_success);
    state.register_export(Xam, 0x02, "NetDll_WSACleanup", stub_success);

    // Input
    state.register_export(Xam, 0x0190, "XamInputGetCapabilities", xam_input_get_capabilities);
    state.register_export(Xam, 0x0191, "XamInputGetState", xam_input_get_state);
    state.register_export(Xam, 0x0192, "XamInputSetState", xam_input_set_state);
    state.register_export(Xam, 0x0198, "XamInputGetKeystrokeEx", xam_input_get_keystroke);

    // Inactivity
    state.register_export(Xam, 0x01A0, "XamEnableInactivityProcessing", stub_success);
    state.register_export(Xam, 0x01A1, "XamResetInactivity", stub_success);

    // Loader
    state.register_export(Xam, 0x01A4, "XamLoaderLaunchTitle", xam_loader_launch_title);
    state.register_export(Xam, 0x01A9, "XamLoaderTerminateTitle", xam_loader_terminate_title);

    // Task
    state.register_export(Xam, 0x01AF, "XamTaskSchedule", xam_task_schedule);
    state.register_export(Xam, 0x01B1, "XamTaskCloseHandle", stub_success);
    state.register_export(Xam, 0x01B3, "XamTaskShouldExit", stub_return_zero);

    // Alloc
    state.register_export(Xam, 0x01EA, "XamAlloc", xam_alloc);
    state.register_export(Xam, 0x01EC, "XamFree", stub_success);

    // Msg
    state.register_export(Xam, 0x01F4, "XMsgInProcessCall", stub_success);
    state.register_export(Xam, 0x01F7, "XMsgStartIORequest", stub_success);
    state.register_export(Xam, 0x01FC, "XMsgStartIORequestEx", stub_success);

    // User
    state.register_export(Xam, 0x020A, "XamUserGetXUID", xam_user_get_xuid);
    state.register_export(Xam, 0x020E, "XamUserGetName", xam_user_get_name);
    state.register_export(Xam, 0x0210, "XamUserGetSigninState", xam_user_get_signin_state);
    state.register_export(Xam, 0x0219, "XamUserReadProfileSettings", xam_user_read_profile_settings);
    state.register_export(Xam, 0x021A, "XamUserWriteProfileSettings", stub_success);

    // Enum
    state.register_export(Xam, 0x0250, "XamEnumerate", stub_error_no_more_files);

    // Content
    state.register_export(Xam, 0x0258, "XamContentCreate", stub_success);
    state.register_export(Xam, 0x025A, "XamContentClose", stub_success);
    state.register_export(Xam, 0x025B, "XamContentDelete", stub_success);
    state.register_export(Xam, 0x025C, "XamContentCreateEnumerator", stub_success);
    state.register_export(Xam, 0x025E, "XamContentGetDeviceData", stub_success);
    state.register_export(Xam, 0x025F, "XamContentGetDeviceName", stub_success);
    state.register_export(Xam, 0x0260, "XamContentSetThumbnail", stub_success);
    state.register_export(Xam, 0x0262, "XamContentGetCreator", stub_success);
    state.register_export(Xam, 0x0265, "XamContentGetDeviceState", stub_success);

    // System
    state.register_export(Xam, 0x0280, "XamGetExecutionId", xam_get_execution_id);
    state.register_export(Xam, 0x0282, "XamGetSystemVersion", xam_get_system_version);

    // Notify
    state.register_export(Xam, 0x028A, "XamNotifyCreateListener", xam_notify_create_listener);
    state.register_export(Xam, 0x028B, "XNotifyGetNext", xnotify_get_next);
    state.register_export(Xam, 0x028C, "XNotifyPositionUI", stub_success);

    // Achievements/Stats
    state.register_export(Xam, 0x02EE, "XamUserCreateAchievementEnumerator", stub_success);
    state.register_export(Xam, 0x02F7, "XamUserCreateStatsEnumerator", stub_success);

    // UI
    state.register_export(Xam, 0x02BC, "XamShowSigninUI", stub_success);
    state.register_export(Xam, 0x02C1, "XamShowKeyboardUI", stub_success);
    state.register_export(Xam, 0x02CB, "XamShowDeviceSelectorUI", stub_success);
    state.register_export(Xam, 0x02D5, "XamShowGamerCardUIForXUID", stub_success);
    state.register_export(Xam, 0x02D9, "XamShowDirtyDiscErrorUI", stub_success);
    state.register_export(Xam, 0x02DC, "XamShowMessageBoxUIEx", stub_success);

    // Session
    state.register_export(Xam, 0x0316, "XamSessionCreateHandle", xam_session_create_handle);
    state.register_export(Xam, 0x0317, "XamSessionRefObjByHandle", stub_success);

    // Locale
    state.register_export(Xam, 0x03CB, "XGetAVPack", xget_avpack);
    state.register_export(Xam, 0x03CC, "XGetGameRegion", xget_game_region);
    state.register_export(Xam, 0x03CD, "XGetLanguage", xget_language);
    state.register_export(Xam, 0x03D1, "XGetVideoMode", xget_video_mode);
}

// ===== Generic stubs =====

fn stub_success(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
    ctx.gpr[3] = 0;
}

fn stub_return_zero(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
    ctx.gpr[3] = 0;
}

fn stub_error_no_more_files(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
    ctx.gpr[3] = 0x12; // ERROR_NO_MORE_FILES
}

// ===== Input =====

/// Helper: pack a `GamepadState` into a 12-byte key used to detect input
/// changes. Cheap to compare across frames.
fn gamepad_key(state: &xenia_hid::GamepadState) -> u128 {
    let mut bytes = [0u8; 16];
    bytes[0..2].copy_from_slice(&state.buttons.to_be_bytes());
    bytes[2] = state.left_trigger;
    bytes[3] = state.right_trigger;
    bytes[4..6].copy_from_slice(&state.left_stick_x.to_be_bytes());
    bytes[6..8].copy_from_slice(&state.left_stick_y.to_be_bytes());
    bytes[8..10].copy_from_slice(&state.right_stick_x.to_be_bytes());
    bytes[10..12].copy_from_slice(&state.right_stick_y.to_be_bytes());
    u128::from_be_bytes(bytes)
}

fn xam_input_get_capabilities(
    ctx: &mut PpcContext,
    mem: &GuestMemory,
    state: &mut KernelState,
) {
    // r3 = user_index, r4 = flags, r5 = out X_INPUT_CAPABILITIES*
    let user = ctx.gpr[3] as u32;
    let out_ptr = ctx.gpr[5] as u32;
    let connected = state.ui.as_ref().is_some_and(|ui| ui.is_connected(user));
    if !connected {
        ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64;
        return;
    }
    xenia_hid::write_input_capabilities(mem, out_ptr);
    ctx.gpr[3] = xenia_hid::errors::SUCCESS as u64;
}

fn xam_input_get_state(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
    // r3 = user_index, r4 = flags, r5 = out X_INPUT_STATE*
    let user = ctx.gpr[3] as u32;
    let out_ptr = ctx.gpr[5] as u32;
    let Some(ui) = state.ui.as_ref() else {
        ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64;
        return;
    };
    if !ui.is_connected(user) {
        ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64;
        return;
    }
    let gamepad = ui.snapshot_gamepad();
    let key = gamepad_key(&gamepad);
    if key != state.last_input_bytes {
        state.input_packet_number = state.input_packet_number.wrapping_add(1);
        state.last_input_bytes = key;
    }
    xenia_hid::write_input_state(mem, out_ptr, state.input_packet_number, &gamepad);
    ctx.gpr[3] = xenia_hid::errors::SUCCESS as u64;
}

fn xam_input_set_state(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
    // r3 = user_index, r4 = flags, r5 = X_INPUT_VIBRATION*
    // Rumble is out of scope for Phase 1; we accept the call and return
    // success so games don't retry in a tight loop, but we never actually
    // shake anything.
    let user = ctx.gpr[3] as u32;
    let connected = state.ui.as_ref().is_some_and(|ui| ui.is_connected(user));
    if !connected {
        ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64;
        return;
    }
    ctx.gpr[3] = xenia_hid::errors::SUCCESS as u64;
}

fn xam_input_get_keystroke(
    ctx: &mut PpcContext,
    _mem: &GuestMemory,
    _state: &mut KernelState,
) {
    // No keyboard input in Phase 1 — always "queue empty". Games that only
    // use the gamepad ignore this return code; those that drive text entry
    // through the keystroke queue simply get a permanently empty queue, which
    // manifests as no virtual-keyboard input — acceptable for minimal UI.
    ctx.gpr[3] = xenia_hid::errors::EMPTY as u64;
}

// ===== Loader =====

fn xam_loader_launch_title(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
    tracing::warn!("XamLoaderLaunchTitle called");
    ctx.gpr[3] = 0;
}

fn xam_loader_terminate_title(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
    tracing::warn!("XamLoaderTerminateTitle called");
    ctx.gpr[3] = 0;
}

// ===== Task =====

/// `XamTaskSchedule(callback, message, optional_ptr, handle_ptr_out)` —
/// spawn a guest thread that runs `callback(message)` asynchronously.
/// Mirrors xenia-canary's `XamTaskSchedule_entry` (xam_task.cc:43-80):
/// stack is `max(0x4000, page-aligned default)`, the new thread enters at
/// `callback` with `message` in r3, and the resulting thread handle is
/// written to `handle_ptr_out`.
fn xam_task_schedule(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
    let callback = ctx.gpr[3] as u32;
    let message_ptr = ctx.gpr[4] as u32;
    let optional_ptr = ctx.gpr[5] as u32;
    let handle_ptr = ctx.gpr[6] as u32;

    if optional_ptr != 0 {
        let v1 = mem.read_u32(optional_ptr);
        let v2 = mem.read_u32(optional_ptr + 4);
        tracing::info!("XamTaskSchedule: args v1={:#010x} v2={:#010x}", v1, v2);
    }

    let stack_size = std::cmp::max(0x4000u32, (0x10_0000u32 + 0xFFF) & !0xFFF);

    let Some(image) = allocate_thread_image(state, mem, stack_size, 0) else {
        tracing::error!("XamTaskSchedule: failed to allocate thread image");
        ctx.gpr[3] = 0xC000_009A; // STATUS_INSUFFICIENT_RESOURCES
        return;
    };

    use std::sync::atomic::Ordering;
    let tid = state.next_thread_id.fetch_add(1, Ordering::Relaxed);
    let handle = state.alloc_handle_for(KernelObject::Thread {
        id: tid,
        hw_id: None,
        exit_code: None,
        waiters: Vec::new(),
    });

    let tls_slot_count = state.next_tls_index.load(Ordering::Relaxed);
    let params = SpawnParams {
        entry: callback,
        start_context: message_ptr,
        stack_base: image.stack_base,
        stack_size: image.stack_size,
        pcr_base: image.pcr_base,
        tls_base: image.tls_base,
        thread_handle: handle,
        guest_tid: tid,
        create_suspended: false,
        is_initial: false,
        tls_slot_count,
        affinity_mask: 0,
        priority: 0,
        ideal_processor: None,
    };
    match state.scheduler.spawn(params, &mut GuestMemoryPcr(mem)) {
        Ok(hw_id) => {
            metrics::counter!("scheduler.spawn.ok").increment(1);
            if let Some(KernelObject::Thread { hw_id: slot, .. }) = state.objects.get_mut(&handle) {
                *slot = Some(hw_id);
            }
            if handle_ptr != 0 {
                mem.write_u32(handle_ptr, handle);
            }
            state.audit_create_with_ctx(handle, "Thread", ctx, mem, "XamTaskSchedule");
            tracing::info!(
                "XamTaskSchedule: tid={} handle={:#x} hw={} callback={:#010x} message={:#010x}",
                tid,
                handle,
                hw_id,
                callback,
                message_ptr,
            );
            ctx.gpr[3] = 0; // STATUS_SUCCESS
        }
        Err(_) => {
            metrics::counter!("scheduler.spawn.rejected").increment(1);
            tracing::error!("XamTaskSchedule: no free HW thread slot");
            ctx.gpr[3] = 0xC000_009A;
        }
    }
}

// ===== Alloc =====

fn xam_alloc(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
    // r3 = flags, r4 = size, r5 = out_ptr_ptr
    let size = ctx.gpr[4] as u32;
    let out_ptr = ctx.gpr[5] as u32;

    match state.heap_alloc(size, mem) {
        Some(addr) => {
            if out_ptr != 0 {
                mem.write_u32(out_ptr, addr);
            }
            ctx.gpr[3] = 0; // SUCCESS
        }
        None => {
            ctx.gpr[3] = 0x8007_000E; // E_OUTOFMEMORY
        }
    }
}

// ===== User =====

fn xam_user_get_xuid(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
    // r3 = user_index, r4 = xuid_ptr
    let xuid_ptr = ctx.gpr[4] as u32;
    if xuid_ptr != 0 {
        mem.write_u64(xuid_ptr, 0); // No XUID
    }
    ctx.gpr[3] = 0;
}

fn xam_user_get_name(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
    // r3 = user_index, r4 = buffer, r5 = buffer_size
    let buffer = ctx.gpr[4] as u32;
    if buffer != 0 {
        mem.write_u8(buffer, 0); // Empty string
    }
    ctx.gpr[3] = 0;
}

fn xam_user_read_profile_settings(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
    // Return error — no profile
    ctx.gpr[3] = 0x0000_048B; // ERROR_NOT_FOUND
}

fn xam_user_get_signin_state(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
    // r3 = user_index
    let user_index = ctx.gpr[3] as u32;
    ctx.gpr[3] = if user_index == 0 { 1 } else { 0 };
}

// ===== System =====

fn xam_get_execution_id(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
    // r3 = execution_id_ptr_ptr — write pointer to execution info
    let ptr_ptr = ctx.gpr[3] as u32;
    if ptr_ptr != 0 {
        // Allocate and fill a fake XEX_EXECUTION_ID structure
        if let Some(exec_id_addr) = state.heap_alloc(0x18, mem) {
            mem.write_u32(exec_id_addr, 0x535107D4); // title_id (Project Sylpheed)
            mem.write_u32(exec_id_addr + 4, 0x2D2E2EEB); // media_id
            mem.write_u16(exec_id_addr + 8, 0); // version
            mem.write_u16(exec_id_addr + 10, 0); // base_version
            mem.write_u16(exec_id_addr + 12, 1); // disc_number
            mem.write_u16(exec_id_addr + 14, 1); // disc_count
            mem.write_u32(ptr_ptr, exec_id_addr);
        }
    }
    ctx.gpr[3] = 0;
}

fn xam_get_system_version(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
    ctx.gpr[3] = 0x2000_0000; // System version
}

// ===== Notify =====

const K_X_NOTIFY_SYSTEM: u64 = 0x00000001;
const K_X_NOTIFY_LIVE: u64 = 0x00000002;
const K_X_NOTIFICATION_SYSTEM_UI: u32 = 0x09;
const K_X_NOTIFICATION_SYSTEM_SIGN_IN_CHANGED: u32 = 0x0A;
const K_X_NOTIFICATION_LIVE_CONNECTION_CHANGED: u32 = 0x02000001;
const K_X_NOTIFICATION_LIVE_LINK_STATE_CHANGED: u32 = 0x02000003;

fn notification_mask_index(id: u32) -> u32 {
    (id >> 25) & 0x3F
}

fn notification_version(id: u32) -> u32 {
    (id >> 16) & 0x1FF
}

fn enqueue_notification(obj: &mut KernelObject, id: u32, data: u32) {
    if let KernelObject::NotifyListener { mask, max_version, queue, .. } = obj {
        let idx = notification_mask_index(id);
        if (*mask & (1u64 << idx)) == 0 {
            return;
        }
        if notification_version(id) > *max_version {
            return;
        }
        queue.push_back((id, data));
    }
}

fn xam_notify_create_listener(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
    let mask = ctx.gpr[3];
    let mut max_version = ctx.gpr[4] as u32;
    if max_version > 10 {
        max_version = 10;
    }
    let handle = state.alloc_handle_for(KernelObject::NotifyListener {
        mask,
        max_version,
        queue: std::collections::VecDeque::new(),
        waiters: Vec::new(),
    });

    let mut startup_pending: Vec<(u32, u32)> = Vec::new();
    if !state.has_notified_startup && (mask & K_X_NOTIFY_SYSTEM) != 0 {
        state.has_notified_startup = true;
        startup_pending.push((K_X_NOTIFICATION_SYSTEM_UI, 0));
        startup_pending.push((K_X_NOTIFICATION_SYSTEM_SIGN_IN_CHANGED, 1));
    }
    if !state.has_notified_live_startup && (mask & K_X_NOTIFY_LIVE) != 0 {
        state.has_notified_live_startup = true;
        startup_pending.push((K_X_NOTIFICATION_LIVE_CONNECTION_CHANGED, 0x001510F1));
        startup_pending.push((K_X_NOTIFICATION_LIVE_LINK_STATE_CHANGED, 0));
    }
    if let Some(obj) = state.objects.get_mut(&handle) {
        for (id, data) in startup_pending {
            enqueue_notification(obj, id, data);
        }
    }

    let _ = mem;
    ctx.gpr[3] = handle as u64;
}

fn xnotify_get_next(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
    let handle = ctx.gpr[3] as u32;
    let match_id = ctx.gpr[4] as u32;
    let id_ptr = ctx.gpr[5] as u32;
    let param_ptr = ctx.gpr[6] as u32;

    if param_ptr != 0 {
        mem.write_u32(param_ptr, 0);
    }
    if id_ptr == 0 {
        ctx.gpr[3] = 0;
        return;
    }
    mem.write_u32(id_ptr, 0);

    let Some(KernelObject::NotifyListener { queue, .. }) = state.objects.get_mut(&handle) else {
        ctx.gpr[3] = 0;
        return;
    };

    let dequeued = if match_id != 0 {
        let pos = queue.iter().position(|&(id, _)| id == match_id);
        match pos {
            Some(p) => {
                let (id, data) = queue.remove(p).unwrap();
                Some((id, data))
            }
            None => None,
        }
    } else {
        queue.pop_front()
    };

    match dequeued {
        Some((id, data)) => {
            mem.write_u32(id_ptr, id);
            if param_ptr != 0 {
                mem.write_u32(param_ptr, data);
            }
            ctx.gpr[3] = 1;
        }
        None => {
            ctx.gpr[3] = 0;
        }
    }
}

// ===== Session =====

fn xam_session_create_handle(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
    // r3 = handle_ptr
    let handle_ptr = ctx.gpr[3] as u32;
    let handle = state.alloc_handle();
    if handle_ptr != 0 {
        mem.write_u32(handle_ptr, handle);
    }
    ctx.gpr[3] = 0;
}

// ===== Locale =====

fn xget_avpack(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
    ctx.gpr[3] = 8;
}

fn xget_game_region(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
    ctx.gpr[3] = 0xFF; // All regions
}

fn xget_language(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
    ctx.gpr[3] = 1; // English
}

fn xget_video_mode(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
    // r3 = video_mode_ptr
    let ptr = ctx.gpr[3] as u32;
    if ptr != 0 {
        mem.write_u32(ptr, 1280);     // width
        mem.write_u32(ptr + 4, 720);  // height
        mem.write_u32(ptr + 8, 0);    // is_interlaced
        mem.write_u32(ptr + 12, 1);   // is_widescreen
        mem.write_u32(ptr + 16, 60);  // refresh_rate
    }
    ctx.gpr[3] = 0;
}

#[cfg(test)]
mod tests {
    use super::*;
    use xenia_memory::page_table::MemoryProtect;

    const SCRATCH_BASE: u32 = 0x4000_0000;

    fn fresh() -> (PpcContext, GuestMemory, KernelState) {
        let mut mem = GuestMemory::new().expect("memory init");
        mem.alloc(SCRATCH_BASE, 0x1000, MemoryProtect::READ | MemoryProtect::WRITE)
            .expect("scratch page must commit");
        let mut state = KernelState::new();
        state.install_initial_thread(
            PpcContext::default(),
            0x7000_0000,
            0x10_0000,
            SCRATCH_BASE + 0x800,
            SCRATCH_BASE + 0xC00,
            0xF000_0001,
            &mut mem,
        );
        state.scheduler.begin_slot_visit(0);
        (PpcContext::default(), mem, state)
    }

    #[test]
    fn xam_task_schedule_spawns_real_thread() {
        let (mut ctx, mut mem, mut state) = fresh();

        let callback_pc: u32 = 0x824a_93c8;
        let message_ptr: u32 = SCRATCH_BASE + 0x100;
        let handle_out: u32 = SCRATCH_BASE + 0x200;
        ctx.gpr[3] = callback_pc as u64;
        ctx.gpr[4] = message_ptr as u64;
        ctx.gpr[5] = 0;
        ctx.gpr[6] = handle_out as u64;
        ctx.lr = 0x824a_9a14;

        xam_task_schedule(&mut ctx, &mut mem, &mut state);

        assert_eq!(ctx.gpr[3], 0, "XamTaskSchedule must return STATUS_SUCCESS");

        let handle = mem.read_u32(handle_out);
        assert!(handle >= 0x1000, "handle must be allocated, got {:#x}", handle);

        let r = state
            .scheduler
            .find_by_handle(handle)
            .expect("spawned thread must be findable by handle");
        let new_ctx = state.scheduler.ctx_mut_ref(r);
        assert_eq!(new_ctx.pc, callback_pc, "entry PC must be the callback");
        assert_eq!(
            new_ctx.gpr[3] as u32, message_ptr,
            "r3 must hold the message pointer"
        );

        match state.objects.get(&handle) {
            Some(KernelObject::Thread { hw_id: Some(_), .. }) => {}
            other => panic!("expected Thread object with hw_id set, got {:?}", other),
        }
    }

    #[test]
    fn xget_avpack_returns_hdmi() {
        let (mut ctx, mem, mut state) = fresh();
        xget_avpack(&mut ctx, &mem, &mut state);
        assert_eq!(ctx.gpr[3], 8);
    }

    #[test]
    fn xam_user_get_signin_state_user0_signed_in_locally() {
        let (mut ctx, mem, mut state) = fresh();
        ctx.gpr[3] = 0;
        xam_user_get_signin_state(&mut ctx, &mem, &mut state);
        assert_eq!(ctx.gpr[3], 1);

        ctx.gpr[3] = 1;
        xam_user_get_signin_state(&mut ctx, &mem, &mut state);
        assert_eq!(ctx.gpr[3], 0);

        ctx.gpr[3] = 4;
        xam_user_get_signin_state(&mut ctx, &mem, &mut state);
        assert_eq!(ctx.gpr[3], 0);
    }

    fn drain_notifications(state: &mut KernelState, mem: &GuestMemory, handle: u32) -> Vec<(u32, u32)> {
        let id_ptr = SCRATCH_BASE + 0x100;
        let param_ptr = SCRATCH_BASE + 0x104;
        let mut out = Vec::new();
        loop {
            let mut ctx = PpcContext::default();
            ctx.gpr[3] = handle as u64;
            ctx.gpr[4] = 0;
            ctx.gpr[5] = id_ptr as u64;
            ctx.gpr[6] = param_ptr as u64;
            xnotify_get_next(&mut ctx, mem, state);
            if ctx.gpr[3] == 0 {
                break;
            }
            let id = mem.read_u32(id_ptr);
            let data = mem.read_u32(param_ptr);
            out.push((id, data));
            if out.len() > 16 {
                panic!("runaway dequeue");
            }
        }
        out
    }

    fn create_listener(state: &mut KernelState, mem: &GuestMemory, mask: u64, max_version: u32) -> u32 {
        let mut ctx = PpcContext::default();
        ctx.gpr[3] = mask;
        ctx.gpr[4] = max_version as u64;
        xam_notify_create_listener(&mut ctx, mem, state);
        ctx.gpr[3] as u32
    }

    #[test]
    fn first_listener_with_full_mask_gets_4_startup_notifications() {
        let (_ctx, mem, mut state) = fresh();
        let h = create_listener(&mut state, &mem, 0x2F, 10);
        let drained = drain_notifications(&mut state, &mem, h);
        assert_eq!(
            drained,
            vec![
                (K_X_NOTIFICATION_SYSTEM_UI, 0),
                (K_X_NOTIFICATION_SYSTEM_SIGN_IN_CHANGED, 1),
                (K_X_NOTIFICATION_LIVE_CONNECTION_CHANGED, 0x001510F1),
                (K_X_NOTIFICATION_LIVE_LINK_STATE_CHANGED, 0),
            ]
        );
    }

    #[test]
    fn second_listener_does_not_re_fire_startup() {
        let (_ctx, mem, mut state) = fresh();
        let h1 = create_listener(&mut state, &mem, 0x2F, 10);
        let _ = drain_notifications(&mut state, &mem, h1);
        let h2 = create_listener(&mut state, &mem, 0x2F, 10);
        assert!(drain_notifications(&mut state, &mem, h2).is_empty());
    }

    #[test]
    fn system_only_mask_filters_live() {
        let (_ctx, mem, mut state) = fresh();
        let h = create_listener(&mut state, &mem, K_X_NOTIFY_SYSTEM, 10);
        let drained = drain_notifications(&mut state, &mem, h);
        assert_eq!(
            drained,
            vec![
                (K_X_NOTIFICATION_SYSTEM_UI, 0),
                (K_X_NOTIFICATION_SYSTEM_SIGN_IN_CHANGED, 1),
            ]
        );
    }

    #[test]
    fn version_filter_drops_too_new() {
        let (_ctx, mem, mut state) = fresh();
        let h = create_listener(&mut state, &mem, 0x2F, 0);
        let drained = drain_notifications(&mut state, &mem, h);
        let kept_versions: Vec<u32> = drained.iter().map(|&(id, _)| notification_version(id)).collect();
        assert!(kept_versions.iter().all(|&v| v == 0));
    }

    #[test]
    fn xnotify_get_next_returns_zero_for_unknown_handle() {
        let (mut ctx, mem, mut state) = fresh();
        let id_ptr = SCRATCH_BASE + 0x100;
        let param_ptr = SCRATCH_BASE + 0x104;
        ctx.gpr[3] = 0xDEAD_BEEF;
        ctx.gpr[4] = 0;
        ctx.gpr[5] = id_ptr as u64;
        ctx.gpr[6] = param_ptr as u64;
        xnotify_get_next(&mut ctx, &mem, &mut state);
        assert_eq!(ctx.gpr[3], 0);
        assert_eq!(mem.read_u32(id_ptr), 0);
        assert_eq!(mem.read_u32(param_ptr), 0);
    }
}
